react源码学习(二)scheduleWork任务调度

任务调度

在上一篇中说过了render的过程,最后会调用scheduleWork来执行任务。react将不同的任务分为了不同的优先级,有些任务可以异步执行,有的必须要同步执行。这样区分开来也是确保了对于用户的行为需要立即响应,所以也有了插队的机制。我们一起来看下这个过程,在源码中最后实际调用的就是scheduleUpdateOnFiber这个方法。从名字上就可以看出这里的更新主要是对fiber进行的更新,这也是为什么hooks是函数但也可以保持state的原因,因为状态是在fiber上的。这里就要看一个例子比如我要渲染这样一个组件

export default class FiberComponent extends Component{
  constructor(){
    super()
    this.state = {
      list:[1,2,3]
    }
  }
  render(){
    const { list } = this.state
    return (
      <div>
        <div>
          <input />
        </div>
        <ul>
          {
            list.map((item, index) => (
              <li key={index}>
                {item}
              </li>
            ))
          }
        </ul>
      </div>
    )
  }
}

这是一个最基本的组件渲染,在渲染时首先会创建一个fiber树。类似这样的结构。有一个唯一的root节点和rootFiber记录整个树上的一些属性。然后就是rootFiber的child指向自己第一个子节点,子节点的return指向父元素,子元素的sibling指向兄弟节点。就是用这种数据结构将整颗树串联起来的,而不是传统的多叉树。

fiber树.png

现在我们来看看scheduleUpdateOnFiber这个方法的源码

/**
 * 开始进行任务调度
 * @param {*} fiber
 * @param {*} expirationTime
 */
export function scheduleUpdateOnFiber(
  fiber: Fiber,
  expirationTime: ExpirationTime,
) {
  // 检查最大update的数量是否超过了最大值
  checkForNestedUpdates();
  warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);

  // 更新当前fiber对象和当前rootfiber根元素,root根元素的到期时间
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  if (root === null) {
    // 如果找不到root报警告
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return;
  }
  // 查看当前是否能被打断
  checkForInterruption(fiber, expirationTime);
  // 用来记录debug信息
  recordScheduleUpdate();
  const priorityLevel = getCurrentPriorityLevel();
  // 如果当前是同步更新的
  if (expirationTime === Sync) {
    if (
      // 如果正在执行的上下文是unbatchUpdate不是批量更新
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      // 检查不是render或者commit阶段
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      // GUESS: 应该是调试代码
      schedulePendingInteractions(root, expirationTime);
      // 同步调用任务
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root);
      schedulePendingInteractions(root, expirationTime);
      if (executionContext === NoContext) {
        // 执行同步更新队列
        flushSyncCallbackQueue();
      }
    }
  } else {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
  }
}

附上整个任务调度的流程图


scheduleWork任务调度.png

我们传入的是Fiber对象,在初次更新传入的是rootFiber对象,例如通过setState触发的更新就是当前dom所在的组件对应的fiber对象。
第一步我们需要用fiber对象找到根节点root元素。react遵循的是单向数据流更新都是从上到下的。这个过程就是沿着return属性一路去找到根元素,并一路更新遍历到的fiber的expirationTime
第二步检查当前有没有正在执行的任务,如果当前优先级高就需要打断之前正在执行的任务。这个有个interruptedBy看代码主要是用来调试用的
接下来会判断过期时间是同步更新的会进行一些检查就是上一篇说的unbatchedUpdates会设置一个执行上下文,并且当前没有render和commit阶段,就会直接执行performSyncWorkOnRoot方法,这个方法后边会说,先看大体流程,虽是同步更新但是是批量更新,或者当前已经进入commit阶段,那么也不能直接执行,因为js是单线程,也需要排队等待。
可以看到异步的优先级和同步的分支都会调用ensureRootIsScheduled这个方法

下边就来看看这个方法,这个是真正任务调度的入口

