react源码阅读- fiber架构探索(二)
React 团队在 React 的v16版本中重写了 React 的核心算法 - reconciliation,称为fiber reconciler,简称为Fiber。
Fiber Tree
Fiber Reconciler 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树。这棵树是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。
Fiber 树在首次渲染的时候会一次过生成。在后续需要 Diff 的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程:
如果过程中有优先级更高的任务需要进行,则 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