11.React源码学习-任务调度

任务调度

任务调度图解:

[图片上传失败...(image-8a93f2-1595402854629)]

源码在 react-reconciler 下的 ReactFiberScheduler.js 内:

scheduleWork:
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
    // 更新Fiber及所有子树的expirationTime,
    // 返回FiberRoot
  const root = scheduleWorkToRoot(fiber, expirationTime);
  if (root === null) {
    // 去掉__DEV__代码
    return;
  }
  if (
    !isWorking &&
    nextRenderExpirationTime !== NoWork &&
    expirationTime > nextRenderExpirationTime
  ) {
    // This is an interruption. (Used for performance tracking.)
    interruptedBy = fiber;
    // 优先执行高优先级的任务
    resetStack();
  }
  markPendingPriorityLevel(root, expirationTime);
  if (
    // If we're in the render phase, we don't need to schedule this root
    // for an update, because we'll do it before we exit...
    !isWorking ||
    isCommitting ||
    // ...unless this is a different root than the one we're rendering.
    nextRoot !== root
  ) {
    const rootExpirationTime = root.expirationTime;
    requestWork(root, rootExpirationTime);
  }
  if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
    // Reset this back to zero so subsequent updates don't throw.
    // 防止更新中修改state,无限循环进入更新
    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:
  • 加入到root调度队列
  • 判断是否批量更新
  • 根据expirationTime判断调度类型
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
  // 处理firstScheduledRoot,lastScheduledRoot,root的expirationTime
  addRootToSchedule(root, expirationTime);
  if (isRendering) {
    // 已经开始
    return;
  }
  if (isBatchingUpdates) {
    // Flush work at the end of the batch.
    if (isUnbatchingUpdates) {
      // ...unless we're inside unbatchedUpdates, in which case we should
      // flush it now.
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, false);
    }
    return;
  }
  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }
}

setState 是同步的还是异步的?

setState本身的方法调用是同步的,但是调用setState并不标志着React的state立马就更新了,这个更新是要根据我们当前的执行环境的上文来判断的如果处于isBatchingUpdates环境下不会同步更新的,另还有异步更新调度也不会同步更新。

scheduler包(被提到与react-reconciler同级的目录):
  • 维护时间片
  • 模拟requestIdleCallback(等浏览器把要做的事做完后来调取回调)
  • 调度列表和超时判断

主要用到的方法:

1

function scheduleCallbackWithExpirationTime(root: FiberRoot, expirationTime: ExpirationTime,){}

异步进行root任务调度就是通过这个方法来做的,这里最主要的就是调用了scheduler的scheduleDeferredCallback方法(在scheduler包中是scheduleWork)

传入的的是回调函数performAsyncWork,以及一个包含timeout超时事件的对象

2

function unstable_scheduleCallback(callback, deprecated_options){}

创建一个调度节点newNode,并按照timoutAt的顺序加入到CallbackNode链表,调用ensureHostCallbackIsScheduled

这里面的expirationTime是调用时传入的timeoutAt加上当前时间形成的过期时间。

3

function ensureHostCallbackIsScheduled(){}

如果已经在调用回调了,就 return,因为本来就会继续调用下去,isExecutingCallback在flushWork的时候会被修改为true

如果isHostCallbackScheduled为false,也就是还没开始调度,那么设为true,如果已经开始了,就直接取消,因为顺序可能变了。

调用requestHostCallback开始调度

4

requestHostCallback = function(callback, absoluteTimeout){}

开始进入调度,设置调度的内容,用scheduledHostCallback和timeoutTime这两个全局变量记录回调函数和对应的过期时间

调用requestAnimationFrameWithTimeout,其实就是调用requestAnimationFrame在加上设置了一个100ms的定时器,防止requestAnimationFrame太久不触发。

调用回调animtionTick并设置isAnimationFrameScheduled全局变量为true

5

var animationTick = function(rafTime) {}

只要scheduledHostCallback还在就继续调要requestAnimationFrameWithTimeout因为这一帧渲染完了可能队列还没情况,本身也是要进入再次调用的,这边就省去了requestHostCallback在次调用的必要性

接下去一段代码是用来计算相隔的requestAnimationFrame的时差的,这个时差如果连续两次都小鱼当前的activeFrameTime,说明平台的帧率是很高的,这种情况下会动态得缩小帧时间。

