React :元素構成組件,組件又構成應用。
React核心思想是組件化,其中 組件 通過屬性(props) 和 狀態(state)傳遞數據。
State 與 Props 區別
props 是組件對外的接口,state 是組件對內的接口
。組件內可以引用其他組件,組件之間的引用形成了一個樹狀結構(組件樹),如果下層組件需要使用上層組件的數據或方法,上層組件就可以通過下層組件的props屬性進行傳遞,因此props是組件對外的接口。組件除了使用上層組件傳遞的數據外,自身也可能需要維護管理數據,這就是組件對內的接口state。根據對外接口props 和對內接口state,組件計算出對應界面的UI。
主要區別:
- State是可變的,是一組用於反映組件UI變化的狀態集合;
- 而Props對於使用它的組件來說,是隻讀的,要想修改Props,只能通過該組件的父組件修改。
在組件狀態上移的場景中,父組件正是通過子組件的Props, 傳遞給子組件其所需要的狀態。
Props的使用
當一個組件被注入一些屬性(Props )值時,屬性值來源於它的父級元素,所以人們常說,屬性在 React 中是單向流動的:從父級到子元素。
1、props(屬性) 默認爲 “true”
如果你沒給 prop(屬性) 傳值,那麼他默認爲 true 。下面兩個 JSX 表達式是等價的:
<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />
通常情況下,我們不建議使用這種類型,因爲這會與ES6中的對象shorthand混淆 。ES6 shorthand 中 {foo} 指的是 {foo: foo} 的簡寫,而不是 {foo: true} 。這種行爲只是爲了與 HTML 的行爲相匹配。
(舉個例子,在 HTML 中,< input type=“radio” value=“1” disabled /> 與 < input type=“radio” value=“1” disabled=“true” /> 是等價的。JSX 中的這種行爲就是爲了匹配 HTML 的行爲。)
2、props擴展
如果你已經有一個 object 類型的 props,並且希望在 JSX 中傳入,你可以使用擴展操作符 … 傳入整個 props 對象。這兩個組件是等效的:
function App1() {
return <Greeting firstName="Ben" lastName="Hector" />;
}
function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}
顯然下面的方法更方便:因爲它將數據進行了包裝,而且還簡化了賦值的書寫
State
一、State是什麼?
React 的核心思想是組件化
,而組件中最重要的概念是State(狀態),State是一個組件的UI數據模型,是組件渲染時的數據依據。
狀態(state) 和 屬性(props) 類似,都是一個組件所需要的一些數據集合,但是state是私有的,可以認爲state是組件的“私有屬性(或者是局部屬性)”。
如何判斷是否爲State ?
組件中用到的一個變量是不是應該作爲組件State,可以通過下面的4條依據進行判斷:
- 這個變量是否是通過Props從父組件中獲取?如果是,那麼它不是一個狀態。
- 這個變量是否在組件的整個生命週期中都保持不變?如果是,那麼它不是一個狀態。
- 這個變量是否可以通過其他狀態(State)或者屬性(Props)計算得到?如果是,那麼它不是一個狀態。
- 這個變量是否在組件的render方法中使用?如果不是,那麼它不是一個狀態。這種情況下,這個變量更適合定義爲組件的一個普通屬性,例如組件中用到的定時器,就應該直接定義爲this.timer,而不是this.state.timer。
並不是組件中用到的所有變量都是組件的狀態!
當存在多個組件共同依賴一個狀態時,一般的做法是狀態上移,將這個狀態放到這幾個組件的公共父組件中。
二、如何正確使用 State
1、用setState 修改State
直接修改state,組件並不會重新觸發render()
// 錯誤
this.state.comment = 'Hello';
正確的修改方式是使用setState()
// 正確
this.setState({comment: 'Hello'});
2、State 的更新是異步的
- 調用setState後,setState會把要修改的狀態放入一個隊列中(因而 組件的state並不會立即改變);
- 之後React 會優化真正的執行時機,來優化性能,所以優化過程中有可能會將多個 setState 的狀態修改合併爲一次狀態修改,因而state更新可能是異步的。
- 所以不要依賴當前的State,計算下個State。當真正執行狀態修改時,依賴的this.state並不能保證是最新的State,因爲React會把多次State的修改合併成一次,這時,this.state將還是這幾次State修改前的State。
另外需要注意的事,同樣不能依賴當前的Props計算下個狀態,因爲Props一般也是從父組件的State中獲取,依然無法確定在組件狀態更新時的值。
綜上所述:
this.props 和 this.state 可能是異步更新的,你不能依賴他們的值計算下一個state(狀態)
例:
這樣 counter(計數器)會更新失敗
// 錯誤
this.setState({
counter: this.state.counter + this.props.increment,
});
要彌補這個問題,使用 setState() 的另一種形式,它接受一個函數而不是一個對象。這個函數有兩個參數:
(1)第一個參數: 是當前最新狀態的前一個狀態(本次組件狀態修改前的狀態)
(2)第二個參數:是當前最新的屬性props
// 正確
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
//注意:下面這樣是錯的
this.setState((prevState, props) => { //沒將{}用()括起來,所以會解析成代碼塊
counter: prevState.counter + props.increment
});
如果你還不懂沒關係,看下面例子:
我們現在渲染出一個button,想每點擊一下,counter就+3
看下面代碼:
class App extends React.Component {
state = {
counter: 0,
}
handleClick = () => {
const { counter } = this.state;
//或者 const counter = this.state.counter;
this.setState({ counter: counter + 1 });
this.setState({ counter: counter + 1 });
this.setState({ counter: counter + 1 });
}
render() {
return (
<div>
counter is: {this.state.counter}
<button onClick={this.handleClick} >點我</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
輸出:
每點擊一下,加+1,並不是+3
之所以+1,不是+3,是因爲 state 的更新可能是異步的,React 會把傳入多個 setState的多個 Object “batch” 起來合併成一個。合併成一個就相當於把傳入 setState 的多個 Object 進行 shallow merge
,像這樣:
const update = {
counter: counter + 1,
counter: counter + 1,
counter: counter + 1
//因爲上面三句話都一樣,所以會當一句話執行
}
我們可以這麼做就會成功:看下面
class App extends React.Component {
state = {
counter: 0,
}
handleClick = () => {
this.setState(prev => ({ counter: prev.counter + 1 }));
this.setState(prev => ({ counter: prev.counter + 1 }));
this.setState(prev => ({ counter: prev.counter + 1 }));
//這樣是錯的 this.setState(prev => {counter: prev.counter + 1});
//這樣是錯的 this.setState(prev => {counter:++prev.counter});
//這樣是錯的 this.setState(prev => {counter:prev.counter++});
}
render() {
return (
<div>
counter is: {this.state.counter}
<button onClick={this.handleClick} >點我</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
之所以成功是因爲:傳入多個 setState 的多個 Object 會被 shallow Merge,而傳入多個 setState 的多個 function 會被 "queue" 起來,queue 裏的 function 接收到的 state(上面是 prev )都是前一個 function 操作過的 state。
3、State更新會被合併
官方文檔看不懂不要緊,直接舉個例子你就懂了。
例如一個組件的狀態爲:
this.state = {
title : 'React',
content : 'React is an wonderful JS library!'
}
當只需要修改狀態title時,只需要將修改後的title傳給setState:
this.setState({title: 'Reactjs'});
React會合並新的title到原來的組件狀態中,同時保留原有的狀態content,合併後的State爲:
{
title : 'Reactjs',
content : 'React is an wonderful JS library!'
}
4、setState裏順序更新
//history 爲數組
this.setState({
history: history.concat([1]), //(1)
current: history.length, //(2)
nextPlayer: !nextPlayer, //(3)
});
執行setState時:先更新history,然後再用更新改變後的history計算current的值
,最後再更新nextPlayer
三、根據State類型 更新
當狀態發生變化時,如何創建新的狀態?根據狀態的類型,可以分成三種情況:
1、 狀態的類型是不可變類型(數字,字符串,布爾值,null, undefined)
這種情況最簡單,直接給要修改的狀態賦一個新值即可
//原state
this.state = {
count: 0,
title : 'React',
success:false
}
//改變state
this.setState({
count: 1,
title: 'bty',
success: true
})
2、 狀態的類型是數組
數組是一個引用,React 執行 diff 算法時比較的是兩個引用,而不是引用的對象。所以直接修改原對象,引用值不發生改變的話,React 不會重新渲染。因此,修改狀態的數組或對象時,要返回一個新的數組或對象。
(1)增加
如有一個數組類型的狀態books,當向books中增加一本書(chinese)時,使用數組的concat方法或ES6的數組擴展語法
// 方法一:將state先賦值給另外的變量,然後使用concat創建新數組
let books = this.state.books;
this.setState({
books: books.concat(['chinese'])
})
// 方法二:使用preState、concat創建新數組
this.setState(preState => ({
books: preState.books.concat(['chinese'])
}))
// 方法三:ES6 spread syntax
this.setState(preState => ({
books: [...preState.books, 'chinese']
}))
(2)截取
當從books中截取部分元素作爲新狀態時,使用數組的slice方法:
// 方法一:將state先賦值給另外的變量,然後使用slice創建新數組
let books = this.state.books;
this.setState({
books: books.slice(1,3)
})
//
// 方法二:使用preState、slice創建新數組
this.setState(preState => ({
books: preState.books.slice(1,3)
}))
(3)條件過濾
當從books中過濾部分元素後,作爲新狀態時,使用數組的filter方法:
// 方法一:將state先賦值給另外的變量,然後使用filter創建新數組
var books = this.state.books;
this.setState({
books: books.filter(item => {
return item != 'React';
})
})
// 方法二:使用preState、filter創建新數組
this.setState(preState => ({
books: preState.books.filter(item => {
return item != 'React';
})
}))
注意:不要使用push、pop、shift、unshift、splice等方法修改數組類型的狀態,因爲這些方法都是在原數組的基礎上修改,而concat、slice、filter會返回一個新的數組。
3、狀態的類型是普通對象(不包含字符串、數組)
對象是一個引用,React 執行 diff 算法時比較的是兩個引用,而不是引用的對象。所以直接修改原對象,引用值不發生改變的話,React 不會重新渲染。因此,修改狀態的數組或對象時,要返回一個新的對象。
使用ES6 的Object.assgin方法
// 方法一:將state先賦值給另外的變量,然後使用Object.assign創建新對象
var owner = this.state.owner;
this.setState({
owner: Object.assign({}, owner, {name: 'Jason'})
})
// 方法二:使用preState、Object.assign創建新對象
this.setState(preState => ({
owner: Object.assign({}, preState.owner, {name: 'Jason'})
}))
使用對象擴展語法(object spread properties)
// 方法一:將state先賦值給另外的變量,然後使用對象擴展語法創建新對象
var owner = this.state.owner;
this.setState({
owner: {...owner, name: 'Jason'}
})
// 方法二:使用preState、對象擴展語法創建新對象
this.setState(preState => ({
owner: {...preState.owner, name: 'Jason'}
}))
綜上所述:
創建新的狀態對象的關鍵是,避免使用會直接修改原對象的方法,而是使用可以返回一個新對象的方法。
四、State向下流動
我們說 props 是組件對外的接口,state 是組件對內的接口。
一個組件可以選擇將 state(狀態) 向下傳遞,作爲其子組件的 props(屬性):
<MyComponent title={this.state.title}/>
這通常稱爲一個“從上到下”,或者“單向”的數據流。任何 state(狀態) 始終由某個特定組件所有,並且從該 state(狀態) 導出的任何數據 或 UI 只能影響樹中 “下方” 的組件。
如果把組件樹想像爲 props(屬性) 的瀑布,所有組件的 state(狀態) 就如同一個額外的水源匯入主流,且只能隨着主流的方向向下流動。