一.幾個(gè)開發(fā)中經(jīng)常會(huì)遇到的問題
以下幾個(gè)問題是我們?cè)趯?shí)際開發(fā)中經(jīng)常會(huì)遇到的場景,下面用幾個(gè)簡單的示例代碼來還原一下。
1.setState是同步還是異步的,為什么有的時(shí)候不能立即拿到更新結(jié)果而有的時(shí)候可以?
1.1 鉤子函數(shù)和React合成事件中的setState
現(xiàn)在有兩個(gè)組件
?componentDidMount() {
? ?console.log('parent componentDidMount');
?}
?render() {
? ?return (
? ? ?<div>
? ? ? ?<SetState2></SetState2>
? ? ? ?<SetState></SetState>
? ? ?</div>
? ?);
?}
組件內(nèi)部放入同樣的代碼,并在Setstate1中的componentDidMount中放入一段同步延時(shí)代碼,打印延時(shí)時(shí)間:
?componentWillUpdate() {
? ?console.log('componentWillUpdate');
?}
?componentDidUpdate() {
? ?console.log('componentDidUpdate');
?}
?componentDidMount() {
? ?console.log('SetState調(diào)用setState');
? ?this.setState({
? ? ?index: this.state.index + 1
? ?})
? ?console.log('state', this.state.index);
? ?console.log('SetState調(diào)用setState');
? ?this.setState({
? ? ?index: this.state.index + 1
? ?})
? ?console.log('state', this.state.index);
?}
下面是執(zhí)行結(jié)果:
說明:
1.調(diào)用setState不會(huì)立即更新
2.所有組件使用的是同一套更新機(jī)制,當(dāng)所有組件didmount后,父組件didmount,然后執(zhí)行更新
3.更新時(shí)會(huì)把每個(gè)組件的更新合并,每個(gè)組件只會(huì)觸發(fā)一次更新的生命周期。
1.2 異步函數(shù)和原生事件中的setstate?
在setTimeout中調(diào)用setState(例子和在瀏覽器原生事件以及接口回調(diào)中執(zhí)行效果相同)
?componentDidMount() {
? ?setTimeout(() => {
? ? ?console.log('調(diào)用setState');
? ? ?this.setState({
? ? ? ?index: this.state.index + 1
? ? ?})
? ? ?console.log('state', this.state.index);
? ? ?console.log('調(diào)用setState');
? ? ?this.setState({
? ? ? ?index: this.state.index + 1
? ? ?})
? ? ?console.log('state', this.state.index);
? ?}, 0);
?}
執(zhí)行結(jié)果:
說明:
1.在父組件didmount后執(zhí)行
2.調(diào)用setState同步更新
2.為什么有時(shí)連續(xù)兩次setState只有一次生效?
分別執(zhí)行以下代碼:
?componentDidMount() {
? ?this.setState({ index: this.state.index + 1 }, () => {
? ? ?console.log(this.state.index);
? ?})
? ?this.setState({ index: this.state.index + 1 }, () => {
? ? ?console.log(this.state.index);
? ?})
?}
?componentDidMount() {
? ?this.setState((preState) => ({ index: preState.index + 1 }), () => {
? ? ?console.log(this.state.index);
? ?})
? ?this.setState(preState => ({ index: preState.index + 1 }), () => {
? ? ?console.log(this.state.index);
? ?})
?}
執(zhí)行結(jié)果:
1
1
2
2
說明:
1.直接傳遞對(duì)象的setstate會(huì)被合并成一次
2.使用函數(shù)傳遞state不會(huì)被合并
二.setState執(zhí)行過程
由于源碼比較復(fù)雜,就不貼在這里了,有興趣的可以去github上clone一份然后按照下面的流程圖去走一遍。
1.流程圖
partialState:setState傳入的第一個(gè)參數(shù),對(duì)象或函數(shù)
_pendingStateQueue:當(dāng)前組件等待執(zhí)行更新的state隊(duì)列
isBatchingUpdates:react用于標(biāo)識(shí)當(dāng)前是否處于批量更新狀態(tài),所有組件公用
dirtyComponent:當(dāng)前所有處于待更新狀態(tài)的組件隊(duì)列
transcation:react的事務(wù)機(jī)制,在被事務(wù)調(diào)用的方法外包裝n個(gè)waper對(duì)象,并一次執(zhí)行:waper.init、被調(diào)用方法、waper.close
FLUSH_BATCHED_UPDATES:用于執(zhí)行更新的waper,只有一個(gè)close方法
2.執(zhí)行過程
對(duì)照上面流程圖的文字說明,大概可分為以下幾步:
1.將setState傳入的partialState參數(shù)存儲(chǔ)在當(dāng)前組件實(shí)例的state暫存隊(duì)列中。
2.判斷當(dāng)前React是否處于批量更新狀態(tài),如果是,將當(dāng)前組件加入待更新的組件隊(duì)列中。
3.如果未處于批量更新狀態(tài),將批量更新狀態(tài)標(biāo)識(shí)設(shè)置為true,用事務(wù)再次調(diào)用前一步方法,保證當(dāng)前組件加入到了待更新組件隊(duì)列中。
4.調(diào)用事務(wù)的waper方法,遍歷待更新組件隊(duì)列依次執(zhí)行更新。
5.執(zhí)行生命周期componentWillReceiveProps。
6.將組件的state暫存隊(duì)列中的state進(jìn)行合并,獲得最終要更新的state對(duì)象,并將隊(duì)列置為空。
7.執(zhí)行生命周期componentShouldUpdate,根據(jù)返回值判斷是否要繼續(xù)更新。
8.執(zhí)行生命周期componentWillUpdate。
9.執(zhí)行真正的更新,render。
10.執(zhí)行生命周期componentDidUpdate。
三.總結(jié)
1.鉤子函數(shù)和合成事件中:
在react的生命周期和合成事件中,react仍然處于他的更新機(jī)制中,這時(shí)isBranchUpdate為true。
按照上述過程,這時(shí)無論調(diào)用多少次setState,都會(huì)不會(huì)執(zhí)行更新,而是將要更新的state存入_pendingStateQueue,將要更新的組件存入dirtyComponent。
當(dāng)上一次更新機(jī)制執(zhí)行完畢,以生命周期為例,所有組件,即最頂層組件didmount后會(huì)將isBranchUpdate設(shè)置為false。這時(shí)將執(zhí)行之前累積的setState。
2.異步函數(shù)和原生事件中
由執(zhí)行機(jī)制看,setState本身并不是異步的,而是如果在調(diào)用setState時(shí),如果react正處于更新過程,當(dāng)前更新會(huì)被暫存,等上一次更新執(zhí)行后在執(zhí)行,這個(gè)過程給人一種異步的假象。
在生命周期,根據(jù)JS的異步機(jī)制,會(huì)將異步函數(shù)先暫存,等所有同步代碼執(zhí)行完畢后在執(zhí)行,這時(shí)上一次更新過程已經(jīng)執(zhí)行完畢,isBranchUpdate被設(shè)置為false,根據(jù)上面的流程,這時(shí)再調(diào)用setState即可立即執(zhí)行更新,拿到更新結(jié)果。
3.partialState合并機(jī)制
我們看下流程中_processPendingState的代碼,這個(gè)函數(shù)是用來合并state暫存隊(duì)列的,最后返回一個(gè)合并后的state。
?_processPendingState: function (props, context) {
? ?var inst = this._instance;
? ?var queue = this._pendingStateQueue;
? ?var replace = this._pendingReplaceState;
? ?this._pendingReplaceState = false;
? ?this._pendingStateQueue = null;
? ?if (!queue) {
? ? ?return inst.state;
? ?}
? ?if (replace && queue.length === 1) {
? ? ?return queue[0];
? ?}
? ?var nextState = _assign({}, replace ? queue[0] : inst.state);
? ?for (var i = replace ? 1 : 0; i < queue.length; i++) {
? ? ?var partial = queue[i];
? ? ?_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
? ?}
? ?return nextState;
?},
我們只需要關(guān)注下面這段代碼:
_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
如果傳入的是對(duì)象,很明顯會(huì)被合并成一次:
Object.assign(
?nextState,
?{index: state.index+ 1},
?{index: state.index+ 1}
)
如果傳入的是函數(shù),函數(shù)的參數(shù)preState是前一次合并后的結(jié)果,所以計(jì)算結(jié)果是準(zhǔn)確的。
4.componentDidMount調(diào)用setstate
在componentDidMount()中,你 可以立即調(diào)用setState()。它將會(huì)觸發(fā)一次額外的渲染,但是它將在瀏覽器刷新屏幕之前發(fā)生。這保證了在此情況下即使render()將會(huì)調(diào)用兩次,用戶也不會(huì)看到中間狀態(tài)。謹(jǐn)慎使用這一模式,因?yàn)樗?dǎo)致性能問題。在大多數(shù)情況下,你可以 在constructor()中使用賦值初始狀態(tài)來代替。然而,有些情況下必須這樣,比如像模態(tài)框和工具提示框。這時(shí),你需要先測(cè)量這些DOM節(jié)點(diǎn),才能渲染依賴尺寸或者位置的某些東西。
以上是官方文檔的說明,不推薦直接在componentDidMount直接調(diào)用setState,由上面的分析:componentDidMount本身處于一次更新中,我們又調(diào)用了一次setState,就會(huì)在未來再進(jìn)行一次render,造成不必要的性能浪費(fèi),大多數(shù)情況可以設(shè)置初始值來搞定。
當(dāng)然在componentDidMount我們可以調(diào)用接口,再回調(diào)中去修改state,這是正確的做法。
當(dāng)state初始值依賴dom屬性時(shí),在componentDidMount中setState是無法避免的。
5.componentWillUpdatecomponentDidUpdate
這兩個(gè)生命周期中不能調(diào)用setState。
由上面的流程圖很容易發(fā)現(xiàn),在它們里面調(diào)用setState會(huì)造成死循環(huán),導(dǎo)致程序崩潰。
6.推薦使用方式
在調(diào)用setState時(shí)使用函數(shù)傳遞state值,在回調(diào)函數(shù)中獲取最新更新后的state。