摘要:接下來我們就是正式的工作了,用循環(huán)從某個節(jié)點開始遍歷樹。最后一步判斷全局變量是否存在,如果存在則把這次遍歷樹產(chǎn)生的所有更新一次更新到真實的上去。
前情提要
上一篇我們提到如果 setState 之后,虛擬 dom diff 比較耗時,那么導(dǎo)致瀏覽器 FPS 降低,使得用戶覺得頁面卡頓。那么 react 新的調(diào)度算法就是把原本一次 diff 的過程切分到各個幀去執(zhí)行,使得瀏覽器在 diff 過程中也能響應(yīng)用戶事件。接下來我們具體分析下新的調(diào)度算法是怎么回事。
原虛擬DOM問題假設(shè)我們有一個 react 應(yīng)用如下:
class App extends React.Component { render() { return (); } }{this.props.name}
- {this.props.items[0]}
- {this.props.items[1]}
整個 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 樹的時候是遞歸的逐層比較(如下圖)。這個過程是一次完成的,如果要按上一篇我們說的把 diff 過程切割成好多時間片來執(zhí)行,難度是如何記住狀態(tài)且恢復(fù)現(xiàn)場。譬如說你 diff 到一半函數(shù)返回了,等下一個時間片繼續(xù) diff。如果只記住上次遞歸到哪個節(jié)點,那么你只能順著他的 children 繼續(xù) diff,而它的兄弟節(jié)點就丟失了。如果要完美恢復(fù)現(xiàn)場保存的結(jié)構(gòu)估計得挺復(fù)雜。所以 react16 改造了虛擬dom的結(jié)構(gòu),引入了 fiber 的鏈表結(jié)構(gòu)。
現(xiàn)在解決方案 - fiberfiber 節(jié)點相當(dāng)于以前的虛擬 dom 節(jié)點,結(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: [] };
先講重要的幾個屬性: return 存儲的是當(dāng)前節(jié)點的父節(jié)點(元素),child 存儲的是第一個子節(jié)點(元素),sibling 存儲的是他右邊第一個的兄弟節(jié)點(元素)。alternate 保存是當(dāng)更新發(fā)生時候同一個節(jié)點帶有新的 props 和 state 生成的新 fiber 節(jié)點。 那么虛擬 dom 的存儲結(jié)構(gòu)用鏈表的形式描述了整棵樹。
從頂層開始左序深度優(yōu)先遍歷如下圖所示:
我們在遍歷 dom 樹 diff 的時候,即使中斷了,我們只需要記住中斷時候的那么一個節(jié)點,就可以在下個時間片恢復(fù)繼續(xù)遍歷并 diff。這就是 fiber 數(shù)據(jù)結(jié)構(gòu)選用鏈表的一大好處。我先用文字大致描述下 fiber diff 算法的過程再來看代碼。從跟節(jié)點開始遍歷,碰到一個節(jié)點和 alternate 比較并記錄下需要更新的東西,并把這些更新提交到當(dāng)前節(jié)點的父親。當(dāng)遍歷完這顆樹的時候,再通過 return 回溯到根節(jié)點。這個過程中把所有的更新全部帶到根節(jié)點,再一次更新到真實的 dom 中去。
從根節(jié)點開始:
div1 通過 child 到 div2。
div2 和自己的 alternate 比較完把更新 commit1 通過 return 提交到 div1。
div2 通過 sibling 到 ul1。
ul1 和自己的 alternate 比較完把更新 commit2 通過 return 提交到 div1。
ul1 通過 child 到 li1。
li1 和自己的 alternate 比較完把更新 commit3 通過 return 提交到 ul1。
li1 通過 sibling 到 li2。
li2 和自己的 alternate 比較完把更新 commit4 通過 return 提交到 ul1。
遍歷完整棵樹開始回溯,li2 通過 return 回到 ul1。
把 commit3 和 commit4 通過 return 提交到 div1。
ul1 通過 return 回到 div1。
獲取到所有更新 commit1-4,一次更新到真是的 dom 中去。
使用fiber算法更新的代碼實現(xiàn)React.Component.prototype.setState = function( partialState, callback ) { updateQueue.pus( { stateNode: this, partialState: partialState } ); requestIdleCallback(performWork); // 這里就開始干活了 } function performWork(deadline) { workLoop(deadline) if (nextUnitOfWork || updateQueue.length > 0) { requestIdleCallback(performWork) //繼續(xù)干 } }
setState 先把此次更新放到更新隊列 updateQueue 里面,然后調(diào)用調(diào)度器開始做更新任務(wù)。performWork 先調(diào)用 workLoop 對 fiber 樹進行遍歷比較,就是我們上面提到的遍歷過程。當(dāng)此次時間片時間不夠遍歷完整個 fiber 樹,或者遍歷并比較完之后 workLoop 函數(shù)結(jié)束。接下來我們判斷下 fiber 樹是否遍歷完或者更新隊列 updateQueue 是否還有待更新的任務(wù)。如果有則調(diào)用 requestIdleCallback 在下個時間片繼續(xù)干活。nextUnitOfWork 是個全局變量,記錄 workLoop 遍歷 fiber 樹中斷在哪個節(jié)點。
function workLoop(deadline) { if (!nextUnitOfWork) { //一個周期內(nèi)只創(chuàng)建一次 nextUnitOfWork = createWorkInProgress(updateQueue) } while (nextUnitOfWork && deadline.timeRemaining() > EXPIRATION_TIME) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) } if (pendingCommit) { //當(dāng)全局 pendingCommit 變量被負(fù)值 commitAllwork(pendingCommit) } }
剛開始遍歷的時候判斷全局變量 nextUnitOfWork 是否存在?如果存在表示上次任務(wù)中斷了,我們繼續(xù),如果不存在我們就從更新隊列里面取第一個任務(wù),并生成對應(yīng)的 fiber 根節(jié)點。接下來我們就是正式的工作了,用循環(huán)從某個節(jié)點開始遍歷 fiber 樹。performUnitOfWork 根據(jù)我們上面提到的遍歷規(guī)則,在對當(dāng)前節(jié)點處理完之后,返回下一個需要遍歷的節(jié)點。循環(huán)除了要判斷是否有下一個節(jié)點(是否遍歷完),還要判斷當(dāng)前給你的時間是否用完,如果用完了則需要返回,讓瀏覽器響應(yīng)用戶的交互事件,然后再在下個時間片繼續(xù)。workLoop 最后一步判斷全局變量 pendingCommit 是否存在,如果存在則把這次遍歷 fiber 樹產(chǎn)生的所有更新一次更新到真實的 dom 上去。注意 pendingCommit 在完成一次完整的遍歷過程之前是不會有值的。
function createWorkInProgress(updateQueue) { const updateTask = updateQueue.shift() if (!updateTask) return if (updateTask.partialState) { // 證明這是一個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 拿出更新隊列 updateQueue 第一個任務(wù),然后看觸發(fā)這個任務(wù)的節(jié)點是什么類型。如果不是根節(jié)點,則通過循環(huán)迭代節(jié)點的 return 找到最上層的根節(jié)點。最后生成一個新的 fiber 節(jié)點,這個節(jié)點就是當(dāng)前 fiber 節(jié)點的 alternate 指向的,也就是說下面會在當(dāng)前節(jié)點和這個新生成的節(jié)點直接進行 diff。
function performUnitOfWork(workInProgress) { const nextChild = beginWork(workInProgress) if (nextChild) return nextChild // 沒有 nextChild, 我們看看這個節(jié)點有沒有 sibling let current = workInProgress while (current) { //收集當(dāng)前節(jié)點的effect,然后向上傳遞 completeWork(current) if (current.sibling) return current.sibling //沒有 sibling,回到這個節(jié)點的父親,看看有沒有sibling current = current.return } }
performUnitOfWork 做的工作是 diff 當(dāng)前節(jié)點,diff 完看看有沒有子節(jié)點,如果沒有子節(jié)點則把更新先提交到父節(jié)點。然后再看有沒有兄弟節(jié)點,如果有則返回出去當(dāng)作下次遍歷的節(jié)點。如果還是沒有,說明整個 fiber 樹已經(jīng)遍歷完了,則進入到回溯過程,把所有的更新都集中到根節(jié)點進行更新真實 dom。
function completeWork(currentFiber) { if (currentFiber.tag === tag.classComponent) { // 用于回溯最高點的 root currentFiber.stateNode._internalfiber = currentFiber } if (currentFiber.return) { const currentEffect = currentFiber.effects || [] //收集當(dāng)前節(jié)點的 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é)點是根節(jié)點的時候才賦值 pendingCommit 整個全局變量。
function commitAllwork(topFiber) { topFiber.effects.forEach(f => { commitWork(f) }) topFiber.stateNode._rootContainerFiber = topFiber topFiber.effects = [] nextUnitOfWork = null pendingCommit = null }
當(dāng)回溯完,有了 pendingCommit,則 commitAllwork 會被調(diào)用。它做的工作就是循環(huán)遍歷根節(jié)點的 effets 數(shù)據(jù),里面保存著所有要更新的內(nèi)容。commitWork 就是執(zhí)行具體更新的函數(shù),這里就不展開了(因為這篇主要想講的是 fiber 更新的調(diào)度算法)。
所以你們看遍歷 dom 數(shù) diff 的過程是可以被打斷并且在后續(xù)的時間片上接著干,只是最后一步 commitAllwork 是同步的不能打斷的。這樣 react 使用新的調(diào)度算法優(yōu)化了更新過程中執(zhí)行時間過長導(dǎo)致的頁面卡頓現(xiàn)象。
參考文獻(xiàn)為 Luy 實現(xiàn) React Fiber 架構(gòu) - 更詳細(xì)的代碼實現(xiàn)可以看這片文章。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/109170.html
摘要:接下來看下偽代碼調(diào)度算法偽代碼原來這段寫的匆忙且不好,重新更新了一篇講調(diào)度算法的大概實現(xiàn)性能改善的原理二。 問題背景 React16 更新了底層架構(gòu),新架構(gòu)主要解決更新節(jié)點過多時,頁碼卡頓的問題。譬如如下代碼,根據(jù)用戶輸入的文字生成10000行數(shù)據(jù),用戶輸入框會出現(xiàn)卡頓現(xiàn)象。 class App extends React.Component { constructor( prop...
摘要:打包分析與性能優(yōu)化背景在去年年末參與的一個項目中,項目技術(shù)棧使用,生產(chǎn)環(huán)境全量構(gòu)建將近三分鐘,項目業(yè)務(wù)模塊多達(dá)數(shù)百個,項目依賴數(shù)千個,并且該項目協(xié)同前后端開發(fā)人員較多,提高構(gòu)建效率,成為了改善團隊開發(fā)效率的關(guān)鍵之一。 webpack打包分析與性能優(yōu)化 背景 在去年年末參與的一個項目中,項目技術(shù)棧使用react+es6+ant-design+webpack+babel,生產(chǎn)環(huán)境全量構(gòu)建將...
摘要:感謝王下邀月熊分享的前端每周清單,為方便大家閱讀,特整理一份索引。王下邀月熊大大也于年月日整理了自己的前端每周清單系列,并以年月為單位進行分類,具體內(nèi)容看這里前端每周清單年度總結(jié)與盤點。 感謝 王下邀月熊_Chevalier 分享的前端每周清單,為方便大家閱讀,特整理一份索引。 王下邀月熊大大也于 2018 年 3 月 31 日整理了自己的前端每周清單系列,并以年/月為單位進行分類,具...
摘要:本周在支持機票的項目中對做了大量改進,包括性能上與結(jié)構(gòu)上的改進。但通過一些簡化改改良,代碼的可靠性大大提高了。此外,還有周邊的優(yōu)化在目錄下提供一個,用于在舊式中替換。改善,里面內(nèi)置了一個補丁,也是用于改善性能,或中的性能好差。 本周在支持機票的項目中對anujs做了大量改進,包括性能上與結(jié)構(gòu)上的改進。與1.1.3一樣,還是差一個組件就完全兼容阿里的antd UI庫。 框架本身的改進有:...
閱讀 2570·2021-11-23 09:51
閱讀 3120·2019-08-30 15:54
閱讀 1070·2019-08-30 14:14
閱讀 3542·2019-08-30 13:59
閱讀 1393·2019-08-29 17:09
閱讀 1468·2019-08-29 16:24
閱讀 2848·2019-08-29 15:43
閱讀 911·2019-08-29 12:45