React State(狀態(tài))
React 把組件看成是一個(gè)狀態(tài)機(jī)(State Machines)匕垫。通過與用戶的交互,實(shí)現(xiàn)不同狀態(tài)除破,然后渲染 UI牧氮,讓用戶界面和數(shù)據(jù)保持一致。
React 里瑰枫,只需更新組件的 state踱葛,然后根據(jù)新的 state 重新渲染用戶界面(不要操作 DOM)。
以下實(shí)例中創(chuàng)建了 LikeButton 組件光坝,getInitialState 方法用于定義初始狀態(tài)尸诽,也就是一個(gè)對(duì)象,這個(gè)對(duì)象可以通過 this.state 屬性讀取盯另。當(dāng)用戶點(diǎn)擊組件性含,導(dǎo)致狀態(tài)變化,this.setState 方法就修改狀態(tài)值鸳惯,每次修改以后商蕴,自動(dòng)調(diào)用 this.render 方法,再次渲染組件芝发。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>菜鳥教程 React 實(shí)例</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>我绪商。點(diǎn)我切換狀態(tài)。
</p>
);
}
});
ReactDOM.render(
<LikeButton />,
document.getElementById('example')
);
</script>
</body>
</html>
我們都知道辅鲸,React通過this.state來訪問state格郁,通過this.setState()方法來更新state。當(dāng)this.setState()方法被調(diào)用的時(shí)候独悴,React會(huì)重新調(diào)用render方法來重新渲染UI
setState異步更新
setState方法通過一個(gè)隊(duì)列機(jī)制實(shí)現(xiàn)state更新例书,當(dāng)執(zhí)行setState的時(shí)候,會(huì)將需要更新的state合并之后放入狀態(tài)隊(duì)列绵患,而不會(huì)立即更新this.state(可以和瀏覽器的事件隊(duì)列類比)雾叭。如果我們不使用setState而是使用this.state.key來修改,將不會(huì)觸發(fā)組件的re-render落蝙。如果將this.state賦值給一個(gè)新的對(duì)象引用织狐,那么其他不在對(duì)象上的state將不會(huì)被放入狀態(tài)隊(duì)列中暂幼,當(dāng)下次調(diào)用setState并對(duì)狀態(tài)隊(duì)列進(jìn)行合并時(shí),直接造成了state丟失移迫。(這里特別感謝@Dcatfly的指正)
我們來看一下React文檔中對(duì)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.
翻譯一下旺嬉,第二個(gè)參數(shù)是一個(gè)回調(diào)函數(shù),在setState的異步操作結(jié)束并且組件已經(jīng)重新渲染的時(shí)候執(zhí)行厨埋。也就是說邪媳,我們可以通過這個(gè)回調(diào)來拿到更新的state的值。
React也正是利用狀態(tài)隊(duì)列機(jī)制實(shí)現(xiàn)了setState的異步更新荡陷,避免頻繁地重復(fù)更新state(pending的意思是未定的雨效,即將發(fā)生的)
//將新的state合并到狀態(tài)更新隊(duì)列中
var nextState = this._processPendingState(nextProps, nextContext);
//根據(jù)更新隊(duì)列和shouldComponent的狀態(tài)來判斷是否需要更新組件
var shouldUpdate =
this._pendingForceUpdate ||
!inst.shouldComponentUpdate ||
inst.shouldComponentUpdate(nextProps, nextState, nextContext);
setState循環(huán)調(diào)用風(fēng)險(xiǎn)
當(dāng)調(diào)用setState時(shí),實(shí)際上會(huì)執(zhí)行enqueueSetState方法废赞,并對(duì)partialState以及_pending-StateQueue更新隊(duì)列進(jìn)行合并操作徽龟,最終通過enqueueUpdate執(zhí)行state更新
而performUpdateIfNecessary方法會(huì)獲取pendingElement, pendingStateQueue,_ pending-ForceUpdate唉地,并調(diào)用receiveComponent和updateComponent方法進(jìn)行組件更新
如果在shouldComponentUpdate或者componentWillUpdate方法中調(diào)用setState据悔,此時(shí)this._pending-StateQueue != null,就會(huì)造成循環(huán)調(diào)用耘沼,使得瀏覽器內(nèi)存占滿后崩潰
調(diào)用棧
既然setState最終是通過enqueueUpdate執(zhí)行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
我們來看一個(gè)簡(jiǎn)化的setState的調(diào)用棧
this.setState(newState) =>
newState存入pending隊(duì)列 =>
調(diào)用enqueueUpdate =>
是否處于批量更新模式 =>
是的話將組件保存到dirtyComponents
不是的話遍歷dirtyComponents浸赫,調(diào)用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,則對(duì)所有隊(duì)列中的更新執(zhí)行batchedUpdates方法赃绊,否則只把當(dāng)前組件(即調(diào)用了setState的組件)放入dirtyComponents數(shù)組中,例子中4次setState調(diào)用的表現(xiàn)之所以不同羡榴,這里的邏輯判斷起了關(guān)鍵作用
事務(wù)
事務(wù)就是將需要執(zhí)行的方法使用wrapper封裝起來碧查,再通過事務(wù)提供的perform方法執(zhí)行,先執(zhí)行wrapper中的initialize方法校仑,執(zhí)行完perform之后忠售,在執(zhí)行所有的close方法,一組initialize及close方法稱為一個(gè)wrapper迄沫。
那么事務(wù)和setState方法的不同表現(xiàn)有什么關(guān)系稻扬,首先我們把4次setState簡(jiǎn)單歸類,前兩次屬于一類羊瘩,因?yàn)樗鼈冊(cè)谕徽{(diào)用棧中執(zhí)行泰佳,setTimeout中的兩次setState屬于另一類盼砍。
在setState調(diào)用之前,已經(jīng)處在batchedUpdates執(zhí)行的事務(wù)中了逝她。那么這次batchedUpdates方法是誰調(diào)用的呢浇坐,原來是ReactMount.js中的_renderNewRootComponent方法。也就是說黔宛,整個(gè)將React組件渲染到DOM中的過程就是處于一個(gè)大的事務(wù)中近刘。而在componentDidMount中調(diào)用setState時(shí),batchingStrategy的isBatchingUpdates已經(jīng)被設(shè)為了true臀晃,所以兩次setState的結(jié)果沒有立即生效觉渴。
再反觀setTimeout中的兩次setState,因?yàn)闆]有前置的batchedUpdates調(diào)用徽惋,所以導(dǎo)致了新的state馬上生效案淋。