React(二)——React組件之狀態與通信

目錄

1.組件類型

1.1函數式組件

1.2類式組件

2.創建FriendList組件——案例

3.組件複用——數據抽取

4.組件複用——數據傳入

4.1property

 4.2接收參數——props

 4.3通過參數動態渲染組件結構

4.4子組件提取

5.組件狀態state

6.組件交互

6.1綁定事件

6.2獲取事件對象

6.3原生DOM對象

6.4獲取原生DOM對象

7.狀態更新

Object.defineProperties()

7.1setState方法

 7.1.1更新異步

 7.1.2更新合併

7.1.3setState的回調函數

7.1.4子組件抽離

 8.props和state的區別

8.1無狀態式組件

8.2函數式組件

9.組件通信與數據流

9.1狀態提升

9.2數據流

9.2.1props函數

9.2.2更新父組件

10.key的唯一性

10.1渲染優化

10.1.1Virtual DOM

10.1.2Diffing 算法


主要內容:函數式組件,類式組件,組件複用(數據抽取/傳入),組件狀態state,組件交互(綁定事件,獲取事件對象,原生DOM對象,獲取原生DOM對象),狀態更新setstate,props與state區別,組件通信與數據流,key的唯一性。

1.組件類型

web 組件就是對 web 中的數據、結構、方法等進行封裝,複用,與 JavaScript 中功能函數封裝類似,但更關注的是對 web 元素(標籤)的封裝與擴展(擴展:webComponent

React 提供了兩種組件構建方式

  • 函數式組件
  • 類式組件

1.1函數式組件

React.js 中,定義一個組件的最簡單的方式就是 函數

如果當前文件中用到了JSX,就必須導入react。

function Kaikeba() {
    return (
        <div>
            <h2>開課吧!</h2>
        </div>
    );
}
ReactDOM.render(
    <Kaikeba />,
    document.getElementById('app')
);
  • 函數的名稱就是組件的名稱
  • 函數的返回值就是組件要渲染的內容

**函數的名稱(組件的名稱),必須是首字母大寫

1.2類式組件

我們還可以通過 類(class)類定義組件。

無論類式組件還是函數式組件,最後都要渲染內容。

class Miaov extends React.Component {
    render() {
        return (
            <div>
                <h2>妙味!</h2>
            </div>
        );
    }
}
  • 組件類必須繼承父類 React.Component
  • 組件類必須有 render 方法
  • render 方法的返回值就是組件要渲染的內容

**類的名稱(組件的名稱),也必須是首字母大寫

2.創建FriendList組件——案例

不從實際案例開始的知識點就是耍 LM

樣式

.friend-list {
    border: 1px solid #000000;
    width: 200px;
}
.friend-group dt {
    padding: 10px;
    background-color: rgb(64, 158, 255);
    font-weight: bold;
}
.friend-group dd {
    padding: 10px;
    display: none;
}
.friend-group.expanded dd {
    display: block;
}
.friend-group dd.checked {
    background: green;
}

創建 FriendList 組件:

import React from 'react';
import './FriendList.css'

function FriendList(){
    return (
        <div className="friend-list">
                <div className="friend-group">
                    <dt>家人</dt>
                    <dd>爸爸</dd>
                    <dd>媽媽</dd>
                </div>
                <div className="friend-group">
                    <dt>朋友</dt>
                    <dd>張三</dd>
                    <dd>李四</dd>
                    <dd>王五</dd>
                </div>
                <div className="friend-group">
                    <dt>客戶</dt>
                    <dd>阿里</dd>
                    <dd>騰訊</dd>
                    <dd>頭條</dd>
                </div>
            </div>
    );
}

export default FriendList;

報錯 'React' must be in scope when using JSX react/react-in-jsx-scope 

解決:組建中也使用到了JSX,所以必須引入react。import React from 'react';

3.組件複用——數據抽取

爲了提高組件的複用性,通常會把組件中的一些可變數據提取出來

let datas = {
    family: {
        title: '家人',
        list: [
            {name: '爸爸'},
            {name: '媽媽'}
        ]
    },
    friend: {
        title: '朋友',
        list: [
            {name: '張三'},
            {name: '李四'},
            {name: '王五'}
        ]
    },
    customer: {
        title: '客戶',
        list: [
            {name: '阿里'},
            {name: '騰訊'},
            {name: '頭條'}
        ]
    }
};

export default datas;

 

import React from 'react';
import './FriendList.css';
import datas from './datas/friends1';

function FriendList(){
    return (
        <div className="friend-list">
                <div className="friend-group">
                    {Object.keys(datas).map((key, index) => (
                    <div className="friend-group" key={key}>
                        <dt>{datas[key].title}</dt>
                		{datas[key].list.map(list => <dd key={list.name}>{list.name}</dd>)}
                    </div>
                ))}
                </div>
            </div>
    );
}

export default FriendList;

4.組件複用——數據傳入

數據雖然分離了,但是爲了降低組件與數據之前的依賴,我們應該儘量避免在組件內部直接訪問數據,通過傳參的方法來進行解耦。

4.1property

使用組件的時候通過 標籤屬性-property 的方式傳入數據,在組件內部通過構造函數參數(如果是函數組件,則通過函數參數)來接收傳入的數據 。

<組件名稱 屬性名稱="值" />
// 使用表達式
<組件名稱 屬性名稱={表達式} />
ReactDOM.render(
    <FriendList datas={datas} />,
    document.getElementById('app')
);

 4.2接收參數——props

在組件對應的函數或類中通過:

  • 函數式組件:會傳遞給函數的第一個參數來接收
  • 類式組件:會傳遞給類的props屬性(這個屬性在父類React.Component中就已經存在),再通過構造函數第一個參數來接收

無論是函數式組件還是類式組件,都會把傳入的參數組裝成一個對象:

<組件名稱 屬性名稱1="值1" 屬性名稱二={表達式二} />

// 函數式組件
function 組件名稱(參數) {
  	// 參數的結構爲
  	參數 = {
      	屬性名稱1: "值1",
      	屬性名稱二: 表達式二的值
    }
  
	  return <div>組件結構</div>
}
// 類式組件
class 組件名稱 extends React.Component {
  	constructor(參數) {
      	super(參數);
      
      	this.props = {
          	屬性名稱1: "值1",
          	屬性名稱2: 表達式二的值
        }
    }
  
  	render() {
      	return <div>組件結構</div>
    }
}

在類式組件中,需要注意:

  • 當子類重寫 constructor ,則必須調用父類 super - 類的知識點

  • 把接收到的參數傳入父類構造函數,父類構造函數中會創建一個對象屬性:props 類存儲傳入的參數數據

 最後示例代碼:

函數式組件:

Friends_func.js數據:

export default {
    jser: {
        title: 'js愛好者',
        list: [
            {name: '寶哥'},
            {name: '莫濤'},
            {name: '大海'}
        ]
    },
    tser: {
        title: 'ts愛好者',
        list: [
            {name: '雨軒'},
            {name: '小姐姐'}
        ]
    }
};

 friendlist_func.js函數式組件代碼:

// 注意寫組件時只要用到了JSX都需要引入react
import React from 'react';
import '../css/FriendList.css';

//函數式組件外部傳入數據是通過函數參數進行接收
function FriendListFunc(props){
    // 將外部傳入的數據進行結構
    let {data} = props;
    
    //注意一個組件中只能有一個return
    return (
        <div className="friend-list">
            {
                Object.keys(data).map(key=>(
                    <div className="friend-group" key={key}>
                        <dt>{data[key].title}</dt>
                        {
                            // 注意map()括號中由於只有一個參數所以可以直接跟返回值,而JSX代碼不止一行需要使用()括號括起來方便代碼查看,不括起來也不會出錯
                            data[key].list.map(dd=>(
                                <dd key={dd.name}>{dd.name}</dd>
                            ))
                        }
                    </div>
                ))
            }
          
      </div>
  );
}

export default FriendListFunc;

類式組件數據:

export default {
    family: {
        title: '家人',
        list: [
            {name: '爸爸'},
            {name: '媽媽'}
        ]
    },
    friend: {
        title: '朋友',
        list: [
            {name: '張三'},
            {name: '李四'},
            {name: '王五'}
        ]
    },
    customer: {
        title: '客戶',
        list: [
            {name: '阿里'},
            {name: '騰訊'},
            {name: '頭條'}
        ]
    }
};

 類式組件代碼:

import React from "react";

class FriendListClass extends React.Component{
    //類式組件外部傳入的數據,需要通過構造函數的參數進行接收
    constructor(props){
        // 當子類重寫constructor ,則必須調用父類super類的知識點
        super(props);

        // 把接收到的參數傳入父類構造函數,父類構造函數中會創建一個對象屬性:<u>props</u> 類存儲傳入的參數數據
        this.props = props;
    }
    //注意render(){ return () }結構不能寫錯
    render(){
        let {data} = this.props;
        
        return (
            <div className="friend-list">
                {
                    Object.keys(data).map(key => (
                        <div className="friend-group" key={key}>
                            <dt>{data[key].title}</dt>
                                {
                                    data[key].list.map(item=>(
                                    <dd key={item.name}>{item.name}</dd>
                                    ))
                                }
                        </div>
                    ))
                }
            
            </div>
        );
    }
}

//不管是函數組件還是類式組件都需要導出
export default FriendListClass;

 類式組件和函數式組件最終調用及外部數據傳入的不同調用:

import React from 'react';
import logo from './logo.svg';
import './App.css';

import FriendListFunc from './components/friendlist_func';
import FriendListClass from './components/friendlist_class';
//通過屬性將外部數據傳入組件
import FriendFuncData from './datas/friends_Func'

//通過屬性將外部數據傳入組件
import FriendClassData from './datas/friends_class'

function App() {
  return (
    <div className="App">
      <FriendListFunc data={FriendFuncData}/>
      <hr/>
      <FriendListClass data={FriendClassData}/>
    </div>
  );
}

export default App;

 4.3通過參數動態渲染組件結構

函數式組件:

APP.js

import React from 'react';
// import FriendList from './compenents/FriendList';
import FriendListProps from './compenents/FriendListProps';
import datas from './compenents/datas/friends1';

function App() {
  return (
    <div className="App">
      <h1>hi, react!</h1>
      {/* <FriendList /> */}
      <FriendListProps datas={datas} name='列表渲染'/>
    </div>
  );
}

export default App;

FriendsListProp.js:

import React from 'react';
import './FriendList.css';
// import datas from './datas/friends1';

function FriendListProps(props){
// 函數式組件:通過函數的第一個參數來接收
    console.log(props);
    let {datas} = props;
    return (
        <div className="friend-list">
                <div className="friend-group">
                    {Object.keys(datas).map((key, index) => (
                    <div className="friend-group" key={key}>
                        <dt>{datas[key].title}</dt>
                		{datas[key].list.map(list => <dd key={list.name}>{list.name}</dd>)}
                    </div>
                ))}
                </div>
            </div>
    );
}

export default FriendListProps;

 類式組件:

friends.js:

let datas = {
    jser: {
        title: 'JS愛好者',
        list: [
            {name: '姍鳳'},
            {name: '姐大'},
            {name: '鼕鼕'}
        ]
    },
    tser: {
        title: 'TS愛好者',
        list: [
            {name: 'lili'},
            {name: '君君'},
            {name: '香兒'}
        ]
    }
};

export default datas;

APP.js

import React from 'react';
// import FriendList from './compenents/FriendList';
// import FriendListProps from './compenents/FriendListProps';
import FriendList2 from './compenents/FriendList2';
// import datas from './compenents/datas/friends1';
import datas from './compenents/datas/friends2';

function App() {
  return (
    <div className="App">
      <h1>hi, react!</h1>
      {/* <FriendList /> */}
      {/* <FriendListProps datas={datas} name='列表渲染'/> */}
      <FriendList2 datas={datas} name='列表渲染'/>
    </div>
  );
}

export default App;

FriendList2.js: 

import React from 'react';
import './FriendList.css';

class FriendList2 extends React.Component {
    // 類式組件:通過類的構造函數第一個參數來接收
    constructor(props){
        super(props);
        this.props = props;
    }
    render(){
        let {datas} = this.props;
        return (
            <div className="friend-list">
                <div className="friend-group">
                    {Object.keys(datas).map((key, index) => (
                    <div className="friend-group" key={key}>
                        <dt>{datas[key].title}</dt>
                		{datas[key].list.map(list => <dd key={list.name}>{list.name}</dd>)}
                    </div>
                ))}
                </div>
            </div>
        );
    }
}


export default FriendList2;

4.4子組件提取

組件與類一樣,是一個不斷提取的過程,當我們發現某個部分可複用或者結構複雜的時候,我們可以對它再次進行提取 。

class FriendGroup extends React.Component {
		constructor(props) {
				super(props);
    }
  
  	render() {
      	let {data} = this.props;
      	return (
        		<div className="friend-group">
                <dt>{data.title}</dt>
                {data.list.map((list, index) => {
                		<dd key={index}>{list}</dd>
                })}
            </div>
        );
    }
}

class FriendList extends React.Component {
  	constructor(props) {
      	super(props);
    }
  
  	render() {
      	let {datas} = this.props;
      	return (
          	<div className="friend-list">
            		{Object.keys(datas).map((key, index) => (
                    <FriendGroup data={datas[key]} key={key} />
                ))}
            </div>
        );
    }
}

5.組件狀態state

有的時候,一個組件除了可以接收外部傳入的數據,還會有屬於自己的內部私有數據,同時組件私有數據的更新還會導致組件的重新渲染,比如上面的例子中的每一個好友面板會有一個內部私有數據來控制面板的展開與收縮,我們又把這種數據稱爲組件狀態 。

props和state區別:

  • props:由外部控制傳入數據;
  • state:組件自己控制和維護內容私有數據;

state:存儲組件私有狀態(數據)的對象。當state狀態發生改變時,組件UI也會相應進行重新渲染,如果是自定義屬性不會進行重新渲染。

函數式組件沒有狀態state(最新hooks除外)。函數式組件只提供了外部傳入組件props方式,而沒有內容私有數據控制的state。因爲有狀態有可能會導致不可控性,一般能寫無狀態的函數式組件就寫這個。現在流行的也是函數式組件。

示例:通過類式組件的state,有內部傳入數據控制組件內部收縮展開功能。

import React from 'react';
import '../css/FriendList.css';

/**
 * 用戶手動控制好友列表展開/收縮
 */
class FriendList extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            //此處手動控制expanded爲true/false時,好友列表會展開/收縮
            expanded : true
        };
    }
    render() {
        //注意解構後的變量名一定要和對象props中已存在的變量名一致
        let { datas } = this.props;
        //通過解構獲得expanded標識
        let {expanded} = this.state;

        return (
            <div className="friend-list">
                {
                    Object.keys(datas).map(item => (
                        // 此處使用JSX中數組輸出方式設置多個className的值
                        <div className={["friend-group",expanded && "expanded"].join(' ')} key={item}>
                            <dt>{datas[item].title}</dt>
                            {
                                datas[item].list.map(key=><dd key={key.name}>{key.name}</dd>)
                            }
                        </div>
                    ))
                }

            </div>
        );
    }
}

