成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

React 架構的演變 - 從遞歸到循環

開發 架構
React 15 的遞歸更新邏輯是先將需要更新的組件放入臟組件隊列(這里在上篇文章已經介紹過,沒看過的可以先看看《React 架構的演變 - 從同步到異步》),然后取出組件進行一次遞歸,不停向下尋找子節點來查找是否需要更新。

[[344647]]

遞歸更新的實現

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

下面使用一段代碼來簡單描述一下這個過程:

  1. updateComponent (prevElement, nextElement) { 
  2.   if ( 
  3.   // 如果組件的 type 和 key 都沒有發生變化,進行更新 
  4.     prevElement.type === nextElement.type && 
  5.     prevElement.key === nextElement.key 
  6.   ) { 
  7.     // 文本節點更新 
  8.     if (prevElement.type === 'text') { 
  9.         if (prevElement.value !== nextElement.value) { 
  10.             this.replaceText(nextElement.value) 
  11.         } 
  12.     } 
  13.     // DOM 節點的更新 
  14.     else { 
  15.       // 先更新 DOM 屬性 
  16.       this.updateProps(prevElement, nextElement) 
  17.       // 再更新 children 
  18.       this.updateChildren(prevElement, nextElement) 
  19.     } 
  20.   } 
  21.   // 如果組件的 type 和 key 發生變化,直接重新渲染組件 
  22.   else { 
  23.     // 觸發 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.   // 省略通過 key 重新排序的 diff 過程 
  33.   if (prevChildren === null) { } // 渲染新的子節點 
  34.   if (nextChildren === null) { } // 清空所有子節點 
  35.   // 子節點對比 
  36.   prevChildren.forEach((prevChild, index) => { 
  37.     const nextChild = nextChildren[index
  38.     // 遞歸過程 
  39.     this.updateComponent(prevChild, nextChild) 
  40.   }) 

為了更清晰的看到這個過程,我們還是寫一個簡單的Demo,構造一個 3 * 3 的 Table 組件。

Table

  1. // https://codesandbox.io/embed/react-sync-demo-nlijf 
  2. class Col extends React.Component { 
  3.   render() { 
  4.     // 渲染之前暫停 8ms,給 render 制造一點點壓力 
  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.     // 構造一個 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.   } 

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

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 組件每次調用 setState,React 會先判斷該組件的類型有沒有發生修改,如果有就整個組件進行重新渲染,如果沒有會更新 state,然后向下判斷 table 組件,table 組件繼續向下判斷 tr 組件,tr 組件再向下判斷 td 組件,最后發現 td 組件下的文本節點發生了修改,通過 DOM API 更新。

Update

 

通過 Performance 的函數調用堆棧也能清晰的看到這個過程,updateComponent 之后 的 updateChildren 會繼續調用子組件的 updateComponent,直到遞歸完所有組件,表示更新完成。

調用堆棧

 

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

可循環的 Fiber

這里說的鏈表結構就是 Fiber 了,鏈表結構最大的優勢就是可以通過循環的方式來遍歷,只要記住當前遍歷的位置,即使中斷后也能快速還原,重新開始遍歷。

我們先看看一個 Fiber 節點的數據結構:

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

下面舉個例子,我們這里有一段普通的 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 會轉化為 createElement 方法,創建樹形結構的虛擬 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 架構下,結構如下:

  1. // 有所簡化,并非與 React 真實的 Fiber 結構一致 
  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' // 子節點僅有文本節點 
  22.         }, 
  23.         child: null
  24.         text: '1' // 子節點僅有文本節點 
  25.       } 
  26.     }, 
  27.     child: { 
  28.       type: 'td'
  29.       return: FiberNode, // tr 的 FiberNode 
  30.       sibling: null
  31.       child: null
  32.       text: '1' // 子節點僅有文本節點 
  33.     } 
  34.   } 

Fiber

 

循環更新的實現

那么,在 setState 的時候,React 是如何進行一次 Fiber 的遍歷的呢?

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

Fiber 的遍歷本質上就是一個循環,全局有一個 workInProgress 變量,用來存儲當前正在 diff 的節點,先通過 beginWork 方法對當前節點然后進行 diff 操作(diff 之前會調用 render,重新計算 state、prop),并返回當前節點的第一個子節點( fiber.child)作為新的工作節點,直到不存在子節點。然后,對當前節點調用 completedWork 方法,存儲 beginWork 過程中產生的副作用,如果當前節點存在兄弟節點( fiber.sibling),則將工作節點修改為兄弟節點,重新進入 beginWork 流程。直到 completedWork 重新返回到根節點,執行 commitRoot將所有的副作用反應到真實 DOM 中。

Fiber work loop

 

在一次遍歷過程中,每個節點都會經歷 beginWork、completeWork ,直到返回到根節點,最后通過 commitRoot 將所有的更新提交,關于這部分的內容可以看:《React 技術揭秘》。

時間分片的秘密

前面說過,Fiber 結構的遍歷是支持中斷恢復,為了觀察這個過程,我們將之前的 3 * 3 的 Table 組件改成 Concurrent 模式,線上地址:https://codesandbox.io/embed/react-async-demo-h1lbz。由于每次調用 Col 組件的 render 部分需要耗時 8ms,會超出了一個時間片,所以每個 td 部分都會暫停一次。

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

在這個 3 * 3 組件里,一共有 9 個 Col 組件,所以會有 9 次耗時任務,分散在 9 個時間片進行,通過 Performance 的調用??梢钥吹骄唧w情況:

異步模式的調用棧

 

在非 Concurrent 模式下,Fiber 節點的遍歷是一次性進行的,并不會切分多個時間片,差別就是在遍歷的時候調用了 workLoopSync 方法,該方法并不會判斷時間片是否用完。

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

同步模式的調用棧

 

通過上面的分析可以看出, shouldYield 方法決定了當前時間片是否已經用完,這也是決定 React 是同步渲染還是異步渲染的關鍵。如果去除任務優先級的概念,shouldYield 方法可以說很簡單,就是判斷了當前的時間,是否已經超過了預設的 deadline。

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

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

異步調用棧

  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.     // 重置超時時間  
  7.     deadline = currentTime + yieldInterval 
  8.      
  9.     var hasTimeRemaining = true 
  10.     var hasMoreWork = scheduledHostCallback() 
  11.  
  12.     if (!hasMoreWork) { 
  13.       // 已經沒有任務了,修改狀態  
  14.       isMessageLoopRunning = false
  15.       scheduledHostCallback = null
  16.     } else { 
  17.       // 還有任務,放到下個任務隊列執行,給瀏覽器喘息的機會  
  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.     // 推送消息,下個隊列隊列調用 callback 
  30.     port.postMessage (null
  31.   } 

超時時間的設置就是在當前時間的基礎上加上了一個 yieldInterval, 這個 yieldInterval的值,默認是 5ms。

  1. deadline = currentTime + yieldInterval 

同時 React 也提供了修改 yieldInterval 的手段,通過手動指定 fps,來確定一幀的具體時間(單位:ms),fps 越高,一個時間分片的時間就越短,對設備的性能要求就越高。

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

總結

下面我們將異步邏輯、循環更新、時間分片串聯起來。先回顧一下之前的文章講過,Concurrent 模式下,setState 后的調用順序:

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

scheduleCallback 方法會將傳入的回調(performConcurrentWorkOnRoot)組裝成一個任務放入 taskQueue 中,然后調用 requestHostCallback 發送一個消息,進入異步任務。performWorkUntilDeadline 接收到異步消息,從 taskQueue 取出任務開始執行,這里的任務就是之前傳入的 performConcurrentWorkOnRoot 方法,這個方法最后會調用workLoopConcurrent(workLoopConcurrent 前面已經介紹過了,這個不再重復)。如果 workLoopConcurrent 是由于超時中斷的,hasMoreWork 返回為 true,通過 postMessage 發送消息,將操作延遲到下一個任務隊列。

 


 

流程圖

 

 

到這里整個流程已經結束,希望大家看完文章能有所收獲,下一篇文章會介紹 Fiber 架構下 Hook 的實現。

本文轉載自微信公眾號「更了不起的前端」,可以通過以下二維碼關注。轉載本文請聯系更了不起的前端公眾號。

 

責任編輯:武曉燕 來源: 更了不起的前端
相關推薦

2020-09-24 08:45:10

React架構源碼

2020-10-28 09:12:48

React架構Hooks

2020-10-13 08:36:30

React 架構機制

2024-08-14 08:16:53

2023-05-29 13:56:00

JSReact

2019-04-18 14:24:52

技術互聯網架構

2019-07-04 15:16:42

數據架構Flink數據倉庫

2022-11-15 17:31:35

邊緣計算架構人工智能

2023-08-09 08:00:00

數據倉庫數據架構

2017-08-02 16:44:32

架構

2018-06-05 08:36:47

內部部署云存儲

2009-08-26 18:20:42

三層架構

2024-05-10 09:36:36

架構消息隊列

2021-04-20 14:57:20

架構運維技術

2022-07-04 08:14:24

架構演變Tomcat容器架構

2013-05-29 10:33:16

2024-12-30 09:55:44

2019-07-04 13:05:18

MySQL設計數據庫

2021-05-12 23:07:16

服務器處理連接

2014-06-17 14:01:34

Mysql網站架構
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 免费在线h视频 | 精品日本中文字幕 | 小草久久久久久久久爱六 | 日韩国产黄色片 | 一区二区三区精品在线 | 日韩精品一区二区在线观看 | 国产午夜精品一区二区三区在线观看 | 色噜噜狠狠色综合中国 | 国产一区亚洲二区三区 | 精品一区二区三区在线播放 | 精品久久久久久久久久久下田 | 国产目拍亚洲精品99久久精品 | 欧美国产91| 亚洲视频www| 成人免费一区二区三区视频网站 | 呦呦在线视频 | 国产精品久久久久久久久久 | 国产福利在线 | 国产精品久久久久影院色老大 | 免费一级黄 | 日韩电影一区 | 欧美日韩国产一区二区三区 | 国产精品免费看 | 在线观看中文字幕 | 亚洲精品一区二区三区中文字幕 | 国产精品视频免费看 | 色网站在线 | av国产精品 | 久久久国产精品一区 | 影音先锋中文字幕在线观看 | 国产伦精品一区二区三区视频金莲 | 亚洲高清三级 | 精品久久久久久久久久 | 亚洲毛片在线观看 | 国产999精品久久久久久 | 在线成人免费视频 | 99国产视频| 欧美影院 | 成人午夜免费福利视频 | 国产精品久久久久久福利一牛影视 | 国产成人精品综合 |