一.幾個開發(fā)中經(jīng)常會遇到的問題
以下幾個問題是我們在實際開發(fā)中經(jīng)常會遇到的場景家卖,下面用幾個簡單的示例代碼來還原一下罢荡。
1.setState是同步還是異步的灶挟,為什么有的時候不能立即拿到更新結(jié)果而有的時候可以?
1.1 鉤子函數(shù)和React合成事件中的setState
現(xiàn)在有兩個組件
?componentDidMount() {
? ?console.log('parent componentDidMount');
?}
?render() {
? ?return (
? ? ?<div>
? ? ? ?<SetState2></SetState2>
? ? ? ?<SetState></SetState>
? ? ?</div>
? ?);
?}
組件內(nèi)部放入同樣的代碼誉察,并在Setstate1中的componentDidMount中放入一段同步延時代碼高诺,打印延時時間:
?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不會立即更新
2.所有組件使用的是同一套更新機制摘悴,當所有組件didmount后峭梳,父組件didmount,然后執(zhí)行更新
3.更新時會把每個組件的更新合并蹂喻,每個組件只會觸發(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.為什么有時連續(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.直接傳遞對象的setstate會被合并成一次
2.使用函數(shù)傳遞state不會被合并
二.setState執(zhí)行過程
由于源碼比較復雜孵运,就不貼在這里了,有興趣的可以去github上clone一份然后按照下面的流程圖去走一遍蔓彩。
1.流程圖
partialState:setState傳入的第一個參數(shù)治笨,對象或函數(shù)
_pendingStateQueue:當前組件等待執(zhí)行更新的state隊列
isBatchingUpdates:react用于標識當前是否處于批量更新狀態(tài),所有組件公用
dirtyComponent:當前所有處于待更新狀態(tài)的組件隊列
transcation:react的事務機制赤嚼,在被事務調(diào)用的方法外包裝n個waper對象大磺,并一次執(zhí)行:waper.init、被調(diào)用方法探膊、waper.close
FLUSH_BATCHED_UPDATES:用于執(zhí)行更新的waper杠愧,只有一個close方法
2.執(zhí)行過程
對照上面流程圖的文字說明,大概可分為以下幾步:
1.將setState傳入的partialState參數(shù)存儲在當前組件實例的state暫存隊列中逞壁。
2.判斷當前React是否處于批量更新狀態(tài)流济,如果是,將當前組件加入待更新的組件隊列中腌闯。
3.如果未處于批量更新狀態(tài)绳瘟,將批量更新狀態(tài)標識設置為true,用事務再次調(diào)用前一步方法姿骏,保證當前組件加入到了待更新組件隊列中糖声。
4.調(diào)用事務的waper方法,遍歷待更新組件隊列依次執(zhí)行更新。
5.執(zhí)行生命周期componentWillReceiveProps蘸泻。
6.將組件的state暫存隊列中的state進行合并琉苇,獲得最終要更新的state對象,并將隊列置為空悦施。
7.執(zhí)行生命周期componentShouldUpdate并扇,根據(jù)返回值判斷是否要繼續(xù)更新。
8.執(zhí)行生命周期componentWillUpdate抡诞。
9.執(zhí)行真正的更新穷蛹,render。
10.執(zhí)行生命周期componentDidUpdate昼汗。
三.總結(jié)
1.鉤子函數(shù)和合成事件中:
在react的生命周期和合成事件中肴熏,react仍然處于他的更新機制中,這時isBranchUpdate為true顷窒。
按照上述過程扮超,這時無論調(diào)用多少次setState,都會不會執(zhí)行更新蹋肮,而是將要更新的state存入_pendingStateQueue出刷,將要更新的組件存入dirtyComponent。
當上一次更新機制執(zhí)行完畢坯辩,以生命周期為例馁龟,所有組件,即最頂層組件didmount后會將isBranchUpdate設置為false漆魔。這時將執(zhí)行之前累積的setState坷檩。
2.異步函數(shù)和原生事件中
由執(zhí)行機制看,setState本身并不是異步的改抡,而是如果在調(diào)用setState時矢炼,如果react正處于更新過程,當前更新會被暫存阿纤,等上一次更新執(zhí)行后在執(zhí)行句灌,這個過程給人一種異步的假象。
在生命周期欠拾,根據(jù)JS的異步機制胰锌,會將異步函數(shù)先暫存,等所有同步代碼執(zhí)行完畢后在執(zhí)行藐窄,這時上一次更新過程已經(jīng)執(zhí)行完畢资昧,isBranchUpdate被設置為false,根據(jù)上面的流程荆忍,這時再調(diào)用setState即可立即執(zhí)行更新格带,拿到更新結(jié)果撤缴。
3.partialState合并機制
我們看下流程中_processPendingState的代碼,這個函數(shù)是用來合并state暫存隊列的叽唱,最后返回一個合并后的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);
如果傳入的是對象,很明顯會被合并成一次:
Object.assign(
?nextState,
?{index: state.index+ 1},
?{index: state.index+ 1}
)
如果傳入的是函數(shù)尔觉,函數(shù)的參數(shù)preState是前一次合并后的結(jié)果凉袱,所以計算結(jié)果是準確的芥吟。
4.componentDidMount調(diào)用setstate
在componentDidMount()中侦铜,你 可以立即調(diào)用setState()。它將會觸發(fā)一次額外的渲染钟鸵,但是它將在瀏覽器刷新屏幕之前發(fā)生钉稍。這保證了在此情況下即使render()將會調(diào)用兩次,用戶也不會看到中間狀態(tài)棺耍。謹慎使用這一模式贡未,因為它常導致性能問題。在大多數(shù)情況下蒙袍,你可以 在constructor()中使用賦值初始狀態(tài)來代替俊卤。然而,有些情況下必須這樣害幅,比如像模態(tài)框和工具提示框消恍。這時,你需要先測量這些DOM節(jié)點以现,才能渲染依賴尺寸或者位置的某些東西狠怨。
以上是官方文檔的說明,不推薦直接在componentDidMount直接調(diào)用setState邑遏,由上面的分析:componentDidMount本身處于一次更新中佣赖,我們又調(diào)用了一次setState,就會在未來再進行一次render记盒,造成不必要的性能浪費憎蛤,大多數(shù)情況可以設置初始值來搞定。
當然在componentDidMount我們可以調(diào)用接口纪吮,再回調(diào)中去修改state蹂午,這是正確的做法。
當state初始值依賴dom屬性時彬碱,在componentDidMount中setState是無法避免的豆胸。
5.componentWillUpdatecomponentDidUpdate
這兩個生命周期中不能調(diào)用setState。
由上面的流程圖很容易發(fā)現(xiàn)巷疼,在它們里面調(diào)用setState會造成死循環(huán)晚胡,導致程序崩潰灵奖。
6.推薦使用方式
在調(diào)用setState時使用函數(shù)傳遞state值,在回調(diào)函數(shù)中獲取最新更新后的state估盘。