天天看點

面試官:“年輕人,我看你很懂setState原理,你來說說是同步還是異步的?”

這一次,我将帶你一次性搞懂 React 中常見的 setState 原理。

面試官:“年輕人,我看你很懂setState原理,你來說說是同步還是異步的?”

setState 本身的預設行為

在進入主題之前,你肯定需要先學會 React 的基本使用。如果不會,請點贊離開;如果會用 React ,那就點贊收藏後離開(●'◡'●)。

我們在使用 React 的時候,經常會用到 state(一句廢話),但是真正能完全搞清楚 setState 的帥哥美女,确實沒幾個。畢竟程式員都不太可能像我一樣博學(和好看)。那麼,要搞清楚它,應該去投胎(整容)嗎?

不,你需要先搞清楚 setState 本身的預設行為。

其實也很簡單,我們都知道,setState可以傳遞對象形式的狀态,也可以傳遞函數形式的狀态。而不論狀态是對象形式還是函數形式,它都會先将所有狀态儲存起來,然後進行狀态合并,所有狀态合并完成後再進行一次性 DOM 更新。

如果狀态是對象形式,後面的狀态會直接覆寫前面的狀态。類似于 Object.assign() 的合并操作。

對于對象狀态這一點,我們有請翠花,上代碼:

面試官:“年輕人,我看你很懂setState原理,你來說說是同步還是異步的?”

運作代碼,Dom 中展示的結果為 1。很顯然兩次 setState 隻有一次生效了。

真的嗎?其實兩次都有生效,隻不過這兩次 setState 在執行前,被合并成了一個。你不能說到底是那個生效,你可以說兩個都沒生效,因為最終執行的是被合并的那個代碼。

如果狀态是函數形式,那麼依次調用函數進行狀态累積,所有函數調用完成後, 得到最終狀态,最終進行一次性 DOM 更新。

翠花,再來一段代碼……

面試官:“年輕人,我看你很懂setState原理,你來說說是同步還是異步的?”

明顯不一樣的結果就能說明,兩次都執行了,因為函數狀态并不會合并,而是以此運作。

好了,翠花可以先下去休息了,前置隻是我們已經梳理完了,那麼,對于 setState 的研究就結束了嗎?當然不是,接下來,讓我們換個場子,繼續掰滔(battle)。

setState 同步 OR 異步

在面試場景中,隻要和 React 相關,面試官一定會舔着臉問你:“ 寶子,setState 是同步還是異步的呀?” 。

面對這樣的無恥刁難,我們需要先明确,從 API 層面上說,它就是普通的調用執行的函數,自然是同步 API 。

是以,這裡所說的同步和異步指的是 API 調用後更新 DOM 是同步還是異步的。

來,我們有請娜塔莎,上代碼……

面試官:“年輕人,我看你很懂setState原理,你來說說是同步還是異步的?”

果然,洋妹子端上來的代碼确實不好消化,通過結果我們發現,非常奇怪的一個現象:

  • 第一次事件執行顯然為異步的,先列印了兩個 0,Dom 随之改變為 1 ;
  • 第二次同樣是異步的,但是我們發現多次執行沒效果 (異步?);
  • 而第三次又是同步執行的了;

這什麼情況,洋妹子給我們下了迷藥嗎?看我葵花寶典戳破它。

先說結論,首先,同步和異步主要取決于它被調用的環境。

  • 如果 setState 在 React 能夠控制的範圍被調用,它就是異步的。

比如合成事件處理函數, 生命周期函數, 此時會進行批量更新, 也就是将狀态合并後再進行 DOM 更新。

  • 如果 setState 在原生 JavaScript 控制的範圍被調用,它就是同步的。

比如原生事件處理函數中, 定時器回調函數中, Ajax 回調函數中, 此時 setState 被調用後會立即更新 DOM 。

為什麼會這樣呢?

其實,我們看到的所謂的 “異步”,是開啟了 “批量更新” 模式的。

批量更新模式可以減少真實 DOM 渲染的次數,是以隻要是 React 能夠控制的範圍,出于性能因素考慮,一定是批量更新模式。批量更新會先合并狀态,再一次性做 DOM 更新。

那麼假設沒有批量更新呢?

從生命周期的角度來看,每一次的 setState 都是一個完整的更新流程,這裡面就包含了重新渲染 (re-render) 在内的很多操作,大體的流程如下:

shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate;

re-render 本身涉及對 DOM 的操作,它會帶來較大的性能開銷。假如說 “一次 setState 就觸發一個完整的更新流程” 這個結論成立,那麼每一次 setState的調用都會觸發一次 re-render,我們的視圖很可能沒重新整理幾次就卡死了,渲染就會出現下面這樣的流程:

面試官:“年輕人,我看你很懂setState原理,你來說說是同步還是異步的?”

是以,setState 異步(或者說是批量更新)的一個重要動機就是避免頻繁的 re-render。

在實際的 React 運作時中,setState 異步的實作方式有點類似于浏覽器裡的 Event-Loop:

每來一個setState,就把它塞進一個隊列裡。等時機成熟,再把隊列裡的 state 結果做合并,最後隻針對最新的 state 值走一次更新流程。

這個過程,叫作“批量更新”,批量更新的過程正如下面代碼中的箭頭流程圖所示:

