likes
comments
collection
share

react源码debugger之理解调度(使用react17版本)

作者站长头像
站长
· 阅读数 64

一 为什么会出现调度

之前的react更新模式同步模式:这就好比单线程模式,处理react任务就只有一个线程,上一个任务没有被处理掉,下一个任务就会等待。假设我们在渲染一个列表,比如一个需要条件搜索出来的列表。如果很长,需要渲染的过程很长,这时候用户同时去做搜索框的操作,那么操作搜索框的这个操作就会被阻塞,因为列表渲染操作还没有完成。这不是一个很好的用户体验。为了优化体验新加入的模式调度模式:调度模式允许高优先级的任务可以打断低优先级任务的执行,实现任务的插队。如上面的例子,如果用户去操作搜索框的时候我们可以暂停列表的渲染,等待用户操作完之后再去进行列表渲染的任务是不是体验会更好一点。在讲解调度模式之前,我们先了解一个概念时间切片时间切片指的是一种将多个粒度小的任务放入时间片段中去执行。既然叫切片,就是指一小段的时间。比如我们按照5毫秒为单位执行任务,5毫秒执行一段,5毫秒执行一段。那为什么要这样去划分,因为调度模式的任务有优先级可以插队。时间切片就能辅助我们实现任务插队,每执行5秒我们就可以去检查有没有更紧急任务需要执行,如果有我们就可以先去执行紧急任务来实现插队。也就是按照一个时间片段为单位,一个时间片段就去检查一次有没有更高优先级任务需要执行。从源码入口开始:

function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime();
  var startTime;
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }
  // 根据任务优先级给出不同任务过期时间
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority: 
      timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;// 250
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT; // 5000
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT; // 1000
      break;
  }

  // 计算出过期时间
  var expirationTime = startTime + timeout;

  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,// expirationTime越大任务优先级越低
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }
  
  // 如果startTime > currentTime证明options.delay有值 说明这是个可以晚一点执行的延时任务
  if (startTime > currentTime) {
    // This is a delayed task.
    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.
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      // 延时delay毫秒循环做任务(handleTimeout 循环做任务(handleTimeout 主任务有先做主任务,主任务执行完了才做延时任务) 
      // 生生 taskTimeoutID 提供给cancelHostTimeout使用 取消延时执行任务
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // delay没有值表示是正常任务
    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;
}

unstable_scheduleCallback中option还有配置delay,如果有值表示这是一个延时任务,不需要马上执行,我们先不看这个延时任务。首先通过priorityLevel去匹配出过期时间。priorityLevel是更新的优先级,任务的优先级也就是通过priorityLevel推算出来的。react将任务优先级通过更新的等级划分成几个等级的过期时间,分别为

IMMEDIATE_PRIORITY_TIMEOUT; // -1 马上过期也就是需要立即执行
USER_BLOCKING_PRIORITY_TIMEOUT;// 250 之后过期可能是某些用户行为
IDLE_PRIORITY_TIMEOUT; // 最低优先级
LOW_PRIORITY_TIMEOUT; // 5000 之后过期表示低优先级任务
NORMAL_PRIORITY_TIMEOUT; // 1000 一般正常的任务过期时间

然后

// 计算出过期时间
  var expirationTime = startTime + timeout;

// 构建任务对象
 var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,// expirationTime越大任务优先级越低
    sortIndex: -1,
  };
    
// 只看这部分 startTime > currentTime里面的部分是延时任务

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);
}

requestHostCallback的工作就是把flushWork存放在了scheduledHostCallback这个变量中,然后调用schedulePerformWorkUntilDeadline开启调度,主要就是schedulePerformWorkUntilDeadline这个方法。