export default FriendList;

 示例解析:

  • 通過class樣式以語法形式控制:

className={[
                "friend-group",
                expanded && "expanded"
            ].join(' ')}

  • JSX表達式{}中數據類型爲數組轉爲字符串會以逗號隔開,所以需要使用join(' ')將class樣式以空格隔開,且只有當expanded爲true時,纔會展開有expanded樣式,所以使用expanded && "expanded",也可以使用三目運算符。
  • expanded && "expanded"原本應該是this.state.expanded && "expanded",可以通過let {expanded} = this.state進行解構,當使用時即可直接使用expanded && "expanded"即可。

6.組件交互

一個組件除了有結構、樣式,有的時候還會有交互,如上面的例子:組件初始化時是收縮的,現在我們希望當用戶點擊組件面板 title 的時候,面板組件的展開收縮狀態就會發生改變 。

6.1綁定事件

import React from 'react';
import '../css/FriendList.css';

/**
 * 實現用戶交互:點擊列表後實現展開收縮(事件綁定)
 */
class FriendList extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            //此處手動控制expanded爲true/false時,好友列表會展開/收縮
            expanded : true
        };
        //將改變this綁定掛載到實例上,在onClick時就可以直接使用this.expand了
        this.expand = this.expand.bind(this)
    }

    //事件綁定:用戶點擊列表
    expand(){
        
        //如果直接將this.state.expanded = !this.state.expanded;取反,會報錯,因爲react中時間綁定中,this默認指向undefined,所以需要調用時綁定this指向爲這個實例
        //React的state控制私有數據,但是需要調用setState()方法才能進行狀態更新
        this.setState({
            expanded : !this.state.expanded
        })
    }

    render() {
        //注意解構後的變量名一定要和對象props中已存在的變量名一致
        let { datas } = this.props;
        //通過解構獲得expanded標識
        let {expanded} = this.state;
        
        return (
            <div className="friend-list">
                {
                    Object.keys(datas).map(item => (
                        // 此處使用JSX中數組輸出方式設置多個className的值
                        <div className={["friend-group",expanded && "expanded"].join(' ')} key={item}>
                            <dt onClick={this.expand}>{datas[item].title}</dt>
                            {
                                datas[item].list.map(key=><dd key={key.name}>{key.name}</dd>)
                            }
                        </div>
                    ))
                }

            </div>
        );
    }
}

