一 为什么会呈现调度
之前的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; } }
至此任务调度的一个过程就实现(并没有讲延时工作,延时工作也不简单,也就是设定了延迟时间就提早执行,延时工作工夫到了就放在主工作队列中去执行)