纵横研究院React技术专题社区

react源码阅读- fiber架构探索(二)

2019-06-07  本文已影响11人  konnga

react源码阅读- fiber架构探索(二)

React 团队在 React 的v16版本中重写了 React 的核心算法 - reconciliation,称为fiber reconciler,简称为Fiber。

Fiber Tree

Fiber Reconciler 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树。这棵树是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。

image

Fiber 树在首次渲染的时候会一次过生成。在后续需要 Diff 的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程:

image

如果过程中有优先级更高的任务需要进行,则 Fiber Reconciler 会丢弃正在生成的树,在空闲的时候再重新执行一遍。

在构造 Fiber 树的过程中,Fiber Reconciler 会将需要更新的节点信息保存在Effect List当中,在阶段二执行的时候,会批量更新相应的节点。

Fiber reconciler

reconcile过程分为2个阶段(phase):

1.(可中断)render/reconciliation 通过构造workInProgress tree得出change

2.(不可中断)commit 应用这些DOM change

render/reconciliation

以fiber tree为蓝本,把每个fiber作为一个工作单元,自顶向下逐节点构造workInProgress tree(构建中的新fiber tree);

具体过程如下(以组件节点为例):

1. 如果当前节点不需要更新,直接把子节点clone过来,跳到5;要更新的话打个tag

2. 更新当前节点状态(props, state, context等)

3. 调用shouldComponentUpdate(),false的话,跳到5

4. 调用render()获得新的子节点,并为子节点创建fiber(创建过程会尽量复用现有fiber,子节点增删也发生在这里)

5. 如果没有产生child fiber,该工作单元结束,把effect list归并到return,并把当前节点的sibling作为下一个工作单元;否则把child作为下一个工作单元

6. 如果没有剩余可用时间了,等到下一次主线程空闲时才开始下一个工作单元;否则,立即开始做

7. 如果没有下一个工作单元了(回到了workInProgress tree的根节点),第1阶段结束,进入pendingCommit状态

实际上是1-6的工作循环,7是出口,工作循环每次只做一件事,做完看要不要喘口气。工作循环结束时,workInProgress tree的根节点身上的effect list就是收集到的所有side effect(因为每做完一个都向上归并)

所以,构建workInProgress tree的过程就是diff的过程,通过requestIdleCallback来调度执行一组任务,每完成一个任务后回来看看有没有插队的(更紧急的),每完成一组任务,把时间控制权交还给主线程,直到下一次requestIdleCallback回调再继续构建workInProgress tree

Fiber之前的reconciler被称为Stack reconciler,就是因为这些调度上下文信息是由系统栈来保存的。虽然之前一次性做完,强调栈没什么意义,起个名字只是为了便于区分Fiber reconciler

requestIdleCallback

通知主线程,在有空闲时执行回调。


window.requestIdleCallback(callback[, options])

// 示例

let handle = window.requestIdleCallback((idleDeadline) => {

    const {didTimeout, timeRemaining} = idleDeadline;

    console.log(`超时了吗?${didTimeout}`);

    console.log(`可用时间剩余${timeRemaining.call(idleDeadline)}ms`);

    // do some stuff

    const now = +new Date, timespent = 10;

    while (+new Date < now + timespent);

    console.log(`花了${timespent}ms搞事情`);

    console.log(`可用时间剩余${timeRemaining.call(idleDeadline)}ms`);

}, {timeout: 1000});

// 输出结果

// 超时了吗?false

// 可用时间剩余49.535000000000004ms

// 花了10ms搞事情

// 可用时间剩余38.64ms

需要注意的是,requestIdleCallback调度只是希望做到流畅体验,并不能绝对保证什么,例如:


// do some stuff

const now = +new Date, timespent = 300;

while (+new Date < now + timespent);

如果对应React中的生命周期函数等时间上不受React控制的东西就花了300ms,什么机制也保证不了流畅。

commit

第2阶段直接一口气做完:

1. 处理effect list(包括3种处理:更新DOM树、调用组件生命周期函数以及更新ref等内部状态)

2. 出对结束,第2阶段结束,所有更新都commit到DOM树上了

同步执行,这个阶段的实际工作量是比较大的,所以尽量不要在后3个生命周期函数里干重活儿。

生命周期hook

生命周期函数也被分为2个阶段了:


// 第1阶段 render/reconciliation

componentWillMount

componentWillReceiveProps

shouldComponentUpdate

componentWillUpdate

// 第2阶段 commit

componentDidMount

componentDidUpdate

componentWillUnmount

第1阶段的生命周期函数可能会被多次调用,默认以low优先级(后面介绍的6种优先级之一)执行,被高优先级任务打断的话,稍后重新执行。

参考文章:

1. react

2. reconciliation

上一篇下一篇

猜你喜欢

热点阅读