export default FriendList;

 React.js 的事件綁定需要注意:

  • 事件名稱是駝峯命名的

  • 事件綁定函數的 this 指向

    • 通過 bind 改變 this 指向,爲了能夠在方法中調用組件對象的屬性和其它方法,我們需要把 this 指向組件

    • 通過箭頭函數處理 this

注意:

事件不要寫成行間樣式,儘量單獨寫成方法,再在onClick事件中調用該方法,調用時不能寫括號。

事件中,直接使用this.state.expanded = !this.state.expanded會報錯。因爲在react中,事件綁定的函數中的this(並不是所有react中的this都是指向undefined)默認指向undefined。在使用時需要通過bind()方法改變react中原有的this指向,onClick={this.expand.bind(this)},這樣就會將this指向組件本身。爲了增加複用性會在類初始化構造函數中,直接使用this.expand = this.expand.bind(this);而在html行間onClick={this.expand}。

6.2獲取事件對象

事件綁定函數的第一個參數是事件對象

6.3原生DOM對象

通過事件對象可以獲取到原生 DOM 對象

6.4獲取原生DOM對象

有的時候我們也是需要操作原生 DOM 對象的,除了可以通過事件源來獲取,還可以通過 ref 的方式來獲取

class MyComponent extends React.Component {
  	render() {
      	return (
          	<div>
            		<input ref={el=>this.el=el} />
            		<button onClick={this.todo.bind(this)}>Button</button>
          	</div>
        );
    }
  	todo() {
      	console.log(this.el.value);
    }
}

