react-fiber

手写简易版 React 来彻底搞懂 fiber 架构 - 知乎

概述

React 是通过 jsx 来描述界面结构的,会把 jsx 编译成 render function,然后执行 render function 产生 vdom:

img

在 v16 之前的 React 里,是直接递归遍历 vdom,通过 dom api 增删改 dom 的方式来渲染的。但当 vdom 过大,频繁调用 dom api 会比较耗时,而且递归又不能打断,所以有性能问题。

img

后来就引入了 fiber 架构,先把 vdom 树转成 fiber 链表,然后再渲染 fiber。

img

本来 vdom 里通过 children 关联父子节点,而 fiber 里面则是通过 child 关联第一个子节点,然后通过 sibling 串联起下一个,所有的节点可以 return 到父节点。

vdom 转 fiber 的过程叫做 reconcile,是可打断的,React 加入了 schedule 的机制在空闲时调度 reconcile,reconcile 的过程中会做 diff,打上增删改的标记(effectTag),并把对应的 dom 创建好。然后就可以一次性把 fiber 渲染到 dom,也就是 commit。

这个 schedule、reconcile、commit 的流程就是 fiber 架构。注意:对应的这个数据结构也叫 fiber。

既能够映射真实的 dom 也能作为分割的单元

Schedule

requestIdleCallback

那么 fiber 是如何被时间片异步执行的呢,提供一种思路,示例如下:

let firstFiber
let nextFiber = firstFiber
let shouldYield = false

/**
 * vDOM 转 fiber
 */
function performUnitOfWork(nextFiber) {
    //...
    return nextFiber.next
}

function workLoop(deadline) {
    while (nextFiber && !shouldYield) {
        nextFiber = performUnitOfWork(nextFiber)
        shouldYield = deadline.timeReaming < 1
    }
    // 全部处理完了就 commit
    if (!nextFiber) {
        commitRoot();
    }

    requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

我们知道浏览器有一个 api 叫做requestIdleCallback,它可以在浏览器空闲的时候执行一些任务,我们用这个 api 执行 react 的更新,让高优先级的任务优先响应。对于 requsetIdleCallback函数,下面是其原理。

const temp = window.requestIdleCallback(callback[, options]);

对于普通的用户交互,上一帧的渲染到下一帧的渲染时间是属于系统空闲时间,Input 输入,最快的单字符输入时间平均是 33ms(通过持续按同一个键来触发),相当于,上一帧到下一帧中间会存在大于 16.4ms 的空闲时间,就是说任何离散型交互,最小的系统空闲时间也有 16.4ms,也就是说,离散型交互的最短帧长一般是 33ms。

requestIdleCallback 回调调用时机是在回调注册完成的上一帧渲染到下一帧渲染之间的空闲时间执行

callback 是要执行的 回调函数,会传入 deadline 对象作为参数,deadline 包含:

  1. timeRemaining:剩余时间,单位 ms,指的是该帧剩余时间。
  2. didTimeout:布尔型,true 表示该帧里面没有执行回调,超时了。

options 里面有个重要参数 timeout,如果给定 timeout,那到了时间,不管有没有剩余时间,都会立刻执行回调 callback。

但事实是requestIdleCallback 存在着浏览器的兼容性和触发不稳定的问题,所以我们需要用 js 实现一套时间片运行的机制,在 react 中这部分叫做 scheduler。同时 React 团队也没有看到任何浏览器厂商在正向的推动 requestIdleCallback 的覆盖进程,所以 React 只能采用了偏 hack 的 polyfill方案

requestIdleCallback Polyfill 方案 (Scheduler)

上面说到 requestIdleCallback 存在的问题,在 react 中实现的时间片运行机制叫做 scheduler,了解时间片的前提是了解通用场景下页面渲染的整个流程被称为一帧,浏览器渲染的一次完整流程大致为

执行 JS--->计算 Style--->构建布局模型 (Layout)--->绘制图层样式 (Paint)--->组合计算渲染呈现结果 (Composite)

帧的特性:

那么 Polyfill 方案是如何在固定帧数内控制任务执行的呢,究其根本是借助 requestAnimationFrame 让一批扁平的任务恰好控制在一块一块的 33ms 这样的时间片内执行

Lane

以上是我们的异步调度策略,但是仅有异步调度,我们怎么确定应该调度什么任务呢,哪些任务应该被先调度,哪些应该被后调度,这就引出了类似于微任务宏任务的Lane

有了异步调度,我们还需要细粒度的管理各个任务的优先级,让高优先级的任务优先执行,各个 Fiber 工作单元还能比较优先级,相同优先级的任务可以一起更新

关于 lane 的设计可以看下这篇:

https://github.com/facebook/react/pull/18796​github.com/facebook/react/pull/18796

应用场景

有了上面所介绍的这样一套异步可中断分配机制,我们就可以实现 batchUpdates 批量更新等一系列操作

img

更新 fiber 前

img

更新 fiber 后

以上除了 cpu 的瓶颈问题,还有一类问题是和副作用相关的问题,比如获取数据、文件操作等。不同设备性能和网络状况都不一样,react 怎样去处理这些副作用,让我们在编码时最佳实践,运行应用时表现一致呢,这就需要 react 有分离副作用的能力。

FAQ

#faq/framework

React 到底从哪里 Fiber 开始 "diff"

react 到底从哪里 fiber 开始 "diff" - 掘金

如何手动创建立即执行(高优先级)的任务?

scheduler 是 react 内部的调度过程, 不知道如何创建, 难道要引用 react 暴露出来的 scheduleCallback 吗?

为什么 Vue 不需要 Fiber

react 在每次试图更新视图的时候要做的计算工作(粗略称为 reconcile)比较多,如果页面复杂到一定程度,一次连续 reconcile 所需要的 cpu 时间可能会超过 16ms,这时就会卡住页面渲染任务不能按时进行,出现用户可感知的页面卡顿或者停滞,也就是感觉不顺滑不跟手。所以需要 fiber 这样比较复杂的技术来把一次 reconcile 些工作切分成多个更小的单元,分多次执行,从而避免卡住页面。

vue 曾经一度也是想要搞类似的功能的,但后来发现因为 vue 页面更新原理不同,是细粒度追踪数据变化,比较精准地更新 dom 的,每次页面更新所需要的 cpu 计算时间比 react 少很多,几乎不太可能超过 16ms,不太可能卡住页面,fiber 几乎没有必要,只会无谓增加体积和复杂度,所以后来就放弃开发这个功能了。