上一篇咱們提到若是 setState 以後,虛擬 dom diff 比較耗時,那麼致使瀏覽器 FPS 下降,使得用戶以爲頁面卡頓。那麼 react 新的調度算法就是把本來一次 diff 的過程切分到各個幀去執行,使得瀏覽器在 diff 過程當中也能響應用戶事件。接下來咱們具體分析下新的調度算法是怎麼回事。javascript
假設咱們有一個 react 應用以下:java
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> ); } }
整個 app 的虛擬 dom 大體是這樣的:react
var rootHost = { type: 'div', children: [ { type: 'div', children: [ {type: 'text'} ] }. { type: 'ul', children: [ { type: 'li', children:[ {type: 'text'} ] }, { type: 'li', children:[ {type: 'text'} ] } ] } ] }
當更新發生 diff 兩棵新老虛擬 dom 樹的時候是遞歸的逐層比較(以下圖)。這個過程是一次完成的,若是要按上一篇咱們說的把 diff 過程切割成好多時間片來執行,難度是如何記住狀態且恢復現場。譬如說你 diff 到一半函數返回了,等下一個時間片繼續 diff。若是隻記住上次遞歸到哪一個節點,那麼你只能順着他的 children 繼續 diff,而它的兄弟節點就丟失了。若是要完美恢復現場保存的結構估計得挺複雜。因此 react16 改造了虛擬dom的結構,引入了 fiber 的鏈表結構。算法
fiber 節點至關於之前的虛擬 dom 節點,結構以下:segmentfault
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 存儲的是當前節點的父節點(元素),child 存儲的是第一個子節點(元素),sibling 存儲的是他右邊第一個的兄弟節點(元素)。alternate 保存是當更新發生時候同一個節點帶有新的 props 和 state 生成的新 fiber 節點。 那麼虛擬 dom 的存儲結構用鏈表的形式描述了整棵樹。瀏覽器
從頂層開始左序深度優先遍歷以下圖所示:數據結構
咱們在遍歷 dom 樹 diff 的時候,即便中斷了,咱們只須要記住中斷時候的那麼一個節點,就能夠在下個時間片恢復繼續遍歷並 diff。這就是 fiber 數據結構選用鏈表的一大好處。我先用文字大體描述下 fiber diff 算法的過程再來看代碼。從跟節點開始遍歷,碰到一個節點和 alternate 比較並記錄下須要更新的東西,並把這些更新提交到當前節點的父親。當遍歷完這顆樹的時候,再經過 return 回溯到根節點。這個過程當中把全部的更新所有帶到根節點,再一次更新到真實的 dom 中去。架構
從根節點開始:app
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) //繼續幹 } }
setState 先把這次更新放到更新隊列 updateQueue 裏面,而後調用調度器開始作更新任務。performWork 先調用 workLoop 對 fiber 樹進行遍歷比較,就是咱們上面提到的遍歷過程。當這次時間片時間不夠遍歷完整個 fiber 樹,或者遍歷並比較完以後 workLoop 函數結束。接下來咱們判斷下 fiber 樹是否遍歷完或者更新隊列 updateQueue 是否還有待更新的任務。若是有則調用 requestIdleCallback 在下個時間片繼續幹活。nextUnitOfWork 是個全局變量,記錄 workLoop 遍歷 fiber 樹中斷在哪一個節點。dom
function workLoop(deadline) { if (!nextUnitOfWork) { //一個週期內只建立一次 nextUnitOfWork = createWorkInProgress(updateQueue) } while (nextUnitOfWork && deadline.timeRemaining() > EXPIRATION_TIME) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) } if (pendingCommit) { //當全局 pendingCommit 變量被負值 commitAllwork(pendingCommit) } }
剛開始遍歷的時候判斷全局變量 nextUnitOfWork 是否存在?若是存在表示上次任務中斷了,咱們繼續,若是不存在咱們就從更新隊列裏面取第一個任務,並生成對應的 fiber 根節點。接下來咱們就是正式的工做了,用循環從某個節點開始遍歷 fiber 樹。performUnitOfWork 根據咱們上面提到的遍歷規則,在對當前節點處理完以後,返回下一個須要遍歷的節點。循環除了要判斷是否有下一個節點(是否遍歷完),還要判斷當前給你的時間是否用完,若是用完了則須要返回,讓瀏覽器響應用戶的交互事件,而後再在下個時間片繼續。workLoop 最後一步判斷全局變量 pendingCommit 是否存在,若是存在則把此次遍歷 fiber 樹產生的全部更新一次更新到真實的 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 第一個任務,而後看觸發這個任務的節點是什麼類型。若是不是根節點,則經過循環迭代節點的 return 找到最上層的根節點。最後生成一個新的 fiber 節點,這個節點就是當前 fiber 節點的 alternate 指向的,也就是說下面會在當前節點和這個新生成的節點直接進行 diff。
function performUnitOfWork(workInProgress) { const nextChild = beginWork(workInProgress) if (nextChild) return nextChild // 沒有 nextChild, 咱們看看這個節點有沒有 sibling let current = workInProgress while (current) { //收集當前節點的effect,而後向上傳遞 completeWork(current) if (current.sibling) return current.sibling //沒有 sibling,回到這個節點的父親,看看有沒有sibling current = current.return } }
performUnitOfWork 作的工做是 diff 當前節點,diff 完看看有沒有子節點,若是沒有子節點則把更新先提交到父節點。而後再看有沒有兄弟節點,若是有則返回出去看成下次遍歷的節點。若是仍是沒有,說明整個 fiber 樹已經遍歷完了,則進入到回溯過程,把全部的更新都集中到根節點進行更新真實 dom。
function completeWork(currentFiber) { if (currentFiber.tag === tag.classComponent) { // 用於回溯最高點的 root currentFiber.stateNode._internalfiber = currentFiber } if (currentFiber.return) { const currentEffect = currentFiber.effects || [] //收集當前節點的 effect list const currentEffectTag = currentFiber.effectTag ? [currentFiber] : [] const parentEffects = currentFiber.return.effects || [] currentFiber.return.effects = parentEffects.concat(currentEffect, currentEffectTag) } else { // 到達最頂端了 pendingCommit = currentFiber } }
咱們看到 completeWork 中當判斷到當前節點是根節點的時候才賦值 pendingCommit 整個全局變量。
function commitAllwork(topFiber) { topFiber.effects.forEach(f => { commitWork(f) }) topFiber.stateNode._rootContainerFiber = topFiber topFiber.effects = [] nextUnitOfWork = null pendingCommit = null }
當回溯完,有了 pendingCommit,則 commitAllwork 會被調用。它作的工做就是循環遍歷根節點的 effets 數據,裏面保存着全部要更新的內容。commitWork 就是執行具體更新的函數,這裏就不展開了(由於這篇主要想講的是 fiber 更新的調度算法)。
因此大家看遍歷 dom 數 diff 的過程是能夠被打斷而且在後續的時間片上接着幹,只是最後一步 commitAllwork 是同步的不能打斷的。這樣 react 使用新的調度算法優化了更新過程當中執行時間過長致使的頁面卡頓現象。