7.狀態更新

當我們希望更新組件狀態的時候,不要直接修改 state 的值,而是需要調用 setState 方法來進行更新。

在ES6中通過Object.defineProperty() 和Proxy來進行動態渲染更新(也可以自定義狀態更新如setObj()但是涉及到生命週期一般不建議)。但在react中只能使用特定的setState方法進行更新。

ES6中Object.defineProperty()和Object.defineProperties():

Object.defineProperty()功能:
方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。如果不指定configurable, writable, enumerable ,則這些屬性默認值爲false,如果不指定value, get, set,則這些屬性默認值爲undefined

語法: Object.defineProperty(obj, prop, descriptor)

obj: 需要被操作的目標對象
prop: 目標對象需要定義或修改的屬性的名稱
descriptor: 將被定義或修改的屬性的描述符

var obj = new Object();

Object.defineProperty(obj, 'name', {
    configurable: false,
    writable: true,
    enumerable: true,
    value: '張三'
})

console.log(obj.name)  //張三

Object.defineProperties()

功能:
方法直接在一個對象上定義一個或多個新的屬性或修改現有屬性,並返回該對象。

語法: Object.defineProperties(obj, props)

obj: 將要被添加屬性或修改屬性的對象
props: 該對象的一個或多個鍵值對定義了將要爲對象添加或修改的屬性的具體配置

