乐趣区

关于react.js:react源码debugger之理解调度使用react17版本

一 为什么会呈现调度

之前的 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;
      }
    }

至此任务调度的一个过程就实现(并没有讲延时工作,延时工作也不简单,也就是设定了延迟时间就提早执行,延时工作工夫到了就放在主工作队列中去执行)

退出移动版