共计 19733 个字符,预计需要花费 50 分钟才能阅读完成。
关键词:react react-scheduler scheduler 工夫切片 任务调度 workLoop
背景
本文所有对于 React 源码的探讨,基于 React v17.0.2 版本。
文章背景
工作中始终有在用 React 相干的技术栈,但却始终没有花工夫好好思考一下其底层的运行逻辑,碰巧身边的小伙伴们也有相似的打算,所以决定组团卷一波,对 React 自身探个到底。
本文是基于泛滥的源码剖析文章,退出本人的了解,而后输入的一篇常识梳理。如果你也感兴趣,倡议多看看参考资料中的诸多援用文章,置信你也会有不一样的播种。
本文不会具体阐明 React 中 react-reconciler、react-dom、fiber、dom diff、lane 等常识,仅针对 scheduler 这一细节进行分析。
知识点背景
在我尝试了解 React 中 Scheduler 模块的过程中,发现有很多概念了解起来比拟绕,也是在一直问本人为什么的过程中,发现如果自顶向下的先有一些根本的认知,再深刻了解 Scheduler 在 React 中所做的事件,就变得容易很多。
浏览器的 EventLoop 简略阐明
此处默认你曾经晓得了 EventLoop 及浏览器渲染的相干常识
一个 frame 渲染(帧渲染)的过程,按 60fps 来计算,大略有 16.6ms,在这个过程中浏览器要做很多货色,包含“执行 JS -> 闲暇 -> 绘制(16ms)”,在执行 JS 的过程中,即是浏览器的 JS 线程执行 eventloop 的过程,外面包含了 marco task 和 mirco task 的执行,其中执行多少个 macro task 的数量是由浏览器决定的,而这个数量并没有明确的限度。
因为 whatwg 标准规范中只是倡议浏览器尽可能保障 60fps 的渲染体验,因而,不同的浏览器的实现也并没有明确阐明。同时须要留神,并不是每一帧都会执行绘制操作。如果某一个 macro task 及其后执行 mirco task 工夫太长,都会延后浏览器的绘制操作,也就是咱们常见的掉帧、卡顿。
React 的 Scheduler 的简略阐明
React 为了解决 15 版本存在的问题:组件的更新是递归执行,所以更新一旦开始,中途就无奈中断。当层级很深时,递归更新工夫超过了 16ms,用户交互就会卡顿。
React 引入了 Fiber 的架构,同时配合 Schedduler 的任务调度器,在 Concurrent 模式下能够将 React 的组件更新工作变成可中断、复原的执行,就缩小了组件更新所造成的页面卡顿。
目录
-
常见问题
- Scheduler 是什么,作用是什么
- 理论生产中咱们的 React 库有用到 Scheduler 调度吗
- 为什么用 MessageChannel,而不必 setTimeout?
- 为什么不必 Generator、Webworkers 来做任务调度
-
外围逻辑解析
- 概念阐明
- 外围流程图
- 如何实现的工作切片
- 如何实现工作的中断
- 如何实现工作的复原
- 集体的一点了解
-
Demo 示例
- 利用 Scheduler 任务调度的示例
- 不必 Scheduler 任务调度的示例
- 设置切片工夫为 0ms 时 的情景
- 实现一个 Scheduler 外围逻辑——判断单个工作的实现状态
-
拓展
- Scheduler 的开源打算
- Scheduler 为浏览器提供标准
- React 18 的离屏渲染
- Vue 和 React 的两种计划的抉择
常见问题
Scheduler 是什么,作用是什么
Scheduler 是一个独立的包,不仅仅在 React 中能够应用。
Scheduler 是一个任务调度器,它会依据工作的优先级对工作进行调用执行。
在有多个工作的状况下,它会先执行优先级高的工作。如果一个工作执行的工夫过长,Scheduler 会中断当前任务,让出线程的执行权,防止造成用户操作时界面的卡顿。在下一次复原未实现的工作的执行。
Scheduler 是 React 团队开发的一个用于事务调度的包,内置于 React 我的项目中。其团队的愿景是孵化实现后,使这个包独立于 React,成为一个能有更宽泛应用的工具。参考 React 实战视频解说:进入学习
理论生产中咱们的 React 库有用到 Scheduler 调度吗
这个问题,其实是我集体想阐明的一个点
因为在我看的很多文章中,大家都在一直强调 Scheduler 的各种益处,各种原理,以至于我最开始也认为只有引入了 React 16-17 的版本,就能领会到这样的“优化”成果。然而当我开启源码调试时,就产生了困惑,因为齐全没有依照套路来输入我辛辛苦苦打的 console.log。
直到我应用 Concurrent 模式才领会到 Scheduler 的任务调度外围逻辑。这个模式直到 React 17 都没有裸露稳固的 API,只是提供了一个非稳定版的 unstable_createRoot 办法。
论断:Scheduler 的逻辑有被 React 应用,然而其外围的切片、工作中断、工作复原并没有在稳定版中采纳,你能够了解当初的 React 在执行 Fiber 工作时,还是一撸到底。
为什么用 MessageChannel,而不首选 setTimeout
如果以后环境不反对 MessageChannel 时,会默认应用 setTimeout
-
MessageChannel 的作用
- 生成浏览器 Eventloops 中的一个宏工作,实现将主线程还给浏览器,以便浏览器更新页面
- 浏览器更新页面后可能继续执行未实现的 Scheduler 中的工作
- tips:不必微工作迭代起因是,微工作将在页面更新前全副执行完,达不到将主线程还给浏览器的目标
- 抉择 MessageChannel 的起因是因为 setTimeout(fn,0) 所创立的宏工作,会有至多 4ms 的执行时差,setInterval 同理
- 代码示例:MessageChannel 总会在 setTimeout 工作之前执行,且执行耗费的工夫总会小于 setTimeout
// setTimeout 的执行示例
var date1 = Date.now()
console.log('setTimeout 执行的工夫戳 1:',date1)
setTimeout(()=>{var date2 = Date.now()
console.log('setTimeout 执行的工夫戳 2:',date2)
console.log('setTimeout 时差:',date2 - date1)
},0)
// messageChannel 的执行示例
var channel = new MessageChannel()
var port1 = channel.port1;
var port2 = channel.port2;
port1.onmessage = ()=>{var cTime2 = Date.now()
console.log('messageChannel 执行的工夫戳 2:',cTime2)
console.log('messageChannel 时差:', cTime2-cTime1)
}
var cTime1 = Date.now()
console.log('messageChannel 执行的工夫戳 1:',cTime1)
port2.postMessage(null)
React v16.10.0 之后齐全应用 postMessage
- 不抉择 requestIdelCallback 的起因
从 React 的 issues 及之前版本(在 15.6 的源码中能搜到)中能够看到,requestIdelCallback 办法也被 React 尝试过,只是起初因为兼容性、不同机器及浏览器执行效率的问题又被 requestAnimationFrame + setTimeout 的 polyfill 办法代替了
- 不抉择 requestAnimationFrame 的起因
在 React 16.10.0 之前还是应用的 requestAnimationFrame + setTimeout 的办法,配合动静帧计算的逻辑来解决工作,起初也因为这样的成果并不现实,所以 React 团队才决定彻底放弃此办法
requestAnimationFrame 还有个特点,就是当页面解决未激活的状态下,requestAnimationFrame 会进行执行;当页面前面再转为激活时,requestAnimationFrame 又会接着上次的中央继续执行。
为什么不必 Generator、Webworkers 来做任务调度
针对 Generator,其实 React 团队为此做过一些致力
- Generator 不能在栈两头让出。比方你想在嵌套的函数调用两头让出, 首先你须要将这些函数都包装成 Generator,另外这种栈两头的让出解决起来也比拟麻烦,难以了解。除了语法开销,现有的生成器实现开销比拟大,所以不如不必。
- Generator 是有状态的, 很难在两头复原这些状态。
针对 Webworkers,React 团队同样做过一些剖析和探讨
对于在 React 中引入 Webworkers 的探讨,我这里仅贴一下在 issues 中看到的局部,因为没有深刻去钻研前因后果,暂不做翻译
- How do you start a worker?
For now I can see the following solutions for this problem:
- separate file that includes only what is necessary for the worker, which would require extra build steps
- create a worker on the fly (blob), which will not work in every browser and I expect would have performance penalties. Also resolving dependencies here for the worker is going to be painful – if not impossible without extra build steps.
- start the entire build in multiple workers, still this would still require the usage of a build tool
So yeah, for now I don’t see this working without a build tool. My preference would go to the first one.
- How do you determine the root to render into?
I would expect the “main” React to always start in the main thread, and components leaving stubs in this thread to which they can write when they want to. Of course writing to the DOM still needs to be done via the normal React reconciliation mechanism.
It should be possible to have a single worker which is used for multiple components, which makes it a bit more challenging. Probably an extra id needs to be given to communicate to the right component.
- How do we unit test the system?
If you would be testing a render function, it would initially only show the webworker stubs – and testing the result of a webworker would be something different. Something like a callback for a webworker result could work here (waitFor(webworkerId) comes to mind).
If there are other options here or I’m missing something, I would definitely like to hear it!
外围逻辑解析
概念阐明
为了不便后续的了解,先对源码中常见的概念或代码块做一个解读
-
Concurrent 模式:
- 将渲染工作合成为多个局部,对工作进行暂停和复原操作以防止阻塞浏览器。这意味着 React 能够在提交之前屡次调用渲染阶段生命周期的办法,或者在不提交的状况下调用它们(默认状况下未启用)
- 整个 Scheduler 的任务调度、工夫切片、工作中断及复原都是依赖于 Concurent 模式及 Fiber 数据结构。
-
Scheduler task
- task 对象
// 一个 scheduler 的工作 var newTask = { id: taskIdCounter++, // 工作 id,在 react 中是一个全局变量,每次新增 task 会自增 +1 callback: callback, // 在调度过程中被执行的回调函数 priorityLevel: priorityLevel, // 通过 Scheduler 和 React Lanes 优先级交融过的工作优先级 startTime: startTime, // 工作开始工夫 expirationTime: expirationTime, // 工作过期工夫 sortIndex: -1 // 排序索引, 全等于过期工夫. 保障过期工夫越小, 越紧急的工作排在最后面 };
-
task 执行的实质
- 执行逻辑在 scheduler 包中的 workLoop 办法中,代码如下:
function workLoop(hasTimeRemaining, initialTime) { // ... 其余逻辑 while (currentTask !== null && !(enableSchedulerDebugging)) { // ... 其余逻辑 if (typeof callback === 'function') { // ... 其余逻辑 // 此处即执行 callback var continuationCallback = callback(didUserCallbackTimeout); // ... 其余逻辑 } } // ... 其余逻辑 }
-
task 执行的办法本质
newTask
中的callback
是由unstable_scheduleCallback(priorityLevel, callback, options)
传入unstable_scheduleCallback
办法中的callback
是在scheduleCallback(reactPriorityLevel, callback, options)
办法中传入scheduleCallback
办法中的callback
是在ensureRootIsScheduled
中的newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
设置- 因而能够看到
newTask
实质执行的办法是performConcurrentWorkOnRoot
,即构建 Fiber 树的工作函数
-
timerQueue 与 taskQueue
- timerQueue:根据工作的过期工夫(expirationTime)排序,过期工夫越早,阐明越紧急,过期工夫小的排在后面。过期工夫依据工作优先级计算得出,优先级越高,过期工夫越早。
- taskQueue:根据工作的开始工夫(startTime)排序,开始工夫越早,说明会越早开始,开始工夫小的排在后面。工作进来的时候,开始工夫默认是以后工夫,如果进入调度的时候传了延迟时间,开始工夫则是以后工夫与延迟时间的和。
-
两者的分割
- 在创立新的 task 时,如果发现这个工作的执行工夫并不紧急,则会将其先放入 timerQueue 队列
- 优先执行的 task 在 taskQueue 队列中
- 在不同的执行阶段会通过
advanceTimers
办法,从 timerQueue 中将快过期的工作让如到 taskQueue 队列
function advanceTimers(currentTime) { // Check for tasks that are no longer delayed and add them to the queue. var timer = peek(timerQueue); while (timer !== null) {if (timer.callback === null) { // Timer was cancelled. pop(timerQueue); } else if (timer.startTime <= currentTime) { // Timer fired. Transfer to the task queue. pop(timerQueue); timer.sortIndex = timer.expirationTime; push(taskQueue, timer); } else { // Remaining timers are pending. return; } timer = peek(timerQueue); } }
-
Scheduler 与 React 的分割
- 阐明:因为 Scheduler 实质能够和 React 拆散,在 Scheduler 中也有其本人的工作优先级定义,而 React 中也利用 Lanes 的优先级模型,所以 React 在应用 Scheduler 的任务调度时,须要有一个工作优先级的转换过程
- 源码示例:
function scheduleCallback(reactPriorityLevel, callback, options) { // 将 React 的工作优先级转换为 Scheduler 的工作优先级 var priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel); return Scheduler_scheduleCallback(priorityLevel, callback, options); }
外围逻辑解析
依然举荐大家看一下 7kms 大佬的 React 外围流程图,每深刻一个模块,再回过头来看这张图都会有不一样的了解。
外围流程图
- 应用了 Scheduler 任务调度的流程图(Conurrent 模式)
- 没有应用 Scheduler 工作的调度的流程图(默认模式,Legacry 模式)
- 从源码能够看到,区别非常简单,就是循环中多了有一个
!shouldYield()
的判断,用于做工夫切片
// concurrent 模式
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {performUnitOfWork(workInProgress);
}
}
// legacy 模式
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {performUnitOfWork(workInProgress);
}
}
如何实现的工作切片
-
判断条件
- 定义了 yieldInterval 变量,默认写死的是 5ms
- 导出了条件办法 unstable_shouldYield
- 代码局部
var yieldInterval = 5; var deadline = 0; // TODO: Make this configurable { // `isInputPending` is not available. Since we have no way of knowing if // there's pending input, always yield at the end of the frame. exports.unstable_shouldYield = function () {return exports.unstable_now() >= deadline; }; // Since we yield every frame regardless, `requestPaint` has no effect.
requestPaint = function () {};
}
- 外围逻辑
- 在 react-reconciler 中的 workLoopConcurrent 中利用如下
// shouldYield() 办法即 unstable_shouldYield 自身
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
- 在 react-scheduler 中的 workLoop 中利用如下
function workLoop(hasTimeRemaining, initialTime) {
var currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
// unstable_shouldYield 用于判断是否要中断
while (currentTask !== null && !(enableSchedulerDebugging)) {
if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || exports.unstable_shouldYield())) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
// 省略其余代码
}
- 阐明:
- 在了解切片的过程中,我始终思考错了方向,总想着是先把工作按工夫切好之后,再依次执行,以此达到切片的成果
- 其实是另一种实现,举个例子:比方咱们切萝卜,并不是先标记好每一段在哪才下手,而是达到肯定长度就下手,最终实现了按将萝卜切成类似的一段一段
- 有了这层思考之后,了解切片,其实就是到工夫点就进行,到工夫点就进行,以此循环,最终看到的后果便是按肯定时间段切割的成果
如何实现工作的中断
在了解了上述工作的切片之后,再了解工作的中断就变得非常容易,工作的中断即在 reconciler 和 scheduler 中两个 workLoop 循环的 break
在工作中断的同时,还有两处须要留神的逻辑,即 react 是如何保留中断那一时刻的工作,以便后续复原
- 在 scheduler 中,在每次执行 workLoop 中的循环时,是在执行 performConcurrentWorkOnRoot 办法
function workLoop(hasTimeRemaining, initialTime) {
var currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
// 针对 taskQueue 办法进行循环遍历
while (currentTask !== null && !(enableSchedulerDebugging)) {if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || exports.unstable_shouldYield())) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
// 从以后的 task 中获取执行的办法
var callback = currentTask.callback;
// 如果执行的办法存在,则持续
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
var didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// 此时,执行 callback,即 performConcurrentWorkOnRoot 办法
// 在执行 performConcurrentWorkOnRoot 办法的过程中,如果 reconciler 中的 workLoop 中断了
// 会返回 performConcurrentWorkOnRoot 本身办法,也就是 continuationCallback 会被放到以后 task 的 callback
// 此时 workLoop 的 while 循环中断,然而因为以后 task 并没有从队列中进去,// 所以下一次执行 workLoop 时,依然会执行本次存储的 continuationCallback
var continuationCallback = callback(didUserCallbackTimeout);
currentTime = exports.unstable_now();
if (typeof continuationCallback === 'function') {currentTask.callback = continuationCallback;} else {if (currentTask === peek(taskQueue)) {pop(taskQueue);
}
}
advanceTimers(currentTime);
} // 执行的办法不存在,则将当前任务从 taskQueue 移除
else {pop(taskQueue);
}
// 获取队列中下一个办法
currentTask = peek(taskQueue);
} // Return whether there's additional work
if (currentTask !== null) {return true;} else {var firstTimer = peek(timerQueue);
if (firstTimer !== null) {requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
- reconciler 中的 performConcurrentWorkOnRoot 办法,会在执行时,通过逻辑判断,返回不同的值,当返回的值为其本身时,能够视作是一种中断前的状态保留
function performConcurrentWorkOnRoot(){
// 其余逻辑
// 当 fiber 链表的 callbackNode 在执行时,并没有产生扭转
// 则阐明当前任务和之前是雷同的工作,即上一次执行的工作还能够持续
// 便将其本身返回,用于 scheduler 中的 continuationCallback
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performConcurrentWorkOnRoot.bind(null, root);
}
// 其余逻辑
}
如何实现工作的复原
其实到这里,能够发现,在理解了上述的工作切片和工作中断之后,工作复原的逻辑就很容易了解了。
换一个角度思考,即如果在 reconciler 中的 workLoopConcurrent 被中断了,则会返回一个 performConcurrentWorkOnRoot 办法,在 scheduler 中的 workLoop 发现 continuationCallback 返回的值为一个办法,则会存下以后中断的回调,且不让以后执行的工作出栈,也就意味着以后的 task 没有执行完,下一次循环时能够继续执行,而执行的办法便是 continuationCallback。
以此,实现了工作的复原。
集体的一点了解
要了解 scheduler,要从浏览器的 eventloop 开始了解,就会发现,这其实是 3 个 loop 循环的配合
- 一个比拟泛的流程示例,仅给大家提供一些思考方向
在 React 中宏观来看,针对浏览器、Scheduler、Reconciler 其实是有 3 层 Loop。浏览器级别的 eventLoop,Scheduler 级别的 workLoop,Reconciler 级别 workLoopConcurrent。
-
浏览器的 eventLoop 与 Scheduler 的关系
- 每次 eventLoop 会执行宏工作的队列的宏工作,而 React 中的 Scheduler 就是用宏工作 messageChannel 触发的。
- 当 eventLoop 开始执行跟 Scheduler 无关的宏工作时,Scheduler 会启动一次 workloop,就是在遍历执行 Scheduler 中已存在的 taskQueue 队列的每个 task。
-
Scheduler 与 Reconciler 的关系
- Scheduler 中的 workLoop 中每执行一次 task,是通过调用 Reconciler 中的 performConcurrentWorkOnRoot 办法,即每一个 task 能够了解为是一个 performConcurrentWorkOnRoot 办法的调用。
- performConcurrentWorkOnRoot 办法每次调用,其本质是在执行 workLoopConcurrent 办法,这个办法是在循环 performUnitOfWork 这个构建 Fiber 树中每个 Fiber 的办法。
因而能够梳理进去,3 个大循环,从最开始的 eventLoop 的单个宏工作执行,会逐渐触发 Scheduler 和 Reconciler 的工作循环执行。
工作的中断与复原,实现中断与复原的逻辑分了 2 个局部,第一个是 Scheduler 中正在执行的 workloop 的工作中断,第二个是 Reconciler 中正在执行的 workLoopConcurrent 的工作中断
- Reconciler 中的工作中断与复原:在 workLoopConcurrent 的 while 循环中,通过 shouldYield() 办法来判断以后构建 fiber 树的执行过程是否超时,如果超时,则中断以后的 while 循环。因为每次 while 执行的 fiber 构建办法,即 performUnitOfWork 是依照每个 fiberNode 来遍历的,也就是说每实现一次 fiberNode 的 beginWork + completeWork 树的构建过程,会设置下一次 nextNode 的值,能够了解为中断时曾经保留了下一次要构建的 fiberNode 指针,以至于不会下一次不晓得从哪里持续。
- Scheduler 中的工作中断与复原:当执行工作工夫超时后,如果 Reconciler 中的 performConcurrentWorkOnRoot 办法没有执行实现,会返回其本身。在 Scheduler 中,发现当前任务还有下一个工作没有执行完,则不会将当前任务从 taskQueue 中取出,同时会把 reconciler 中返回的待执行的回调函数持续赋值给当前任务,于是下一次持续启动 Scheduler 的工作时,也就连贯上了。同时退出这次中断的工作前,会通过 messageChannel 向 eventLoop 的宏工作队列放入一个新的宏工作。
- 所以工作的复原,其实就是从下一次 eventLoop 开始执行 Scheduler 相干的宏工作,而执行的宏工作也是 Reconciler 中断前赋值的 fiberNode,也就实现了整体的工作复原。
Demo 示例
示例仅采取了一些要害代码的示例。
tips:如何调试 React 源码,大家能够查看参考资料中的《React 技术揭秘》中的调试代码环节
不必 Scheduler 任务调度的示例
-
代码示例
- 创立 React 我的项目后的 index.js 代码
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; // React 默认的渲染模式,即 legacy 模式 // 此模式会应用到 Scheduler 的办法,但并不会做工夫切片、工作中断、复原的相干逻辑 ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') );
- App.js 代码示例
import List from './scheduler-demo/list' function App() { return ( <div className="App"> <List /> </div> ); } export default App;
- list.js 代码示例
import React from 'react' export default function List () { return <ul> {Array(3000).fill(0).map((_, i) => <li>{i}</li>)} </ul> }
- 成果示例
![图 -Demo-1.jpg](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2b3e21bb5f7040e0905b44b958e90ff6~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp?)
- 后果阐明
- 能够从图中示例看到,在没有任务调度的状况下,如果咱们存在大量的 DOM 计算,则会将一次计算 DOM 相干的计算进行到底,之后对立输入渲染,能够看到渲染 3000 个 `<li>` 节点,大概耗时 180ms
- 次要关注 React 的逻辑解决,即 `scheduleUpdateOnFiber` 的入口函数
- 能够看到主流程的逻辑,根本都带有 `xxxSync` 的同步命名,也根本阐明了在 `legacy` 模式下执行的是同步解决逻辑
##### 利用 Scheduler 任务调度的示例
- 代码示例
- 创立 React 我的项目后的 index.js 代码
import React from ‘react’;
import ReactDOM from ‘react-dom’;
import ‘./index.css’;
import App from ‘./App’;
// React 的 concurrent 渲染模式
// 此模式会应用到 Scheduler 的办法,并且会做工夫切片、工作中断、复原的相干逻辑
ReactDOM.unstable_createRoot(document.getElementById(‘root’)).render(<React.StrictMode>
<App />
</React.StrictMode>);
- App.js 代码示例、list.js 代码示例不须要调整
-
![图 -Demo-2.jpg](https://img-blog.csdnimg.cn/img_convert/026df66dd7b52f983bbf87facb354f7c.webp?x-oss-process=image/format,png)
- 后果阐明
- 能够从图中示例看到,在有任务调度的状况下,会将 DOM 计算的过程切割成一段一段 5ms 左右的宏工作
- 次要关注 React 的逻辑解决,能够看到调用了很多带有 `xxxConcurrent` 的 concurrent 模式特有的办法
- 须要留神并不是每个工作都是齐全依照 5ms 这个值进行切割的,会或多或少的相似 5.1 ms、5.2 ms 的切片,这是因为在做切割逻辑时,也会有 js 执行的工夫损耗。- 同时如果某个工作执行过程比拟久,也会占用较为大的工夫,比方在呈现较为稳固的 5ms 切片工作前的第一个工作,大概耗时了 24 ms,也是因为以后的执行逻辑还并未走进切片逻辑,是其余的 React 执行所耗时。##### 设置切片工夫为 0ms 时 的情景
- 代码示例
- index.js、App.js、list.js 的文件不须要调整,同 concurrent 模式
- 批改引入的 React 源码,次要设置 yieldInterval 的赋值逻辑,示例如下:
// 在 scheduler 相干的源码中
var isMessageLoopRunning = false;
var scheduledHostCallback = null;
var taskTimeoutID = -1; // Scheduler periodically yields in case there is other work on the main
// thread, like user events. By default, it yields multiple times per frame.
// It does not attempt to align with frame boundaries, since most tasks don’t
// need to be frame aligned; for those that do, use requestAnimationFrame.
var yieldInterval = 0; // 将此处的值由原来的 5 改为 0
var deadline = 0; // TODO: Make this configurable
- 成果示例
![图 -Demo-3.gif](https://img-blog.csdnimg.cn/img_convert/b13f71cf9f8cc4bd85d8da9f7015705f.gif)
![图 -Demo-3.jpg](https://img-blog.csdnimg.cn/img_convert/55aef897425c25f049f09ee3a9d5a251.webp?x-oss-process=image/format,png)
- 后果阐明
- 从成果示例中能够看到,当切片工夫由 5ms 变为 0ms 后,渲染时长变的很长,大概是 5s 之后才将 DOM 渲染进去
- 从 Performance 中能够看出,工作依据 0ms 一段切割成了 n 个宏工作片段,并且很难找到(其实还是有)concurrent 模式下的 React 办法执行
- 所以能够得出一个论断,在 concurrent 模式下,将切片工夫由 5ms 变为 0ms 后,Scheduler 还是会切割工作,因为 js 执行自身也是有工夫损耗的,所以每一次的 task 执行齐全依赖于浏览器外部对于这些产生的宏工作的解决,曾经脱离了 Scheduler 自身能管制的范畴。即只有用了 concurrent 模式,都会有工作切割、中断、回复,然而产生的成果如何,齐全依赖于代码逻辑以及浏览器执行底层的解决。- 从 Scheduler 的角度登程,大家能够依据状况去设置这个工夫切片的节点,还是不倡议改为 0(演示除外)##### 实现一个 Scheduler 外围逻辑
- 示例代码
const result = 3
let currentResult = 0
function 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
- 后果阐明
- 本示例次要展现的是 ` 如何判断单个工作的实现状态 `
- 本示例展现 Scheduler 中如何对工作中断后如何进行复原 `typeof taskCallback === function`
- 本示例次要展现了工作实现的逻辑解决
- 本示例并未退出切片的逻辑,其实要退出也并不简单,即在 `workLoop` 退出循环的判断条件即可,参考 Scheduler 源码
#### 拓展
##### Scheduler 的开源打算
从 Scheduler 源码的 README.md 中能够看到,React 团队是心愿它变得更通用,不仅仅服务于 React,只是现阶段更多是用于 React 中。- npm 地址:www.npmjs.com/package/sch…
- README.md 原文:
This is a package for cooperative scheduling in a browser environment. It is currently used internally by React, but we plan to make it more generic.
The public API for this package is not yet finalized.
##### Scheduler 为浏览器提供标准
调度零碎的限度:- 调度零碎只能有一个,如果同时存在两个调度零碎,就无奈保障调度的正确性。- 调度零碎能力无限,只能在浏览器提供的能力范畴内进行调度,而无奈影响比方 HTML 渲染、内存回收周期。为了解决这个问题,Chrome 正在与 React、Polymer、Ember、Google Maps、Web Standars Community 独特创立一个浏览器调度标准,提供浏览器级别 API,能够让调度管制更底层的渲染机会,也保障调度器的唯一性。##### React 18 的离屏渲染
> React 的离屏渲染是在 React 18 中的一个新 API,作用能够先视作 keep-alive 的实现
之所以在这里提一下离屏渲染,是因为这也是一种晋升用户体验,缩小用户卡顿的优化体验。如果说 Scheduler 任务调度器是为了可能让一个工作不至于将用户页面卡死,那么离屏渲染则是可能让用户在看到页面时就不须要再期待。- React 18 中提出的新 API
- 原文如下,避免变味不做硬翻
The main motivation for the new Offscreen API (and the effects changes described in this post) is to allow React to preserve state like this by hiding components instead of unmounting them. To do this React will call the same lifecycle hooks as it does when unmounting– but it will also preserve the state of both React components and DOM elements.
- 离屏渲染的拓展(此处的阐明已与 React 无关):- 概念:指的是 GPU 在以后屏幕缓冲区以外新开拓一个缓冲区 (离屏缓存区) 进行渲染操作。等所有数据都在离屏渲染区实现渲染后才会提交到帧缓存区,而后再被显示。- 利用场景:Android、IOS、Electron
- 集体了解:须要利用 GPU 做辅助渲染,不便 CPU 在应用时间接显示。如果某一天浏览器(比方在 React)中要实现相似的性能,那么必然须要借助 Canvas 3D 模式 + WebGL 才有可能触发 GPU 的计算和渲染,那时前端能做的事件将更加炫酷,当然这个和当初的图形图像方向并非一件事。##### Vue 和 React 优化计划的抉择
JavaScript 是单线程运行的,它要负责页面的 JS 解析和执行、绘制、事件处理、动态资源加载和解决。> Javascript 引擎是单线程运行的。严格来说,Javascript 引擎和页面渲染引擎在同一个渲染线程,GUI 渲染和 Javascript 执行 两者是互斥的. 另外异步 I/O 操作底层实际上可能是多线程的在驱动。它只是一个 JavaScript,同时只能做一件事件,这个和 DOS 的单任务操作系统一样的,事件只能一件一件的干。要是后面有一个工作长期霸占 CPU,前面什么事件都干不了,浏览器会出现卡死的状态,这样的用户体验就会十分差。对于“前端框架”来说,解决这种问题有三个方向:- 优化每个工作,让它有多快就多快。挤压 CPU 运算量
- 疾速响应用户,让用户感觉够快,不能阻塞用户的交互
- 尝试 Worker 多线程
Vue 抉择的是第 1 种, 因为对于 Vue 来说,应用模板让它有了很多优化的空间,配合响应式机制能够让 Vue 能够准确地进行节点更新;而 React 抉择了第 2 种。对于 Worker 多线程渲染计划也有人尝试,要保障状态和视图的一致性相当麻烦。集体了解:- Vue 通过 Object.defineProperty/Proxy 等形式,管制每次执行的点,每次只须要更新须要的局部。因为每次能够只更新局部
- React 则是通过 Fiber、Scheduler 的联合,管制每次执行的量,每次尽可能不影响浏览器主流程的状况下尽可能多的执行工作,因为每次都会走一遍 Fiber 的遍历
### 杂谈
- React-Scheduler 的源码中,也应用了数据结构和算法,timerQueue、taskQueue 就应用了小顶堆排序的数据结构及算法,感兴趣的同学能够去深刻理解
- 如果你要抓浏览器的 performance,最好在无痕模式,因为这样的话能够防止一些插件的烦扰
- 在 React 的 issues 中搜寻 requestIdleCallback、requestAnimateCallback、MessageChannel 能够看到很多对于这 3 个问题的渐进式迭代过程,以及相干的探讨和起因
- 在摸索 React 相干的问题中,有一个感触就是,在 React 一直迭代的过程中,其团队会在源码中尝试各种想法,然而并不影响其最终发版的文档版本。比方从 15.6 版本中就呈现了 Fiber,然而并未向外裸露,当咱们去看最终稳定版时,并没有相干源码。所以当咱们看到很多概念,在源码中并没有找到时,或者当你发现一些稳定版没有的内容时,不要急于否定。因为开发版和稳定版往往是通过最终发包的不同做了辨别。咱们能够多去 issues 中探寻一些痕迹,会帮忙咱们了解 React 团队的整个思考过程
- 学习办法倡议:看文章肯定要多看几篇,尤其是要优先看官网文档、源代码,之后再配合一些成体系的文章、以及单篇的精讲(比方本文),单篇的精讲也要多找一些,兼听则明。因为不同的作者在其钻研相干知识点的过程中,除了一些共识点外,也会流露出一些他们思考的形式及思考的维度。而恰好是这些值得发散的点,往往能帮忙咱们了解外围的细节。切记:不要背文章,也不必仅置信一篇文章(包含本文)。