react为什么需要fiber架构,vue为什么不需要fiber架构

10,046 阅读7分钟

react 基于fiber 数据结构,和requestIdleCallback api,实现了可以中断的diff 和dom 更新过程,自己组件更新优先级,因此在极端情况性能会比vue 好。

vue的组件更新一旦开始就不能结束,在一个微任务中进行。

最近这篇文章有很多点赞、收藏,感谢大家的认可。诚惶诚恐。虽然内容不多,但里面有一些个人的理解,本人也是在找实习的过程中写的这篇文章,限于水平,可能有一些错误。此外,对于网络上的文章,据我之前的观察,对于React的分析存在一些落后,最典型的是setState的同步和异步问题,根据调试以及之前一位网友的评论,React18中都是异步更新的,各位多加辨别,建议自己多阅读源码以及打断点调试。

读源码可以直接在github上fork vue或react的项目,创建自己的comments分支,添加注释,以及上传笔记,还能顺便练习git操作。

React 16 之前的不足:

首先我们了解一下 React 的工作过程,当我们通过render()setState() 进行组件渲染和更新的时候,React 主要有两个阶段:

v2-69ad15a23f9e2bf78bb443fe04ec3964_720w.webp

调和阶段(Reconciler):官方解释。React 会自顶向下通过递归,遍历新数据生成新的 Virtual DOM,然后通过 Diff 算法,找到需要变更的元素(Patch)(如果树结构很多,将非常耗时),放到更新队列里面去。

渲染阶段(Renderer):遍历更新队列,通过调用宿主环境的API,实际更新渲染对应元素。宿主环境,比如 DOM、Native、WebGL 等。

在调和阶段阶段,由于是采用的递归的遍历方式(同步代码),这种也被成为 Stack Reconciler,主要是为了区别 Fiber Reconciler 取的一个名字。这种方式有一个特点:一旦任务开始进行,就无法中断,那么 js 将一直占用主线程, 一直要等到整棵 Virtual DOM 树计算完成之后,才能把执行权交给渲染引擎,那么这就会导致一些用户交互、动画等任务无法立即得到处理,就会有卡顿,非常的影响用户体验。

整个组件树虚拟dom的生成以及查找需要变动的地方(diff),当组件很复杂时这些操作需要耗费很多时间,由于js是单线程的,可能会导致页面卡顿,无法响应用户事件。因此出现了fiber架构,fiber架构需要能够中断执行,那么它必须能够记录当前遍历的上下文,因此fiber节点有三个指针,指向父节点,下一个兄弟节点,子节点。遍历顺序:1)如果存在子节点,先遍历子节点;2)不存在子节点,存在兄弟节点,生成当前节点的虚拟DOM,遍历下一个兄弟节点;3)不存在子节点,不存在下一个兄弟节点,生成当前节点的虚拟DOM,遍历父节点;4)当前节点的所有子节点生成完毕,生成当前节点虚拟DOM,判断有无下一个兄弟节点。重复执行上述操作,树的后序遍历,保证所有叶子节点的虚拟DOM最先被生成,通过setState已经更新了,此时只是生成新虚拟DOM,找出DOM的更新策略。

react fiber是通过requestIdleCallback这个api去控制的组件渲染的“进度条”。

requesetIdleCallback是一个属于宏任务的回调,就像setTimeout一样。不同的是,setTimeout的执行时机由我们传入的回调时间去控制,requesetIdleCallback会在事件循环空闲时调用回调函数。当在callback中递归调用requesetIdleCallback时,又会等到事件循环队列为空时才执行回调。让用户的事件得到响应。

它的回调函数可以获取本次可以执行的时间,每一个16ms除了requesetIdleCallback的回调之外,还有其他工作,所以能使用的时间是不确定的,但只要时间到了,就会停下节点的遍历。

requestIdleCallback的回调函数可以通过传入的参数deadLine.timeRemaining()检查当下还有多少时间供自己使用。(但由于兼容性不好,加上该回调函数被调用的频率太低,react实际使用的是一个polyfill(自己实现的api),而不是requestIdleCallback。)

总结:

react因为先天的不足——无法精确更新,所以需要react fiber把组件渲染工作切片;而vue基于数据劫持,更新粒度很小,没有这个压力;

React Fiber是React 16提出的一种更新机制,使用链表取代了树将虚拟dom连接使得组件更新的流程可以被中断恢复;它把组件渲染的工作分片,到时会主动让出渲染主线程。

react fiber这种数据结构使得节点可以回溯到其父节点,只要保留下中断的节点索引,就可以恢复之前的工作进度;

const workLoop = (deadLine) => {
    let shouldYield = false;// 是否该让出线程
    while(!shouldYield){
        console.log('working')
        //遍历节点等工作
      	// *****
      	// 遍历结束,判断是否应该
        shouldYield = deadLine.timeRemaining()<1;
    }
    requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop);

Fiber 的主要工作流程:

  • ReactDOM.render() 引导 React 启动或调用 setState() 的时候开始创建或更新 Fiber 树。
  • 从根节点开始遍历 Fiber Node Tree, 并且构建 WokeInProgress Tree(reconciliation 阶段)。
    • 本阶段可以暂停、终止、和重启,会导致 react 相关生命周期重复执行。
    • React 会生成两棵树,一棵是代表当前状态的 current tree,一棵是待更新的 workInProgress tree。
    • 遍历 current tree,重用或更新 Fiber Node 到 workInProgress tree,workInProgress tree 完成后会替换 current tree。
    • 每更新一个节点,同时生成该节点对应的 Effect List。
    • 为每个节点创建更新任务。
  • 将创建的更新任务加入任务队列,等待调度。
    • 调度由 scheduler 模块完成,其核心职责是执行回调。
    • scheduler 模块实现了跨平台兼容的 requestIdleCallback。
    • 每处理完一个 Fiber Node 的更新,可以中断、挂起,或恢复。
  • 根据 Effect List 更新 DOM (commit 阶段)。
    • React 会遍历 Effect List 将所有变更一次性更新到 DOM 上。
    • 这一阶段的工作会导致用户可见的变化。因此该过程不可中断,必须一直执行直到更新完成。

React 调度流程图:

7000.png

为什么vue不需要fiber架构?

react知道哪个组件触发了更新,但是不知道哪些子组件会受到影响。因此react需要生成改组件下的所有虚拟DOM结构,与原本的虚拟DOM结构进行对比,找出变动的部分。

在vue中,一切影响页面内容的数据都应该是响应式的,vue通过拦截响应式数据的修改,知道哪些组件应该被修改。不需要遍历所有子树。vue的diff算法是对组件内部的diff,如果存在子组件,会判断子组件上与渲染相关的属性是否发生变化,无需变化的化则复用原本的DOM,不会处理子组件。 模板语法让vue能够进行更好地编译时分析,提高优化过程的效率,react缺少这部分,无法识别哪些是静态节点,哪些是动态节点。

参考资料