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}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章