【React源码笔记5】- scheduleWork1

React | 2020-07-06 23:27:17 646次 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 次就停止并抛出错误。

比如我们在 rendershouldComponentUpdatecomponentWillUpdate 钩子内触发了 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人赞

分享到: