目的
通過觀察呼叫棧和其他部落格的介紹親身體驗下setState過程中fiber幹了什麼事情
1、create-react-app建立一個demo
下圖是一個典型的create-react-app建立的專案,其中Text.js是我新增的子元件,在App.js中引用到。
2、修改App.js檔案和新增Text.js檔案
App.js:
import React, { Component } from 'react';
import { Text } from './Text'
class App extends Component {
constructor(props) {
super(props)
this.state = {
tab: 'Welcome to React'
}
}
updateState = () => {
this.setState(() => ({tab: 'Bye Bye'}))
}
render() {
return (
<div className="App">
<Text tab={this.state.tab} updateState={this.updateState} />
</div>
);
}
}
export default App;
複製程式碼
Text.js:
import React from 'react'
export const Text = (props) => {
return (
<span onClick={() => props.updateState()}>{props.tab}</span>
)
}
複製程式碼
3、執行setState
state.tab的初始值是'Welcome to React',在setState中,傳入了一個箭頭函式,去更新state.tab的值為'Bye Bye',
//partialState為() => ({tab: ''Bye Bye}),callback沒有傳入
Component.prototype.setState = function (partialState, callback) {
//this為當前元件的例項
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製程式碼
接著,執行了updater上的enqueueSetState方法,每一個例項都會有一個updater(更新器),updater的作用在下面介紹,在當前App元件例項中,_reactInternalFiber是當前元件的fiber,而_reactInternalInstance是在react15使用的物件。
4、updater更新器
updater有3個方法,只用關心enqueueSetState
var updater = {
isMounted: isMounted,
/*
* instance: 上一步傳入的App元件例項,
* partialState:需要執行更新的箭頭函式,
* callback:undefined
*/
enqueueSetState: function (instance, partialState, callback) {
//獲取到當前例項上的fiber
var fiber = get(instance);
//計算當前fiber的到期時間(優先順序)
var expirationTime = computeExpirationForFiber(fiber);
//一次更新需要的配置引數
var update = {
expirationTime: expirationTime, //優先順序
partialState: partialState, //更新的state,通常是函式而不推薦物件寫法
callback: callback, //更新之後執行的回撥函式
isReplace: false, //
isForced: false, //是否強制更新
capturedValue: null, //捕獲的值
next: null, //
};
//將update上需要更新的資訊新增到fiber中
insertUpdateIntoFiber(fiber, update);
//排程器排程fiber任務
scheduleWork(fiber, expirationTime);
},
//替換更新state,不關注
enqueueReplaceState: function (instance, state, callback) {},
//執行強制更新state,不關注
enqueueForceUpdate: function (instance, callback) {}
};
複製程式碼
下面按步驟詳細看看這個函式內部的執行流程。
獲取fiber:key === instance,fiber很重要,記錄了很多有用的資訊,比如當前元件例項的各種屬性和狀態、優先順序、標識等。
function get(key) {
return key._reactInternalFiber;
}
複製程式碼
也許你會很好奇fiber長什麼樣,圖上展示的是元件例項上注入的fiber資料結構。
計算到期時間:也就是計算當前fiber任務的優先順序,從程式碼上看需要判斷的條件比較多,既可以是非同步更新,也可以是同步更新。在當前測試中,進入的是同步更新的流程。而同步對應的優先順序就是1,所以expirationTime = 1。
//用來計算fiber的到期時間,到期時間用來表示任務的優先順序。
function computeExpirationForFiber(fiber) {
var expirationTime = void 0;
if (expirationContext !== NoWork) {
//
expirationTime = expirationContext;
} else if (isWorking) {
if (isCommitting) {
//同步模式,立即處理任務,預設是1
expirationTime = Sync;
} else {
//渲染階段的更新應該與正在渲染的工作同時過期。
expirationTime = nextRenderExpirationTime;
}
} else {
//沒有到期時間的情況下,建立一個到期時間
if (fiber.mode & AsyncMode) {
if (isBatchingInteractiveUpdates) {
// 這是一個互動式更新
var currentTime = recalculateCurrentTime();
expirationTime = computeInteractiveExpiration(currentTime);
} else {
// 這是一個非同步更新
var _currentTime = recalculateCurrentTime();
expirationTime = computeAsyncExpiration(_currentTime);
}
} else {
// 這是一個同步更新
expirationTime = Sync;
}
}
if (isBatchingInteractiveUpdates) {
//這是一個互動式的更新。跟蹤最低等待互動過期時間。這允許我們在需要時同步重新整理所有互動更新。
if (lowestPendingInteractiveExpirationTime === NoWork || expirationTime > lowestPendingInteractiveExpirationTime) {
lowestPendingInteractiveExpirationTime = expirationTime;
}
}
return expirationTime;
}
複製程式碼
將update上需要更新的資訊新增到fiber中:這個函式的作用就是把我們在上面通過計算之後得到的update更新到fiber上面,實際操作是物件的賦值,跟合併是一個意思。
function insertUpdateIntoFiber(fiber, update) {
//確保更新佇列存在,不存在則建立
ensureUpdateQueues(fiber);
//上一步已經將q1和q2佇列進行了處理,定義2個區域性變數queue1和queue2來儲存佇列資訊。
var queue1 = q1;
var queue2 = q2;
// 如果只有一個佇列,請將更新新增到該佇列並退出。
if (queue2 === null) {
insertUpdateIntoQueue(queue1, update);
return;
}
// 如果任一佇列為空,我們需要新增到兩個佇列中。
if (queue1.last === null || queue2.last === null) {
//將update的值更新到佇列1和佇列2上,然後退出該函式
insertUpdateIntoQueue(queue1, update);
insertUpdateIntoQueue(queue2, update);
return;
}
// 如果兩個列表都不為空,則由於結構共享,兩個列表的最後更新都是相同的。所以,我們應該只追加到其中一個列表。
insertUpdateIntoQueue(queue1, update);
// 但是我們仍然需要更新queue2的`last`指標。
queue2.last = update;
}
複製程式碼
初始化的時候,fiber中的updateQueue是null,這時候,就要建立createUpdateQueue一個更新佇列。alternate本質上也是fiber,它記錄的是上一次setState操作的fiber,同時alternate又是fiber的一個屬性。
ensureUpdateQueues的作用是確保更新佇列不為null。
var q1 = void 0;
var q2 = void 0;
function ensureUpdateQueues(fiber) {
q1 = q2 = null;
// 我們將至少有一個和最多兩個不同的更新佇列。
//alternate是fiber上的一個屬性,初始化是null,執行了setState的過程中,會將當前的FiberNode儲存到alternate上,下次setState時,就能讀取到,可以用來做狀態回滾。
var alternateFiber = fiber.alternate;
var queue1 = fiber.updateQueue;
if (queue1 === null) {
// 沒有佇列,就建立佇列
queue1 = fiber.updateQueue = createUpdateQueue(null);
}
var queue2 = void 0;
if (alternateFiber !== null) {
queue2 = alternateFiber.updateQueue;
if (queue2 === null) {
queue2 = alternateFiber.updateQueue = createUpdateQueue(null);
}
} else {
queue2 = null;
}
queue2 = queue2 !== queue1 ? queue2 : null;
// 使用模組變數
q1 = queue1;
q2 = queue2;
}
複製程式碼
//將update的值更新到queue中。
function insertUpdateIntoQueue(queue, update) {
// 將更新附加到列表的末尾。
if (queue.last === null) {
// 佇列是空的
queue.first = queue.last = update;
} else {
queue.last.next = update;
queue.last = update;
}
if (queue.expirationTime === NoWork || queue.expirationTime > update.expirationTime) {
queue.expirationTime = update.expirationTime;
}
}
複製程式碼
scheduleWork進行排程:上面的幾個步驟記得做了什麼嗎?拿到元件例項上的fiber,然後通過計算得到優先順序和其他需要更新的fiber屬性,最後更新到fiber上,同時建立了更新佇列。但是react還沒開始幹活是不是,更新佇列有了,fibre也有了,react的大腦該對fiber進行排程了。
排程的邏輯很複雜,因為影響因素太多了,我無法一一列舉,只能根據當前的呼叫棧識別用到的部分。
//傳入2個引數,fiber和優先順序,內部又巢狀了一個函式scheduleWorkImpl,這個函式才是邏輯部分。
function scheduleWork(fiber, expirationTime) {
return scheduleWorkImpl(fiber, expirationTime, false);
}
複製程式碼
請注意當前傳入的fiber是合併了update屬性之後的fiber。
scheduleWorkImpl有寫讓人迷惑,司徒大佬的文章也沒有解釋清楚這個函式,一步步來看的話,recordScheduleUpdate的作用是先判斷當前有沒有正在提交更新或者已經在更新中的任務,應該是等updater執行完後,要用到的一些條件預設。
然後將node = fiber,別糾結為什麼是直接相等,接著執行迴圈,當前node也就是fiber不為空,根據條件,要在迴圈過程中對node清空,清空之後退出函式。那麼,這個清空的過程做了什麼事情呢?
先是判斷node裡面的到期時間是不是等於NoWork,NoWork表示的是0,它表示的是當前沒有在排程中的fiber,然後判斷node的到期時間是不是大於傳入的到期時間,如果滿足條件,就將node的到期時間更新為新傳入的到期時間。
然後判斷alternate不為空的情況下,alternate在沒有執行過setState,通常是初始化的時候是空狀態,當執行過一次setState之後,就會將舊的FiberNode賦值給alternate,下面的函式中,如果alternate不為空,並且expirationTime和上一個if的判斷一致的情況下,就更新alternate中的expirationTime。
上2個條件是更新到期時間的,第3個條件是判斷return是不是等於null,return的含義在完全理解fiber一文中有說到,表示當前的fiber任務向誰提交。在本demo中,當前是第一次執行,所有它的return為null。
function scheduleWorkImpl(fiber, expirationTime, isErrorRecovery) {
//記錄排程的狀態
recordScheduleUpdate();
var node = fiber;
while (node !== null) {
// 將父路徑移到根目錄並更新每個節點的到期時間。
if (node.expirationTime === NoWork || node.expirationTime > expirationTime) {
node.expirationTime = expirationTime;
}
if (node.alternate !== null) {
if (node.alternate.expirationTime === NoWork || node.alternate.expirationTime > expirationTime) {
node.alternate.expirationTime = expirationTime;
}
}
if (node['return'] === null) {
if (node.tag === HostRoot) {
var root = node.stateNode;
if (!isWorking && nextRenderExpirationTime !== NoWork && expirationTime < nextRenderExpirationTime) {
// 是一箇中斷。 (用於效能跟蹤。)
interruptedBy = fiber;
resetStack();
}
if (
// 如果我們處於渲染階段,我們不需要為此更新安排此根目錄,因為我們將在退出之前執行此操作。
!isWorking || isCommitting ||
// ......除非這是與我們渲染的根不同的root。
nextRoot !== root) {
// 將root新增到root排程
requestWork(root, expirationTime);
}
} else {
return;
}
}
node = node['return'];
}
}
複製程式碼
//顧名思義,記錄排程的狀態
function recordScheduleUpdate() {
if (enableUserTimingAPI) {
if (isCommitting) {
//當前是否有正在提交的排程任務,肯定沒有啦。
hasScheduledUpdateInCurrentCommit = true;
}
//currentPhase表示當前執行到哪個生命週期了。
if (currentPhase !== null && currentPhase !== 'componentWillMount' && currentPhase !== 'componentWillReceiveProps') {
//當前是否有排程到某個生命週期階段的任務
hasScheduledUpdateInCurrentPhase = true;
}
}
}
複製程式碼
到這裡為止,updater的函式執行完了,我們總結一下它到底做了什麼事情。一共有4點:
- 找到例項上的fiber
- 計算得到當前fiber的優先順序
- 將要更新的fiber推送到更新佇列
- 根據fiber樹上的優先順序確定更新工作,從當前fiber的return為起點,開始遞迴,直至到達根節點,根節點的return=null。
後續
根據呼叫棧,我們看到了setState函式的執行過程,但是此時並沒有在瀏覽器上看到更新,因為具體的排程工作還是要依靠react的核心演算法去執行,updater只是將fiber更新到佇列中,和確定了更新的優先順序。
後面要經歷react的事件合成,Diff演算法,虛擬DOM解析,生命週期執行等過程。非常多的程式碼,要是一一解釋,可以寫一本書出來了。
以後要是有時間,可以將後半部分關於setState裡的() => ({tab: 'Bye Bye'})是如何更新的說說。 一切都要從createWorkInProgress(current, pendingProps, expirationTime)開始說起。
無論是國內哪位大神的部落格,只要是介紹fiber的,萬變不離其宗,看國外的這篇文章:fiber詳解
Bye Bye,各位。