面試官:“年輕人,我看你很懂setState原理,你來說說是同步還是異步的?”

隻要我們的同步代碼還在執行,“進隊列” 這個動作就不會停止。是以就算我們在React 中寫了一個 N 次的 setState 循環,也隻是會增加 state 任務入隊的次數,并不會帶來頻繁的 re-render。當 N 次調用結束後,僅僅是 state 的任務隊列内容發生了變化, state 本身并不會立刻改變。

為了更好地讓你吃下娜塔莎,哦不對,是娜塔莎端上來的美食,我幫你梳理了 setState 的執行流程圖:

面試官:“年輕人,我看你很懂setState原理,你來說說是同步還是異步的?”

當然,你可能看不懂這個流程圖(是有多笨啊),沒關系,下面還會有的。

如果為非批量更新模式,調用多少次 setState 就會渲染多少次真實 DOM,性能較低。

但是我們在某些條件下需要對 JS 控制的區域實作批量更新 ( 異步更新 DOM ) ,那應該怎麼做呢?

強制批量更新

其實很簡單,我都不好意思說 so easy ,因為這玩意簡直就是 so TM 的 easy 。

我們隻需要将代碼包裹在 unstable_batchedUpdates 方法的回調函數中就可以實作強制批量更新。

具體使用方式也很簡單,從 react-dom 中引入進來,然後将代碼放入調用函數中就可以了。

(翠花和娜塔莎結婚了,我來給大家上代碼)

面試官:“年輕人,我看你很懂setState原理,你來說說是同步還是異步的?”

截止到現在,我們成就了一對完美的愛情,啊,呸~

我們基本搞清楚了原理流程,那具體的代碼是如何實作的呢?

在 setState 的調用中,有一個合成事件起到了關鍵性的作用。接下來,我們先去搞清楚這個小可愛,再來看具體的 setState 的代碼實作。

合成事件

首先明确定義,在 React 中為元素添加的事件被叫做合成事件。

合成事件的好處有兩個:

  • 一是屏蔽了浏覽器之間關于事件處理的相容性問題,為合成事件對象内部提供了統一的 API;
  • 二是性能的提升, 事件都被委托給 document 。

React 并不會将事件添加到真正的 DOM 元素身上,它會将所有事件委托給 document 執行。如下圖所示:

面試官:“年輕人,我看你很懂setState原理,你來說說是同步還是異步的?”

React 會在擁有事件的 DOM 對象身上添加一個 store 對象,在 store 對象中存儲事件名稱及事件處理函數,然後通過 document 分發事件。

當事件被觸發後,通過擷取事件源對象,檢視事件源對象中是否存在 store 對象,擷取 store 對象中事件處理函數,執行事件處理函數。

合成事件的事件對象在使用完成以後會被銷毀。我長得帥,是以,我寫了一段模拟性的代碼,你看不看就随意了:

面試官:“年輕人,我看你很懂setState原理,你來說說是同步還是異步的?”

代碼的注釋中說的已經非常清楚了,愛看不看吧,就這樣……

setState 實作原理

接着,我們再把上面的圖拿出來,我分為了四段,進行了具體的梳理。先看圖,再看字,最後上代碼 👇

面試官:“年輕人,我看你很懂setState原理,你來說說是同步還是異步的?”

1、當 setState 方法被調用後,方法會将狀态傳遞給元件更新器,讓元件更新器将狀态臨時存儲起來。每個元件都會有自己的元件更新器,當需要更新元件時調用元件更新器。

2、狀态臨時儲存完成後判斷目前是否為批量更新模式,如果是,将元件更新器添加到更新隊列中;如果不是,直接更新元件。

批量更新模式是如何設定的:當觸發合成事件時, 在事件處理函數執行之前,會先将批量更新模式設定為 true,然後執行事件處理函數收集狀态。當事件處理函數執行完成後,執行批量更新操作,即從更新隊列中擷取元件更新器并調用。元件更新器調用完成後再将批量更新模式設定為 false。

3、更新元件時,先判斷是否有狀态需要更新,如果有就先計算最新狀态,将得出的最新狀态重新設定給元件。

計算狀态時,如果狀态是函數類型,調用函數傳入目前狀态,傳回最新狀态。如果狀态是對象類型,使用對象狀态覆寫原有狀态。

4、元件狀态計算完成後,通過調用元件内部的 render 方法擷取新的 VirtualDOM,再通過 DOM 對象擷取舊的虛拟 DOM,然後調用 diff 方法進行比對,對比完成後将差異更新到真實 DOM 對象中。

下面的代碼,就是配合前面的流程圖和文字描述實作的具體代碼了。我看你也挺聰明的,是以加了非常詳細的注釋,就是為了遏制你的智力增長,如果還看不懂,那就算了吧,建議你去和娜塔莎搶婚,單身程式員這條路可能不适合你喲 (●'◡'●)。

面試官:“年輕人,我看你很懂setState原理,你來說說是同步還是異步的?”

好了,就到這裡吧。沒想到你竟然真的沒看代碼,就知道往下拖進度條,哎,放棄吧,翠花是娜塔莎的,不可以插足!

當然,如果能點贊,我可以考慮送你一隻翠花┗|`O′|┛。

本文作者:北瑤

繼續閱讀