React | 2020-07-06 23:27:17 823次 0次
前面四篇基本上没有多少核心逻辑,都是一些基本数据结构创建,到了这里才开始真正的调度,react 中目前分为三个阶段:
Scheduler(调度器,可中断)—— 调度任务的优先级,高优任务优先进入Reconciler
Reconciler(协调器,可中断)—— 又称为 Render阶段,主线是构建workInprogress Fibe节点树,钩子函数、diff等,准备好线性任务链effect list
Renderer(渲染器,不可中断)—— commit阶段,主要目标是根据线性任务链完成finishedWork Fibe节点树中记录的任务,实现UI的更新。
往基础数据上挂载一些渲染用的节点新和更新信息,调度 update 任务,其中初始渲染、数据更新都会触发这里,先简要看下这里面大致流程:
ReactFiberWorkLoop.js export const scheduleWork = scheduleUpdateOnFiber; export function scheduleUpdateOnFiber( fiber: Fiber, expirationTime: ExpirationTime, ) { //避免无限循环update checkForNestedUpdates(); //开发环境 略 warnAboutRenderPhaseUpdatesInDEV(fiber); //拿到FiberRoot根节点并更新时间 const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime); //root不存在校验给出错误提示 if (root === null) { warnAboutUpdateOnUnmountedFiberInDEV(fiber); return; } //判断是否有高优先级任务打断当前正在执行的任务 checkForInterruption(fiber, expirationTime); //报告调度更新,测试环境用的,可不看 recordScheduleUpdate(); //获取当前调度任务的优先级 const priorityLevel = getCurrentPriorityLevel(); //之前提到的 三种模式 legacy 模式(默认) blocking 模式 concurrent 模式 //默认 expirationTime 设置为 Sync if (expirationTime === Sync) { if ( //executionContext这个值初始为8,所以初始这两个条件初始都是成立的, //后面调度发生一些变化,这个值会随着改变 // 未开启批量更新 LegacyUnbatchedContext不变 0b000000 & 0b001000 = 0b000000 (executionContext & LegacyUnbatchedContext) !== NoContext && // 未渲染过 也就是初次渲染 (executionContext & (RenderContext | CommitContext)) === NoContext ) { //用来处理交互引起的更新,跟踪这些更新,并计数、检测它们是否会报错 schedulePendingInteractions(root, expirationTime); //同步模式,这里后续估计官方也会继续修改,源码一直变化中,尤其是调度这里 performSyncWorkOnRoot(root); } else { //批量更新的模式下进入调度,但是同时多个setState操作会被return掉,确保异步更新 ensureRootIsScheduled(root); schedulePendingInteractions(root, expirationTime); //如果处于非批量更新的状态下会进入这里立即执行了 //(比如定时器中的多个set操作,除非手动调用那个批量钩子,修改 executionContext 的值) if (executionContext === NoContext) { //将所有同步任务推入真正的任务队列。如果第一次的同步任务会直接加入调度队列 flushSyncCallbackQueue(); } } } else { ensureRootIsScheduled(root); schedulePendingInteractions(root, expirationTime); } //DiscreteEvent 离散事件. 例如blur、focus、 click、 submit、 touchStart. // UserBlockingEvent用户阻塞事件. 例如touchMove、mouseMove、scroll、drag、dragOver等等。 // 这些事件会'阻塞'用户的交互。 // ContinuousEvent可连续事件。例如load、error、loadStart、abort、animationEnd. 这个优先级最高, //也就是说它们应该是立即同步执行的,这就是Continuous的意义,即可连续的执行,不被打断. //和事件系统 flushDiscreteUpdates这个api有关,官方还在维护中,这个先简单了解 if ( (executionContext & DiscreteEventContext) !== NoContext && (priorityLevel === UserBlockingPriority || priorityLevel === ImmediatePriority) ) { //处理低优先级,必要时更新 if (rootsWithPendingDiscreteUpdates === null) { rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]); } else { const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root); if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) { rootsWithPendingDiscreteUpdates.set(root, expirationTime); } } } }
下面进行逐步看下这个里面的具体做的事情。
一、checkForNestedUpdates
检测无线循环触发更新,超过 50 次就停止并抛出错误。
比如我们在 render、shouldComponentUpdate、componentWillUpdate 钩子内触发了 setState 操作,则会造成一直触发更新的操作,则会爆栈,所以这里需要检测下,所以我们要避免这种操作。nestedUpdateCount 更新会自加一次,NESTED_UPDATE_LIMIT 默认 50。
//避免无限循环update 最深50层,给出友好提示, 比如render中触发setState等情况 function checkForNestedUpdates() { if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { nestedUpdateCount = 0; rootWithNestedUpdates = null; 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.', ); } ... }
二、markUpdateTimeFromFiberToRoot
这个操作用来找到 rootFiber 并遍历更新子节点的 expirationTime,初次渲染时候这个里面的 fiber 里面没有构成数据关系,只有 stateNode(FiberRoot)的值和 updateQueue 被创建。这里说明react每次都是从根节点开始再调度的,因为一直往上寻找根。
//目标fiber会向上寻找rootFiber对象,在寻找的过程中会进行一些操作 function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { // 提升 fiber 优先级 默认创建时这个值为0 if (fiber.expirationTime < expirationTime) { fiber.expirationTime = expirationTime; } //初始不存在,后面为了优化,复用节点 let alternate = fiber.alternate; //提升优先级 if (alternate !== null && alternate.expirationTime < expirationTime) { alternate.expirationTime = expirationTime; } //向上遍历父节点,直到root节点,在遍历的过程中更新子节点的expirationTime let node = fiber.return; let root = null; //找到 RootFiber 节点,返回它的stateNode即 FiberRoot if (node === null && fiber.tag === HostRoot) { root = fiber.stateNode; } else { //反之继续向上查找 这里初始渲染不会走 while (node !== null) { alternate = node.alternate; /** * expirationTime代表节点本身是否有更新,如果节点本身有更新,那么它的更新可能会影响子树; * childExpirationTime表示它的子树是否产生了更新; * 如果两个都没有,那么子树是不需要更新的。 * * 同一个节点产生的连续两次更新,最红在父节点上只会体现一次childExpirationTime * 不同子树产生的更新,最终体现在跟节点上的是优先级最高的那个更新 */ if (node.childExpirationTime < expirationTime) { node.childExpirationTime = expirationTime; if ( alternate !== null && alternate.childExpirationTime < expirationTime ) { alternate.childExpirationTime = expirationTime; } //更新 alternate 过期时间 } else if ( alternate !== null && alternate.childExpirationTime < expirationTime ) { alternate.childExpirationTime = expirationTime; } // FiberRoot if (node.return === null && node.tag === HostRoot) { root = node.stateNode; break; } node = node.return; } } if (root !== null) { if (workInProgressRoot === root) { // Received an update to a tree that's in the middle of rendering. Mark // that's unprocessed work on this root. workInProgressRootNextUnprocessedUpdateTime值更新 markUnprocessedUpdateTime(expirationTime); } // Mark that the root has a pending update. markRootUpdatedAtTime(root, expirationTime); } return root; }
前面的逻辑很好理解,while 里面的逻辑主要是更新时候会用到,这里先熟悉下。markUnprocessedUpdateTime 方法用来更新 workInProgressRootNextUnprocessedUpdateTime 标记未处理的更新。最后执行 markRootUpdatedAtTime 更新一些时间的概念,初始的时候只会改变了 firstPendingTime,里面的逻辑等后面更新时候再分析下,目前还不清楚都是用在哪里。
三、checkForInterruption
如果当前 fiber 的优先级更高,需要打断当前执行的任务,立即执行该 fiber 上的 update,则更新 interruptedBy,这个函数开发环境才会执行。
function checkForInterruption( fiberThatReceivedUpdate: Fiber, updateExpirationTime: ExpirationTime, ) { // enableUserTimingAPI 为 __DEV__ 打包后没有这个东西了就 // 如果处于更新状态 & 更新的优先级高于现在渲染的情况,赋值给 interruptedBy 标志着打断现在的渲染 if ( enableUserTimingAPI && workInProgressRoot !== null && updateExpirationTime > renderExpirationTime ) { interruptedBy = fiberThatReceivedUpdate; } }
四、getCurrentPriorityLevel
获取当前调度任务的优先级,默认返回的是 97 这个标志数字。
//优先级标识 越大优先级越高 export const ImmediatePriority: ReactPriorityLevel = 99; export const UserBlockingPriority: ReactPriorityLevel = 98; export const NormalPriority: ReactPriorityLevel = 97; export const LowPriority: ReactPriorityLevel = 96; export const IdlePriority: ReactPriorityLevel = 95; //Scheduler_getCurrentPriorityLevel来自 scheduler 这个包,默认值返回 Scheduler_NormalPriority export function getCurrentPriorityLevel(): ReactPriorityLevel { switch (Scheduler_getCurrentPriorityLevel()) { case Scheduler_ImmediatePriority: return ImmediatePriority; case Scheduler_UserBlockingPriority: return UserBlockingPriority; case Scheduler_NormalPriority: return NormalPriority; case Scheduler_LowPriority: return LowPriority; case Scheduler_IdlePriority: return IdlePriority; default: invariant(false, 'Unknown priority level.'); } }
五、schedulePendingInteractions
错误跟踪,__interactionsRef.current 是一个 Set 数据结构,能用来识别更新是由什么引起的,尽管这个追踪更新的 API 依然是实验性质的。利用 FiberRoot 的 pendingInteractionMap 属性和不同的expirationTime,获取每次 schedule 所需的 update 任务的集合,记录它们的数量,并检测这些任务是否会出错。
function schedulePendingInteractions(root, expirationTime) { ... scheduleInteractions(root, expirationTime, __interactionsRef.current); } function scheduleInteractions(root, expirationTime, interactions) { ... if (interactions.size > 0) { ... const subscriber = __subscriberRef.current; if (subscriber !== null) { const threadID = computeThreadID(root, expirationTime); subscriber.onWorkScheduled(interactions, threadID); } } }
//遍历检测,抛出错误 function onWorkScheduled( interactions: Set<Interaction>, threadID: number, ): void { let didCatchError = false; let caughtError = null; subscribers.forEach(subscriber => { try { subscriber.onWorkScheduled(interactions, threadID); } catch (error) { if (!didCatchError) { didCatchError = true; caughtError = error; } } }); if (didCatchError) { throw caughtError; } }
六、ensureRootIsScheduled
到这里进入一个分水岭,开始调度,本来想先把 performSyncWorkOnRoot 放在这里,因为在初始渲染时候同步模式下直接调用了(里面又调用的调度方法),后来发现 performSyncWorkOnRoot 的过程应该属于 render 阶段了,里面进行工作循环,由于 ensureRootIsScheduled 方法中调用了同步调度方法和异步调度的方法(这里面去执行同步 or 异步任务),所以单独的同步渲染就直接从异步渲染的分支中进行看,因为有过期任务的时候是必须立即走同步的渲染。还是直接先看调度这个方法,开始安排调度:
将root加入schedule,root上每次只能存在一个scheduled的任务
每次创建update后都会调用这个函数,需要考虑如下情况:
1. root上有过期任务,需要以ImmediatePriority(同步不中断)立刻调度该任务
2. root上已有schedule但还未到时间执行的任务,比较新旧任务expirationTime和优先级处理
3. root上还没有已有schedule的任务,则开始该任务的render阶段
function ensureRootIsScheduled(root: FiberRoot) { const lastExpiredTime = root.lastExpiredTime; //lastExpiredTime 初始值为 noWork,只有当任务过期时,会被更改为过期时间(markRootExpiredAtTime方法) if (lastExpiredTime !== NoWork) { //过期的任务 需要立马执行(这种情况肯定是需要快速的同步执行完) root.callbackExpirationTime = Sync; root.callbackPriority = ImmediatePriority; //推入 syncQueue 队列,批量更新 root.callbackNode = scheduleSyncCallback( performSyncWorkOnRoot.bind(null, root), ); return; } //获取任务的下一个到期时间 const expirationTime = getNextRootExpirationTimeToWorkOn(root); const existingCallbackNode = root.callbackNode; if (expirationTime === NoWork) { // 没有任务 if (existingCallbackNode !== null) { root.callbackNode = null; root.callbackExpirationTime = NoWork; root.callbackPriority = NoPriority; } return; } //获取当前时间 const currentTime = requestCurrentTimeForUpdate(); // 根据过去时间和当前时间计算出任务优先级 const priorityLevel = inferPriorityFromExpirationTime( currentTime, expirationTime, ); // 如果存在一个渲染任务 if (existingCallbackNode !== null) { const existingCallbackPriority = root.callbackPriority; const existingCallbackExpirationTime = root.callbackExpirationTime; if ( // Callback must have the exact same expiration time. existingCallbackExpirationTime === expirationTime && // Callback must have greater or equal priority. existingCallbackPriority >= priorityLevel ) { // 该root已经存在的任务expirationTime和新udpate产生的expirationTime一致 // 这代表他们可能是同一个事件触发产生的update // 且已经存在的任务优先级更高,则可以取消这次update的render return; } // 否则代表新udpate产生的优先级更高,取消之前的schedule,重新开始一次新的调度 cancelCallback(existingCallbackNode); } root.callbackExpirationTime = expirationTime; root.callbackPriority = priorityLevel; // 保存 Scheduler保存的 当前正在进行的异步任务 let callbackNode; if (expirationTime === Sync) { // 过期任务立即走同步执行 callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root)); } else if (disableSchedulerTimeoutBasedOnReactExpirationTime) { //忽略 callbackNode = scheduleCallback( priorityLevel, performConcurrentWorkOnRoot.bind(null, root), ); } else { // 正常的异步任务和Concurrent首次渲染走走这里 callbackNode = scheduleCallback( priorityLevel, performConcurrentWorkOnRoot.bind(null, root), // 根据expirationTime,为任务计算一个timeout // timeout会影响任务执行优先级 {timeout: expirationTimeToMs(expirationTime) - now()}, ); } root.callbackNode = callbackNode; }
这个调度中做了两件主要的事情,添加同步或异步的两个调度方法:scheduleSyncCallback 和 scheduleCallback 。
export function scheduleCallback( reactPriorityLevel: ReactPriorityLevel, callback: SchedulerCallback, options: SchedulerCallbackOptions | void | null, ) { //这里和上面的优先级相反的值 const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel); return Scheduler_scheduleCallback(priorityLevel, callback, options); } export function scheduleSyncCallback(callback: SchedulerCallback) { // Push this callback into an internal queue. We'll flush these either in // the next tick, or earlier if something calls `flushSyncCallbackQueue`. if (syncQueue === null) { syncQueue = [callback]; // Flush the queue in the next tick, at the earliest. immediateQueueCallbackNode = Scheduler_scheduleCallback( Scheduler_ImmediatePriority, //最高优先级 flushSyncCallbackQueueImpl, ); } else { ... syncQueue.push(callback); } return fakeCallbackNode; }
这两个方法最终调用的都是 Scheduler_scheduleCallback,不同的是同步任务只是将任务推进一个任务队列,等待被 flushSyncCallbackQueueImpl 批量执行,异步任务则会直接推入一个调度队列。
下一篇继续进行调度过程分析,进入 unstable_scheduleCallback。
0人赞