自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

React 架構(gòu)的演變 - 從遞歸到循環(huán)

開(kāi)發(fā) 架構(gòu)
React 15 的遞歸更新邏輯是先將需要更新的組件放入臟組件隊(duì)列(這里在上篇文章已經(jīng)介紹過(guò),沒(méi)看過(guò)的可以先看看《React 架構(gòu)的演變 - 從同步到異步》),然后取出組件進(jìn)行一次遞歸,不停向下尋找子節(jié)點(diǎn)來(lái)查找是否需要更新。

[[344647]]

遞歸更新的實(shí)現(xiàn)

React 15 的遞歸更新邏輯是先將需要更新的組件放入臟組件隊(duì)列(這里在上篇文章已經(jīng)介紹過(guò),沒(méi)看過(guò)的可以先看看《React 架構(gòu)的演變 - 從同步到異步》),然后取出組件進(jìn)行一次遞歸,不停向下尋找子節(jié)點(diǎn)來(lái)查找是否需要更新。

下面使用一段代碼來(lái)簡(jiǎn)單描述一下這個(gè)過(guò)程:

  1. updateComponent (prevElement, nextElement) { 
  2.   if ( 
  3.   // 如果組件的 type 和 key 都沒(méi)有發(fā)生變化,進(jìn)行更新 
  4.     prevElement.type === nextElement.type && 
  5.     prevElement.key === nextElement.key 
  6.   ) { 
  7.     // 文本節(jié)點(diǎn)更新 
  8.     if (prevElement.type === 'text') { 
  9.         if (prevElement.value !== nextElement.value) { 
  10.             this.replaceText(nextElement.value) 
  11.         } 
  12.     } 
  13.     // DOM 節(jié)點(diǎn)的更新 
  14.     else { 
  15.       // 先更新 DOM 屬性 
  16.       this.updateProps(prevElement, nextElement) 
  17.       // 再更新 children 
  18.       this.updateChildren(prevElement, nextElement) 
  19.     } 
  20.   } 
  21.   // 如果組件的 type 和 key 發(fā)生變化,直接重新渲染組件 
  22.   else { 
  23.     // 觸發(fā) unmount 生命周期 
  24.     ReactReconciler.unmountComponent(prevElement) 
  25.     // 渲染新的組件 
  26.     this._instantiateReactComponent(nextElement) 
  27.   } 
  28. }, 
  29. updateChildren (prevElement, nextElement) { 
  30.   var prevChildren = prevElement.children 
  31.   var nextChildren = nextElement.children 
  32.   // 省略通過(guò) key 重新排序的 diff 過(guò)程 
  33.   if (prevChildren === null) { } // 渲染新的子節(jié)點(diǎn) 
  34.   if (nextChildren === null) { } // 清空所有子節(jié)點(diǎn) 
  35.   // 子節(jié)點(diǎn)對(duì)比 
  36.   prevChildren.forEach((prevChild, index) => { 
  37.     const nextChild = nextChildren[index
  38.     // 遞歸過(guò)程 
  39.     this.updateComponent(prevChild, nextChild) 
  40.   }) 

為了更清晰的看到這個(gè)過(guò)程,我們還是寫一個(gè)簡(jiǎn)單的Demo,構(gòu)造一個(gè) 3 * 3 的 Table 組件。

Table

  1. // https://codesandbox.io/embed/react-sync-demo-nlijf 
  2. class Col extends React.Component { 
  3.   render() { 
  4.     // 渲染之前暫停 8ms,給 render 制造一點(diǎn)點(diǎn)壓力 
  5.     const start = performance.now() 
  6.     while (performance.now() - start < 8) 
  7.     return <td>{this.props.children}</td> 
  8.   } 
  9.  
  10. export default class Demo extends React.Component { 
  11.   state = { 
  12.     val: 0 
  13.   } 
  14.   render() { 
  15.     const { val } = this.state 
  16.     const array = Array(3).fill() 
  17.     // 構(gòu)造一個(gè) 3 * 3 表格 
  18.     const rows = array.map( 
  19.       (_, row) => <tr key={row}> 
  20.         {array.map( 
  21.           (_, col) => <Col key={col}>{val}</Col> 
  22.         )} 
  23.       </tr> 
  24.     ) 
  25.     return ( 
  26.       <table className="table"
  27.         <tbody>{rows}</tbody> 
  28.       </table
  29.     ) 
  30.   } 

然后每秒對(duì) Table 里面的值更新一次,讓 val 每次 + 1,從 0 ~ 9 不停循環(huán)。

Table Loop

  1. // https://codesandbox.io/embed/react-sync-demo-nlijf 
  2. export default class Demo extends React.Component { 
  3.  tick = () => { 
  4.     setTimeout(() => { 
  5.       this.setState({ val: next < 10 ? next : 0 }) 
  6.       this.tick() 
  7.     }, 1000) 
  8.   } 
  9.   componentDidMount() { 
  10.     this.tick() 
  11.   } 

完整代碼的線上地址:https://codesandbox.io/embed/react-sync-demo-nlijf。Demo 組件每次調(diào)用 setState,React 會(huì)先判斷該組件的類型有沒(méi)有發(fā)生修改,如果有就整個(gè)組件進(jìn)行重新渲染,如果沒(méi)有會(huì)更新 state,然后向下判斷 table 組件,table 組件繼續(xù)向下判斷 tr 組件,tr 組件再向下判斷 td 組件,最后發(fā)現(xiàn) td 組件下的文本節(jié)點(diǎn)發(fā)生了修改,通過(guò) DOM API 更新。

Update

 

通過(guò) Performance 的函數(shù)調(diào)用堆棧也能清晰的看到這個(gè)過(guò)程,updateComponent 之后 的 updateChildren 會(huì)繼續(xù)調(diào)用子組件的 updateComponent,直到遞歸完所有組件,表示更新完成。

調(diào)用堆棧

 

遞歸的缺點(diǎn)很明顯,不能暫停更新,一旦開(kāi)始必須從頭到尾,這與 React 16 拆分時(shí)間片,給瀏覽器喘口氣的理念明顯不符,所以 React 必須要切換架構(gòu),將虛擬 DOM 從樹(shù)形結(jié)構(gòu)修改為鏈表結(jié)構(gòu)。

可循環(huán)的 Fiber

這里說(shuō)的鏈表結(jié)構(gòu)就是 Fiber 了,鏈表結(jié)構(gòu)最大的優(yōu)勢(shì)就是可以通過(guò)循環(huán)的方式來(lái)遍歷,只要記住當(dāng)前遍歷的位置,即使中斷后也能快速還原,重新開(kāi)始遍歷。

我們先看看一個(gè) Fiber 節(jié)點(diǎn)的數(shù)據(jù)結(jié)構(gòu):

  1. function FiberNode (tag, key) { 
  2.   // 節(jié)點(diǎn) key,主要用于了優(yōu)化列表 diff 
  3.   this.key = key 
  4.   // 節(jié)點(diǎn)類型;FunctionComponent: 0, ClassComponent: 1, HostRoot: 3 ... 
  5.   this.tag = tag 
  6.  
  7.  // 子節(jié)點(diǎn) 
  8.   this.child = null 
  9.   // 父節(jié)點(diǎn) 
  10.   this.return = null  
  11.   // 兄弟節(jié)點(diǎn) 
  12.   this.sibling = null 
  13.    
  14.   // 更新隊(duì)列,用于暫存 setState 的值 
  15.   this.updateQueue = null 
  16.    
  17.   // 節(jié)點(diǎn)更新過(guò)期時(shí)間,用于時(shí)間分片 
  18.   // react 17 改為:lanes、childLanes 
  19.   this.expirationTime = NoLanes 
  20.   this.childExpirationTime = NoLanes 
  21.  
  22.   // 對(duì)應(yīng)到頁(yè)面的真實(shí) DOM 節(jié)點(diǎn) 
  23.   this.stateNode = null 
  24.   // Fiber 節(jié)點(diǎn)的副本,可以理解為備胎,主要用于提升更新的性能 
  25.   this.alternate = null 

下面舉個(gè)例子,我們這里有一段普通的 HTML 文本:

  1. <table class="table"
  2.   <tr> 
  3.     <td>1</td> 
  4.     <td>1</td> 
  5.   </tr> 
  6.   <tr> 
  7.     <td>1</td> 
  8.   </tr> 
  9. </table

在之前的 React 版本中,jsx 會(huì)轉(zhuǎn)化為 createElement 方法,創(chuàng)建樹(shù)形結(jié)構(gòu)的虛擬 DOM。

  1. const VDOMRoot = { 
  2.   type: 'table'
  3.   props: { className: 'table' }, 
  4.   children: [ 
  5.     { 
  6.       type: 'tr'
  7.       props: { }, 
  8.       children: [ 
  9.         { 
  10.           type: 'td'
  11.           props: { }, 
  12.           children: [{type: 'text', value: '1'}] 
  13.         }, 
  14.         { 
  15.           type: 'td'
  16.           props: { }, 
  17.           children: [{type: 'text', value: '1'}] 
  18.         } 
  19.       ] 
  20.     }, 
  21.     { 
  22.       type: 'tr'
  23.       props: { }, 
  24.       children: [ 
  25.         { 
  26.           type: 'td'
  27.           props: { }, 
  28.           children: [{type: 'text', value: '1'}] 
  29.         } 
  30.       ] 
  31.     } 
  32.   ] 

Fiber 架構(gòu)下,結(jié)構(gòu)如下:

  1. // 有所簡(jiǎn)化,并非與 React 真實(shí)的 Fiber 結(jié)構(gòu)一致 
  2. const FiberRoot = { 
  3.   type: 'table'
  4.   returnnull
  5.   sibling: null
  6.   child: { 
  7.     type: 'tr'
  8.     return: FiberNode, // table 的 FiberNode 
  9.     sibling: { 
  10.       type: 'tr'
  11.       return: FiberNode, // table 的 FiberNode 
  12.       sibling: null
  13.       child: { 
  14.         type: 'td'
  15.         return: FiberNode, // tr 的 FiberNode 
  16.         sibling: { 
  17.           type: 'td'
  18.           return: FiberNode, // tr 的 FiberNode 
  19.           sibling: null
  20.           child: null
  21.           text: '1' // 子節(jié)點(diǎn)僅有文本節(jié)點(diǎn) 
  22.         }, 
  23.         child: null
  24.         text: '1' // 子節(jié)點(diǎn)僅有文本節(jié)點(diǎn) 
  25.       } 
  26.     }, 
  27.     child: { 
  28.       type: 'td'
  29.       return: FiberNode, // tr 的 FiberNode 
  30.       sibling: null
  31.       child: null
  32.       text: '1' // 子節(jié)點(diǎn)僅有文本節(jié)點(diǎn) 
  33.     } 
  34.   } 

Fiber

 

循環(huán)更新的實(shí)現(xiàn)

那么,在 setState 的時(shí)候,React 是如何進(jìn)行一次 Fiber 的遍歷的呢?

  1. let workInProgress = FiberRoot 
  2.  
  3. // 遍歷 Fiber 節(jié)點(diǎn),如果時(shí)間片時(shí)間用完就停止遍歷 
  4. function workLoopConcurrent() { 
  5.   while ( 
  6.     workInProgress !== null && 
  7.     !shouldYield() // 用于判斷當(dāng)前時(shí)間片是否到期 
  8.   ) { 
  9.     performUnitOfWork(workInProgress) 
  10.   } 
  11.  
  12. function performUnitOfWork() { 
  13.   const next = beginWork(workInProgress) // 返回當(dāng)前 Fiber 的 child 
  14.   if (next) { // child 存在 
  15.     // 重置 workInProgress 為 child 
  16.     workInProgress = next 
  17.   } else { // child 不存在 
  18.     // 向上回溯節(jié)點(diǎn) 
  19.     let completedWork = workInProgress 
  20.     while (completedWork !== null) { 
  21.       // 收集副作用,主要是用于標(biāo)記節(jié)點(diǎn)是否需要操作 DOM 
  22.       completeWork(completedWork) 
  23.  
  24.       // 獲取 Fiber.sibling 
  25.       let siblingFiber = workInProgress.sibling 
  26.       if (siblingFiber) { 
  27.         // sibling 存在,則跳出 complete 流程,繼續(xù) beginWork 
  28.         workInProgress = siblingFiber 
  29.         return
  30.       } 
  31.  
  32.       completedWork = completedWork.return 
  33.       workInProgress = completedWork 
  34.     } 
  35.   } 
  36.  
  37. function beginWork(workInProgress) { 
  38.   // 調(diào)用 render 方法,創(chuàng)建子 Fiber,進(jìn)行 diff 
  39.   // 操作完畢后,返回當(dāng)前 Fiber 的 child 
  40.   return workInProgress.child 
  41. function completeWork(workInProgress) { 
  42.   // 收集節(jié)點(diǎn)副作用 

Fiber 的遍歷本質(zhì)上就是一個(gè)循環(huán),全局有一個(gè) workInProgress 變量,用來(lái)存儲(chǔ)當(dāng)前正在 diff 的節(jié)點(diǎn),先通過(guò) beginWork 方法對(duì)當(dāng)前節(jié)點(diǎn)然后進(jìn)行 diff 操作(diff 之前會(huì)調(diào)用 render,重新計(jì)算 state、prop),并返回當(dāng)前節(jié)點(diǎn)的第一個(gè)子節(jié)點(diǎn)( fiber.child)作為新的工作節(jié)點(diǎn),直到不存在子節(jié)點(diǎn)。然后,對(duì)當(dāng)前節(jié)點(diǎn)調(diào)用 completedWork 方法,存儲(chǔ) beginWork 過(guò)程中產(chǎn)生的副作用,如果當(dāng)前節(jié)點(diǎn)存在兄弟節(jié)點(diǎn)( fiber.sibling),則將工作節(jié)點(diǎn)修改為兄弟節(jié)點(diǎn),重新進(jìn)入 beginWork 流程。直到 completedWork 重新返回到根節(jié)點(diǎn),執(zhí)行 commitRoot將所有的副作用反應(yīng)到真實(shí) DOM 中。

Fiber work loop

 

在一次遍歷過(guò)程中,每個(gè)節(jié)點(diǎn)都會(huì)經(jīng)歷 beginWork、completeWork ,直到返回到根節(jié)點(diǎn),最后通過(guò) commitRoot 將所有的更新提交,關(guān)于這部分的內(nèi)容可以看:《React 技術(shù)揭秘》。

時(shí)間分片的秘密

前面說(shuō)過(guò),F(xiàn)iber 結(jié)構(gòu)的遍歷是支持中斷恢復(fù),為了觀察這個(gè)過(guò)程,我們將之前的 3 * 3 的 Table 組件改成 Concurrent 模式,線上地址:https://codesandbox.io/embed/react-async-demo-h1lbz。由于每次調(diào)用 Col 組件的 render 部分需要耗時(shí) 8ms,會(huì)超出了一個(gè)時(shí)間片,所以每個(gè) td 部分都會(huì)暫停一次。

  1. class Col extends React.Component { 
  2.   render() { 
  3.     // 渲染之前暫停 8ms,給 render 制造一點(diǎn)點(diǎn)壓力 
  4.     const start = performance.now(); 
  5.     while (performance.now() - start < 8); 
  6.     return <td>{this.props.children}</td> 
  7.   } 

在這個(gè) 3 * 3 組件里,一共有 9 個(gè) Col 組件,所以會(huì)有 9 次耗時(shí)任務(wù),分散在 9 個(gè)時(shí)間片進(jìn)行,通過(guò) Performance 的調(diào)用棧可以看到具體情況:

異步模式的調(diào)用棧

 

在非 Concurrent 模式下,F(xiàn)iber 節(jié)點(diǎn)的遍歷是一次性進(jìn)行的,并不會(huì)切分多個(gè)時(shí)間片,差別就是在遍歷的時(shí)候調(diào)用了 workLoopSync 方法,該方法并不會(huì)判斷時(shí)間片是否用完。

  1. // 遍歷 Fiber 節(jié)點(diǎn) 
  2. function workLoopSync() { 
  3.   while (workInProgress !== null) { 
  4.     performUnitOfWork(workInProgress) 
  5.   } 

同步模式的調(diào)用棧

 

通過(guò)上面的分析可以看出, shouldYield 方法決定了當(dāng)前時(shí)間片是否已經(jīng)用完,這也是決定 React 是同步渲染還是異步渲染的關(guān)鍵。如果去除任務(wù)優(yōu)先級(jí)的概念,shouldYield 方法可以說(shuō)很簡(jiǎn)單,就是判斷了當(dāng)前的時(shí)間,是否已經(jīng)超過(guò)了預(yù)設(shè)的 deadline。

  1. function getCurrentTime() { 
  2.   return performance.now() 
  3. function shouldYield() { 
  4.   // 獲取當(dāng)前時(shí)間 
  5.   var currentTime = getCurrentTime() 
  6.   return currentTime >= deadline 

deadline 又是如何得的呢?可以回顧上一篇文章(《React 架構(gòu)的演變 - 從同步到異步》)提到的 ChannelMessage,更新開(kāi)始的時(shí)候會(huì)通過(guò) requestHostCallback(即:port2.send)發(fā)送異步消息,在 performWorkUntilDeadline (即:port1.onmessage)中接收消息。performWorkUntilDeadline 每次接收到消息時(shí),表示已經(jīng)進(jìn)入了下一個(gè)任務(wù)隊(duì)列,這個(gè)時(shí)候就會(huì)更新 deadline。

異步調(diào)用棧

  1. var channel = new MessageChannel() 
  2. var port = channel.port2 
  3. channel.port1.onmessage = function performWorkUntilDeadline() { 
  4.   if (scheduledHostCallback !== null) { 
  5.     var currentTime = getCurrentTime() 
  6.     // 重置超時(shí)時(shí)間  
  7.     deadline = currentTime + yieldInterval 
  8.      
  9.     var hasTimeRemaining = true 
  10.     var hasMoreWork = scheduledHostCallback() 
  11.  
  12.     if (!hasMoreWork) { 
  13.       // 已經(jīng)沒(méi)有任務(wù)了,修改狀態(tài)  
  14.       isMessageLoopRunning = false
  15.       scheduledHostCallback = null
  16.     } else { 
  17.       // 還有任務(wù),放到下個(gè)任務(wù)隊(duì)列執(zhí)行,給瀏覽器喘息的機(jī)會(huì)  
  18.       port.postMessage (null); 
  19.     } 
  20.   } else { 
  21.     isMessageLoopRunning = false
  22.   } 
  23.  
  24. requestHostCallback = function (callback) { 
  25.   //callback 掛載到 scheduledHostCallback 
  26.   scheduledHostCallback = callback 
  27.   if (!isMessageLoopRunning) { 
  28.     isMessageLoopRunning = true 
  29.     // 推送消息,下個(gè)隊(duì)列隊(duì)列調(diào)用 callback 
  30.     port.postMessage (null
  31.   } 

超時(shí)時(shí)間的設(shè)置就是在當(dāng)前時(shí)間的基礎(chǔ)上加上了一個(gè) yieldInterval, 這個(gè) yieldInterval的值,默認(rèn)是 5ms。

  1. deadline = currentTime + yieldInterval 

同時(shí) React 也提供了修改 yieldInterval 的手段,通過(guò)手動(dòng)指定 fps,來(lái)確定一幀的具體時(shí)間(單位:ms),fps 越高,一個(gè)時(shí)間分片的時(shí)間就越短,對(duì)設(shè)備的性能要求就越高。

  1. forceFrameRate = function (fps) { 
  2.   if (fps < 0 || fps > 125) { 
  3.     // 幀率僅支持 0~125 
  4.     return 
  5.   } 
  6.  
  7.   if (fps > 0) { 
  8.     // 一般 60 fps 的設(shè)備 
  9.     // 一個(gè)時(shí)間分片的時(shí)間為 Math.floor(1000/60) = 16 
  10.     yieldInterval = Math.floor(1000 / fps) 
  11.   } else { 
  12.     // reset the framerate 
  13.     yieldInterval = 5 
  14.   } 

總結(jié)

下面我們將異步邏輯、循環(huán)更新、時(shí)間分片串聯(lián)起來(lái)。先回顧一下之前的文章講過(guò),Concurrent 模式下,setState 后的調(diào)用順序:

  1. Component.setState() 
  2.   => enqueueSetState() 
  3.  => scheduleUpdate() 
  4.   => scheduleCallback(performConcurrentWorkOnRoot) 
  5.   => requestHostCallback() 
  6.   => postMessage() 
  7.   => performWorkUntilDeadline() 

scheduleCallback 方法會(huì)將傳入的回調(diào)(performConcurrentWorkOnRoot)組裝成一個(gè)任務(wù)放入 taskQueue 中,然后調(diào)用 requestHostCallback 發(fā)送一個(gè)消息,進(jìn)入異步任務(wù)。performWorkUntilDeadline 接收到異步消息,從 taskQueue 取出任務(wù)開(kāi)始執(zhí)行,這里的任務(wù)就是之前傳入的 performConcurrentWorkOnRoot 方法,這個(gè)方法最后會(huì)調(diào)用workLoopConcurrent(workLoopConcurrent 前面已經(jīng)介紹過(guò)了,這個(gè)不再重復(fù))。如果 workLoopConcurrent 是由于超時(shí)中斷的,hasMoreWork 返回為 true,通過(guò) postMessage 發(fā)送消息,將操作延遲到下一個(gè)任務(wù)隊(duì)列。

 


 

流程圖

 

 

到這里整個(gè)流程已經(jīng)結(jié)束,希望大家看完文章能有所收獲,下一篇文章會(huì)介紹 Fiber 架構(gòu)下 Hook 的實(shí)現(xiàn)。

本文轉(zhuǎn)載自微信公眾號(hào)「更了不起的前端」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系更了不起的前端公眾號(hào)。

 

責(zé)任編輯:武曉燕 來(lái)源: 更了不起的前端
相關(guān)推薦

2020-09-24 08:45:10

React架構(gòu)源碼

2020-10-28 09:12:48

React架構(gòu)Hooks

2020-10-13 08:36:30

React 架構(gòu)機(jī)制

2024-08-14 08:16:53

2023-05-29 13:56:00

JSReact

2019-07-04 15:16:42

數(shù)據(jù)架構(gòu)Flink數(shù)據(jù)倉(cāng)庫(kù)

2019-04-18 14:24:52

技術(shù)互聯(lián)網(wǎng)架構(gòu)

2017-08-02 16:44:32

架構(gòu)

2022-11-15 17:31:35

邊緣計(jì)算架構(gòu)人工智能

2023-08-09 08:00:00

數(shù)據(jù)倉(cāng)庫(kù)數(shù)據(jù)架構(gòu)

2018-06-05 08:36:47

內(nèi)部部署云存儲(chǔ)

2024-05-10 09:36:36

架構(gòu)消息隊(duì)列

2013-05-29 10:33:16

2009-08-26 18:20:42

三層架構(gòu)

2021-04-20 14:57:20

架構(gòu)運(yùn)維技術(shù)

2022-07-04 08:14:24

架構(gòu)演變Tomcat容器架構(gòu)

2021-05-12 23:07:16

服務(wù)器處理連接

2022-08-15 09:00:00

JavaScript前端架構(gòu)

2014-06-17 14:01:34

Mysql網(wǎng)站架構(gòu)

2024-12-30 09:55:44

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)