面試官:“寶子,setState 是同步還是異步的呀?”
本文轉載自微信公眾號「勾勾的前端世界」,作者西嶺。轉載本文請聯(lián)系勾勾的前端世界公眾號。
這一次,我將帶你一次性搞懂 React 中常見的 setState 原理。
setState 本身的默認行為
在進入主題之前,你肯定需要先學會 React 的基本使用。如果不會,請點贊離開;如果會用 React ,那就點贊收藏后離開(●'?'●)。
我們在使用 React 的時候,經常會用到 state(一句廢話),但是真正能完全搞清楚 setState 的帥哥美女,確實沒幾個。畢竟程序員都不太可能像我一樣博學(和好看)。那么,要搞清楚它,應該去投胎(整容)嗎?
不,你需要先搞清楚 setState 本身的默認行為。
其實也很簡單,我們都知道,setState可以傳遞對象形式的狀態(tài),也可以傳遞函數形式的狀態(tài)。而不論狀態(tài)是對象形式還是函數形式,它都會先將所有狀態(tài)保存起來,然后進行狀態(tài)合并,所有狀態(tài)合并完成后再進行一次性 DOM 更新。
如果狀態(tài)是對象形式,后面的狀態(tài)會直接覆蓋前面的狀態(tài)。類似于 Object.assign() 的合并操作。
對于對象狀態(tài)這一點,我們有請翠花,上代碼:
運行代碼,Dom 中展示的結果為 1。很顯然兩次 setState 只有一次生效了。
真的嗎?其實兩次都有生效,只不過這兩次 setState 在執(zhí)行前,被合并成了一個。你不能說到底是那個生效,你可以說兩個都沒生效,因為最終執(zhí)行的是被合并的那個代碼。
如果狀態(tài)是函數形式,那么依次調用函數進行狀態(tài)累積,所有函數調用完成后, 得到最終狀態(tài),最終進行一次性 DOM 更新。
翠花,再來一段代碼……
明顯不一樣的結果就能說明,兩次都執(zhí)行了,因為函數狀態(tài)并不會合并,而是以此運行。
好了,翠花可以先下去休息了,前置只是我們已經梳理完了,那么,對于 setState 的研究就結束了嗎?當然不是,接下來,讓我們換個場子,繼續(xù)掰滔(battle)。
setState 同步 OR 異步
在面試場景中,只要和 React 相關,面試官一定舔著臉問你:“ 寶子,setState 是同步還是異步的呀 ” 。
面對這樣的無恥刁難,我們需要先明確,從 API 層面上說,它就是普通的調用執(zhí)行的函數,自然是同步 API 。
因此,這里所說的同步和異步指的是 API 調用后更新 DOM 是同步還是異步的。
來,我們有請娜塔莎,上代碼……
果然,洋妹子端上來的代碼確實不好消化,通過結果我們發(fā)現,非常奇怪的一個現象:
第一次事件執(zhí)行顯然為異步的,先打印了兩個 0,Dom 隨之改變?yōu)?1 ;
第二次同樣是異步的,但是我們發(fā)現多次執(zhí)行沒效果 (異步?);
而第三次又是同步執(zhí)行的了;
這什么情況,洋妹子給我們下了迷藥嗎?看我葵花寶典戳破它。
先說結論,首先,同步和異步主要取決于它被調用的環(huán)境。
- 如果 setState 在 React 能夠控制的范圍被調用,它就是異步的。
比如合成事件處理函數, 生命周期函數, 此時會進行批量更新, 也就是將狀態(tài)合并后再進行 DOM 更新。
- 如果 setState 在原生 JavaScript 控制的范圍被調用,它就是同步的。
比如原生事件處理函數中, 定時器回調函數中, Ajax 回調函數中, 此時 setState 被調用后會立即更新 DOM 。
為什么會這樣呢?
其實,我們看到的所謂的 “異步”,是開啟了 “批量更新” 模式的。
批量更新模式可以減少真實 DOM 渲染的次數,所以只要是 React 能夠控制的范圍,出于性能因素考慮,一定是批量更新模式。批量更新會先合并狀態(tài),再一次性做 DOM 更新。
那么假設沒有批量更新呢?
從生命周期的角度來看,每一次的 setState 都是一個完整的更新流程,這里面就包含了重新渲染 (re-render) 在內的很多操作,大體的流程如下:
- shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate;
re-render 本身涉及對 DOM 的操作,它會帶來較大的性能開銷。假如說 “一次 setState 就觸發(fā)一個完整的更新流程” 這個結論成立,那么每一次 setState的調用都會觸發(fā)一次 re-render,我們的視圖很可能沒刷新幾次就卡死了,渲染就會出現下面這樣的流程:
因此,setState 異步(或者說是批量更新)的一個重要動機就是避免頻繁的 re-render。
在實際的 React 運行時中,setState 異步的實現方式有點類似于瀏覽器里的 Event-Loop:
每來一個setState,就把它塞進一個隊列里。等時機成熟,再把隊列里的 state 結果做合并,最后只針對最新的 state 值走一次更新流程。
這個過程,叫作“批量更新”,批量更新的過程正如下面代碼中的箭頭流程圖所示:
只要我們的同步代碼還在執(zhí)行,“進隊列” 這個動作就不會停止。因此就算我們在React 中寫了一個 N 次的 setState 循環(huán),也只是會增加 state 任務入隊的次數,并不會帶來頻繁的 re-render。當 N 次調用結束后,僅僅是 state 的任務隊列內容發(fā)生了變化, state 本身并不會立刻改變。
為了更好地讓你吃下娜塔莎,哦不對,是娜塔莎端上來的美食,我?guī)湍闶崂砹?setState 的執(zhí)行流程圖:
當然,你可能看不懂這個流程圖(是有多笨啊),沒關系,下面還會有的。
如果為非批量更新模式,調用多少次 setState 就會渲染多少次真實 DOM,性能較低。
但是我們在某些條件下需要對 JS 控制的區(qū)域實現批量更新 ( 異步更新 DOM ) ,那應該怎么做呢?
強制批量更新
其實很簡單,我都不好意思說 so easy ,因為這玩意簡直就是 so TM 的 easy 。
我們只需要將代碼包裹在 unstable_batchedUpdates 方法的回調函數中就可以實現強制批量更新。
具體使用方式也很簡單,從 react-dom 中引入進來,然后將代碼放入調用函數中就可以了。
(翠花和娜塔莎結婚了,我來給大家上代碼)
截止到現在,我們成就了一對完美的愛情,啊,呸~