React Fiber 是什麼?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"React 15 以及之前的版本有一個主要的問題 —— ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"虛擬 dom 的 diff 操作是同步完成的","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"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":"這就意味着當頁面上有大量 DOM 節點時,diff 的時間可能過長,從而導致交互卡頓,或者直接沒有反饋。","attrs":{}}]},{"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 來處理這樣的問題。","attrs":{}}]},{"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":"爲了保證閱讀效果,建議讀者邊閱讀邊動手實操,點擊","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/WangYuLue/react-in-deep","title":""},"content":[{"type":"text","text":"這裏","attrs":{}}]},{"type":"text","text":"可以下載源碼。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"這篇文章是什麼?不是什麼?","attrs":{}}]},{"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":"這篇文章將從一個簡單的例子入手,主要聚焦於解釋 ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"React Fiber 是怎麼做到異步可中斷更新的","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"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 中其他有趣的事情,例如 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"workInProgress","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Effects list","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"updateQueue","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"performUnitOfWork","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"render phase & commit phase","attrs":{}}],"attrs":{}},{"type":"text","text":",這方面已經有足夠優秀的文章幫我們弄清楚這些事情,感興趣的同學可以閱讀下面的文章列表:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://juejin.im/post/6844903844825006087","title":""},"content":[{"type":"text","text":"Inside Fiber: in-depth overview of the new reconciliation algorithm in React","attrs":{}}]}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://segmentfault.com/a/1190000023573713","title":""},"content":[{"type":"text","text":"React Fiber 源碼解析","attrs":{}}]}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://github.com/facebook/react/issues/13186#issuecomment-403959161","title":""},"content":[{"type":"text","text":"React issue 13186","attrs":{}}]}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://juejin.im/post/6844903975112671239","title":""},"content":[{"type":"text","text":"這可能是最通俗的 React Fiber 打開方式","attrs":{}}]}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"從一個問題開始","attrs":{}}]},{"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 之前,我們先來看一個簡單的例子:","attrs":{}}]},{"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","attrs":{}}],"text":"現在有 10000 個節點,每個節點計算耗時 1ms,如何保證 10000 個節點順利執行完成,又能讓用戶感知不到卡頓?","attrs":{}}]},{"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":"爲了方便測試,我們用計算斐波那契數列來模擬節點耗時:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"export function fibonacci(n) {\n if (n === 0) return 0;\n else if (n === 1) return 1;\n return fibonacci(n - 1) + fibonacci(n - 2);\n}\n\nexport const fibonacciWithTime = (n) => {\n console.time('計算斐波那契數列');\n const res = fibonacci(n);\n console.timeEnd('計算斐波那契數列');\n return res;\n};","attrs":{}}]},{"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":"先看看最壞的情況:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"import { fibonacciWithTime } from './utils';\n\nlet count = 0;\n\nconst $root = document.getElementById('root');\n\nfunction render(times) {\n while (count < times) {\n // 計算 25 的斐波那契數列大概耗時 1ms,所以這裏選擇 25\n fibonacciWithTime(25);\n count++;\n $root.innerText = '當前計算個數:' + count;\n }\n}\n\nrender(10000);","attrs":{}}]},{"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":"頁面會 \"卡死\" 10秒鐘,期間用戶的交互不會有任何反饋,而且頁面不會有任何更新。只有這 10000 個節點執行完了,頁面纔會作出反饋。","attrs":{}}]},{"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":"運行 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"yarn demo21","attrs":{}}],"attrs":{}},{"type":"text","text":" 查看效果,或者在 ","attrs":{}},{"type":"link","attrs":{"href":"https://codesandbox.io/s/github/WangYuLue/react-in-deep/tree/main/article02/demo01","title":""},"content":[{"type":"text","text":"codesandbox","attrs":{}}]},{"type":"text","text":" 中嘗試:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6d/6d31ce2585bfc6529c1ec50f900b6368.gif","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"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":"這是因爲 JavaScript 是單線程的,上面的代碼長期佔據 JavaScript 線程,導致其他動作無法執行。","attrs":{}}]},{"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":"查看調用棧會發現線程都被JS佔滿了:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/44/446b4dd35a82db15b29d7b36e2f25122.png","alt":null,"title":null,"style":null,"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":"解決這個問題主要有如下一些思路:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"提升計算機的算力,讓計算機在極短的時間內就能完成更新操作。但是我們的應用可能跑在千差萬別的設備上,總會有很多設備硬件水平不是很高。況且我們的應用將來會越來越複雜,dom 節點會越來越多,所以這個思路也沒法從根本上解決問題。","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"使用 web worker,讓 diff 操作在另外一個線程中並行執行。這是個好思路,但是這可能會帶來額外的開銷,react 官方並沒有採用這個策略,原因可以參考 ","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/facebook/react/issues/3092#issuecomment-183154290","title":""},"content":[{"type":"text","text":"react issue 3092","attrs":{}}]},{"type":"text","text":"。不過也有人在這個思路上做了不少[嘗試](https://segmentfault.com/a/1190000016008108)","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"將 diff 操作變成可中斷的,只有當瀏覽器空閒時再做 diff。避免 diff 更新長時間佔據瀏覽器線程。React Fiber 就是用的這個思路。","attrs":{}}]}],"attrs":{}}]},{"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":"事實上,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"我們要解決的其實並不是性能問題,而是調度問題","attrs":{}},{"type":"text","text":"。用戶的交互事件屬於高優先級,需要儘快響應。而 diff 操作優先級相對沒那麼高,可以在幾個時間段內分片執行。","attrs":{}}]},{"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":"接着上面的問題,我們應該如何優化?","attrs":{}}]},{"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":"聰明的讀者會想到,我們可以自定義調度策略。例如一次執行 10 節點,然後空出 15ms 讓瀏覽器做別的事情,以此循環。好想法,我們看看代碼實現:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"import { fibonacciWithTime } from './utils';\n\nlet count = 0;\n\nconst $root = document.getElementById('root');\n\nfunction render(times) {\n setTimeout(() => {\n let currentCount = 1;\n while (count < times && currentCount < 10) {\n fibonacciWithTime(25);\n count++;\n currentCount++;\n $root.innerText = '當前計算個數:' + count;\n }\n render(times);\n }, 15);\n}\n\nrender(10000);","attrs":{}}]},{"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":"可以看到運行的效果還是很流暢的,運行 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"yarn demo22","attrs":{}}],"attrs":{}},{"type":"text","text":" 查看效果,或者在 ","attrs":{}},{"type":"link","attrs":{"href":"https://codesandbox.io/s/github/WangYuLue/react-in-deep/tree/main/article02/demo02","title":""},"content":[{"type":"text","text":"codesandbox","attrs":{}}]},{"type":"text","text":" 中嘗試:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2c/2c331a10715851fdd9d9904a761c188d.gif","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"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":"但是上面的調度策略雖然可用,但是也有一些問題。比如,假設計算節點變得複雜,需要 10ms 才能計算完一個,那麼 10個節點就需要 100ms,這個時長用戶就能感知到卡頓。再比如,留給瀏覽器的 15ms 瀏覽器可能根本用不到,導致render時間變長。","attrs":{}}]},{"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":"查看其調用棧會發現線程中浪費了大量的空閒時間:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/23/23ee334a10a35c1c7e4dbc07a246841c.png","alt":null,"title":null,"style":null,"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":"有沒有更好的方式呢?翻一翻瀏覽器的 API,","attrs":{}},{"type":"link","attrs":{"href":"https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback","title":""},"content":[{"type":"text","text":"requestIdleCallback","attrs":{}}]},{"type":"text","text":" 進入我們眼簾。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"requestIdleCallback","attrs":{}}],"attrs":{}},{"type":"text","text":" 方法將在瀏覽器的空閒時段內調用函數。這使開發者能夠在主事件循環上執行後臺和低優先級工作,而不會影響延遲關鍵事件,如動畫和輸入響應。","attrs":{}}]},{"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 的設備,幀間隔應小於16ms),他們兩之差就是空閒時間","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/59/59a9af0817a683e8533b046ec9ee8d2a.png","alt":null,"title":null,"style":null,"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":"codeinline","content":[{"type":"text","text":"requestIdleCallback","attrs":{}}],"attrs":{}},{"type":"text","text":" 的形參是一個函數,這個函數上有兩個重要的方法,一個是 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"timeRemaining","attrs":{}}],"attrs":{}},{"type":"text","text":",表示當前一幀中是否還有空閒時間。另外一個是 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"didTimeout","attrs":{}}],"attrs":{}},{"type":"text","text":",表示是否超時,這個通常結合 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"requestIdleCallback","attrs":{}}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"的第二個參數使用,例如:","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"requestIdleCallback(run, { timeout: 2000 })","attrs":{}}],"attrs":{}},{"type":"text","text":",則表示 2 秒會超時。","attrs":{}}]},{"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":"有了上面的思路,我們再來看看代碼實現:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"import { fibonacciWithTime } from './utils';\n\nlet count = 0;\n\nconst $root = document.getElementById('root');\n\nfunction render(times) {\n const run = (deadline) => {\n while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && count < times) {\n fibonacciWithTime(25);\n count++;\n $root.innerText = '當前計算個數:' + count;\n }\n if (count < times) {\n requestIdleCallback(run);\n }\n };\n requestIdleCallback(run);\n}\n\nrender(10000);","attrs":{}}]},{"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":"可以看到運行的效果相對 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"demo22","attrs":{}}],"attrs":{}},{"type":"text","text":" 而言更加流暢,而且執行時間也變快了很多。運行 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"yarn demo23","attrs":{}}],"attrs":{}},{"type":"text","text":" 查看效果,或者在 ","attrs":{}},{"type":"link","attrs":{"href":"https://codesandbox.io/s/github/WangYuLue/react-in-deep/tree/main/article02/demo03","title":""},"content":[{"type":"text","text":"codesandbox","attrs":{}}]},{"type":"text","text":" 中嘗試:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/50/50f00b038af409ab141f2c40483da101.gif","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"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":"其調用棧如下,調度的很完美:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/21/21859ec85c50b198b734eefcba8b4ee2.png","alt":null,"title":null,"style":null,"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":"但是由於原生提供的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"requestIdleCallback","attrs":{}}],"attrs":{}},{"type":"text","text":" 方法的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"timeRemaining()","attrs":{}}],"attrs":{}},{"type":"text","text":" 最大返回是 50ms,也就是 20fps,達不到頁面流暢度的要求,並且該 API 兼容性也比較差。所以 React 團隊沒有直接使用原生的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"requestIdleCallback","attrs":{}}],"attrs":{}},{"type":"text","text":",而是自己 ","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/facebook/react/blob/master/packages/scheduler/src/forks/SchedulerDOM.js","title":""},"content":[{"type":"text","text":"polyfill","attrs":{}}]},{"type":"text","text":" 了一個。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"如何讓 React 的 diff 可中斷?","attrs":{}}]},{"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":"虛擬dom 是一個樹狀結構,diff 操作實際上就是遞歸遍歷了一遍這顆樹。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/19/19448b822374a346d4465faf12f0db22.png","alt":null,"title":null,"style":null,"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":"用代碼表示,類似如下這樣:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"function traversal(node) {\n if (!node) return;\n // Do something with node\n node.children.forEach(child => traversal(child))\n}","attrs":{}}]},{"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":"不同於前面一直遞增到 10000 就結束的簡單例子,在遞歸中中斷以及恢復狀態很麻煩。如果改成類似鏈表的結構那就好辦很多,可以一直 next,知道 next 爲 null 就知道遍歷結束了。","attrs":{}}]},{"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":"也就是說,我們需要","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"將遞歸操作變成遍歷操作","attrs":{}},{"type":"text","text":",Fiber 恰巧也是這麼做的。","attrs":{}}]},{"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 中鏈表鏈接的對象的層級結構和它們之間的連接細節:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b1/b120fcad1a826ca44032b794da1a380f.png","alt":null,"title":null,"style":null,"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 的數據格式可以表示爲:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"interface Fiber {\n // 指向父節點\n return: Fiber | null,\n // 指向子節點\n child: Fiber | null,\n // 指向兄弟節點\n sibling: Fiber | null,\n \n [props: string]: any\n};","attrs":{}}]},{"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":"如何建立這些連接?其實很簡單,參考下面的代碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"// 這是一顆 dom 樹\nconst element = {\n name: 'a1',\n children: [\n {\n name: 'b1',\n children: []\n },\n {\n name: 'b2',\n children: [\n {\n name: 'c1',\n children: [\n {\n name: 'd1',\n children: []\n },\n {\n name: 'd2',\n children: []\n }\n ]\n }\n ]\n },\n {\n name: 'b3',\n children: [\n {\n name: 'c2',\n children: []\n }\n ]\n }\n ]\n}\n\n// 建立 dom 元素與其子元素的聯繫\nfunction link(element) {\n if (element) {\n let previous;\n element.children.forEach((item, index) => {\n item.return = element;\n if (index === 0) {\n element.child = item;\n } else {\n previous.sibling = item;\n }\n previous = item;\n // 這裏爲了方便演示,用了遞歸調用\n // 在 React 中爲了提高性能並沒有遞歸調用,而是 diff 到哪個節點便在那個節點 link。\n link(item);\n });\n }\n}\n\nlink(element)","attrs":{}}]},{"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 團隊核心成員 SebastianMarkbåge 在 ","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/facebook/react/issues/7942","title":""},"content":[{"type":"text","text":"React issue 7942","attrs":{}}]},{"type":"text","text":" 中已經爲我們提供了答案:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"function walk(fiber) {\n let root = fiber;\n let node = fiber;\n while (true) {\n // Do something with node\n if (node.child) {\n node = node.child;\n continue;\n }\n if (node === root) {\n return;\n }\n while (!node.sibling) {\n if (!node.return || node.return === root) {\n return;\n }\n node = node.return;\n }\n node = node.sibling;\n }\n}","attrs":{}}]},{"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":"這有點像二叉樹的深度遍歷,有了類似鏈表的結構,我們可以隨時中斷它,並在合適的時候再恢復它。","attrs":{}}]},{"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":"運行 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"yarn demo24","attrs":{}}],"attrs":{}},{"type":"text","text":" 查看效果。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"總結","attrs":{}}]},{"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":"總結一下,爲了解決 diff 時間過長導致的卡頓問題,React Fiber 用類似 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"requestIdleCallback","attrs":{}}],"attrs":{}},{"type":"text","text":" 的機制來做異步 diff。但是之前的數據結構不支持這樣的實現異步 diff,於是 React 實現了一個類似鏈表的數據結構,將原來的 遞歸diff 變成了現在的 遍歷diff,這樣就能方便的做中斷和恢復了。","attrs":{}}]},{"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":"上面例子的完整代碼可以點","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/WangYuLue/react-in-deep","title":""},"content":[{"type":"text","text":"這裏","attrs":{}}]},{"type":"text","text":"查看,如果覺得寫的不錯,可以給筆者一個 star,感謝閱讀。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"相關閱讀","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://github.com/acdlite/react-fiber-architecture","title":""},"content":[{"type":"text","text":"react-fiber-architecture","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://zhuanlan.zhihu.com/p/109971435","title":""},"content":[{"type":"text","text":"理解 React Fiber & Concurrent Mode","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://juejin.im/post/6844903753347252237","title":""},"content":[{"type":"text","text":"The how and why on React’s usage of linked list in Fiber to walk the component’s tree","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://juejin.im/post/6844903844825006087","title":""},"content":[{"type":"text","text":"Inside Fiber: in-depth overview of the new reconciliation algorithm in React","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章