解析 React 性能利器 — Fiber

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"什麼是刷新率?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大部分顯示器屏幕都有固定的刷新率(比如最新的一般在 60Hz),所以瀏覽器更新最好是在 60fps。如果在兩次硬件刷新之間瀏覽器進行兩次重繪是沒有意義的只會消耗性能。 瀏覽器會利用這個間隔 16ms(一幀)適當地對繪製進行節流,如果在 16ms 內做了太多事情,會阻塞渲染,造成頁面卡頓, 因此 16ms 就成爲頁面渲染優化的一個關鍵時間。"}]},{"type":"horizontalrule"},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一幀做了哪些事情"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/d6\/d6c896c70ffd17c96a87438e0afc433a.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"events"},{"type":"text","text":": 點擊事件、鍵盤事件、滾動事件等"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"macro"},{"type":"text","text":": 宏任務,如 "},{"type":"codeinline","content":[{"type":"text","text":"setTimeout"}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"micro"},{"type":"text","text":": 微任務,如 "},{"type":"codeinline","content":[{"type":"text","text":"Promise"}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"rAF"},{"type":"text","text":": "},{"type":"text","marks":[{"type":"strong"}],"text":"requestAnimationFrame"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"window.requestAnimationFrame() 告訴瀏覽器——你希望執行一個動畫,並且要求瀏覽器在下次重繪之前調用指定的回調函數更新動畫。該方法需要傳入一個回調函數作爲參數,該回調函數會在瀏覽器下一次重繪之前執行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Layout"},{"type":"text","text":": CSS 計算,頁面佈局"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Paint"},{"type":"text","text":": 頁面繪製"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"rIC: requestIdleCallback"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"window.requestIdleCallback()方法將在瀏覽器的空閒時段內調用的函數排隊。這使開發者能夠在主事件循環上執行後臺和低優先級工作,而不會影響延遲關鍵事件,如動畫和輸入響應。函數一般會按先進先調用的順序執行,然而,"},{"type":"text","marks":[{"type":"strong"}],"text":"如果回調函數指定了執行超時時間 timeout,則有可能爲了在超時前執行函數而打亂執行順序"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個幀內要做這麼多事情…… 如果 js 執行時間過長超過 16ms,就會 block 住,那麼就會丟掉一次幀的繪製"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"宏任務的執行總在微任務之後,但是與其他的順序不太確定"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TIPS:協調的概念:比較虛擬 DOM 樹,找出需要變更的節點,更新,稱爲協調(Reconcliation)"}]}]},{"type":"horizontalrule"},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"React16 之前的協調"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/3d\/3d17cf1ad5aed65b8d140c517a424fee.gif","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"特點:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸調用,通過 React DOM 樹級關係構成的棧遞歸"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 virtualDOM 的比對過程中,發現一個 instance 有更新,會立即執行 DOM 操作。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同步更新,沒發打斷"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼示例"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有下面這樣一個 Component 組件,用他來模擬 DOM Diff 過程:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"const Component = (\n  
\n    
\n      
\n      
\n    \n    
\n  \n)\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Diff 過程:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面定義的 Component 組件會首先通過 Babel 轉成 React.CreateElement 生成 ReactElement,也就是我們口中的虛擬 DOM(virtualDOM),如下類似 root 的結構(下面裏面屬性做了很多簡化,只展示了結構)。"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"let root = {\n  key: 'A1',\n  children: [\n    {\n      key: 'B1',\n      children: [\n        {\n          key: 'C1',\n          children: [],\n        },\n        {\n          key: 'C2',\n          children: [],\n        }\n      ],\n    },\n    {\n      key: 'B2',\n      children: [],\n    }\n  ],\n};\n\n\/\/ 深度優先遍歷\nfunction walk(vdom) {\n  doWork(vdom);\n  vdom.children.forEach(child => {\n    walk(child);\n  })\n}\n\/\/ 更新操作\nfunction doWork(vdom) {\n  console.log(vdom.key);\n}\n\nwalk(root);\n"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"缺點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據上面代碼會發現,如果有大量更新或者有很深的組件結構樹,執行 diff 操作的執行棧會越來越深並不能及時釋放,那麼 js 將一直佔用主線程,一直要等到整棵 virtualDOM 樹計算完成之後,才能把執行權交給渲染引擎,這就會導致用戶的交互操作以及頁面動畫得不到響應,就會有明顯感覺卡頓(掉幀),影響用戶體驗。"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解決:"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"把一個耗時長的任務分成很多小片,每一個小片的運行時間很短,雖然總時間依然很長,但是在每個小片執行完之後,都給其他任務一個執行的機會,這樣唯一的線程就不會被獨佔,其他任務依然有運行的機會,所以 React 在 15 版本更新 16 版本時候推出了 Fiber 協調的概念。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Fiber 概念"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Fiber 是對 React 核心算法的重構,2 年重構的產物就是 Fiber Reconciler"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"核心目標:擴大其適用性,包括動畫,佈局和手勢。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/86\/86f7d72dc3063574382884db1be12836.gif","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"把可中斷的工作拆分成小任務"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對正在做的工作調整優先次序、重做、複用上次(做了一半的)成果"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在父子任務之間從容切換(yield back and forth),以支持 React 執行過程中的佈局刷新"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"支持 "},{"type":"codeinline","content":[{"type":"text","text":"render()"}]},{"type":"text","text":" 返回多個元素"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"更好地支持 error boundary"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每一個 Virtual DOM 節點內部都會生成對應的 Fiber。"}]}]},{"type":"horizontalrule"},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Fiber 前置知識"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"怎麼中斷一個任務:實現一個類似於 Fiber 可中斷的 workLoop。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function sleep(delay) {\n  for (let start = Date.now(); Date.now() - start <= delay;) {}\n}\n\n\/\/ 每一個子項可以認爲是一個 fiber\nconst works = [\n  () => {\n    console.log('第一個任務開始');\n    sleep(20);\n    console.log('第一個任務結束');\n  },\n  () => {\n    console.log('第 2 個任務開始');\n    sleep(20);\n    console.log('第 2 個任務結束');\n  },\n  () => {\n    console.log('第 3 個任務開始');\n    sleep(20);\n    console.log('第 3 個任務結束');\n  },\n];\n\nwindow.requestIdleCallback(workLoop, { timeout: 1000});\n\nfunction workLoop(deadLine) {\n  console.log('本幀的剩餘時間剩', parseInt(deadLine.timeRemaining()));\n  \/*\n  * deadLine {\n  *   timeRemaining(), 返回此幀還剩下多少 ms 供用戶使用\n  *   didTimeout 返回 cb 任務是否超時\n  * }\n  *\/\n  while ((deadLine.timeRemaining() > 0 || deadLine.didTimeout) && works.length > 0) { \/\/ 對象 兩個屬性 timeRemaining()\n    performUnitOfWord();\n  }\n\n  if (works.length > 0) {\n    window.requestIdleCallback(workLoop, { timeout: 1000});\n  }\n}\n\nfunction performUnitOfWord() {\n  works.shift()(); \/\/ 取出第一個元素執行\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單鏈表"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"存儲數據的數據結構"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數據以節點的形式表示,每個節點的構成:元素 + 指針(後續元素存儲位置),元素就是存儲數據的存儲單元"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單鏈表是 Fiber 中很重要的一個數據結構,很多異步更新邏輯都是通過單鏈表結構來實現的(setState 中的 UpdateQueue 更新鏈表也是基於單鏈表結構)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模擬一個類似 React 中 setState 批量更新的邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/**\n  Fiber 很多地方用到鏈表(單鏈表),尾指針沒有指向\n *\/\n\nclass Update {\n  constructor(payload, nextUpdate) {\n    this.payload = payload;\n    this.nextUpdate = nextUpdate; \/\/ 下一個節點的指針\n  }\n}\n\nclass UpdateQueue {\n  constructor(payload) {\n    this.baseState = null; \/\/ 原狀態\n    this.firstUpdate = null; \/\/ 第一次更新\n    this.lastUpdate = null; \/\/ 最後一次更新\n  }\n\n  enqueueUpdate(update) {\n    if (this.firstUpdate === null) {\n      this.firstUpdate = this.lastUpdate =update;\n    } else {\n      this.lastUpdate.nextUpdate = update;\n      this.lastUpdate = update;\n    }\n  }\n  \/\/ 獲取老狀態,遍歷鏈表,進行更新\n  forceUpdate() {\n    let currentState = this.baseState || {}; \/\/ 初始狀態\n    let currentUpdate = this.firstUpdate;\n    while (currentUpdate) {\n      let nextState = typeof currentUpdate.payload === 'function'\n                      ? currentUpdate.payload(currentState)\n                      : currentUpdate.payload;\n      currentState = {\n        ...currentState,\n        ...nextState,\n      }; \/\/ 使用當前更新得到最新的狀態\n      currentUpdate = currentUpdate.nextUpdate; \/\/ 找下一個節點\n    }\n    this.firstUpdate = this.lastUpdate = null; \/\/ 更新結束清空鏈表\n    this.baseState = currentState;\n    return currentState;\n  }\n}\n\/\/ 鏈表可以中斷和恢復\n\/\/ 每次 setState 都會通過一個鏈表保存起來,最後合併\n\/\/ enqueueUpdate 可以類比爲 setState 操作\nlet queue = new UpdateQueue();\nqueue.enqueueUpdate(new Update({ name: '微醫集團' }));\nqueue.enqueueUpdate(new Update({ number: 0 }));\nqueue.enqueueUpdate(new Update(state => ({ number: state.number + 1 })));\nqueue.enqueueUpdate(new Update(state => ({ number: state.number + 1 })));\nconsole.log(queue)\nqueue.forceUpdate();\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/8c\/8c14862d6d7a2151eaea67f4c1251721.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"思考:爲什麼 setState 在合成事件中會是異步去更新的? 解釋:我們通過僞代碼發現,每次的 setState 並沒有對 UpdataQueue 中的 state 做任何更新,只是把每次需要更新的值(或函數),放到了 UpdataQueue 的鏈表上面,在執行 forceUpdate 的時候再做統一處理,處理完之後更新 state,所以沒有執行 forceUpdate 之前,我們拿到的 state 都不是我們預期想要的 state。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"React 中的 Fiber"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Fiber 的兩個執行階段"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"協調 Reconcile(render):對 virtualDOM 操作階段,對應到新的調度算法中,就是通過 Diff Fiber Tree "},{"type":"text","marks":[{"type":"strong"}],"text":"找出要做的更新工作"},{"type":"text","text":","},{"type":"text","marks":[{"type":"strong"}],"text":"生成 Fiber 樹"},{"type":"text","text":"。這是一個 js 計算過程,計算結果可以被緩存,計算過程可以被打斷,也可以恢復執行。 所以,React 介紹 Fiber Reconciler 調度算法時,有提到新算法具有可拆分、可中斷任務的新特性,就是因爲這部分的工作是一個純 js 計算過程,"},{"type":"text","marks":[{"type":"strong"}],"text":"所以是可以被緩存、被打斷和恢復的"},{"type":"text","text":"。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"提交更新 commit: 渲染階段,拿到更新工作,提交更新並調用對應渲染模塊(React-DOM)進行渲染。爲了防止頁面抖動,該 "},{"type":"text","marks":[{"type":"strong"}],"text":"過程是同步且不能被打斷"},{"type":"text","text":"。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"React 中定義一個組件用來創建 Fiber。"}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const Component = (\n  
\n    A1\n    
\n      B1\n      
C1\n      
C2\n    \n    
B2\n  \n)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":5,"normalizeStart":5},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"上面定義的 Component 是一個組件,babel 解析時候會默認調用 React.createElement()方法,最終生成下面代碼所示這樣的 virtualDOM 結構並傳給 ReactDOM.render()方法進行調度。"}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n \"type\":\"div\",\n \"key\":null,\n \"ref\":null,\n \"props\": {\n \"id\":\"A1\",\n \"children\":[\n \"A1\",\n {\n \"type\":\"div\",\n \"key\":null,\n \"ref\":null,\n \"props\":{\n \"id\":\"B1\",\n \"children\":[\n \"B1\",\n {\n \"type\":\"div\",\n \"key\":null,\n \"ref\":null,\n \"props\":{\n \"id\":\"C1\",\n \"children\":\"C1\"\n },\n \"_owner\":null,\n \"_store\":{\n\n }\n },\n {\n \"type\":\"div\",\n \"key\":null,\n \"ref\":null,\n \"props\":{\n \"id\":\"C2\",\n \"children\":\"C2\"\n },\n \"_owner\":null,\n \"_store\":{\n\n }\n }\n ]\n },\n \"_owner\":null,\n \"_store\":{\n\n }\n },\n {\n \"type\":\"div\",\n \"key\":null,\n \"ref\":null,\n \"props\":{\n \"id\":\"B2\",\n \"children\":\"B2\"\n },\n \"_owner\":null,\n \"_store\":{\n\n }\n }\n ]\n },\n \"_owner\":null,\n \"_store\":{\n\n }\n}"}]},{"type":"numberedlist","attrs":{"start":"4","normalizeStart":"4"},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"render 方法會接受 Virtual DOM,爲每個 Virtual DOM 創建 Fiber(render 階段),並且按照一定關係連接接起來。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"fiber 結構"}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"class FiberNode {\n constructor(tag, pendingProps, key, mode) {\n \/\/ 實例屬性\n this.tag = tag; \/\/ 標記不同組件類型,如 classComponent,functionComponent\n this.key = key; \/\/ react 元素上的 key 就是 jsx 上寫的那個 key,也就是最終 ReactElement 上的\n this.elementType = null; \/\/ createElement 的第一個參數,ReactElement 上的 type\n this.type = null; \/\/ 表示 fiber 的真實類型 ,elementType 基本一樣\n this.stateNode = null; \/\/ 實例對象,比如 class 組件 new 完後就掛載在這個屬性上面,如果是 RootFiber,那麼它上面掛的是 FiberRoot\n\n \/\/ fiber\n this.return = null; \/\/ 父節點,指向上一個 fiber\n this.child = null; \/\/ 子節點,指向自身下面的第一個 fiber\n this.sibling = null; \/\/ 兄弟組件, 指向一個兄弟節點\n \n this.index = 0; \/\/ 一般如果沒有兄弟節點的話是 0 當某個父節點下的子節點是數組類型的時候會給每個子節點一個 index,index 和 key 要一起做 diff\n\n this.ref = null; \/\/ reactElement 上的 ref 屬性\n\n this.pendingProps = pendingProps; \/\/ 新的 props\n this.memoizedProps = null; \/\/ 舊的 props\n this.updateQueue = null; \/\/ fiber 上的更新隊列 執行一次 setState 就會往這個屬性上掛一個新的更新, 每條更新最終會形成一個鏈表結構,最後做批量更新\n this.memoizedState = null; \/\/ 對應 memoizedProps,上次渲染的 state,相當於當前的 state,理解成 prev 和 next 的關係\n\n this.mode = mode; \/\/ 表示當前組件下的子組件的渲染方式\n\n \/\/ effects\n\n this.effectTag = NoEffect; \/\/ 表示當前 fiber 要進行何種更新\n this.nextEffect = null; \/\/ 指向下個需要更新的 fiber\n \n this.firstEffect = null; \/\/ 指向所有子節點裏,需要更新的 fiber 裏的第一個\n this.lastEffect = null; \/\/ 指向所有子節點中需要更新的 fiber 的最後一個\n\n this.expirationTime = NoWork; \/\/ 過期時間,代表任務在未來的哪個時間點應該被完成\n this.childExpirationTime = NoWork; \/\/ child 過期時間\n\n this.alternate = null; \/\/ current 樹和 workInprogress 樹之間的相互引用\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Fiber 有很多屬性,所有子節點 Fiber 的連接接是通過 child,return,siblint 鏈接起來,alternate 連接的是每一次更新的狀態,用來對比每次狀態更新以及緩存,我們使用節點的 id 來標識每個 Fiber 組件,轉換爲 Fiber 最終會生成如下圖所示的結構,也是類似於 virtualDOM 結構的,構建的順序是先 child => sibling => return,如果當前節點沒有 child 了,這個節點就會完成。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a6\/a6183899dd7bce8164dcae86693fd215.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Fiber 樹"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"收集依賴"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"收集依賴是在生成 Fiber 過程 (render 階段) 中同時完成的,按照每個節點完成的順序來構建鏈表,每個有了 Fiber 的組件通過自己的 nextEffect 指向下一個需要更新的組件,每一個父節點都有 firstEffect 和 lastEffect 來連接自己子節點的第一次更新和最後一次更新,最終會生成下圖這樣的更新鏈表。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/d6\/d6b91dea4493813e632713d1d79877e2.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"副作用鏈表(更新鏈表)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提交更新 commit"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"全部節點創建完 Fiber 之後,會進入 commit 階段,會從 root 的 fistEffect(所有節點的第一個副作用階段)開始更新,然後找 firstEffect 的 nextEffect 節點,以此類推,一氣呵成全部更新完,然後清空更新鏈表,完成此次更新,這個過程不可打斷。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上是 React 大概工作流程,主要以首次更新全部節點需要創建 Fiber 來討論,後續會更新:基於 Fiber 的 diff、React 中合成事件、各種類型組件 (類組件,Function 組件)、hooks、事件優先級(expirationTime) 在內部如何調度相關。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頭圖:Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:武曉慧"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:https:\/\/mp.weixin.qq.com\/s\/63YJi2Y3tX8CFsXuJqT9dQ"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:解析 React 性能利器 — Fiber"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來源:微醫大前端技術 - 微信公衆號 [ID:wed_fed]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"轉載:著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章