# React任务调度
上篇分析了组件的初始化,原本想把挂载方式也一块解析下,发现这挂载嵌在了任务调度的最后一个环节,而这任务调度实属复杂,本篇就浅析一下这个任务调度。
# Reconciler
在16+的React版本 Fiber Reconciler(调和器) 推出之前,React 用的是 Stack Reconciler,它是自顶向下递归渲染及更新的,持续占用主线程并且是无法中断的,主线程上的布局、动画等周期性任务以及交互响应就无法立即得到处理,假如交互反馈延迟稍微大一丢丢,就会有明显的卡顿,这是影响体验的。
而 Fiber 就是为了解决上面的问题而诞生的,它把渲染或者更新过程拆分成一系列小任务,做完看是否有时间继续下一个任务,有的话继续,没有则自己挂起,主线程优先做更高优先级的任务,待到主线程不忙的时候再继续不太紧急的任务。这一切的实现是在代码层引入了新的数据结构对象 Fiber ,每一个组件实例都对应一个fiber实例,fiber实例负责管理组件实例的更新,渲染以及与其他fiber实例的联系。
初步了解 Fiber 的目的以后,我们回到上一章节的思维导图:
首先我们去看下 Fiber 的数据结构,从 createFiberRoot()
切入,我们发现最终调用 new FiberNode()
创建了一个作用于组件的 Fiber 对象
# FiberNode
function FiberNode(tag, pendingProps, key, mode) {
// 标记不同的组件类型
this.tag = tag;
// ReactElement 里面的 key
this.key = key;
// ReactElement.type,也就是我们调用`createElement`的第一个参数
this.elementType = null;
// fiber 对应的 function/class/module 类型组件名.
this.type = null;
// fiber 所在组件树的根组件 FiberRoot 对象
this.stateNode = null;
// 处理完当前 fiber 后返回的 fiber,
// 返回当前 fiber 所在 fiber 树的父级 fiber 实例
this.return = null;
// fiber 树结构相关属性
// 指向自己的第一个子节点
this.child = null;
// 指向自己的兄弟结构
this.sibling = null;
this.index = 0;
// ref属性
this.ref = null;
// 当前处理过程中的组件 props 对象
this.pendingProps = pendingProps;
// 缓存上一次渲染完成之后的 props 对象
this.memoizedProps = null;
// 该组件状态更新及对应回调函数的存储队列
this.updateQueue = null;
// 上一次渲染的时候的state
this.memoizedState = null;
// 存放这个 fiber 依赖的 context
this.contextDependencies = null;
// 创建时候的标识,用来描述当前 fiber 和它子树的
this.mode = mode;
// Effects
// 用来记录Side Effect
this.effectTag = NoEffect;
// 单链表用来快速查找下一个side effect
this.nextEffect = null;
// 子树中第一个side effect
this.firstEffect = null;
// 子树中最后一个side effect
this.lastEffect = null;
// 更新任务的最晚执行时间,注意不包括他的子树产生的任务
this.expirationTime = NoWork;
// 快速确定子树中是否有不在等待的变化
this.childExpirationTime = NoWork;
// fiber的版本池,记录fiber更新过程,便于在发生冲突需要回退时快速恢复
this.alternate = null;
// 调试相关,收集每个Fiber和子树渲染时间的
if (enableProfilerTimer) {
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN;
this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}
{
this._debugID = debugCounter++;
this._debugSource = null;
this._debugOwner = null;
this._debugIsCurrentlyTiming = false;
this._debugHookTypes = null;
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this);
}
}
}
都说 Fiber 可以切分任务并设置不同优先级,它是如何做的又是怎样表现的?
从上面的数据结构是不是就可以看出来了?就是 expirationTime
,实现调度的方式正是给每一个fiber实例设置到期执行时间,不同时间即代表不同优先级,到期时间越短,则代表优先级越高,需要尽早执行。
# scheduleRootUpdate
下面我们接着看看任务调度那一块 scheduleRootUpdate()
function scheduleRootUpdate(current$$1, element, expirationTime, callback) {
// ...
// 创建一个更新用的初始化对象
var update = createUpdate(expirationTime);
update.payload = { element: element };
callback = callback === undefined ? null : callback;
if (callback !== null) {
// ...
update.callback = callback;
}
// 根据回调来判断是关闭还是跟踪状态
flushPassiveEffects();
// 记录当前fiber的版本,加入更新队列
enqueueUpdate(current$$1, update);
scheduleWork(current$$1, expirationTime);
return expirationTime;
}
# scheduleWork
scheduleWork
这一步非常重要,
function scheduleWork(fiber, expirationTime) {
// 找到当前 Fiber的 root
var root = scheduleWorkToRoot(fiber, expirationTime);
// ...
// 如果不是工作状态,并且之前执行过任务,并且当前任务执行的时间比之前的执行的任务时间要大(就是优先级要低的意思)
if (!isWorking && nextRenderExpirationTime !== NoWork && expirationTime > nextRenderExpirationTime) {
// 中断任务
interruptedBy = fiber;
// 重置所有公共变量
resetStack();
}
// 记录各项时间
markPendingPriorityLevel(root, expirationTime);
// 如果在渲染阶段,我们会在退出之前安排好更新,除非这是一个不同的根(应用程序有多个root)。
if (!isWorking || isCommitting$1 || nextRoot !== root) {
// 更新过期时间
var rootExpirationTime = root.expirationTime;
// 开始处理任务
requestWork(root, rootExpirationTime);
}
// nestedUpdateCount初始值为0,在commit阶段会检查是否这是一个嵌套的更新,如果下一个根之前是一模一样的根,它就是一个嵌套更新,为了防止无限循环就会进行自增,一旦达到50次(NESTED_UPDATE_LIMIT)
if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
// 重置nestedUpdateCount变量,后续不更新
nestedUpdateCount = 0;
invariant(false, 'Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.');
}
}
# requestWork
现在看下任务处理
function requestWork(root, expirationTime) {
// 把 root 加入到调度队列,不会存在两个相同的 root 前后出现在队列中
addRootToSchedule(root, expirationTime);
if (isRendering) {
return;
}
// 这里涉及到事件系统,后续再进行写作分析
if (isBatchingUpdates) {
if (isUnbatchingUpdates) {
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, false);
}
return;
}
// 根据 expirationTime 来执行同步还是异步任务,最终都会调用 performSyncWork
if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpirationTime(root, expirationTime);
}
}
仔细往里面看,会发现同步任务 performSyncWork
和异步任务scheduleCallbackWithExpirationTime
最终都会调用 performSyncWork
方法。
同步异步的处理都在 performWorkOnRoot
里进行,如果有上次遗留的任务,会直接调用completeRoot
进到提交阶段。如果没有就调 renderRoot
开始渲染阶段。
异步任务主要是渲染的时候判断一下时间,如果没时间了,先把 finishedWork
赋给全局,下次循环处理。
# completeRoot
我们先沿着提交阶段的线索 completeRoot
往下看
function completeRoot(root, finishedWork, expirationTime) {
// 检查是否有一批这个过期时间相匹配。
var firstBatch = root.firstBatch;
if (firstBatch !== null && firstBatch._expirationTime >= expirationTime) {
if (completedBatches === null) {
completedBatches = [firstBatch];
} else {
completedBatches.push(firstBatch);
}
if (firstBatch._defer) {
// 满足这个条件的这批根无法提交,直到收到新的更新
root.finishedWork = finishedWork;
root.expirationTime = NoWork;
return;
}
}
// 提交根.
root.finishedWork = null;
// 检查是否这是一个嵌套的更新(同步更新计划中提交阶段)
if (root === lastCommittedRootDuringThisBatch) {
// 如果下一根之前一样的根,这是一个嵌套的更新。为了防止无限循环,增加嵌套的更新计数。
nestedUpdateCount++;
} else {
// 重置根开关
lastCommittedRootDuringThisBatch = root;
nestedUpdateCount = 0;
}
unstable_runWithPriority(unstable_ImmediatePriority, function () {
commitRoot(root, finishedWork);
});
}
最终会更新 expirationTime
值和重置 finishedWork
为 null
。
而 renderRoot
则是开始渲染阶段了,里面有个 workLoop
循环机制不管是同步任务还是异步任务都要进行 performUnitOfWork
通过 beginWork
进行各项子节点的调和更新,直到完成工作进行 createInstance
,创建 DOM 元素并添加至文档,最后通过 onComplete
更新 root
节点的 pendingCommitExpirationTime
为当前的过期时间 expirationTime
值和 finishedWork
的值。想要挖掘 beginWork
里的具体调和更新细节有兴趣的同学可以再钻研进去看看。
# createInstance
function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
var parentNamespace = void 0;
// ...
// 真实创建dom
var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
return domElement;
}
precacheFiberNode
个人认为是新建了一个 new FiberNode()
的实例,而updateFiberProps
方法是将真实 dom 和 fiber,props关联在一起了,互相引用。
看到这里终于知道16+的 React 是在 createInstance 开始创建 dom 实例的,也就是之前所说的组件挂载就是在这里准备开始执行的。
# 总结
# 参考文章
https://react.jokcy.me/