var obj = new Object();
Object.defineProperties(obj, {
    name: {
        value: '張三',
        configurable: false,
        writable: true,
        enumerable: true
    },
    age: {
        value: 18,
        configurable: true
    }
})

console.log(obj.name, obj.age) // 張三, 18

7.1setState方法

setState 方法由父類 React.Component 提供,當該方法被調用的時候,組件的 state 會被更新,同時會重新調用組件的 render 方法對組件進行渲染

不要直接修改 state,而是調用 setState 方法來更新,組件構造函數是唯一可以對 state 直接賦值(初始化)的位置

this.setState({key:val})

 7.1.1更新異步

出於性能考慮,setState 方法的修改並不是立即生效的(異步)。

會在多次調用setState()方法後統一進行更新

// this.state.val = 0
this.setState({
  	val: this.state.val + 1
});
// this.state.val 的值並不是 1
console.log(this.state.val);

 7.1.2更新合併

React 會把多個 setState 合併成一個調用

// this.state.val = 0
this.setState({
  	val: this.state.val + 1
});
this.setState({	// 因爲異步的問題,this.state.val 的值在這裏還是0
  	val: this.state.val + 1
});

如下:setState()採用的是Object.assign()方法進行更新,每次更新都會覆蓋上一次的state值,所以即使多次調用this.state.val值都爲2。即使經過多次調用this.setState({}),val值仍然只會執行最後一次調用

