点击进入React源码调试仓库。
Scheduler作为一个独立的包,能够单独承当起任务调度的职责,你只须要将工作和工作的优先级交给它,它就能够帮你治理工作,安顿工作的执行。这就是React和Scheduler配合工作的模式。
对于多个工作,它会先执行优先级高的。聚焦到单个工作的执行上,会被Scheduler有节制地去执行。换句话说,线程只有一个,它不会始终占用着线程去执行工作。而是执行一会,中断一下,如此往返。用这样的模式,来防止始终占用无限的资源执行耗时较长的工作,解决用户操作时页面卡顿的问题,实现更快的响应。
咱们能够从中梳理出Scheduler中两个重要的行为:多个工作的治理、单个工作的执行管制。
基本概念
为了实现上述的两个行为,它引入两个概念:工作优先级 、 工夫片。
工作优先级让工作依照本身的紧急水平排序,这样能够让优先级最高的工作最先被执行到。
工夫片规定的是单个工作在这一帧内最大的执行工夫,工作一旦执行工夫超过工夫片,则会被打断,有节制地执行工作。这样能够保障页面不会因为工作间断执行的工夫过长而产生卡顿。
原理概述
基于工作优先级和工夫片的概念,Scheduler围绕着它的外围指标 - 任务调度,衍生出了两大外围性能:工作队列治理 和 工夫片下工作的中断和复原。
工作队列治理
工作队列治理对应了Scheduler的多任务治理这一行为。在Scheduler外部,把工作分成了两种:未过期的和已过期的,别离用两个队列存储,前者存到timerQueue中,后者存到taskQueue中。
如何辨别工作是否过期?
用工作的开始工夫(startTime)和以后工夫(currentTime)作比拟。开始工夫大于以后工夫,阐明未过期,放到timerQueue;开始工夫小于等于以后工夫,阐明已过期,放到taskQueue。
不同队列中的工作如何排序?
当工作一个个入队的时候,天然要对它们进行排序,保障紧急的工作排在后面,所以排序的根据就是工作的紧急水平。而taskQueue和timerQueue中工作紧急水平的断定规范是有区别的。
- taskQueue中,根据工作的过期工夫(expirationTime)排序,过期工夫越早,阐明越紧急,过期工夫小的排在后面。过期工夫依据工作优先级计算得出,优先级越高,过期工夫越早。
- timerQueue中,根据工作的开始工夫(startTime)排序,开始工夫越早,说明会越早开始,开始工夫小的排在后面。工作进来的时候,开始工夫默认是以后工夫,如果进入调度的时候传了延迟时间,开始工夫则是以后工夫与延迟时间的和。
工作入队两个队列,之后呢?
如果放到了taskQueue,那么立刻调度一个函数去循环taskQueue,挨个执行外面的工作。
如果放到了timerQueue,那么阐明它外面的工作都不会立刻执行,那就等到了timerQueue外面排在第一个工作的开始工夫,看这个工作是否过期,如果是,则把工作从timerQueue中拿进去放入taskQueue,调度一个函数去循环它,执行掉外面的工作;否则过一会持续查看这第一个工作是否过期。
工作队列治理绝对于单个工作的执行,是宏观层面的概念,它利用工作的优先级去治理工作队列中的工作程序,始终让最紧急的工作被优先解决。
单个工作的中断以及复原
单个工作的中断以及复原对应了Scheduler的单个工作执行管制这一行为。在循环taskQueue执行每一个工作时,如果某个工作执行工夫过长,达到了工夫片限度的工夫,那么该工作必须中断,以便于让位给更重要的事件(如浏览器绘制),等事件实现,再复原执行工作。
例如这个例子,点击按钮渲染140000个DOM节点,为的是让React通过scheduler调度一个耗时较长的更新工作。同时拖动方块,这是为了模仿用户交互。更新工作会占用线程去执行工作,用户交互要也要占用线程去响应页面,这就决定了它们两个是互斥的关系。在React的concurrent模式下,通过Scheduler调度的更新工作遇到用户交互之后,会是上面动图里的成果。
执行React工作和页面响应交互这两件事件是互斥的,但因为Scheduler能够利用工夫片中断React工作,而后让出线程给浏览器去绘制,所以一开始在fiber树的构建阶段,拖动方块会失去及时的反馈。然而前面卡了一下,这是因为fiber树构建实现,进入了同步的commit阶段,导致交互卡顿。剖析页面的渲染过程能够十分直观地看到通过工夫片的管制。主线程被让进来进行页面的绘制(Painting和Rendering,绿色和紫色的局部)。
Scheduler要实现这样的调度成果须要两个角色:工作的调度者、工作的执行者。调度者调度一个执行者,执行者去循环taskQueue,一一执行工作。当某个工作的执行工夫比拟长,执行者会依据工夫片中断工作执行,而后通知调度者:我当初正执行的这个工作被中断了,还有一部分没实现,但当初必须让位给更重要的事件,你再调度一个执行者吧,好让这个工作能在之后被继续执行完(工作的复原)。于是,调度者晓得了工作还没实现,须要持续做,它会再调度一个执行者去持续实现这个工作。
通过执行者和调度者的配合,能够实现工作的中断和复原。
原理小结
Scheduler治理着taskQueue和timerQueue两个队列,它会定期将timerQueue中的过期工作放到taskQueue中,而后让调度者告诉执行者循环taskQueue执行掉每一个工作。执行者管制着每个工作的执行,一旦某个工作的执行工夫超出工夫片的限度。就会被中断,而后以后的执行者登场,登场之前会告诉调度者再去调度一个新的执行者持续实现这个工作,新的执行者在执行工作时依旧会依据工夫片中断工作,而后登场,反复这一过程,直到以后这个工作彻底实现后,将工作从taskQueue出队。taskQueue中每一个工作都被这样解决,最终实现所有工作,这就是Scheduler的残缺工作流程。
这外面有一个关键点,就是执行者如何晓得这个工作到底实现没实现呢?这是另一个话题了,也就是判断工作的实现状态。在解说执行者执行工作的细节时会重点突出。
以上是Scheduler原理的概述,上面开始是对React和Scheduler联结工作机制的具体解读。波及React与Scheduler的连贯、调度入口、工作优先级、工作过期工夫、工作中断和复原、判断工作的实现状态等内容。
具体流程
在开始之前,咱们先看一下React和Scheduler它们二者形成的一个零碎的示意图。
整个零碎分为三局部:
- 产生工作的中央:React
- React和Scheduler交换的翻译者:SchedulerWithReactIntegration
- 工作的调度者:Scheduler
React中通过上面的代码,让fiber树的构建工作进入调度流程:
scheduleCallback( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root),);
工作通过翻译者交给Scheduler,Scheduler进行真正的任务调度,那么为什么须要一个翻译者的角色呢?
React与Scheduler的连贯
Scheduler帮忙React调度各种工作,然而实质上它们是两个齐全不耦合的货色,二者各自都有本人的优先级机制,那么这时就须要有一个两头角色将它们连接起来。
实际上,在react-reconciler中提供了这样一个文件专门去做这样的工作,它就是SchedulerWithReactIntegration.old(new).js
。它将二者的优先级翻译了一下,让React和Scheduler能读懂对方。另外,封装了一些Scheduler中的函数供React应用。
在执行React工作的重要文件ReactFiberWorkLoop.js
中,对于Scheduler的内容都是从SchedulerWithReactIntegration.old(new).js
导入的。它能够了解成是React和Scheduler之间的桥梁。
// ReactFiberWorkLoop.jsimport { scheduleCallback, cancelCallback, getCurrentPriorityLevel, runWithPriority, shouldYield, requestPaint, now, NoPriority as NoSchedulerPriority, ImmediatePriority as ImmediateSchedulerPriority, UserBlockingPriority as UserBlockingSchedulerPriority, NormalPriority as NormalSchedulerPriority, flushSyncCallbackQueue, scheduleSyncCallback,} from './SchedulerWithReactIntegration.old';
SchedulerWithReactIntegration.old(new).js
通过封装Scheduler的内容,对React提供两种调度入口函数:scheduleCallback
和 scheduleSyncCallback
。工作通过调度入口函数进入调度流程。
例如,fiber树的构建工作在concurrent模式下通过scheduleCallback
实现调度,在同步渲染模式下由scheduleSyncCallback
实现。
// concurrentMode// 将本次更新工作的优先级转化为调度优先级// schedulerPriorityLevel为调度优先级const schedulerPriorityLevel = lanePriorityToSchedulerPriority( newCallbackPriority,);// concurrent模式scheduleCallback( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root),);// 同步渲染模式scheduleSyncCallback( performSyncWorkOnRoot.bind(null, root),)
它们两个其实都是对Scheduler中scheduleCallback的封装,只不过传入的优先级不同而已,前者是传递的是曾经本次更新的lane计算得出的调度优先级,后者传递的是最高级别的优先级。另外的区别是,前者间接将工作交给Scheduler,而后者先将工作放到SchedulerWithReactIntegration.old(new).js本人的同步队列中,再将执行同步队列的函数交给Scheduler,以最高优先级进行调度,因为传入了最高优先级,意味着它将会是立刻过期的工作,会立刻执行掉它,这样可能保障在下一次事件循环中执行掉工作。
function scheduleCallback( reactPriorityLevel: ReactPriorityLevel, callback: SchedulerCallback, options: SchedulerCallbackOptions | void | null,) { // 将react的优先级翻译成Scheduler的优先级 const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel); // 调用Scheduler的scheduleCallback,传入优先级进行调度 return Scheduler_scheduleCallback(priorityLevel, callback, options);}function scheduleSyncCallback(callback: SchedulerCallback) { if (syncQueue === null) { syncQueue = [callback]; // 以最高优先级去调度刷新syncQueue的函数 immediateQueueCallbackNode = Scheduler_scheduleCallback( Scheduler_ImmediatePriority, flushSyncCallbackQueueImpl, ); } else { syncQueue.push(callback); } return fakeCallbackNode;}
Scheduler中的优先级
说到优先级,咱们来看一下Scheduler本人的优先级级别,它为工作定义了以下几种级别的优先级:
export const NoPriority = 0; // 没有任何优先级export const ImmediatePriority = 1; // 立刻执行的优先级,级别最高export const UserBlockingPriority = 2; // 用户阻塞级别的优先级export const NormalPriority = 3; // 失常的优先级export const LowPriority = 4; // 较低的优先级export const IdlePriority = 5; // 优先级最低,示意工作能够闲置
工作优先级的作用曾经提到过,它是计算工作过期工夫的重要依据,事关过期工作在taskQueue中的排序。
// 不同优先级对应的不同的工作过期工夫距离var IMMEDIATE_PRIORITY_TIMEOUT = -1;var USER_BLOCKING_PRIORITY_TIMEOUT = 250;var NORMAL_PRIORITY_TIMEOUT = 5000;var LOW_PRIORITY_TIMEOUT = 10000;// Never times outvar IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;...// 计算过期工夫(scheduleCallback函数中的内容)var timeout;switch (priorityLevel) {case ImmediatePriority: timeout = IMMEDIATE_PRIORITY_TIMEOUT; break;case UserBlockingPriority: timeout = USER_BLOCKING_PRIORITY_TIMEOUT; break;case IdlePriority: timeout = IDLE_PRIORITY_TIMEOUT; break;case LowPriority: timeout = LOW_PRIORITY_TIMEOUT; break;case NormalPriority:default: timeout = NORMAL_PRIORITY_TIMEOUT; break;}// startTime可暂且认为是以后工夫var expirationTime = startTime + timeout;
可见,过期工夫是工作开始工夫加上timeout,而这个timeout则是通过工作优先级计算得出。
React中更全面的优先级解说在我写的这一篇文章中:React中的优先级
调度入口 - scheduleCallback
通过下面的梳理,咱们晓得Scheduler中的scheduleCallback是调度流程开始的关键点。在进入这个调度入口之前,咱们先来认识一下Scheduler中的工作是什么模式:
var newTask = { id: taskIdCounter++, // 工作函数 callback, // 工作优先级 priorityLevel, // 工作开始的工夫 startTime, // 工作的过期工夫 expirationTime, // 在小顶堆队列中排序的根据 sortIndex: -1, };
- callback:真正的工作函数,重点,也就是内部传入的工作函数,例如构建fiber树的工作函数:performConcurrentWorkOnRoot
- priorityLevel:工作优先级,参加计算工作过期工夫
- startTime:示意工作开始的工夫,影响它在timerQueue中的排序
- expirationTime:示意工作何时过期,影响它在taskQueue中的排序
- sortIndex:在小顶堆队列中排序的根据,在辨别好工作是过期或非过期之后,sortIndex会被赋值为expirationTime或startTime,为两个小顶堆的队列(taskQueue,timerQueue)提供排序根据
真正的重点是callback,作为工作函数,它的执行后果会影响到工作实现状态的判断,前面咱们会讲到,临时先无需关注。当初咱们先来看看scheduleCallback
做的事件:它负责生成调度工作、依据工作是否过期将工作放入timerQueue或taskQueue,而后触发调度行为,让工作进入调度。残缺代码如下:
function unstable_scheduleCallback(priorityLevel, callback, options) { // 获取以后工夫,它是计算工作开始工夫、过期工夫和判断工作是否过期的根据 var currentTime = getCurrentTime(); // 确定工作开始工夫 var startTime; // 从options中尝试获取delay,也就是推迟时间 if (typeof options === 'object' && options !== null) { var delay = options.delay; if (typeof delay === 'number' && delay > 0) { // 如果有delay,那么工作开始工夫就是以后工夫加上delay startTime = currentTime + delay; } else { // 没有delay,工作开始工夫就是以后工夫,也就是工作须要立即开始 startTime = currentTime; } } else { startTime = currentTime; } // 计算timeout 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; // 1073741823 ms break; case LowPriority: timeout = LOW_PRIORITY_TIMEOUT; // 10000 break; case NormalPriority: default: timeout = NORMAL_PRIORITY_TIMEOUT; // 5000 break; } // 计算工作的过期工夫,工作开始工夫 + timeout // 若是立刻执行的优先级(ImmediatePriority), // 它的过期工夫是startTime - 1,意味着立即就过期 var expirationTime = startTime + timeout; // 创立调度工作 var newTask = { id: taskIdCounter++, // 真正的工作函数,重点 callback, // 工作优先级 priorityLevel, // 工作开始的工夫,示意工作何时能力执行 startTime, // 工作的过期工夫 expirationTime, // 在小顶堆队列中排序的根据 sortIndex: -1, }; // 上面的if...else判断各自分支的含意是: // 如果工作未过期,则将 newTask 放入timerQueue, 调用requestHostTimeout, // 目标是在timerQueue中排在最后面的工作的开始工夫的工夫点查看工作是否过期, // 过期则立即将工作退出taskQueue,开始调度 // 如果工作已过期,则将 newTask 放入taskQueue,调用requestHostCallback, // 开始调度执行taskQueue中的工作 if (startTime > currentTime) { // 工作未过期,以开始工夫作为timerQueue排序的根据 newTask.sortIndex = startTime; push(timerQueue, newTask); if (peek(taskQueue) === null && newTask === peek(timerQueue)) { // 如果当初taskQueue中没有工作,并且以后的工作是timerQueue中排名最靠前的那一个 // 那么须要查看timerQueue中有没有须要放到taskQueue中的工作,这一步通过调用 // requestHostTimeout实现 if (isHostTimeoutScheduled) { // 因为行将调度一个requestHostTimeout,所以如果之前曾经调度了,那么勾销掉 cancelHostTimeout(); } else { isHostTimeoutScheduled = true; } // 调用requestHostTimeout实现工作的转移,开启调度 requestHostTimeout(handleTimeout, startTime - currentTime); } } else { // 工作曾经过期,以过期工夫作为taskQueue排序的根据 newTask.sortIndex = expirationTime; push(taskQueue, newTask); // 开始执行工作,应用flushWork去执行taskQueue if (!isHostCallbackScheduled && !isPerformingWork) { isHostCallbackScheduled = true; requestHostCallback(flushWork); } } return newTask;}
这个过程中的重点是工作过期与否的解决。
针对未过期工作,会放入timerQueue,并依照开始工夫排列,而后调用requestHostTimeout
,为的是等一会,等到了timerQueue中那个应该最早开始的工作(排在第一个的工作)的开始工夫,再去查看它是否过期,如果它过期则放到taskQueue中,这样工作就能够被执行了,否则持续等。这个过程通过handleTimeout
实现。
handleTimeout
的职责是:
- 调用
advanceTimers
,查看timerQueue队列中过期的工作,放到taskQueue中。 查看是否曾经开始调度,如尚未调度,查看taskQueue中是否曾经有工作:
- 如果有,而且当初是闲暇的,阐明之前的advanceTimers曾经将过期工作放到了taskQueue,那么当初立刻开始调度,执行工作
- 如果没有,而且当初是闲暇的,阐明之前的advanceTimers并没有查看到timerQueue中有过期工作,那么再次调用
requestHostTimeout
反复这一过程。
总之,要把timerQueue中的工作全副都转移到taskQueue中执行掉才行。
针对已过期工作,在将它放入taskQueue之后,调用requestHostCallback
,让调度者调度一个执行者去执行工作,也就意味着调度流程开始。
开始调度-找出调度者和执行者
Scheduler通过调用requestHostCallback
让工作进入调度流程,回顾下面scheduleCallback最终调用requestHostCallback执行工作的中央:
if (!isHostCallbackScheduled && !isPerformingWork) { isHostCallbackScheduled = true; // 开始进行调度 requestHostCallback(flushWork);}
它既然把flushWork
作为入参,那么工作的执行者实质上调用的就是flushWork
,咱们先不论执行者是如何执行工作的,先关注它是如何被调度的,须要先找出调度者,这须要看一下requestHostCallback
的实现:
Scheduler辨别了浏览器环境和非浏览器环境,为requestHostCallback
做了两套不同的实现。在非浏览器环境下,应用setTimeout实现.
requestHostCallback = function(cb) { if (_callback !== null) { setTimeout(requestHostCallback, 0, cb); } else { _callback = cb; setTimeout(_flushCallback, 0); } };
在浏览器环境,用MessageChannel实现,对于MessageChannel的介绍就不再赘述。
const channel = new MessageChannel(); const port = channel.port2; channel.port1.onmessage = performWorkUntilDeadline; requestHostCallback = function(callback) { scheduledHostCallback = callback; if (!isMessageLoopRunning) { isMessageLoopRunning = true; port.postMessage(null); } };
之所以有两种实现,是因为非浏览器环境不存在屏幕刷新率,没有帧的概念,也就不会有工夫片,这与在浏览器环境下执行工作有本质区别,因为非浏览器环境根本不胡有用户交互,所以该场景下不判断工作执行工夫是否超出了工夫片限度,而浏览器环境工作的执行会有工夫片的限度。除了这一点之外,尽管两种环境下实现形式不一样,然而做的事件大致相同。
先看非浏览器环境,它将入参(执行工作的函数)存储到外部的变量_callback
上,而后调度_flushCallback
去执行这个此变量_callback,taskQueue被清空。
再看浏览器环境,它将入参(执行工作的函数)存到外部的变量scheduledHostCallback
上,而后通过MessageChannel的port去发送一个音讯,让channel.port1
的监听函数performWorkUntilDeadline
得以执行。performWorkUntilDeadline
外部会执行掉scheduledHostCallback
,最初taskQueue被清空。
通过下面的形容,能够很分明得找出调度者:非浏览器环境是setTimeout,浏览器环境是port.postMessage。而两个环境的执行者也不言而喻,前者是_flushCallback
,后者是performWorkUntilDeadline
,执行者做的事件都是去调用理论的工作执行函数。
因为本文围绕Scheduler的工夫片调度行为开展,所以次要探讨浏览器环境下的调度行为,performWorkUntilDeadline波及到调用工作执行函数去执行工作,这个过程中会波及工作的中断和复原、工作实现状态的判断,接下来的内容将重点对这两点进行解说。
工作执行 - 从performWorkUntilDeadline说起
在文章结尾的原理概述中提到过performWorkUntilDeadline
作为执行者,它的作用是依照工夫片的限度去中断工作,并告诉调度者再次调度一个新的执行者去持续工作。依照这种认知去看它的实现,会很清晰。
const performWorkUntilDeadline = () => { if (scheduledHostCallback !== null) { // 获取以后工夫 const currentTime = getCurrentTime(); // 计算deadline,deadline会参加到 // shouldYieldToHost(依据工夫片去限度工作执行)的计算中 deadline = currentTime + yieldInterval; // hasTimeRemaining示意工作是否还有剩余时间, // 它和工夫片一起限度工作的执行。如果没有工夫, // 或者工作的执行工夫超出工夫片限度了,那么中断工作。 // 它的默认为true,示意始终有剩余时间 // 因为MessageChannel的port在postMessage, // 是比setTimeout还靠前执行的宏工作,这意味着 // 在这一帧开始时,总是会有剩余时间 // 所以当初中断工作只看工夫片的了 const hasTimeRemaining = true; try { // scheduledHostCallback去执行工作的函数, // 当工作因为工夫片被打断时,它会返回true,示意 // 还有工作,所以会再让调度者调度一个执行者 // 继续执行工作 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; };
performWorkUntilDeadline
外部调用的是scheduledHostCallback
,它早在开始调度的时候就被requestHostCallback
赋值为了flushWork
,具体能够翻到下面回顾一下requestHostCallback
的实现。
flushWork
作为真正去执行工作的函数,它会循环taskQueue,逐个调用外面的工作函数。咱们看一下flushWork
具体做了什么。
function flushWork(hasTimeRemaining, initialTime) { ... return workLoop(hasTimeRemaining, initialTime); ...}
它调用了workLoop
,并将其调用的后果return了进来。那么当初工作执行的核心内容看来就在workLoop
中了。workLoop
的调用使得工作最终被执行。
工作中断和复原
要了解workLoop
,须要回顾Scheduler的性能之一:通过工夫片限度工作的执行工夫。那么既然工作的执行被限度了,它必定有可能是尚未实现的,如果未实现被中断,那么须要将它复原。
所以工夫片下的工作执行具备上面的重要特点:会被中断,也会被复原。
不难揣测出,workLoop
作为理论执行工作的函数,它做的事件必定与工作的中断复原无关。咱们先看一下它的构造:
function workLoop(hasTimeRemaining, initialTime) { // 获取taskQueue中排在最后面的工作 currentTask = peek(taskQueue); while (currentTask !== null) { if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())) { // break掉while循环 break } ... // 执行工作 ... // 工作执行结束,从队列中删除 pop(taskQueue); // 获取下一个工作,持续循环 currentTask = peek(taskQueue); } if (currentTask !== null) { // 如果currentTask不为空,阐明是工夫片的限度导致了工作中断 // return 一个 true通知内部,此时工作还未执行完,还有工作, // 翻译成英文就是hasMoreWork return true; } else { // 如果currentTask为空,阐明taskQueue队列中的工作曾经都 // 执行完了,而后从timerQueue中找工作,调用requestHostTimeout // 去把task放到taskQueue中,到时会再次发动调度,然而这次, // 会先return false,通知内部以后的taskQueue曾经清空, // 先进行执行工作,也就是终止任务调度 const firstTimer = peek(timerQueue); if (firstTimer !== null) { requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); } return false; }}
workLoop中能够分为两大部分:循环taskQueue执行工作 和 工作状态的判断。
循环taskQueue执行工作
暂且不论工作如何执行,只关注工作如何被工夫片限度,workLoop中:
if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())) { // break掉while循环 break}
currentTask就是以后正在执行的工作,它停止的判断条件是:工作并未过期,但曾经没有剩余时间了(因为hasTimeRemaining始终为true,这与MessageChannel作为宏工作的执行机会无关,咱们疏忽这个判断条件,只看工夫片),或者应该让出执行权给主线程(工夫片的限度),也就是说currentTask执行得好好的,可是工夫不容许,那只能先break掉本次while循环,使得本次循环上面currentTask执行的逻辑都不能被执行到(此处是中断工作的要害)。然而被break的只是while循环,while下部还是会判断currentTask的状态。
因为它只是被停止了,所以currentTask不可能是null,那么会return一个true通知内部还没完事呢(此处是复原工作的要害),否则阐明全副的工作都曾经执行完了,taskQueue曾经被清空了,return一个false好让内部终止本次调度。而workLoop的执行后果会被flushWork return进来,flushWork实际上是scheduledHostCallback
,当performWorkUntilDeadline
检测到scheduledHostCallback
的返回值(hasMoreWork)为false时,就会进行调度。
回顾performWorkUntilDeadline
中的行为,能够很清晰地将工作中断复原的机制串联起来:
const performWorkUntilDeadline = () => { ... const hasTimeRemaining = true; // scheduledHostCallback去执行工作的函数, // 当工作因为工夫片被打断时,它会返回true,示意 // 还有工作,所以会再让调度者调度一个执行者 // 继续执行工作 const hasMoreWork = scheduledHostCallback( hasTimeRemaining, currentTime, ); if (!hasMoreWork) { // 如果没有工作了,进行调度 isMessageLoopRunning = false; scheduledHostCallback = null; } else { // 如果还有工作,持续让调度者调度执行者,便于持续 // 实现工作 port.postMessage(null); } };
当工作被打断之后,performWorkUntilDeadline
会再让调度者调用一个执行者,继续执行这个工作,直到工作实现。然而这里有一个重点是如何判断该工作是否实现呢?这就须要钻研workLoop
中执行工作的那局部逻辑。
判断单个工作的实现状态
工作的中断复原是一个反复的过程,该过程会始终反复到工作实现。所以判断工作是否实现十分重要,而工作未实现则会反复执行工作函数。
咱们能够用递归函数做类比,如果没到递归边界,就反复调用本人。这个递归边界,就是工作实现的标记。因为递归函数所解决的工作就是它自身,能够很不便地把工作实现作为递归边界去结束任务,然而Scheduler中的workLoop
与递归不同的是,它只是一个执行工作的,这个工作并不是它本人产生的,而是内部的(比方它去执行React的工作循环渲染fiber树),它能够做到反复执行工作函数,但边界(即工作是否实现)却无奈像递归那样间接获取,只能依赖工作函数的返回值去判断。即:若工作函数返回值为函数,那么就阐明当前任务尚未实现,须要持续调用工作函数,否则工作实现。workLoop
就是通过这样的方法判断单个工作的实现状态。
在真正解说workLoop
中的执行工作的逻辑之前,咱们用一个例子来了解一下判断工作实现状态的外围。
有一个工作calculate,负责把currentResult每次加1,始终到3为止。当没到3的时候,calculate不是去调用它本身,而是将本身return进来,一旦到了3,return的是null。这样内部才能够晓得calculate是否曾经实现了工作。
const result = 3let currentResult = 0function calculate() { currentResult++ if (currentResult < result) { return calculate } return null}
下面是工作,接下来咱们模仿一下调度,去执行calculate。但执行应该是基于工夫片的,为了察看成果,只用setInterval去模仿因为工夫片停止复原工作的机制(相当毛糙的模仿,只需明确这是工夫片的模仿即可,重点关注工作实现状态的判断),1秒执行它一次,即一次只实现全副工作的三分之一。
另外Scheduler中有两个队列去治理工作,咱们暂且只用一个队列(taskQueue)存储工作。除此之外还须要三个角色:把工作退出调度的函数(调度入口scheduleCallback)、开始调度的函数(requestHostCallback)、执行工作的函数(workLoop,要害逻辑所在)。
const result = 3let currentResult = 0function calculate() { currentResult++ if (currentResult < result) { return calculate } return null}// 寄存工作的队列const taskQueue = []// 寄存模仿工夫片的定时器let interval// 调度入口----------------------------------------const scheduleCallback = (task, priority) => { // 创立一个专属于调度器的工作 const taskItem = { callback: task, priority } // 向队列中增加工作 taskQueue.push(taskItem) // 优先级影响到工作在队列中的排序,将优先级最高的工作排在最后面 taskQueue.sort((a, b) => (a.priority - b.priority)) // 开始执行工作,调度开始 requestHostCallback(workLoop)}// 开始调度-----------------------------------------const requestHostCallback = cb => { interval = setInterval(cb, 1000)}// 执行工作-----------------------------------------const workLoop = () => { // 从队列中取出工作 const currentTask = taskQueue[0] // 获取真正的工作函数,即calculate const taskCallback = currentTask.callback // 判断工作函数否是函数,若是,执行它,将返回值更新到currentTask的callback中 // 所以,taskCallback是上一阶段执行的返回值,若它是函数类型,则阐明上一次执行返回了函数 // 类型,阐明工作尚未实现,本次继续执行这个函数,否则阐明工作实现。 if (typeof taskCallback === 'function') { currentTask.callback = taskCallback() console.log('正在执行工作,以后的currentResult 是', currentResult); } else { // 工作实现。将以后的这个工作从taskQueue中移除,并革除定时器 console.log('工作实现,最终的 currentResult 是', currentResult); taskQueue.shift() clearInterval(interval) }}// 把calculate退出调度,也就意味着调度开始scheduleCallback(calculate, 1)
最终的执行后果如下:
正在执行工作,以后的currentResult 是 1正在执行工作,以后的currentResult 是 2正在执行工作,以后的currentResult 是 3工作实现,最终的 currentResult 是 3
可见,如果没有加到3,那么calculate会return它本人,workLoop若判断返回值为function,阐明工作还未实现,它就会持续调用工作函数去实现工作。
这个例子只保留了workLoop中判断工作实现状态的逻辑,其余的中央并不欠缺,要以真正的的workLoop为准,当初让咱们贴出它的全副代码,残缺地看一下真正的实现:
function workLoop(hasTimeRemaining, initialTime) { let currentTime = initialTime; // 开始执行前检查一下timerQueue中的过期工作, // 放到taskQueue中 advanceTimers(currentTime); // 获取taskQueue中最紧急的工作 currentTask = peek(taskQueue); // 循环taskQueue,执行工作 while ( currentTask !== null && !(enableSchedulerDebugging && isSchedulerPaused) ) { if ( currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost()) ) { // 工夫片的限度,中断工作 break; } // 执行工作 --------------------------------------------------- // 获取工作的执行函数,这个callback就是React传给Scheduler // 的工作。例如:performConcurrentWorkOnRoot const callback = currentTask.callback; if (typeof callback === 'function') { // 如果执行函数为function,阐明还有工作可做,调用它 currentTask.callback = null; // 获取工作的优先级 currentPriorityLevel = currentTask.priorityLevel; // 工作是否过期 const didUserCallbackTimeout = currentTask.expirationTime <= currentTime; // 获取工作函数的执行后果 const continuationCallback = callback(didUserCallbackTimeout); if (typeof continuationCallback === 'function') { // 查看callback的执行后果返回的是不是函数,如果返回的是函数,则将这个函数作为当前任务新的回调。 // concurrent模式下,callback是performConcurrentWorkOnRoot,其外部依据以后调度的工作 // 是否雷同,来决定是否返回本身,如果雷同,则阐明还有工作没做完,返回本身,其作为新的callback // 被放到以后的task上。while循环实现一次之后,查看shouldYieldToHost,如果须要让出执行权, // 则中断循环,走到下方,判断currentTask不为null,返回true,阐明还有工作,回到performWorkUntilDeadline // 中,判断还有工作,持续port.postMessage(null),调用监听函数performWorkUntilDeadline(执行者), // 持续调用workLoop行工作 // 将返回值持续赋值给currentTask.callback,为得是下一次可能继续执行callback, // 获取它的返回值,持续判断工作是否实现。 currentTask.callback = continuationCallback; } else { if (currentTask === peek(taskQueue)) { pop(taskQueue); } } advanceTimers(currentTime); } else { pop(taskQueue); } // 从taskQueue中持续获取工作,如果上一个工作未实现,那么它将不会 // 被从队列剔除,所以获取到的currentTask还是上一个工作,会持续 // 去执行它 currentTask = peek(taskQueue); } // return 的后果会作为 performWorkUntilDeadline // 中判断是否还须要再次发动调度的根据 if (currentTask !== null) { return true; } else { // 若工作实现,去timerQueue中找须要最早开始执行的那个工作 // 调度requestHostTimeout,目标是等到了它的开始事件时把它 // 放到taskQueue中,再次调度 const firstTimer = peek(timerQueue); if (firstTimer !== null) { requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); } return false; }}
所以,workLoop是通过判断工作函数的返回值去辨认工作的实现状态的。
总结一下判断工作实现状态与工作执行的整体关系:当开始调度后,调度者调度执行者去执行工作,实际上是执行工作上的callback(也就是工作函数)。如果执行者判断callback返回值为一个function,阐明未实现,那么会将返回的这个function再次赋值给工作的callback,因为工作还未实现,所以并不会被剔除出taskQueue,currentTask获取到的还是它,while循环到下一次还是会继续执行这个工作,直到工作实现出队,才会持续下一个。
另外有一个点须要提一下,就是构建fiber树的工作函数:performConcurrentWorkOnRoot
,它承受的参数是fiberRoot。
function performConcurrentWorkOnRoot(root) { ...}
在workLoop中它会被这样调用(callback即为performConcurrentWorkOnRoot
):
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;const continuationCallback = callback(didUserCallbackTimeout);
didUserCallbackTimeout
显著是boolean类型的值,并不是fiberRoot,但performConcurrentWorkOnRoot却能失常调用。这是因为在开始调度,以及后续的return本身的时候,都在bind的时候将root传进去了。
// 调度的时候scheduleCallback( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root),);// 其外部return本身的时候function performConcurrentWorkOnRoot(root) { ... if (root.callbackNode === originalCallbackNode) { return performConcurrentWorkOnRoot.bind(null, root); } return null;}
这样的话,再给它传参数调用它,那这个参数只能作为后续的参数被接管到,performConcurrentWorkOnRoot
中接管到的第一个参数还是bind时传入的那个root,这个特点与bind的实现无关。能够跑一下上面的这个简略例子:
function test(root, b) { console.log(root, b)}function runTest() { return test.bind(null, 'root')}runTest()(false)// 后果:root false
以上,是Scheduler执行工作时的两大外围逻辑:工作的中断与复原 & 工作实现状态的判断。它们协同单干,若工作未实现就中断了工作,那么调度的新执行者会复原执行该工作,直到它实现。到此,Scheduler的外围局部曾经写完了,上面是勾销调度的逻辑。
勾销调度
通过下面的内容咱们晓得,工作执行实际上是执行的工作的callback,当callback是function的时候去执行它,当它为null的时候会产生什么?以后的工作会被剔除出taskQueue,让咱们再来看一下workLoop函数:
function workLoop(hasTimeRemaining, initialTime) { ... // 获取taskQueue中最紧急的工作 currentTask = peek(taskQueue); while (currentTask !== null) { ... const callback = currentTask.callback; if (typeof callback === 'function') { // 执行工作 } else { // 如果callback为null,将工作出队 pop(taskQueue); } currentTask = peek(taskQueue); } ...}
所以勾销调度的要害就是将以后这个工作的callback设置为null。
function unstable_cancelCallback(task) { ... task.callback = null;}
为什么设置callback为null就能勾销任务调度呢?因为在workLoop中,如果callback是null会被移出taskQueue,所以以后的这个工作就不会再被执行了。它勾销的是当前任务的执行,while循环还会继续执行下一个工作。
勾销工作在React的场景是什么呢?当一个更新工作正在进行的时候,忽然有高优先级工作进来了,那么就要勾销掉这个正在进行的工作,这只是泛滥场景中的一种。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { ... if (existingCallbackNode !== null) { const existingCallbackPriority = root.callbackPriority; if (existingCallbackPriority === newCallbackPriority) { return; } // 勾销掉原有的工作 cancelCallback(existingCallbackNode); } ...}
总结
Scheduler用工作优先级去实现多任务的治理,优先解决高优工作,用工作的继续调度来解决工夫片造成的单个工作中断复原问题。工作函数的执行后果为是否应该完结当前任务的调度提供参考,另外,在无限的工夫片内实现工作的一部分,也为浏览器响应交互与实现工作提供了保障。
欢送扫码关注公众号,发现更多技术文章