經過觀察調用棧和其餘博客的介紹親身體驗下setState過程當中fiber幹了什麼事情node
下圖是一個典型的create-react-app建立的項目,其中Text.js是我新增的子組件,在App.js中引用到。 react
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:segmentfault
import React from 'react'
export const Text = (props) => {
return (
<span onClick={() => props.updateState()}>{props.tab}</span>
)
}
複製代碼
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使用的對象。bash
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) {}
};
複製代碼
下面按步驟詳細看看這個函數內部的執行流程。app
獲取fiber:key === instance,fiber很重要,記錄了不少有用的信息,好比當前組件實例的各類屬性和狀態、優先級、標識等。異步
function get(key) {
return key._reactInternalFiber;
}
複製代碼
也許你會很好奇fiber長什麼樣,圖上展現的是組件實例上注入的fiber數據結構。 ide
計算到期時間:也就是計算當前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點:
根據調用棧,咱們看到了setState函數的執行過程,可是此時並無在瀏覽器上看到更新,由於具體的調度工做仍是要依靠react的核心算法去執行,updater只是將fiber更新到隊列中,和肯定了更新的優先級。
後面要經歷react的事件合成,Diff算法,虛擬DOM解析,生命週期執行等過程。很是多的代碼,要是一一解釋,能夠寫一本書出來了。
之後要是有時間,能夠將後半部分關於setState裏的() => ({tab: 'Bye Bye'})是如何更新的說說。 一切都要從createWorkInProgress(current, pendingProps, expirationTime)開始提及。
不管是國內哪位大神的博客,只要是介紹fiber的,萬變不離其宗,看國外的這篇文章:fiber詳解
Bye Bye,各位。