// 每一个root都有一个唯一的调度任务,如果已经存在,我们要确保到期时间与下一级别任务的相同,每一次更新都会调用这个方法
function ensureRootIsScheduled(root: FiberRoot) {
  const lastExpiredTime = root.lastExpiredTime;
  // 最近的到期时间不为NoWork,说明已经过期需要同步更新
  if (lastExpiredTime !== NoWork) {
    // Special case: Expired work should flush synchronously.
    root.callbackExpirationTime = Sync;
    root.callbackPriority = ImmediatePriority;
    root.callbackNode = scheduleSyncCallback(
      performSyncWorkOnRoot.bind(null, root),
    );
    return;
  }
  const expirationTime = getNextRootExpirationTimeToWorkOn(root);
  const existingCallbackNode = root.callbackNode;
  // 说明接下来没有可调度的任务
  if (expirationTime === NoWork) {
    // There's nothing to work on.
    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
    ) {
      // Existing callback is sufficient.
      return;
    }
    cancelCallback(existingCallbackNode);
  }
  // 取消了之前的任务需要重置为当前最新的
  root.callbackExpirationTime = expirationTime;
  root.callbackPriority = priorityLevel;
  let callbackNode;
  if (expirationTime === Sync) { // 如果是同步调用
    // Sync React callbacks are scheduled on a special internal queue
    callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  } else if (disableSchedulerTimeoutBasedOnReactExpirationTime) { // 目前这个版本不会走到这里
    callbackNode = scheduleCallback(
      priorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  } else { // 异步调用
    callbackNode = scheduleCallback(
      priorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
      // Compute a task timeout based on the expiration time. This also affects
      // ordering because tasks are processed in timeout order.
      {timeout: expirationTimeToMs(expirationTime) - now()},
    );
  }
  root.callbackNode = callbackNode;
}

这个方法主要就是为我们执行同步和异步任务的调度,我们可以直接看scheduleSyncCallbackscheduleCallback这两个同步和异步的调度方法

/**
 * 同步任务调度的中间方法,如果队列不为空就加入队列,如果为空就立即推入任务调度队列
 * @param {*} callback
 */
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 {
    // Push onto existing queue. Don't need to schedule a callback because
    // we already scheduled one when we created the queue.
    syncQueue.push(callback);
  }
  return fakeCallbackNode;
}

这个方法主要是将同步任务推入同步队列syncQueue,等待flushSyncCallbackQueue调用将所有同步任务推入真正的任务队列。如果第一次的同步任务会直接加入调度队列

export function scheduleCallback(
  reactPriorityLevel: ReactPriorityLevel,
  callback: SchedulerCallback,
  options: SchedulerCallbackOptions | void | null,
) {
  const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
  return Scheduler_scheduleCallback(priorityLevel, callback, options);
}

异步的任务调度很简单,直接将异步任务推入调度队列

接下来看一下任务调度队列的代码不管同步异步都是用这个方法加入任务队列执行的Scheduler_scheduleCallback对应源码是unstable_scheduleCallback这方法

/**
 * 将一个任务推入任务调度队列
 * @param {*} priorityLevel 当前任务优先级
 * @param {*} callback
 * @param {*} options 异步调度传入timeout类似setTimeout的时间
 */
function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime();// 获取当前的时间的相对值即当前时间减去页面加载记录的时间
  var startTime;
  var timeout;
  if (typeof options === 'object' && options !== null) {
    // 目前这个版本没看到传delay的地方
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
    // 如果没有timeout就是使用优先级计算出来的
    timeout =
      typeof options.timeout === 'number'
        ? options.timeout
        : timeoutForPriorityLevel(priorityLevel);
  } else {
    // 针对不同的优先级算出不同的过期时间
    timeout = timeoutForPriorityLevel(priorityLevel);
    startTime = currentTime;
  }
  // 定义新的过期时间
  var expirationTime = startTime + timeout;
  // 定义一个新的任务
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  // debug代码可以忽略
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  if (startTime > currentTime) {
    // This is a delayed task.
    // 这是一个delay的任务
    newTask.sortIndex = startTime;
    // 将超时的任务推入超时队列
    push(timerQueue, newTask);
    // 如果任务队列为空且新增加的任务是优先级最高
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      // 如果已经有hostTimeout在执行需要取消之前的设定
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      // 
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    newTask.sortIndex = expirationTime;
    // 将新的任务推入任务队列
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // Schedule a host callback, if needed. If we're already performing work,
    // wait until the next time we yield.
    // 执行回调方法,如果已经再工作需要等待一次回调的完成
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }
  return newTask;
}