Object.assign(
  previousState,
  {val: state.val + 1},
  {val: state.val + 1},
  ...
)

7.1.3setState的回調函數

如果,有多個 setState 的調用,後一次的修改如果需要依賴上一次修改的值,那麼可以使用函數的方式

// this.state.val = 0
this.setState((state, props) => {
  	return {val: state.val + 1}
});
this.setState((state, props) => {
  	return {val: state.val + 1}
});

7.1.4子組件抽離

需求:從組件中抽離出子組件,收縮和展開功能點擊時,只控制當前分組。將每單個組件寫成一個子組件,有子組件控制收縮展開狀態。

注意:

  • 子組件的數據也是有父組件從外部傳入的。
  • 引入子組件時此處也是在map循環中,所以也需要唯一key;

父組件:

import React from 'react';
import '../css/FriendList.css';

import Item from './Item';

/**
 * 子組件抽離:原本的state組件交互,只能控制整個列表的同時展開/收縮。
 * 現在需要實現點擊某個分組控制自己展開/收縮
 * 狀態由子組件控制
 */
class FriendList extends React.Component {
    constructor(props) {
        super(props);
    }

    render() {
        let { datas } = this.props;
        
        return (
            <div className="friend-list">
                {
                    Object.keys(datas).map(item => (
                        //注意此處Item組件也在map循環中也需要唯一key
                        <Item data={datas[item]} key={datas[item].title}/>
                    ))
                }

            </div>
        );
    }
}

export default FriendList;

子組件:所有狀態控制和事件綁定都在子組件中實現

import React from 'react';

class Item extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            //此處手動控制expanded爲true/false時,好友列表會展開/收縮
            expanded : true
        };
        //將改變this綁定掛載到實例上,在onClick時就可以直接使用this.expand了
        this.expand = this.expand.bind(this)
    }

    //事件綁定:用戶點擊列表
    expand(){
        
        //如果直接將this.state.expanded = !this.state.expanded;取反,會報錯,因爲react中時間綁定中,this默認指向undefined,所以需要調用時綁定this指向爲這個實例
        //React的state控制私有數據,但是需要調用setState()方法才能進行狀態更新
        this.setState({
            expanded : !this.state.expanded
        })
    }
    render() {
        let { data } = this.props;
        //通過解構獲得expanded標識
        let {expanded} = this.state;
        return (
            // 此處使用JSX中數組輸出方式設置多個className的值
            <div className={["friend-group", expanded && "expanded"].join(' ')} key={data.title}>
                <dt onClick={this.expand}>{data.title}</dt>
                {
                    data.list.map(key => <dd key={key.name}>{key.name}</dd>)
                }
            </div>
        );
    }
}

export default Item;

 8.props和state的區別

state 的主要作用是用於組件保存、控制、修改自己的可變狀態,在組件內部進行初始化,也可以在組件內部進行修改,但是組件外部不能修改組件的 state

props 的主要作用是讓使用該組件的父組件可以傳入參數來配置該組件,它是外部傳進來的配置參數,組件內部無法控制也無法修改

stateprops 都可以決定組件的外觀和顯示狀態。通常,props 做爲不變數據或者初始化數據傳遞給組件,可變狀態使用 state

使用 props 就不要使用 state

8.1無狀態式組件

沒有 state 的組件,我們稱爲無狀態組件,因爲狀態會帶來複雜性,所以,通常我們推薦使用無狀態組件,也鼓勵編寫無狀態組建 。

8.2函數式組件

函數式組件沒有 state ,所以通常我們編寫使用函數式組件來編寫無狀態組件

9.組件通信與數據流

在上面的案例中,我們實現了一個多選式的好友列表(每個獨立的面板都有自己獨立的狀態,互不影響)。下面我們再來看另外一種情況:單選式的好友列表(多組列表同時只能有一個爲展開狀態,也就是多個面板之間的狀態是互相影響的)

