這篇文章是 React 架構演變的第二篇,上一篇主要介紹了更新機制從同步修改成異步,這一篇重點介紹 Fiber 架構下經過循環遍歷更新的過程,之因此要使用循環遍歷的方式,是由於遞歸更新過程一旦開始就不能暫停,只能不斷向下,直到遞歸結束或者出現異常。html
React 15 的遞歸更新邏輯是先將須要更新的組件放入髒組件隊列(這裏在上篇文章已經介紹過,沒看過的能夠先看看《React 架構的演變 - 從同步到異步》),而後取出組件進行一次遞歸,不停向下尋找子節點來查找是否須要更新。react
下面使用一段代碼來簡單描述一下這個過程:瀏覽器
updateComponent (prevElement, nextElement) { if ( // 若是組件的 type 和 key 都沒有發生變化,進行更新 prevElement.type === nextElement.type && prevElement.key === nextElement.key ) { // 文本節點更新 if (prevElement.type === 'text') { if (prevElement.value !== nextElement.value) { this.replaceText(nextElement.value) } } // DOM 節點的更新 else { // 先更新 DOM 屬性 this.updateProps(prevElement, nextElement) // 再更新 children this.updateChildren(prevElement, nextElement) } } // 若是組件的 type 和 key 發生變化,直接從新渲染組件 else { // 觸發 unmount 生命週期 ReactReconciler.unmountComponent(prevElement) // 渲染新的組件 this._instantiateReactComponent(nextElement) } }, updateChildren (prevElement, nextElement) { var prevChildren = prevElement.children var nextChildren = nextElement.children // 省略經過 key 從新排序的 diff 過程 if (prevChildren === null) { } // 渲染新的子節點 if (nextChildren === null) { } // 清空全部子節點 // 子節點對比 prevChildren.forEach((prevChild, index) => { const nextChild = nextChildren[index] // 遞歸過程 this.updateComponent(prevChild, nextChild) }) }
爲了更清晰的看到這個過程,咱們仍是寫一個簡單的Demo,構造一個 3 * 3 的 Table 組件。數據結構
// https://codesandbox.io/embed/react-sync-demo-nlijf class Col extends React.Component { render() { // 渲染以前暫停 8ms,給 render 製造一點點壓力 const start = performance.now() while (performance.now() - start < 8) return <td>{this.props.children}</td> } } export default class Demo extends React.Component { state = { val: 0 } render() { const { val } = this.state const array = Array(3).fill() // 構造一個 3 * 3 表格 const rows = array.map( (_, row) => <tr key={row}> {array.map( (_, col) => <Col key={col}>{val}</Col> )} </tr> ) return ( <table className="table"> <tbody>{rows}</tbody> </table> ) } }
而後每秒對 Table 裏面的值更新一次,讓 val 每次 + 1,從 0 ~ 9 不停循環。架構
// https://codesandbox.io/embed/react-sync-demo-nlijf export default class Demo extends React.Component { tick = () => { setTimeout(() => { this.setState({ val: next < 10 ? next : 0 }) this.tick() }, 1000) } componentDidMount() { this.tick() } }
完整代碼的線上地址: https://codesandbox.io/embed/react-sync-demo-nlijf。Demo 組件每次調用 setState,React 會先判斷該組件的類型有沒有發生修改,若是有就整個組件進行從新渲染,若是沒有會更新 state,而後向下判斷 table 組件,table 組件繼續向下判斷 tr 組件,tr 組件再向下判斷 td 組件,最後發現 td 組件下的文本節點發生了修改,經過 DOM API 更新。異步
經過 Performance 的函數調用堆棧也能清晰的看到這個過程,updateComponent 以後 的 updateChildren 會繼續調用子組件的 updateComponent,直到遞歸完全部組件,表示更新完成。async
遞歸的缺點很明顯,不能暫停更新,一旦開始必須從頭至尾,這與 React 16 拆分時間片,給瀏覽器喘口氣的理念明顯不符,因此 React 必需要切換架構,將虛擬 DOM 從樹形結構修改成鏈表結構。函數
這裏說的鏈表結構就是 Fiber 了,鏈表結構最大的優點就是能夠經過循環的方式來遍歷,只要記住當前遍歷的位置,即便中斷後也能快速還原,從新開始遍歷。oop
咱們先看看一個 Fiber 節點的數據結構:post
function FiberNode (tag, key) { // 節點 key,主要用於了優化列表 diff this.key = key // 節點類型;FunctionComponent: 0, ClassComponent: 1, HostRoot: 3 ... this.tag = tag // 子節點 this.child = null // 父節點 this.return = null // 兄弟節點 this.sibling = null // 更新隊列,用於暫存 setState 的值 this.updateQueue = null // 節點更新過時時間,用於時間分片 // react 17 改成:lanes、childLanes this.expirationTime = NoLanes this.childExpirationTime = NoLanes // 對應到頁面的真實 DOM 節點 this.stateNode = null // Fiber 節點的副本,能夠理解爲備胎,主要用於提高更新的性能 this.alternate = null }
下面舉個例子,咱們這裏有一段普通的 HTML 文本:
<table class="table"> <tr> <td>1</td> <td>1</td> </tr> <tr> <td>1</td> </tr> </table>
在以前的 React 版本中,jsx 會轉化爲 createElement 方法,建立樹形結構的虛擬 DOM。
const VDOMRoot = { type: 'table', props: { className: 'table' }, children: [ { type: 'tr', props: { }, children: [ { type: 'td', props: { }, children: [{type: 'text', value: '1'}] }, { type: 'td', props: { }, children: [{type: 'text', value: '1'}] } ] }, { type: 'tr', props: { }, children: [ { type: 'td', props: { }, children: [{type: 'text', value: '1'}] } ] } ] }
Fiber 架構下,結構以下:
// 有所簡化,並不是與 React 真實的 Fiber 結構一致 const FiberRoot = { type: 'table', return: null, sibling: null, child: { type: 'tr', return: FiberNode, // table 的 FiberNode sibling: { type: 'tr', return: FiberNode, // table 的 FiberNode sibling: null, child: { type: 'td', return: FiberNode, // tr 的 FiberNode sibling: { type: 'td', return: FiberNode, // tr 的 FiberNode sibling: null, child: null, text: '1' // 子節點僅有文本節點 }, child: null, text: '1' // 子節點僅有文本節點 } }, child: { type: 'td', return: FiberNode, // tr 的 FiberNode sibling: null, child: null, text: '1' // 子節點僅有文本節點 } } }
那麼,在 setState 的時候,React 是如何進行一次 Fiber 的遍歷的呢?
let workInProgress = FiberRoot // 遍歷 Fiber 節點,若是時間片時間用完就中止遍歷 function workLoopConcurrent() { while ( workInProgress !== null && !shouldYield() // 用於判斷當前時間片是否到期 ) { performUnitOfWork(workInProgress) } } function performUnitOfWork() { const next = beginWork(workInProgress) // 返回當前 Fiber 的 child if (next) { // child 存在 // 重置 workInProgress 爲 child workInProgress = next } else { // child 不存在 // 向上回溯節點 let completedWork = workInProgress while (completedWork !== null) { // 收集反作用,主要是用於標記節點是否須要操做 DOM completeWork(completedWork) // 獲取 Fiber.sibling let siblingFiber = workInProgress.sibling if (siblingFiber) { // sibling 存在,則跳出 complete 流程,繼續 beginWork workInProgress = siblingFiber return; } completedWork = completedWork.return workInProgress = completedWork } } } function beginWork(workInProgress) { // 調用 render 方法,建立子 Fiber,進行 diff // 操做完畢後,返回當前 Fiber 的 child return workInProgress.child } function completeWork(workInProgress) { // 收集節點反作用 }
Fiber 的遍歷本質上就是一個循環,全局有一個 workInProgress
變量,用來存儲當前正在 diff 的節點,先經過 beginWork
方法對當前節點而後進行 diff 操做(diff 以前會調用 render,從新計算 state、prop),並返回當前節點的第一個子節點( fiber.child
)做爲新的工做節點,直到不存在子節點。而後,對當前節點調用 completedWork
方法,存儲 beginWork
過程當中產生的反作用,若是當前節點存在兄弟節點( fiber.sibling
),則將工做節點修改成兄弟節點,從新進入 beginWork
流程。直到 completedWork
從新返回到根節點,執行 commitRoot
將全部的反作用反應到真實 DOM 中。
在一次遍歷過程當中,每一個節點都會經歷 beginWork
、completeWork
,直到返回到根節點,最後經過 commitRoot
將全部的更新提交,關於這部分的內容能夠看:《React 技術揭祕》。
前面說過,Fiber 結構的遍歷是支持中斷恢復,爲了觀察這個過程,咱們將以前的 3 * 3 的 Table 組件改爲 Concurrent 模式,線上地址:https://codesandbox.io/embed/react-async-demo-h1lbz。因爲每次調用 Col 組件的 render 部分須要耗時 8ms,會超出了一個時間片,因此每一個 td 部分都會暫停一次。
class Col extends React.Component { render() { // 渲染以前暫停 8ms,給 render 製造一點點壓力 const start = performance.now(); while (performance.now() - start < 8); return <td>{this.props.children}</td> } }
在這個 3 * 3 組件裏,一共有 9 個 Col 組件,因此會有 9 次耗時任務,分散在 9 個時間片進行,經過 Performance 的調用棧能夠看到具體狀況:
在非 Concurrent 模式下,Fiber 節點的遍歷是一次性進行的,並不會切分多個時間片,差異就是在遍歷的時候調用了 workLoopSync
方法,該方法並不會判斷時間片是否用完。
// 遍歷 Fiber 節點 function workLoopSync() { while (workInProgress !== null) { performUnitOfWork(workInProgress) } }
經過上面的分析能夠看出, shouldYield
方法決定了當前時間片是否已經用完,這也是決定 React 是同步渲染仍是異步渲染的關鍵。若是去除任務優先級的概念,shouldYield
方法能夠說很簡單,就是判斷了當前的時間,是否已經超過了預設的 deadline
。
function getCurrentTime() { return performance.now() } function shouldYield() { // 獲取當前時間 var currentTime = getCurrentTime() return currentTime >= deadline }
deadline
又是如何得的呢?能夠回顧上一篇文章(《React 架構的演變 - 從同步到異步》)提到的 ChannelMessage,更新開始的時候會經過 requestHostCallback
(即:port2.send
)發送異步消息,在 performWorkUntilDeadline
(即:port1.onmessage
)中接收消息。performWorkUntilDeadline
每次接收到消息時,表示已經進入了下一個任務隊列,這個時候就會更新 deadline
。
var channel = new MessageChannel() var port = channel.port2 channel.port1.onmessage = function performWorkUntilDeadline() { if (scheduledHostCallback !== null) { var currentTime = getCurrentTime() // 重置超時時間 deadline = currentTime + yieldInterval var hasTimeRemaining = true var hasMoreWork = scheduledHostCallback() if (!hasMoreWork) { // 已經沒有任務了,修改狀態 isMessageLoopRunning = false; scheduledHostCallback = null; } else { // 還有任務,放到下個任務隊列執行,給瀏覽器喘息的機會 port.postMessage (null); } } else { isMessageLoopRunning = false; } } requestHostCallback = function (callback) { //callback 掛載到 scheduledHostCallback scheduledHostCallback = callback if (!isMessageLoopRunning) { isMessageLoopRunning = true // 推送消息,下個隊列隊列調用 callback port.postMessage (null) } }
超時時間的設置就是在當前時間的基礎上加上了一個 yieldInterval
, 這個 yieldInterval
的值,默認是 5ms。
deadline = currentTime + yieldInterval
同時 React 也提供了修改 yieldInterval
的手段,經過手動指定 fps,來肯定一幀的具體時間(單位:ms),fps 越高,一個時間分片的時間就越短,對設備的性能要求就越高。
forceFrameRate = function (fps) { if (fps < 0 || fps > 125) { // 幀率僅支持 0~125 return } if (fps > 0) { // 通常 60 fps 的設備 // 一個時間分片的時間爲 Math.floor(1000/60) = 16 yieldInterval = Math.floor(1000 / fps) } else { // reset the framerate yieldInterval = 5 } }
下面咱們將異步邏輯、循環更新、時間分片串聯起來。先回顧一下以前的文章講過,Concurrent 模式下,setState 後的調用順序:
Component.setState() => enqueueSetState() => scheduleUpdate() => scheduleCallback(performConcurrentWorkOnRoot) => requestHostCallback() => postMessage() => performWorkUntilDeadline()
scheduleCallback
方法會將傳入的回調(performConcurrentWorkOnRoot
)組裝成一個任務放入 taskQueue
中,而後調用 requestHostCallback
發送一個消息,進入異步任務。performWorkUntilDeadline
接收到異步消息,從 taskQueue
取出任務開始執行,這裏的任務就是以前傳入的 performConcurrentWorkOnRoot
方法,這個方法最後會調用workLoopConcurrent
(workLoopConcurrent
前面已經介紹過了,這個再也不重複)。若是 workLoopConcurrent
是因爲超時中斷的,hasMoreWork
返回爲 true,經過 postMessage
發送消息,將操做延遲到下一個任務隊列。
到這裏整個流程已經結束,但願你們看完文章能有所收穫,下一篇文章會介紹 Fiber 架構下 Hook 的實現。