异步的方法会传入一个timeout参数可以直接使用,如果没有会根据优先级计算出来一个固定的值,对于每一个任务都会定义一个成一个新的任务task。任务队列实际是一个基于数组实现的最小堆,排序的key就是新计算出来的expirationTime所以这里可以看到不管同步还是异步任务最终都是推入了一个任务队列中等待执行。最后执行requestHostCallback就是用MessageChannel的异步方法来开启任务调度performWorkUntilDeadline

// 执行工作直到超时
const performWorkUntilDeadline = () => {
  // 可能有被取消的情况
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // 设置超时时间根据fps算出来的
    deadline = currentTime + yieldInterval;
    const hasTimeRemaining = true;
    try {
      const hasMoreWork = scheduledHostCallback(
        hasTimeRemaining,
        currentTime,
      );
      // 如果没有更多的工作
      if (!hasMoreWork) {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      } else {
        // 如果有更多的工作就一直触发
        port.postMessage(null);
      }
    } catch (error) {
      port.postMessage(null);
      throw error;
    }
  } else {
    isMessageLoopRunning = false;
  }
  needsPaint = false;
};

这里的scheduledHostCallback就是之前传入的flushWork在这里调用的是workLoop去循环执行所有的任务

// 循环工作将task中的任务都执行了
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue); // 获得当前优先级最高的任务
  // 任务不为空且任务没有被暂停
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime && // 如果当前的到期时间比较长可以先执行其他任务
      (!hasTimeRemaining || shouldYieldToHost()) // 并且可以暂停
    ) {
      // This currentTask hasn't expired, and we've reached the deadline.
      break;
    }
    const callback = currentTask.callback;
    if (callback !== null) {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      markTaskRun(currentTask, currentTime);
      // 执行performWorkOnRoot
      const continuationCallback = callback(didUserCallbackTimeout);
      // 执行完后再获取一次时间
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        currentTask.callback = continuationCallback;
        markTaskYield(currentTask, currentTime);
      } else {
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        // 如果当前任务是最高优先级的直接推出
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  // Return whether there's additional work
  // 返回当前队列里是否还有任务,
  if (currentTask !== null) {
    return true;
  } else {
    let firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

这里其实就是从任务队列中取出优先级最高的任务去执行,对于同步任务执行的是performSyncWorkOnRoot对于异步的任务执行的是performConcurrentWorkOnRoot
最终都会在while循环中之心performUnitOfWork这个方法只不过异步的方法是可以打断的,我们每次调用都要查看是否超时。
这里关于任务调度的内容就都结束了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容

  • 除去惊险刺激的剧情,恢弘壮观的特效和莞尔一笑的幽默,《加勒比海盗5》就是一个创业的故事!全片以三叉戟的市场需求为主...
    最会设计的科研狗阅读 632评论 0 1
  • 从来没有想过自己有一天会去学画画,而且画得那么固执,不喜欢老师改我的作品,没画几个月竟然想画人物了,真得不知...
    Sahara撒拉阅读 261评论 0 0
  • Mother Teresa(中文名包括:特蕾莎修女、德雷莎修女、德兰修女) Mother Teresa是阿尔巴尼亚...
    岩之鹰J阅读 2,719评论 6 13
  • 一、好习惯打卡 1、早睡和早起: 这一周早睡早起做得比好,虽然有时因为太热,晚上会醒来一次,但是不影响睡眠。深睡眠...
    琳千阅读 155评论 0 0