最后更新frameDeadline,然后如果没有触发idleTick则发送消息

6

window.addEventListener('message', idleTick, false)

var idleTick = function(event) {}

首先判断postMessage是不是自己的,不是直接返回

清空scheduledHostCallback和timeoutTime

获取当前时间,对比frameDeadline,查看是否已经超时了,如果超时了,判断一下任务callback的过期时间有没有到,如果没有到,则重新对这个callback进行一次调度,然后返回。如果到了,则设置didTimeout为true

接下去就是调用callback了,这里设置isFlushingHostCallback全局变量为true代表正在执行。并且调用callback也就是flushWork并传入didTimeout

7

function flushWork(didTimeout) {}

先设置isExecutingCallback为true,代表正在调用callback

设置deadlineObject.didTimeout,在 React 业务中可以用来判断任务是否超时

如果didTimeout,会一次从firstCallbackNode向后一直执行,知道第一个没过期的任务

如果没有超时,则依此执行第一个callback,知道帧时间结束为止

最后清理变量,如果任务没有执行完,则再次调用ensureHostCallbackIsScheduled进入调度

顺便把Immedia优先级的任务都调用一遍。

8

 function flushFirstCallback() {}

如果当前队列中只有一个回调,清空队列

调用回调并传入deadline对象,里面有timeRemaining方法通过frameDeadline - now()来判断是否帧时间已经到了

如果回调有返回内容,把这个返回加入到回调队列

performWork:

performWork通过两种方式调用:

  • performAsyncWork 异步方式

异步情况给performWork设置的minExpirationTime是NoWork,并且会判断dl.didTimeout,这个值是指任务的expirationTime是否已经超时,如果超时了,则直接设置newExpirationTimeToWorkOn为当前时间,表示这个任务直接执行就行了,不需要判断是否超过了帧时间

  • performSyncWork 同步方式

同步方式久比较简单了,设置minExpirationTime为Sync也就是1

performWorkOnRoot

这里也分为同步和异步两种情况,但是这两种情况的区别其实非常小。

首先是一个参数的区别,isYieldy在同步的情况下是false,而在异步情况下是true。这个参数顾名思义就是是否可以中断,那么这个区别也就很好理解了。

第二个区别就是在renderRoot之后判断一下shouldYeild,如果时间片已经用完,则不直接completeRoot,而是等到一下次requestIdleCallback之后再执行。

renderRootcompleteRoot 分别对应两个阶段:

  • 渲染阶段
  • 提交阶段

渲染阶段可以被打断,而提交阶段不能

findHighestPriorityRoot

一般情况下我们的 React 应用只会有一个root,所以这里的大部分逻辑其实都不是常见情况。

循环firstScheduledRoot => lastScheduledRootremainingExpirationTimeroot.expirationTime,也就是最早的过期时间。

如果他是NoWork说明他已经没有任务了,从链表中删除。

从剩下的中找到expirationTime最小的也就是优先级最高的root然后把他赋值给nextFlushedRoot并把他的expirationTime赋值给nextFlushedExpirationTime这两个公共变量。

一般来说会直接执行下面这个逻辑

if (root === root.nextScheduledRoot) {
  // This is the only root in the list.
  root.nextScheduledRoot = null;
  firstScheduledRoot = lastScheduledRoot = null;
  break;
}
renderRoot

首先是一个判断是否需要初始化变量的判断

if (
  expirationTime !== nextRenderExpirationTime ||
  root !== nextRoot ||
  nextUnitOfWork === null
) {
  // Reset the stack and start working from the root.
  resetStack()
  nextRoot = root
  nextRenderExpirationTime = expirationTime
  nextUnitOfWork = createWorkInProgress(
    nextRoot.current,
    null,
    nextRenderExpirationTime,
  )
  root.pendingCommitExpirationTime = NoWork
}

他判断的情况是是否有新的更新进来了。假设这种情况:上一个任务因为时间片用完了而中断了,这个时候nextUnitOfWork是有工作的,这时候如果下一个requestIdleCallback进来了,中途没有新的任务进来,那么这些全局变量都没有变过,root的nextExpirationTimeToWorkOn肯定也没有变化,那么代表是继续上一次的任务。而如果有新的更新进来,则势必nextExpirationTimeToWorkOn或者root会变化,那么肯定需要重置变量

