浅谈React Fiber--比Thread更精密的并发处理机制

2,447 阅读7分钟

转载请保留这部分内容,注明出处。
关注公众号“头号前端”,每周新鲜前端好文推送。

另外,头条号前端团队非常 期待你的加入

一、什么是Fiber

1. Fiber

大家应该都清楚进程(Process)和线程(Thread)的概念,在计算机科学中还有一个概念叫做Fiber,英文含义就是“纤维”,意指比Thread更细的线,也就是比线程(Thread)控制得更精密的并发处理机制。

2. React Fiber

React Fiber是对核心算法的一次重新实现,用于替换原本的Stack Reconciler

直观的感受:claudiopro.github.io/react-fiber…

3. 为什么Stack reconsiler会导致丢帧

<div>
   <Foo>
      <Bar />
   </Foo>
</div>

上面的JSX经过编译会变成递归调用的代码,当组件树很深的时候,需要一次性去Diff组件的变化会消耗很长的时间,导致script时间变长。React之前做的优化是类似用 shouldComponentUpdate 来跳过一些组件的Diff。

二、Fiber优化的思路

1. 背景

React的基本架构

react 即 reconsiler(调度者),react-dom则是 renderer 调度者一直都是又 React 本身决定,而 renderer 则可以由社区控制和贡献。如ReactNative、React-dom等等, 而Fiber是属于reconsiler的优化。

2. Fiber细节

2.1 生成Fiber节点

React Element

当一个模板被传入到 JSX 编译器,最终会生成 React Element 。它其实就是 React 组件的 render 方法返回的实际内容,并不是 HTML

class ClickCounter {
    //...
    render() {
        return [
            React.createElement(
                'button',
                {
                    key: '1',
                    onClick: this.onClick
                },
                'Update counter'
            ),
            React.createElement(
                'span',
                {
                    key: '2'
                },
                this.state.count
            )
        ]
    }
}
// 这里调用的  React.createElement  方法返回了如下所示的两个数据结构:
[
    {
        $$typeof: Symbol(react.element),
        type: 'button',
        key: "1",
        props: {
            children: 'Update counter',
            onClick: () => { ... }
        }
    },
    {
        $$typeof: Symbol(react.element),
        type: 'span',
        key: "2",
        props: {
            children: 0
        }
    }
]

Fiber Nodes

fiber 节点就是描述随后要执行工作的一种数据结构,可以类比于调用栈中的帧 ,换而言之,就是一个工作单元。fiber 架构同时还提供了便捷的方式去追踪,调度,暂停,中断协调进程。

当一个 React Element 第一次被转换为 fiber 节点的时候,React 将会从 React Element 种提取数据子并在createFiberFromTypeAndProps函数中创建一个新的 fiber。随后的更新过程中 React会复用这个创建的 fiber 节点,并将对应 React Element 被改变数据更新到这个 fiber 节点上。React 也会移除一些 fiber节点,例如:当同层级上对应 key属性改变时,或 render 方法返回的 React Element 没有该 fiber 对应的 Element 对象时,该 fiber 就会被删除。

因为 React 会为每个得到的 React Element 创建 fiber,这些 fiber 节点被连接起来组成 fiber tree。在我们的例子中它是这样的

2.2 分为Reconciliation 阶段和Commit 阶段

在React Fiber中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,这时候,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来

因为一个更新过程可能被打断,所以React Fiber一个更新过程被分为两个阶段(Phase):第一个阶段Reconciliation Phase和第二阶段Commit Phase。

在第一阶段Reconciliation Phase,React Fiber会找出需要更新哪些DOM,这个阶段是可以被打断的;但是到了第二阶段Commit Phase,那就一鼓作气把DOM更新完,绝不会被打断。

requestIdleCallback

var  = .requestIdleCallback([, ])
  • callback: () :回调即空闲时需要执行的任务,接收一个对象作为入参。其中 [IdleDeadline](https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline) 对象包含:

    • didTimeout ,布尔值,表示任务是否超时,结合 timeRemaining 使用

    • timeRemaining() ,表示当前帧剩余的时间,也可理解为留给任务的时间还有多少

  • options :目前 options 只有一个参数

    • timeout :如果指定了timeout并具有一个正值,并且尚未通过超时毫秒数调用回调,那么回调会在下一次空闲时期被强制执行,尽管这样很可能会对性能造成负面影响。
  • cancelIdleCallback