9.1狀態提升

需求:實現多個分組,同時只有一個分組展開其他都收縮

通過分析可以發現,面板的展開狀態不再是組件內部私有狀態了,多個組件都會受到這個狀態的影響,也就是它們共享了一個狀態。爲了能夠讓多個不同組件共享同一個狀態,這個時候,我們把這個狀態進行提升,交給這些組件最近的公共父組件進行管理

  • 狀態由父組件進行控制,子組件不再控制狀態state的改變;
  • 由於父級不再維護一個分組的展開與否所以不能再使用expanded = false,而需要使用index控制所有分組;
  • 父組件會將index值傳遞給子組件(let {index} = this.state   <Item index={index} n={n}/>)(n爲對象map遍歷生成列表時的索引),子組件接收父組件傳遞過來的index,判斷index與子組件下標相同(index===n)時再展開/收縮;
  • 父組件狀態值發生變化時,子組件會重新進行渲染;
  • 子組件不要直接修改父組件傳入的props。因爲父組件傳入的prop不一定只有當前一個子組件共享。可以通過事件通知或事件回調通知父組件進行修改。
  • 在父組件中的展開收縮方法onExpand()方法中通過this.setState({ index:n}) 改變需要更改的子組件索引狀態,然後通過props屬性onExpand = {this.onExpand.bind(this)}將更改後的值傳遞給子組件 ;在子組件自己的expand方法中,通過調用父組件的展開收縮方法this.props.onExpand && this.props.onExpand(this.props.n)將子組件需要的修改的n通知給父組件;從而達到子組件的狀態修改由父組件控制的目的。

父組件:

import React from 'react';
import '../css/FriendList.css';

import Item from './Item_state_improve';

/**
 * 狀態提升:
 * 所有分組同時只有一個分組展開其他分組都收縮
 * 面板的展開狀態不再是組件內部私有狀態了,多個組件都會受到這個狀態的影響,也就是它們共享了一個狀態。
 * 爲了能夠讓多個不同組件共享同一個狀態,這個時候,我們把這個狀態進行提升,交給這些組件最近的公共父組件進行管理 
 * 
 * 由父組件管理state狀態,並通過父組件將分組index和當前點擊分組的索引n傳遞給子組件
 * 子組件接收父組件傳遞的index和num,通過展開方法回調給父組件進行更新狀態
 */
class FriendList extends React.Component {
    constructor(props) {
        super(props);

        //狀態提升時,將子組件的數據改變回調給父組件進行更改,因爲此時不再是控制某個分組展開/收縮,所以需要記錄所有分組的index
        this.state = {
            index : 1
        };
        this.onExpand = this.onExpand.bind(this)
    }

     //事件綁定:用戶點擊列表
    onExpand(num){
        this.setState({
            index:num
        })
    }
   
    render() {
        let { datas } = this.props;
        
        return (
            <div className="friend-list">
                {
                    Object.keys(datas).map((item,num) => (
                        //將所有分組的index,當前分組的索引num,供子組件回調的展開方法onExpand() 傳遞給子組件
                        <Item data={datas[item]} key={datas[item].title} index={this.state.index} num={num} onExpand={this.onExpand}/>
                    ))
                }

            </div>
        );
    }
}

export default FriendList;

子組件:將狀態的更改回調給父組件進行控制

import React from 'react';

/**
 * 狀態提升子組件
 */
class Item extends React.Component {
    constructor(props) {
        super(props);
        //子組件的this指向綁定到子組件上
        this.expand = this.expand.bind(this);
    }

    //子組件自身展開/收縮方法
    expand(e){
        //子組件自身不能更改狀態,而是回調給父組件統一控制,所以需要調用父組件的展開/收縮方法(父組件通過props屬性傳遞過來)
        //**this.props.num是父級傳遞過來的分組索引,通過子組件點擊後將當前索引回調給父組件控制展開
        this.props.onExpand && this.props.onExpand(this.props.num);
        //通過classList切換展開和收縮
        e.target.parentNode.classList.toggle("expanded");
    }

    render() {
        let { data,index,num,flag } = this.props;
        
        return (
            // 只有當分組索引index和當前點擊分組num一致時才展開
            <div className={["friend-group", (index===num)? "expanded":""].join(' ')} key={data.title}>
                <dt onClick={this.expand}>{data.title}</dt>
                {
                    data.list.map(key => <dd key={key.name}>{key.name}</dd>)
                }
            </div>
        );
    }
}