resetStack如果是被中断的情况,会推出context

然后就进入整体,调用workLoop

function workLoop(isYieldy) {
  if (!isYieldy) {
    // Flush work without yielding
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    }
  } else {
    // Flush asynchronous work until the deadline runs out of time.
    while (nextUnitOfWork !== null && !shouldYield()) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    }
  }
}

workLoop逻辑很简单的,只是判断是否需要继续调用performUnitOfWork

在workLoop执行完之后,就进入收尾阶段了。

首先如果didFatal为true,代表有一个无法处理的错误,直接调用onFatal,不commit

function onFatal(root) {
  root.finishedWork = null
}

如果nextUnitOfWork !== null,代表任务没有执行完,是yield了,执行onYield

function onYield(root) {
  root.finishedWork = null
}

如果以上都没有,说明已经complete整棵树了,如果nextRenderDidError代表有捕获到可处理的错误

这时候先判断是否有优先级更低的任务,有的话把当前的渲染时间设置进suspendTime,同时调用onSuspend

如果不符合再判断是否帧时间超时,如果没有超时并且没有root.didError,并且把root.expirationTime设置为Sync,然后调用onSuspend。

需要注意的是,他们调用onSuspend最后一个参数传递的都是-1,看onSuspend的逻辑可以发现其实什么都不做。什么都不做代表着,他们不会设置root.finishedWork,那么返回到上一层的performWorkOnRoot的时候

finishedWork = root.finishedWork
if (finishedWork !== null) {
  if (!shouldYield()) {
    // Still time left. Commit the root.
    completeRoot(root, finishedWork, expirationTime)
  } else {
    root.finishedWork = finishedWork
  }
}

并不会执行completeRoot也就不会commit,会再返回到performWork找下一个root

function onSuspend(
  root: FiberRoot,
  finishedWork: Fiber,
  suspendedExpirationTime: ExpirationTime,
  rootExpirationTime: ExpirationTime,
  msUntilTimeout: number,
): void {
  root.expirationTime = rootExpirationTime
  if (enableSuspense && msUntilTimeout === 0 && !shouldYield()) {
    // Don't wait an additional tick. Commit the tree immediately.
    root.pendingCommitExpirationTime = suspendedExpirationTime
    root.finishedWork = finishedWork
  } else if (msUntilTimeout > 0) {
    // Wait `msUntilTimeout` milliseconds before committing.
    root.timeoutHandle = scheduleTimeout(
      onTimeout.bind(null, root, finishedWork, suspendedExpirationTime),
      msUntilTimeout,
    )
  }
}

function onTimeout(root, finishedWork, suspendedExpirationTime) {
  if (enableSuspense) {
    // The root timed out. Commit it.
    root.pendingCommitExpirationTime = suspendedExpirationTime
    root.finishedWork = finishedWork
    // Read the current time before entering the commit phase. We can be
    // certain this won't cause tearing related to batching of event updates
    // because we're at the top of a timer event.
    recomputeCurrentRendererTime()
    currentSchedulerTime = currentRendererTime

    if (enableSchedulerTracing) {
      // Don't update pending interaction counts for suspense timeouts,
      // Because we know we still need to do more work in this case.
      suspenseDidTimeout = true
      flushRoot(root, suspendedExpirationTime)
      suspenseDidTimeout = false
    } else {
      flushRoot(root, suspendedExpirationTime)
    }
  }
}

其中scheduleTimeout是不同平台的setTimout

最后一个判断就是真正的挂起任务了,也就是suquense的情况,其实做的事情跟上面两个差不多,唯一的区别是调用onSuspend的时候最后一个参数肯定是大于等于零的。代表着他是立刻就要commit还是在一个timeout之后再commit。因为我们可以看到onTimeout最后是flushRoot,就是以Sync的方式调用performWork

如果以上逻辑都没有,那么直接调用onComplete

function onComplete(
  root: FiberRoot,
  finishedWork: Fiber,
  expirationTime: ExpirationTime,
) {
  root.pendingCommitExpirationTime = expirationTime
  root.finishedWork = finishedWork
}

你可能感兴趣的:(11.React源码学习-任务调度)