前情提要
上一篇我們提到如果 setState 之后,虛擬 dom diff 比較耗時(shí),那么導(dǎo)致瀏覽器 FPS 降低,使得用戶(hù)覺(jué)得頁(yè)面卡頓。那么 react 新的調(diào)度算法就是把原本一次 diff 的過(guò)程切分到各個(gè)幀去執(zhí)行,使得瀏覽器在 diff 過(guò)程中也能響應(yīng)用戶(hù)事件。接下來(lái)我們具體分析下新的調(diào)度算法是怎么回事。
原虛擬DOM問(wèn)題
假設(shè)我們有一個(gè) react 應(yīng)用如下:
class App extends React.Component {
render() {
return (
<div>
<div>{this.props.name}</div>
<ul>
<li>{this.props.items[0]}</li>
<li>{this.props.items[1]}</li>
</ul>
</div>
);
}
}
整個(gè) app 的虛擬 dom 大致是這樣的:
var rootHost = {
type: 'div',
children: [ {
type: 'div',
children: [ {type: 'text'} ]
}. {
type: 'ul',
children: [
{ type: 'li', children:[ {type: 'text'} ] },
{ type: 'li', children:[ {type: 'text'} ] }
]
} ]
}
當(dāng)更新發(fā)生 diff 兩棵新老虛擬 dom 樹(shù)的時(shí)候是遞歸的逐層比較(如下圖)。這個(gè)過(guò)程是一次完成的,如果要按上一篇我們說(shuō)的把 diff 過(guò)程切割成好多時(shí)間片來(lái)執(zhí)行,難度是如何記住狀態(tài)且恢復(fù)現(xiàn)場(chǎng)。譬如說(shuō)你 diff 到一半函數(shù)返回了,等下一個(gè)時(shí)間片繼續(xù) diff。如果只記住上次遞歸到哪個(gè)節(jié)點(diǎn),那么你只能順著他的 children 繼續(xù) diff,而它的兄弟節(jié)點(diǎn)就丟失了。如果要完美恢復(fù)現(xiàn)場(chǎng)保存的結(jié)構(gòu)估計(jì)得挺復(fù)雜。所以 react16 改造了虛擬dom的結(jié)構(gòu),引入了 fiber 的鏈表結(jié)構(gòu)。

現(xiàn)在解決方案 - fiber
fiber 節(jié)點(diǎn)相當(dāng)于以前的虛擬 dom 節(jié)點(diǎn),結(jié)構(gòu)如下:
const Fiber = {
tag: HOST_COMPONENT,
type: "div",
return: parentFiber,
child: childFiber,
sibling: null,
alternate: currentFiber,
stateNode: document.createElement("div")| instance,
props: { children: [], className: "foo"},
partialState: null,
effectTag: PLACEMENT,
effects: []
};
先講重要的幾個(gè)屬性: return 存儲(chǔ)的是當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)(元素),child 存儲(chǔ)的是第一個(gè)子節(jié)點(diǎn)(元素),sibling 存儲(chǔ)的是他右邊第一個(gè)的兄弟節(jié)點(diǎn)(元素)。alternate 保存是當(dāng)更新發(fā)生時(shí)候同一個(gè)節(jié)點(diǎn)帶有新的 props 和 state 生成的新 fiber 節(jié)點(diǎn)。 那么虛擬 dom 的存儲(chǔ)結(jié)構(gòu)用鏈表的形式描述了整棵樹(shù)。

從頂層開(kāi)始左序深度優(yōu)先遍歷如下圖所示:

我們?cè)诒闅v dom 樹(shù) diff 的時(shí)候,即使中斷了,我們只需要記住中斷時(shí)候的那么一個(gè)節(jié)點(diǎn),就可以在下個(gè)時(shí)間片恢復(fù)繼續(xù)遍歷并 diff。這就是 fiber 數(shù)據(jù)結(jié)構(gòu)選用鏈表的一大好處。我先用文字大致描述下 fiber diff 算法的過(guò)程再來(lái)看代碼。從跟節(jié)點(diǎn)開(kāi)始遍歷,碰到一個(gè)節(jié)點(diǎn)和 alternate 比較并記錄下需要更新的東西,并把這些更新提交到當(dāng)前節(jié)點(diǎn)的父親。當(dāng)遍歷完這顆樹(shù)的時(shí)候,再通過(guò) return 回溯到根節(jié)點(diǎn)。這個(gè)過(guò)程中把所有的更新全部帶到根節(jié)點(diǎn),再一次更新到真實(shí)的 dom 中去。

