React源码-调度机制

213 阅读6分钟

react的调度机制是指react任务执行的优先级调配,如果连续有任务ABC,但C的任务优先级最高,那么先优先执行,通过源码来看看是怎么调度的。

调度机制的核心函数代码是:

export let requestHostCallback; 
export let cancelHostCallback; 
export let requestHostTimeout; 
export let cancelHostTimeout; 
export let shouldYieldToHost; 
export let requestPaint;
export let getCurrentTime; 
export let forceFrameRate;

在Scheduler.js文件中有

const localSetImmediate =
  typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom

let schedulePerformWorkUntilDeadline;
// 判断是否有setImmediate 一般是node环境
if (typeof localSetImmediate === 'function') {
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
 // 判断浏览器是否支持MessageChannel机制,具体可以查阅mdn:
 // <https://developer.mozilla.org/zh-CN/docs/Web/API/MessageChannel>
} else if (typeof MessageChannel !== 'undefined') {
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  schedulePerformWorkUntilDeadline = () => {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

然后来看下这个performWorkUntilDeadline函数的实现,执行之前会记录当前时间,这是便于记录占用线程的时间,来调度任务,避免占用线程时间太久。如果还有当前任务要执行,继续回调schedulePerformWorkUntilDeadline。

const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // 记录执行前的时间,主要是要判断占用线程多久的时间,如果占用时间太长且有优先级更高的,要执行优先级高的
    startTime = currentTime;
    const hasTimeRemaining = true;

    // 是否还有要执行的
    let hasMoreWork = true;
    try {
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      if (hasMoreWork) {
        // 调度一次完后还有要执行的再回调执行
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }

  needsPaint = false;
};

requestHostCallback


function requestHostCallback(callback) {
	scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

看到这里应该大致清楚这个调用的过程

  • 创建messageChannel,绑定message事件
  • 调用 requestHostCallback 设置 调度callback,然后执行设置isMessageLoopRunning表示当前线程正在执行,然后调用schedulePerformWorkUntilDeadline
  • schedulePerformWorkUntilDeadline中通过postMessage,channel port在监听message事件汇后,调用performWorkUntilDeadline
  • performWorkUntilDeadline 函数中记录调用的时间,然后调用scheduledHostCallback,执行任务,执行完后如果还有work,继续进行

所以这会我们只需要找到调用requestHostCallback函数的地方,总共有三处

  • handleTimeout
  • unstable_scheduleCallback
  • unstable_continueExecution

先从handleTimeout和unstable_scheduleCallback入手

unstable_scheduleCallback

function unstable_scheduleCallback(priorityLevel, callback, options) {
  var timeout;
  
  // 开始时间
  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;
  }
  
  // 根据优先级,设置调度的总时间,超过时间就要考虑调整了
  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; // maxSigned31BitInt
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT; // 10000
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT; // 5000
      break;
  }
  
  
  // 过期时间 deadline
  var expirationTime = startTime + timeout;
	  
	// 创建task对象
  var newTask = {
    id: taskIdCounter++, 
    callback, // 执行的callback
    priorityLevel, // 执行优先级
    startTime, // 开始时间
    expirationTime, // 过期时间,这个跟优先级对应的,越小说明任务越急
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  // 如果任务开始时间大于当前时间,说明这个目前还不用执行,push到timerQueue里
  if (startTime > currentTime) {
    // This is a delayed task.
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    
  
    // taskQueue peek 为null,说明目前没有要执行的task了,然后timerQueue 同时peek 为当前的task
    // 说明当前task应该是最早执行的,调用requestHostTimeout
    /**
    function requestHostTimeout(callback, ms) {
			  taskTimeoutID = localSetTimeout(() => {
			    callback(getCurrentTime());
			  }, ms);
		}
    */
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // 通过settimeout调度执行
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
	  // 从上面逻辑看这里处于ImmediatePriority级别,push进taskQueue进行执行
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }

		// 没有被执行过久调用requestHostCallback执行
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

这里出现了两个队列,taskQueue 和 timerQueue。一个是任务队列一个是时间延时队列,push进taskQueue表面应该在下次workloop过程中就必须拿出来执行,timerQueue则表示延时的,其实就是优先级还没那么高的。workloop的逻辑在flushWork,接下来看flushWork的逻辑。

FlushWork

function flushWork(hasTimeRemaining, initialTime) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    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 {
      return workLoop(hasTimeRemaining, initialTime);
    }
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
    if (enableProfiling) {
      const currentTime = getCurrentTime();
      markSchedulerSuspended(currentTime);
    }
  }
}

workLoop

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);

  // while 循环 说明目前有task要执行
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      // 1. 过期时间大于当前时间 说明可以延后执行 
      // 2. 没有时间剩余或者能够让步输入相关的更新
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      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') {
        // 还有继续要执行的callback,赋值给currentTask.callback
        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);
    }

    // 执行完将顶部task赋值给currentTask 继续执行
    currentTask = peek(taskQueue);
  }

  // 
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

一些概念

时间切片

在workLoop的while循环中,每次执行task之前都会判断是否出现超时的现象

可中断

workloop中的这段代码其实也是可中断的意思,把continuationCallback赋值给currentTask,然后标记yield,表示中断

currentTask.callback = continuationCallback;
if (enableProfiling) {
   markTaskYield(currentTask, currentTime);
}

shouldYield 逻辑

function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    // 时间间隔小于react定义的一帧时间:5ms,不用暂停
    return false;
  }
  if (enableIsInputPending) {
    if (needsPaint) {
      // There's a pending paint (signaled by `requestPaint`). Yield now.
      return true;
    }
    // 判断用户是否有输入,这里这个输入可能不单只输入框,也可以是mousemove、keydown等
    // react的实现是通过浏览器的navigator.scheduling.isInputPending事件来判断
    // 如果有输入事件判断为true
    // 这里有两个事件,一个是50ms、300ms,如果用户浏览器支持判断输入事件,那么会用isInputPending来判断,否则最大的时间就是300ms
    if (timeElapsed < continuousInputInterval) {
      if (isInputPending !== null) {
        return isInputPending();
      }
    } else if (timeElapsed < maxInterval) {
      if (isInputPending !== null) {
        return isInputPending(continuousOptions);
      }
    } else {
    // 占据线程时间太长了 直接pass 直接yield
      return true;
    }
  }

  return true;
}

这个方法在调和和调度都用

  • 在调度中workLoop,判断task是否能执行中使用
  // while 循环 说明目前有task要执行
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      break;
    }
    ...
  }
  • 在调和中workLoopConcurrent,也有shouldYield的判断,如果当前是停止的状态,禁止进入调和阶段,当然这个是个while循环,很快就又会执行
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

而在workLoopConcurrent调用链的上层是performConcurrentWorkOnRoot,其中是通过shouldTimeSlice来判断是否进入workLoopConcurrent。

  const shouldTimeSlice =
    !includesBlockingLane(root, lanes) &&
    !includesExpiredLane(root, lanes) &&
    (disableSchedulerTimeoutInWorkLoop || !didTimeout);

  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);

💡安利

  • React Internals Explorer:外网一位哥们开发的网站,我觉得很有趣,是一个可视化的react fiber,包括了更新过程的动态图示,很方便理解。可以自己在编辑器上写些代码,fork一份出来就能修改了。