let schedulePerformWorkUntilDeadline;
// 主要是在IE and Node.js + jsdom中开启
if (typeof localSetImmediate === 'function') {
  // Node.js and old IE.
  // There's a few reasons for why we prefer setImmediate.
  //
  // Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.
  // (Even though this is a DOM fork of the Scheduler, you could get here
  // with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)
  // https://github.com/facebook/react/issues/20756
  //
  // But also, it runs earlier which is the semantic we want.
  // If other browsers ever implement it, it's better to use it.
  // Although both of these would be inferior to native scheduling.
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  // DOM and Worker environments.
  // We prefer MessageChannel because of the 4ms setTimeout clamping.
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  // We should only fallback here in non-browser environments.
  schedulePerformWorkUntilDeadline = () => {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

这里面就表示调度开启的几种方式也就是为了兼容不同的环境,ie nodejs环境就不讲了,直接看浏览器环境。也就是MessageChannel的方式:

const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };

Message Channel其实是浏览器提供的一种数据通信接口,可用来实现订阅发布。不了解的可以查一下资料,它的特点就是其两个端口属性支持双向通信和异步发布事件,也就是port1的广播port2能接收到port2的广播port1能接收。在requestHostCallback调用了schedulePerformWorkUntilDeadline也就是在广播事件通知port1。那么port1的onmessage就能响应监听到port2的广播也就会响应监听函数performWorkUntilDeadline,performWorkUntilDeadline代码如下:

const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // Keep track of the start time so we can measure how long the main thread
    // has been blocked.
    startTime = currentTime;
    const hasTimeRemaining = true;

    // If a scheduler task throws, exit the current browser task so the
    // error can be observed.
    //
    // Intentionally not using a try-catch, since that makes some debugging
    // techniques harder. Instead, if `scheduledHostCallback` errors, then
    // `hasMoreWork` will remain true, and we'll continue the work loop.
    let hasMoreWork = true;
    try {
      // workLoop 轮询执行任务 被打断 或者是出错 scheduledHostCallback === flushWork
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      // 任务出错 或者被打断 如果还有更多的任务 可以继续工作
      if (hasMoreWork) {
        // If there's more work, schedule the next message event at the end
        // of the preceding one.
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }
  // Yielding to the browser will give it a chance to paint, so we can
  // reset this.
  needsPaint = false;
};

scheduledHostCallback 也就是上面requestHostCallback传递进来的回掉函数flushWork,flushWork中就在进行任务的轮询,那么轮询过程中就会去检查是否有更高优先级任务 按照时间切片检查是否可以进行任务打断。从异常捕获中可以看出,如果scheduledHostCallback被打断会返回hasMoreWork的结果来通知这边任务要是被打断了 任务队列中是不是还有任务没执行完,如果有也就是hasMoreWork为true就会继续开启工作循环schedulePerformWorkUntilDeadline。接下来看看scheduledHostCallback也就是flushWork:

// 循环执行任务
function flushWork(hasTimeRemaining, initialTime) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  // We'll need a host callback the next time work is scheduled.
  isHostCallbackScheduled = false;
  // 取消执行延时任务
  if (isHostTimeoutScheduled) {
    // We scheduled a timeout but it's no longer needed. Cancel it.
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  // 保存任务的优先级
  const previousPriorityLevel = currentPriorityLevel;
  try {
    if (enableProfiling) {
      try {
        // 轮询执行任务
        return workLoop(hasTimeRemaining, initialTime);
      } catch (error) {
        // 任务执行出错
        if (currentTask !== null) {
          const currentTime = getCurrentTime();
          markTaskErrored(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        throw error;
      }
    } else {
      // No catch in prod code path.
      return workLoop(hasTimeRemaining, initialTime);
    }
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
    if (enableProfiling) {
      const currentTime = getCurrentTime();
      markSchedulerSuspended(currentTime);
    }
  }
}

可以看到 flushWork 正常的流程中 是调用了workLoop进行任务的轮询,workLoop:

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  // 处理延时任务如果有到期的延时任务就把到期的延时任务放进执行队列
  advanceTimers(currentTime);
  // 取第一个任务
  currentTask = peek(taskQueue);
  // 任务执行是可以被打断的
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    // currentTask.expirationTime > currentTime 当前任务还没过期
    // shouldYieldToHost() === true 出现更高优先级任务需要来打断当前的任务执行
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // This currentTask hasn't expired, and we've reached the deadline.
      break;
    }
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      // 任务过期
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      if (enableProfiling) {
        markTaskRun(currentTask, currentTime);

      }
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        currentTask.callback = continuationCallback;
        if (enableProfiling) {
          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 {
    // 任务执行完了 且没有被打断 空闲了 开始执行延时队列任务
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

可以看到while循环一只从任务队列中取任务然后执行任务的callbak,每次任务的执行都会进行一次检查:

currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())

条件成立才继续轮询,这个条件表示过期时间还未到,并且shouldYieldToHost这个函数返回的是true就要中断轮询,shouldYieldToHost返回true就表示可能存在更高优先级的任务需要做。

function shouldYieldToHost() {
  // 计算执行了多久
  const timeElapsed = getCurrentTime() - startTime;
  // 已经执行的时间小于切片时间时间 说明还有剩余执行时间
  // frameInterval = 5 时间切片5秒
  if (timeElapsed < frameInterval) {
    // The main thread has only been blocked for a really short amount of time;
    // smaller than a single frame. Don't yield yet.
    return false;
  }

  // 下面是任务操作5毫秒进行高优先级任务检查
  // The main thread has been blocked for a non-negligible amount of time. We
  // may want to yield control of the main thread, so the browser can perform
  // high priority tasks. The main ones are painting and user input. If there's
  // a pending paint or a pending input, then we should yield. But if there's
  // neither, then we can yield less often while remaining responsive. We'll
  // eventually yield regardless, since there could be a pending paint that
  // wasn't accompanied by a call to `requestPaint`, or other main thread tasks
  // like network events.
  // 处理高优先级任务的插入,比如用户输入,绘画 这些任务的插入都是需要让步的
  if (enableIsInputPending) {
    // 绘画 马上中断 给绘画让步
    if (needsPaint) {
      // There's a pending paint (signaled by `requestPaint`). Yield now.
      return true;
    }
    // 处理第二个等级的事件比如悬停等 限制是50毫秒
    // 处理鼠标悬停等的操作 50毫秒为界限 50毫秒以内通过isInputPending取判断是不是要中断
    if (timeElapsed < continuousInputInterval) {
      // We haven't blocked the thread for that long. Only yield if there's a
      // pending discrete input (e.g. click). It's OK if there's pending
      // continuous input (e.g. mouseover).
      if (isInputPending !== null) {
        return isInputPending();
      }
    } else if (timeElapsed < maxInterval) {
      // 第三等级耗时事件 可能在处理一些很耗时间的任务拖拽事件等 所以给个最大限制300毫秒
      // 300毫秒为界限 300毫秒-50毫秒这个范围的耗时任务 通过isInputPending(continuousOptions)判断是不是要中断
      // Yield if there's either a pending discrete or continuous input.
      if (isInputPending !== null) {
        return isInputPending(continuousOptions);
      }
    } else {
      // We've blocked the thread for a long time. Even if there's no pending
      // input, there may be some other scheduled work that we don't know about,
      // like a network event. Yield now.
      // 可能在处理一些网络事件,阻塞了很久了 已经超过300毫秒 马上中断任务
      return true;
    }
  }

  // `isInputPending` isn't available. Yield now.
  // 不是输入事件 单纯时间切片执行过期了 马上中断
  return true;
}

首先通过getCurrentTime() - startTime计算出已经执行的时间,然后和切片的单位做比较,react的切片单元是5毫秒,如果任务执行的时间小于5毫秒,返回false也就是继续执行不中断。如果超过5毫秒就要检查是不是有高优先级任务了。如果任务被打断,shouldYieldToHost返回true。workLoop接着走while循环后面的部分。

// 任务还有没有执行完的,但是被打断了执行
  if (currentTask !== null) {
    // 任务被打断了
    return true;
  } else {
    // 任务执行完了 且没有被打断 空闲了 开始执行延时队列任务
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }

也就是判定currentTask有没有,没有的主任务了,那就执行延时任务队列,有的话就表示还有任务被中断了,告诉调度的地方里面还有任务,虽然被打断了后面还要继续执行,也就是performWorkUntilDeadline里面的这部分:

let hasMoreWork = true;
    try {
      // workLoop 轮询执行任务 被打断 或者是出错 scheduledHostCallback === flushWork
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      // 任务出错 或者被打断 如果还有更多的任务 可以继续工作
      if (hasMoreWork) {
        // If there's more work, schedule the next message event at the end
        // of the preceding one.
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }

至此任务调度的一个过程就完成(并没有讲延时任务,延时任务也不复杂,也就是设定了延迟时间就延迟执行,延时任务时间到了就放在主任务队列中去执行)