從根節(jié)點(diǎn)開(kāi)始:
- div1 通過(guò) child 到 div2。
- div2 和自己的 alternate 比較完把更新 commit1 通過(guò) return 提交到 div1。
- div2 通過(guò) sibling 到 ul1。
- ul1 和自己的 alternate 比較完把更新 commit2 通過(guò) return 提交到 div1。
- ul1 通過(guò) child 到 li1。
- li1 和自己的 alternate 比較完把更新 commit3 通過(guò) return 提交到 ul1。
- li1 通過(guò) sibling 到 li2。
- li2 和自己的 alternate 比較完把更新 commit4 通過(guò) return 提交到 ul1。
- 遍歷完整棵樹(shù)開(kāi)始回溯,li2 通過(guò) return 回到 ul1。
- 把 commit3 和 commit4 通過(guò) return 提交到 div1。
- ul1 通過(guò) return 回到 div1。
- 獲取到所有更新 commit1-4,一次更新到真是的 dom 中去。
使用fiber算法更新的代碼實(shí)現(xiàn)
React.Component.prototype.setState = function( partialState, callback ) {
updateQueue.pus( {
stateNode: this,
partialState: partialState
} );
requestIdleCallback(performWork); // 這里就開(kāi)始干活了
}
function performWork(deadline) {
workLoop(deadline)
if (nextUnitOfWork || updateQueue.length > 0) {
requestIdleCallback(performWork) //繼續(xù)干
}
}
setState 先把此次更新放到更新隊(duì)列 updateQueue 里面,然后調(diào)用調(diào)度器開(kāi)始做更新任務(wù)。performWork 先調(diào)用 workLoop 對(duì) fiber 樹(shù)進(jìn)行遍歷比較,就是我們上面提到的遍歷過(guò)程。當(dāng)此次時(shí)間片時(shí)間不夠遍歷完整個(gè) fiber 樹(shù),或者遍歷并比較完之后 workLoop 函數(shù)結(jié)束。接下來(lái)我們判斷下 fiber 樹(shù)是否遍歷完或者更新隊(duì)列 updateQueue 是否還有待更新的任務(wù)。如果有則調(diào)用 requestIdleCallback 在下個(gè)時(shí)間片繼續(xù)干活。nextUnitOfWork 是個(gè)全局變量,記錄 workLoop 遍歷 fiber 樹(shù)中斷在哪個(gè)節(jié)點(diǎn)。
function workLoop(deadline) {
if (!nextUnitOfWork) {
//一個(gè)周期內(nèi)只創(chuàng)建一次
nextUnitOfWork = createWorkInProgress(updateQueue)
}
while (nextUnitOfWork && deadline.timeRemaining() > EXPIRATION_TIME) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
if (pendingCommit) {
//當(dāng)全局 pendingCommit 變量被負(fù)值
commitAllwork(pendingCommit)
}
}
剛開(kāi)始遍歷的時(shí)候判斷全局變量 nextUnitOfWork 是否存在?如果存在表示上次任務(wù)中斷了,我們繼續(xù),如果不存在我們就從更新隊(duì)列里面取第一個(gè)任務(wù),并生成對(duì)應(yīng)的 fiber 根節(jié)點(diǎn)。接下來(lái)我們就是正式的工作了,用循環(huán)從某個(gè)節(jié)點(diǎn)開(kāi)始遍歷 fiber 樹(shù)。performUnitOfWork 根據(jù)我們上面提到的遍歷規(guī)則,在對(duì)當(dāng)前節(jié)點(diǎn)處理完之后,返回下一個(gè)需要遍歷的節(jié)點(diǎn)。循環(huán)除了要判斷是否有下一個(gè)節(jié)點(diǎn)(是否遍歷完),還要判斷當(dāng)前給你的時(shí)間是否用完,如果用完了則需要返回,讓瀏覽器響應(yīng)用戶(hù)的交互事件,然后再在下個(gè)時(shí)間片繼續(xù)。workLoop 最后一步判斷全局變量 pendingCommit 是否存在,如果存在則把這次遍歷 fiber 樹(shù)產(chǎn)生的所有更新一次更新到真實(shí)的 dom 上去。注意 pendingCommit 在完成一次完整的遍歷過(guò)程之前是不會(huì)有值的。
function createWorkInProgress(updateQueue) {
const updateTask = updateQueue.shift()
if (!updateTask) return
if (updateTask.partialState) {
// 證明這是一個(gè)setState操作
updateTask.stateNode._internalfiber.partialState = updateTask.partialState
}
const rootFiber =
updateTask.fromTag === tag.HostRoot
? updateTask.stateNode._rootContainerFiber
: getRoot(updateTask.stateNode._internalfiber)
return {
tag: tag.HostRoot,
stateNode: updateTask.stateNode,
props: updateTask.props || rootFiber.props,
alternate: rootFiber // 用于鏈接新舊的 VDOM
}
}
function getRoot(fiber) {
let _fiber = fiber
while (_fiber.return) {
_fiber = _fiber.return
}
return _fiber
}
createWorkInProgress 拿出更新隊(duì)列 updateQueue 第一個(gè)任務(wù),然后看觸發(fā)這個(gè)任務(wù)的節(jié)點(diǎn)是什么類(lèi)型。如果不是根節(jié)點(diǎn),則通過(guò)循環(huán)迭代節(jié)點(diǎn)的 return 找到最上層的根節(jié)點(diǎn)。最后生成一個(gè)新的 fiber 節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)就是當(dāng)前 fiber 節(jié)點(diǎn)的 alternate 指向的,也就是說(shuō)下面會(huì)在當(dāng)前節(jié)點(diǎn)和這個(gè)新生成的節(jié)點(diǎn)直接進(jìn)行 diff。
function performUnitOfWork(workInProgress) {
const nextChild = beginWork(workInProgress)
if (nextChild) return nextChild
// 沒(méi)有 nextChild, 我們看看這個(gè)節(jié)點(diǎn)有沒(méi)有 sibling
let current = workInProgress
while (current) {
//收集當(dāng)前節(jié)點(diǎn)的effect,然后向上傳遞
completeWork(current)
if (current.sibling) return current.sibling
//沒(méi)有 sibling,回到這個(gè)節(jié)點(diǎn)的父親,看看有沒(méi)有sibling
current = current.return
}
}
performUnitOfWork 做的工作是 diff 當(dāng)前節(jié)點(diǎn),diff 完看看有沒(méi)有子節(jié)點(diǎn),如果沒(méi)有子節(jié)點(diǎn)則把更新先提交到父節(jié)點(diǎn)。然后再看有沒(méi)有兄弟節(jié)點(diǎn),如果有則返回出去當(dāng)作下次遍歷的節(jié)點(diǎn)。如果還是沒(méi)有,說(shuō)明整個(gè) fiber 樹(shù)已經(jīng)遍歷完了,則進(jìn)入到回溯過(guò)程,把所有的更新都集中到根節(jié)點(diǎn)進(jìn)行更新真實(shí) dom。
function completeWork(currentFiber) {
if (currentFiber.tag === tag.classComponent) {
// 用于回溯最高點(diǎn)的 root
currentFiber.stateNode._internalfiber = currentFiber
}
if (currentFiber.return) {
const currentEffect = currentFiber.effects || [] //收集當(dāng)前節(jié)點(diǎn)的 effect list
const currentEffectTag = currentFiber.effectTag ? [currentFiber] : []
const parentEffects = currentFiber.return.effects || []
currentFiber.return.effects = parentEffects.concat(currentEffect, currentEffectTag)
} else {
// 到達(dá)最頂端了
pendingCommit = currentFiber
}
}
我們看到 completeWork 中當(dāng)判斷到當(dāng)前節(jié)點(diǎn)是根節(jié)點(diǎn)的時(shí)候才賦值 pendingCommit 整個(gè)全局變量。
function commitAllwork(topFiber) {
topFiber.effects.forEach(f => {
commitWork(f)
})
topFiber.stateNode._rootContainerFiber = topFiber
topFiber.effects = []
nextUnitOfWork = null
pendingCommit = null
}
當(dāng)回溯完,有了 pendingCommit,則 commitAllwork 會(huì)被調(diào)用。它做的工作就是循環(huán)遍歷根節(jié)點(diǎn)的 effets 數(shù)據(jù),里面保存著所有要更新的內(nèi)容。commitWork 就是執(zhí)行具體更新的函數(shù),這里就不展開(kāi)了(因?yàn)檫@篇主要想講的是 fiber 更新的調(diào)度算法)。
所以你們看遍歷 dom 數(shù) diff 的過(guò)程是可以被打斷并且在后續(xù)的時(shí)間片上接著干,只是最后一步 commitAllwork 是同步的不能打斷的。這樣 react 使用新的調(diào)度算法優(yōu)化了更新過(guò)程中執(zhí)行時(shí)間過(guò)長(zhǎng)導(dǎo)致的頁(yè)面卡頓現(xiàn)象。
參考文獻(xiàn)
- 為 Luy 實(shí)現(xiàn) React Fiber 架構(gòu) - 更詳細(xì)的代碼實(shí)現(xiàn)可以看這片文章。