寫到前面
為什麼是setState,因為對於大家而言,大多數使用react的新手或者初學者,大多會直接接觸到setState,而且這個方法也可能是接觸最多的操作方法。那麼要想詳細瞭解setState究竟在React中做了什麼事情,就需要深入瞭解一下。而在最新的React 16版本中,React的核心渲染框架時進行過一次升級的,由之前的React升級到了React Fiber。(PS:本文針對菜鳥、初級工程師而寫,有錯誤不足之處,請各位大佬指出更正。感覺太low,請繞道,謝謝。)
- 為什麼會升級?
- 為什麼瞭解Fiber?
彆著急,讓我來慢慢給你們解答,在16版本之前,React使用的還是舊版的渲染核心,它的渲染過程是一口氣完成,怎麼理解呢?就是會一次性遍歷你所有的Dom節點,這個過程取決於你的應用的複雜程度。當然,這個過程一般比較快,但是也不排除在大型複雜應用中出現比較長的等待時間,這個時間是基於ms級別的。而作為一個前端工程師,效能優化是比較重要的一方面之一,大家都知道,瀏覽器是的渲染引擎是單執行緒的,這就意味著一個時間段之內只能完成一件事。當你的應用過於複雜時,使用者操作變多,弊端就顯示出來了:卡頓,未響應,甚至是頁面崩潰...這就是為什麼React會升級到React Fiber,在未升級之前,渲染模式是這樣的:
假設你的結構是這樣的 A元件 => B元件 => C/D/E元件 D元件 => F元件 未使用Fiber架構的渲染方式
他的舊版渲染模式是這樣的:
以render()函式為分界線。從頂層元件開始,一直往下,直至最底層子元件。然後再往上。元件update階段同理。一直執行,直到完成,這個過程完全不理你。(我喜歡叫狗不理階段)在升級為Fiber之後,就如同游泳一樣,每個一段時間,都需要上岸呼吸一口氣,所以渲染模式就變成更了以下情況:
潛水員會每隔一段時間就上岸,看是否有更重要的事情要做。
加入fiber的react將元件更新分為兩個階段,Reconcile階段和Commit階段。
- Reconcile階段,在這個階段內,React通過diff演算法,判斷哪些組價需要更新,經需要更新的元件打上tag(標記),再將所有需要更新的元件新增到一個陣列中,等待或者執行更新任務。注意:這個階段是可以被打斷的,也就是說在這個階段內,react檢測到有使用者操作行為,或者是其他的一些事情都會打斷,在事件執行完畢之後在重新將進行此階段,是重新進行。
- Commit階段,這個階段是根據Reconcile階段生成的更新的陣列,遍歷更新DOM,這個階段是一次性執行完畢的,並且是不會被打斷的。
通過這個倆個階段,你就會明白,為什麼之前會把componentWillMount、componentWillReviceProps和componentWillUpdate標記為不安全的生命週期函式了,因為在Reconcile階段,被打斷之後是重新進行的,就有可能造成對此的資料請求,對此渲染,造成不必要的資源、效能浪費(這裡有一個比較有意思飢餓問題,聰明的同學應該已經猜出來了,react現在還沒有公佈解決方法哦)。
Fiber具體是什麼樣的?
Fiber其實是一個物件。在Fiber原始碼中,有這麼一段描述
A Fiber is work on a Component that needs to be done or was done. There can be more than one per component.
Fiber就是通過物件記錄元件上需要做或者已經完成的更新,一個元件可以對應多個Fiber。
接下來讓我們看看Fiber具體是什麼樣子的?既然是一個物件,就肯定是{}模式。如下:
{
tag,
key,
elementType,
type,
stateNode,
return,
child,
sibling,
index,
ref,
pendingProps,
memoizedProps,
updateQueue,
memoizedState,
firstContextDependency,
mode,
effectTag,
nextEffect,
firstEffect,
lastEffect,
expirationTime,
childExpirationTime,
alternate,
actualDuration,
actualStartTime,
selfBaseDuration,
treeBaseDuration
}
複製程式碼
在render函式中建立的React Element樹在第一次渲染的時候會建立一顆結構一模一樣的Fiber節點樹。不同的React Element型別對應不同的Fiber節點型別。一個React Element的工作就由它對應的Fiber節點來負責。
Fiber的優先順序如下:
高優先順序會打斷正在執行的低優先順序任務先執行。
一個React Element可以對應不止一個Fiber,因為Fiber在更新的時候,會從原來的Fiber(current)克隆出一個新的Fiber(alternate)。兩個Fiber diff出的變化(side effect)記錄在alternate上。所以一個元件在更新時最多會有兩個Fiber與其對應,在更新結束後alternate會取代之前的current的成為新的current節點。
這是fiber在目前版本v16.6.3所維護的所有屬性,具體想要了解閱讀原始碼請看這裡。ReactFiber.js
setState
在官方文件中,明確指出,要把state認作是不可變的,所以,現在更推崇的寫法不是直接setState,而是通過setState的回撥函式進行更改。
this.setState(() => {[key]: value});
複製程式碼
好,不說題外話了,讓我們進入今天的正題,setState。 大家寫專案的時候,在index.js檔案中,會引入兩個檔案,react,react-dom。setState在react檔案是這樣的:
熟不熟悉?Conmponent類,在這裡面我們可以看看幹了什麼事情,接受props,context和updater,注意我拿紅線標出來的部分,短路運算,再看看註釋,這個updater是隨後注入進去的。先不管是什麼時候注入進去的,讓我們接著往下看,setState肯定會觸發更新,那我們就沿著this.updater往下走,去尋找ReactNoopUpdateQueue(react空操作更新佇列),很多人會犯嘀咕,都空操作了還要更新什麼?耐心點,這裡的確是不進行任何更新操作,只是驗證一個資料格式,和檢驗舊版V8引擎的一些錯誤,並丟擲來。 這是什麼?setState?幹了什麼?引數校驗,如果通過就執行下面的方法,this指的當前例項。 在enqueueSetState方法中,也是例項驗證。驗證例項是否mounted。在你的應用第一次渲染的時候,最主要的是關注react-dom的進行,前面說過updater是隨後注入進去的,就是在react-dom載入的時候注入進去的。接下來,setState帶大家去看看究竟是什麼?
直接來看setState佇列,這裡需要3個引數可以看到分別是例項物件,載荷和回撥函式。在這裡我們先看在最開始生命4個變數分別是幹什麼用的,直接語義化就能猜出個大概來。
Q1:fiber通過get方法獲取一些東西?
A1: 可以看到,原始碼實現的方法,獲再結合當前呼叫方法的上下文可以得知,當前的fiber獲取到時當前例項上的一個_reactInternalFiber的值。這個值是什麼,其實是通過相應的一個set方法,將當前例項和workInProgress傳入,並給賦值給當前例項的_reactInternalFiber屬性。
Q2:currentTime獲取當前的時間?
A2:- 首先判斷是否正在渲染中,是的話就返回最近一次的排程時間
- 如果不在渲染中的話,會檢查是否有上次遺留的待處理的工作。
- 如果nextFlushedExpirationTime === NoWork || nextFlushedExpirationTime === Never,來判斷優先順序。
- 重新計算當前的渲染時作為排程時間,並且return;
- 如果上次有遺留,則直接返回當前排程時間。
- rederingTime 可以隨時更新,currentSechedulerTime只有在沒有新任務的時候才更新
Q3:expirationTime獲取到期時間?什麼鬼?
A3:- 在此時,會進入第一個if條件判斷,通過判斷當前是否存在正在執行的上下文時間,是否正在進行渲染,還是其他情況。
- 如果存在expirationContext,則到期時間就是修改為當前的上下文執行時間。
- 如果正在排程時間的話,判斷是否處於commit階段,是的話就設定為同步優先順序,否則的話就賦值為下次渲染到期時間。
- 如果上述情況都不滿足的情況下,就會計算當前例項fiber的優先順序。
- 這裡分為非同步和同步,分別呼叫不同的方法進行計算,獲得優先順序後則和同步更新一樣, 建立update並放進佇列, 然後呼叫sheuduleWork
- 在這裡還會有互動式重新整理的判斷,是追蹤最短待處理的互動式到期時間。 這允許我們在需要時同步重新整理所有互動式更新。
- 最後返回當前所需要的到期時間。
- 此步驟和2步驟可以合併為計算優先順序
Q4:update建立update佇列?
A4: 這個階段就是通過createUpdate來建立一個更新物件。
在進行了一系列不可描述的過程之後,終於可以進行接下來的操作了。
首先呼叫flushPassiveEffects()來進行重新整理,將被動影響的屬性重新整理一遍,接著是重頭戲,呼叫enqueueUpdate()方法,將需要更新的fiber放入更新佇列。 這裡其實就是這麼個原理:第一部分
- 首先判斷是不是隻有一個fiber,只有一個fiber的話就讓q1等於這個值,然後q2克隆q1
- 如果是有倆個fiber,則q1等於當前例項的fiber.updateQueue,q2就等於alternate.updateQueue;
- 如果兩個fiber都沒有更新佇列。則q1,q2都建立新的。
- 只有一個fiber有更新佇列。克隆以建立一個新的。
- 倆個fiber都有更新佇列。總之就是,q1和q2都需要有一個fiber。
第二部分
- 當q1與q2是相等時,一位置實際上只有一個fiber,將此fiber插入到更新佇列;
- 若q1和q2有一個是非空佇列,則兩個對列都需要更新;
- 當q1和q2兩個佇列都是非空,由於結構共享,兩個列表中的最後一次更新是相同的。因此,只需q1新增到更新佇列即可;
- 最後將q2的lastUpdate指標更新。
最後一步,就是掉用scheduleWork()方法,來進行最後的更新。在此方法中會根據優先順序進行分片式更新。
- 首先呼叫scheduleWorkToRoot()方法,更新fiber的優先順序,遍歷到根元件的父級路徑,並更新子元件的優先順序。
- 為先前未計劃的互動更新掛起的非同步工作計數。
- 更新當前互動的掛起的非同步工作計數。
- 監聽更新列表的變化,返回root。
接下來,在commit階段,一口氣執行完畢。你的DOM就是最新的了。說了這麼多,可能執行起來,就是短短的幾十毫秒... 就比如下面
至此,setState整個過程算是完成了。
總結:這篇文章是鄙人第一次下手書寫,有些地方可能表述不是很準確,可能有點囉嗦,但是我喜歡啊。俗話說萬事開頭難,但是過程也難啊,結果更難啊。對於程式碼也一樣,要堅持下去,堅持下去你就得頸椎病了哦。本文有什麼錯誤的地方,還煩請各路大神指出,鄙人是不會改滴,都會記在心裡噠,上述是我對setState的理解,拋磚引玉,希望幫助大家有方向的去了解react原理機制。