React实现的requestIdleCallback

  1. 由于 requestIdleCallback 兼容性不是特别好

  1. 且只能一秒调用回调 20 次

无法满足实际的需求,React自己实现了一个版本.

实现 requestIdleCallback 函数的核心是:如何多次在浏览器空闲时且是渲染后才调用回调方法?

使用requestAnimationFrame 和 setTimeout完成多次执行

requestAnimationFrame 的回调方法会在每次重绘前执行,另外它还存在一个瑕疵:页面处于后台时该回调函数不会执行,因此我们需要对于这种情况做个补救措施

rAFID = requestAnimationFrame(function(timestamp) {
  // cancel the setTimeout
  localClearTimeout(rAFTimeoutID);
  callback(timestamp);
});
rAFTimeoutID = setTimeout(function() {
  // 定时 100 毫秒是算是一个最佳实践
  localCancelAnimationFrame(rAFID);
  callback(getCurrentTime());
}, 100);

计算当前帧是否空闲

简单来说就是假设当前时间为 5000,浏览器支持 60 帧,那么 1 帧近似 16 毫秒,那么就会计算出下一帧时间为 5016,判断时间是否小于5016.

渲染后执行任务

在渲染以后只有宏任务是最先会被执行的,因此宏任务就是我们实现这一步的操作了。

但是生成一个宏任务有很多种方式并且各自也有优先级,那么为了最快地执行任务,我们肯定得选择优先级高的方式。在这里我们选择了 MessageChannel 来完成这个任务,不选择 setImmediate 的原因是因为兼容性太差。

2.3 Reconciliation的调度过程

  • 首先每个任务都会有各自的优先级,高优先级的任务会打断低优先级任务

  • 在调度之前,判断当前任务是否过期,过期的话无须调度,直接生成一个任务,这样就能在渲染后马上执行过期任务了

  • 如果任务没有过期,就通过 requestAnimationFrame 启动定时器,在重绘前调用回调方法在回调方法中我们首先需要计算每一帧的时间以及下一帧的时间,然后创建一个宏任务去执行。

  • 宏任务会在渲染后被调用,在这个过程中我们首先需要去判断当前时间是否小于下一帧时间。如果小于的话就代表我们尚有空余时间去执行任务;如果大于的话就代表当前帧已经没有空闲时间了,这时候我们需要去判断是否有任务过期,过期的话不管三七二十一还是得去执行这个任务。如果没有过期的话,那就只能把这个任务丢到下一帧看能不能执行了。

3. 整体流程

  1. 初始化渲染,从React Element生成对应的Fiber树

  2. 进行setState操作,触发更新

  3. 创建workInProgress副本,进入Reconciliation执行对应的render更新。

  4. 记录有副作用的fiber节点,放入一个队列

  5. 完成Reconciliation,进入Commit阶段,取出有副作用的fiber节点,通过fiber节点的nextEffect属性访问有副作用的节点,进行更新

4. 关于生命周期

4.1 UNSAFE的声明周期

  • [UNSAFE_]componentWillMount (deprecated)

  • [UNSAFE_]componentWillReceiveProps (deprecated)

  • getDerivedStateFromProps

  • shouldComponentUpdate

  • [UNSAFE_]componentWillUpdate (deprecated)

4.2 componentWillReceiveProps和getDerivedStateFromProps

willXXX可以让用户任意操作DOM。 操作DOM会可能reflow,这是官方不愿意看到的。于是官方推出了getDerivedStateFromProps,让你在render时设置新state,你主要返回一个新对象,它就主动帮你setState。 由于这是一个静态方法,你不能操作instance,这就阻止了你多次操作setState。也没有refs,你也没有机会操作DOM了。这样一来,getDerivedStateFromProps的逻辑应该会很简单,这样就不会出错,不会出错,就不会打断DFS过程。

参考