React 15 Stack Reconciler
是通过递归更新子组件 。由于递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。
React16 Fiber Reconciler
通过把diff算法分成很多小片。当一个小片执行完成时,由浏览器判断是否有时间继续执行新任务,没时间就终止执行,有时间就检查任务列表中有没有新的、优先级更高的任务,有就做这个新任务,一直重复这个操作。
简单理解就是把一个耗时长的任务分解为一个个的工作单元(每个工作单元运行时间很短,不过总时间依然很长)。在执行工作单元之前,由浏览器判断是否有空余时间执行,有时间就执行工作单元,执行完成后,继续判断是否还有空闲时间。没有时间就终止执行让浏览器执行其他任务(如GUI线程等)。等到下一帧执行时判断是否有空余时间,有时间就从终止的地方继续执行工作单元,一直重复到任务结束。
Fiber架构 = Fiber节点 + Fiber调度算法
链表结构
要让终止的任务恢复执行,就必须知道下一工作单元对应那一个。所以要实现工作单元的连接,就要使用链表,在每个工作单元中保存下一个工作单元的指针,就能恢复任务的执行。
requestIdleCallback
要知道每一帧的空闲时间,就需要使用 requestIdleCallback Api。传入回调函数,回调函数接收一个参数(剩余时间),如果有剩余时间,那么就执行工作单元,如果时间不足了,则继续requestIdleCallback,等到下一帧继续判断。
数据结构
使用 Fiber节点
, 来代替虚拟DOM原来的结构。
// 链表结构
export type Fiber = {
// Fiber 类型信息
type: any,
// 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)
stateNode: any,
...
// 指向父节点,或者render该节点的组件
return: Fiber | null,
// 指向第一个子节点
child: Fiber | null,
// 指向下一个兄弟节点
sibling: Fiber | null,
}
通过ReactDOM.render()
和 setState
把待更新的任务会先放入队列中, 然后通过 requestIdleCallback 请求浏览器调度。
// 更新节点 放入数组中
updateQueue.push(updateTask);
requestIdleCallback(performWork, {
timeout});
现在浏览器有空闲或者超时了就会调用performWork
来执行任务:
// performWork 会拿到一个Deadline,表示剩余时间
function performWork(deadline) {
// 循环取出updateQueue中的任务
while (updateQueue.length > 0 && deadline.timeRemaining() > ENOUGH_TIME) {
workLoop(deadline);//
}
// 如果在本次执行中,未能将所有任务执行完毕,那就再请求浏览器调度
if (updateQueue.length > 0) {
requestIdleCallback(performWork);
}
}
这里的nextUnitOfWork
下一个工作单元是Fiber
结构,所以终止了之后也能恢复继续执行。
// 保存当前的处理现场
let nextUnitOfWork: Fiber | undefined // 保存下一个需要处理的工作单元
let topWork: Fiber | undefined // 保存第一个工作单元
function workLoop(deadline: IdleDeadline) {
// updateQueue中获取下一个或者恢复上一次中断的执行单元
if (nextUnitOfWork == null) {
nextUnitOfWork = topWork = getNextUnitOfWork();
}
// 每执行完一个执行单元,检查一次剩余时间
// 如果被中断,下一次执行还是从 nextUnitOfWork 开始处理
while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
// 处理节点 并 返回下一个 要处理得节点
nextUnitOfWork = performUnitOfWork(nextUnitOfWork, topWork);
}
// 提交工作,当任务全部执行完后 一次全部更新 同步执行
if (pendingCommit) {
// commit 阶段
commitAllWork(pendingCommit);
}
}
/**
* 返回下一个 要处理的 nextUnitOfWork
* @params fiber 当前需要处理的节点
* @params topWork 本次更新的根节点
*/
function performUnitOfWork(fiber: Fiber, topWork: Fiber) {
// 对该节点进行处理
// diff算法 为修改的节点打上标签
// 在fiber上 生成对应的stateNode (真实的DOM节点)
beginWork(fiber);
// 如果存在子节点,那么下一个待处理的就是子节点
if (fiber.child) {
return fiber.child;
}
// 没有子节点了,上溯查找兄弟节点
let temp = fiber;
while (temp) {
completeWork(temp);// 收集副作用函数 commit 阶段执行
// 到顶层节点了, 退出
if (temp === topWork) {
break
}
// 找到,下一个要处理的就是兄弟节点
if (temp.sibling) {
return temp.sibling;
}
// 没有, 继续上溯
temp = temp.return;
}
}
diff算法
对比步骤
简介渲染阶段
协调阶段完成后生成了 WorkInProgress Tree
,在有修改的Fiber
节点中都有一个标签,在Renderer 阶段循环 WorkInProgress Tree
进行修改节点然后渲染到页面上。
// 任务都执行完后 进入commit 修改真实Tree
function commitAllWork(fiber) {
if(!fiber) {
return;
}
const parentDom = fiber.return.dom;
if(fiber.effectTag === 'REPLACEMENT' && fiber.dom) {
parentDom.appendChild(fiber.dom);
} else if(fiber.effectTag === 'DELETION') {
parentDom.removeChild(fiber.dom);
} else if(fiber.effectTag === 'UPDATE' && fiber.dom) {
// 更新DOM属性
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}
// 递归操作子元素和兄弟元素
commitRootImpl(fiber.child);
commitRootImpl(fiber.sibling);
}
React 技术揭秘
这可能是最通俗的 React Fiber(时间分片) 打开方式
Deep In React 之浅谈 React Fiber 架构(一)