React State(狀态)
React 把元件看成是一個狀态機(State Machines)。通過與使用者的互動,實作不同狀态,然後渲染 UI,讓使用者界面和資料保持一緻。
React 裡,隻需更新元件的 state,然後根據新的 state 重新渲染使用者界面(不要操作 DOM)。
以下執行個體中建立了 LikeButton 元件,getInitialState 方法用于定義初始狀态,也就是一個對象,這個對象可以通過 this.state 屬性讀取。當使用者點選元件,導緻狀态變化,this.setState 方法就修改狀态值,每次修改以後,自動調用 this.render 方法,再次渲染元件。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>菜鳥教程 React 執行個體</title>
<script src="https://cdn.bootcss.com/react/15.4.2/react.min.js"></script>
<script src="https://cdn.bootcss.com/react/15.4.2/react-dom.min.js"></script>
<script src="https://cdn.bootcss.com/babel-standalone/6.22.1/babel.min.js"></script>
</head>
<body>
<div id="example"></div>
<script type="text/babel">
var LikeButton = React.createClass({
getInitialState: function() {
return {liked: false};
},
handleClick: function(event) {
this.setState({liked: !this.state.liked});
},
render: function() {
var text = this.state.liked ? '喜歡' : '不喜歡';
return (
<p onClick={this.handleClick}>
你<b>{text}</b>我。點我切換狀态。
</p>
);
}
});
ReactDOM.render(
<LikeButton />,
document.getElementById('example')
);
</script>
</body>
</html>
我們都知道,React通過this.state來通路state,通過this.setState()方法來更新state。當this.setState()方法被調用的時候,React會重新調用render方法來重新渲染UI
setState異步更新
setState方法通過一個隊列機制實作state更新,當執行setState的時候,會将需要更新的state合并之後放入狀态隊列,而不會立即更新this.state(可以和浏覽器的事件隊列類比)。如果我們不使用setState而是使用this.state.key來修改,将不會觸發元件的re-render。如果将this.state指派給一個新的對象引用,那麼其他不在對象上的state将不會被放入狀态隊列中,當下次調用setState并對狀态隊列進行合并時,直接造成了state丢失。(這裡特别感謝@Dcatfly的指正)
我們來看一下React文檔中對setState的說明
void setState(
function|object nextState,
[function callback]
)
The second (optional) parameter is a callback function that will be executed once setState is completed and the component is re-rendered.
翻譯一下,第二個參數是一個回調函數,在setState的異步操作結束并且元件已經重新渲染的時候執行。也就是說,我們可以通過這個回調來拿到更新的state的值。
React也正是利用狀态隊列機制實作了setState的異步更新,避免頻繁地重複更新state(pending的意思是未定的,即将發生的)
//将新的state合并到狀态更新隊列中
var nextState = this._processPendingState(nextProps, nextContext);
//根據更新隊列和shouldComponent的狀态來判斷是否需要更新元件
var shouldUpdate =
this._pendingForceUpdate ||
!inst.shouldComponentUpdate ||
inst.shouldComponentUpdate(nextProps, nextState, nextContext);
setState循環調用風險
當調用setState時,實際上會執行enqueueSetState方法,并對partialState以及_pending-StateQueue更新隊列進行合并操作,最終通過enqueueUpdate執行state更新
而performUpdateIfNecessary方法會擷取pendingElement, pendingStateQueue,_ pending-ForceUpdate,并調用receiveComponent和updateComponent方法進行元件更新
如果在shouldComponentUpdate或者componentWillUpdate方法中調用setState,此時this._pending-StateQueue != null,就會造成循環調用,使得浏覽器記憶體占滿後崩潰
調用棧
既然setState最終是通過enqueueUpdate執行state更新,那麼enqueueUpdate到底是如何更新state的呢? 首先看下面的問題
import React, { Component } from 'react';
class Example extends Component {
constructor(){
super();
//在元件初始化可以直接操作this.state
this.state = {
val: 0
}
}
componentDidMount(){
this.setState({
val: this.state.val + 1
});
//第一次輸出
console.log(this.state.val);
this.setState({
val: this.state.val + 1
});
//第二次輸出
console.log(this.state.val);
setTimeout(()=>{
this.setState({val: this.state.val + 1});
//第三次輸出
console.log(this.state.val);
this.setState({
val: this.state.val + 1
});
//第四次輸出
console.log(this.state.val);
}, 0);
}
render(){
return null;
}
}
上述代碼中,4次console.log列印出來的val分别是: 0,0,2 ,3
我們來看一個簡化的setState的調用棧
this.setState(newState) =>
newState存入pending隊列 =>
調用enqueueUpdate =>
是否處于批量更新模式 =>
是的話将元件儲存到dirtyComponents
不是的話周遊dirtyComponents,調用updateComponent,更新pending state or props
enqueueUpdate的源碼如下(上面流程的第三步)(batching的意思是批量的)
function enqueueUpdate(component){
//injected注入的
ensureInjected();
//如果不處于批量更新模式
if(!batchingStrategy.isBatchingUpdates){
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
//如果處于批量更新模式
dirtyComponents.push(component);
}
如果isBatchingUpdates為true,則對所有隊列中的更新執行batchedUpdates方法,否則隻把目前元件(即調用了setState的元件)放入dirtyComponents數組中,例子中4次setState調用的表現之是以不同,這裡的邏輯判斷起了關鍵作用
事務
事務就是将需要執行的方法使用wrapper封裝起來,再通過事務提供的perform方法執行,先執行wrapper中的initialize方法,執行完perform之後,在執行所有的close方法,一組initialize及close方法稱為一個wrapper。
那麼事務和setState方法的不同表現有什麼關系,首先我們把4次setState簡單歸類,前兩次屬于一類,因為它們在同一調用棧中執行,setTimeout中的兩次setState屬于另一類。
在setState調用之前,已經處在batchedUpdates執行的事務中了。那麼這次batchedUpdates方法是誰調用的呢,原來是ReactMount.js中的_renderNewRootComponent方法。也就是說,整個将React元件渲染到DOM中的過程就是處于一個大的事務中。而在componentDidMount中調用setState時,batchingStrategy的isBatchingUpdates已經被設為了true,是以兩次setState的結果沒有立即生效。
再反觀setTimeout中的兩次setState,因為沒有前置的batchedUpdates調用,是以導緻了新的state馬上生效。