export default Item;

 示例2(方法二): 

class FriendGroup extends React.Component {
		constructor(props) {
				super(props);
      
      	this.changeExpand = this.changeExpand.bind(this);
    }
  
  	changeExpand() {
      	typeof this.props.onExpand === 'function' && 			this.props.onExpand(this.props.index);
    }
  
  	render() {
      	let {data, expandedIndex, index} = this.props;
      	return (
        		<div onClick={this.changeExpand} className={[
                "friend-group",
                expandedIndex === index && "expanded"
            ].join(' ')}>
                <dt>{data.title}</dt>
                {data.list.map((list, index) => {
                		<dd key={index}>{list}</dd>
                })}
            </div>
        );
    }
}

class FriendList extends React.Component {
  	constructor(props) {
      	super(props);
      
      	this.state = {
          	expandedIndex: 0
        }
      
      	this.changeExpandedIndex = this.changeExpandedIndex.bind(this);
    }
  
  	changeExpandedIndex(expandedIndex) {
        this.setState({
            expandedIndex
        });
    }
  
  	render() {
      	let {datas} = this.props;
      	return (
          	<div onExpand={this.changeExpandedIndex} className="friend-list">
            		{Object.keys(datas).map((key, index) => (
                    <FriendGroup data={datas[key]} index={index} expandedIndex={expandedIndex} key={key} />
                ))}
            </div>
        );
    }
}

延展練習:點擊完後,再次點擊仍然可以展開收縮如果實現?

9.2數據流

React.js 中,數據是從上自下流動(傳遞)的,也就是一個父組件可以把它的 state / props 通過 props 傳遞給它的子組件,但是子組件不能修改 props - React.js 是單向數據流,如果子組件需要修改父組件狀態(數據),是通過回調函數方式來完成的

9.2.1props函數

父組件在調用子組件的時候,傳入一個函數類型的 props

<FriendGroup data={datas[key]} index={index} expandedIndex={expandedIndex} key={key} />

9.2.2更新父組件

在父組件的代碼中,通過 setState 重新渲染父組件,父組件的更新會重新渲染子組件 。

10.key的唯一性

import React from 'react';

class KeyComponent extends React.Component {

    constructor(props) {
        super(props);
        
        this.state = {
            arr: ['html', 'css', 'javascript']
        }
        this.sort = this.sort.bind(this);
    }

    sort() {
        this.setState({
            arr: this.state.arr.sort( _ => Math.random() - .5 )
        });
    }

    render() {
        let {arr} = this.state;
        return(
            <div>
                <ul>
                    {arr.map( (v, k) => (
                        //在map循環中加上key={v}即可
                        <li key={v}>
                            <input type="checkbox"/>
                            <span>{v}</span>
                        </li>
                    ) )}
                </ul>
                <button onClick={this.sort}>亂序</button>
            </div>
        );
    }
}

export default KeyComponent;

通過控制檯查看結構變化  

問題:發現本來複選框選中的是html選項,但是點擊亂序後,複選框選中的是JavaScript。

問題原因:因爲在react中考慮到性能問題,當數據發生變化時,不是根據數據重新創建結構再覆蓋掉原有虛擬DOM結構,而是會比較虛擬DOM結構與數據之間是否有發生變化,如果沒有變化就不會進行刪除創建結構等,而是複用原有結構。這種在列表循環時就會造成列表結構和數據內容沒有相關聯,所以需要使用唯一key(一般爲id)對虛擬DOM結構和數據內容進行相關聯。不要使用數組下標和時間戳作爲唯一key。

10.1渲染優化

  1. 用JS對象模擬DOM樹 : Virtual DOM
  2. 通過 DOM 操作Virtual DOM 生成真實的 DOM
  3. 後期的更新,會比較 新老 Virtual DOM ,通過 diff 算法優化更新操作

10.1.1Virtual DOM

原生 DOM 對象結構太複雜,同時存在許多情況下用不到的一些特性

Virtual DOM 本質上與 原生 DOM 沒有區別,都是 JavaScript 對象,只是它的結構更加簡潔

10.1.2Diffing 算法

React.js 不只是模擬了一個 DOM,同時在針對 DOM 進行渲染的時候

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章