赞
踩
关键词: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 及浏览器渲染的相关知识
一个 frame 渲染(帧渲染)的过程,按 60fps来计算,大概有16.6ms,在这个过程中浏览器要做很多东西,包括 “执行 JS -> 空闲 -> 绘制(16ms)”,在执行 JS 的过程中,即是浏览器的 JS 线程执行 eventloop 的过程,里面包括了 marco task 和 mirco task 的执行,其中执行多少个 macro task 的数量是由浏览器决定的,而这个数量并没有明确的限制。
因为 whatwg 规范标准中只是建议浏览器尽可能保证 60fps 的渲染体验,因此,不同的浏览器的实现也并没有明确说明。同时需要注意,并不是每一帧都会执行绘制操作。如果某一个 macro task 及其后执行 mirco task 时间太长,都会延后浏览器的绘制操作,也就是我们常见的掉帧、卡顿。
React 为了解决 15 版本存在的问题:组件的更新是递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。
React 引入了 Fiber 的架构,同时配合 Schedduler 的任务调度器,在 Concurrent 模式下可以将 React 的组件更新任务变成可中断、恢复的执行,就减少了组件更新所造成的页面卡顿。
Scheduler是一个独立的包,不仅仅在React中可以使用。
Scheduler 是一个任务调度器,它会根据任务的优先级对任务进行调用执行。
在有多个任务的情况下,它会先执行优先级高的任务。如果一个任务执行的时间过长,Scheduler 会中断当前任务,让出线程的执行权,避免造成用户操作时界面的卡顿。在下一次恢复未完成的任务的执行。
Scheduler 是 React 团队开发的一个用于事务调度的包,内置于 React 项目中。其团队的愿景是孵化完成后,使这个包独立于 React,成为一个能有更广泛使用的工具。参考React实战视频讲解:进入学习
这个问题,其实是我个人想说明的一个点
因为在我看的很多文章中,大家都在不断强调 Scheduler 的各种好处,各种原理,以至于我最开始也以为只要引入了 React 16-17 的版本,就能体会到这样的“优化”效果。但是当我开启源码调试时,就产生了困惑,因为完全没有按照套路来输出我辛辛苦苦打的 console.log 。
直到我使用 Concurrent 模式才体会到 Scheduler 的任务调度核心逻辑。这个模式直到 React 17 都没有暴露稳定的 API,只是提供了一个非稳定版的 unstable_createRoot 方法。
结论:Scheduler 的逻辑有被 React 使用,但是其核心的切片、任务中断、任务恢复并没有在稳定版中采用,你可以理解现在的 React 在执行 Fiber 任务时,还是一撸到底。
如果当前环境不支持 MessageChannel 时,会默认使用 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
从 React 的 issues 及之前版本(在 15.6 的源码中能搜到)中可以看到,requestIdelCallback 方法也被 React 尝试过,只是后来因为兼容性、不同机器及浏览器执行效率的问题又被 requestAnimationFrame + setTimeout 的 polyfill 方法替代了
在 React 16.10.0 之前还是使用的 requestAnimationFrame + setTimeout 的方法,配合动态帧计算的逻辑来处理任务,后来也因为这样的效果并不理想,所以 React 团队才决定彻底放弃此方法
requestAnimationFrame 还有个特点,就是当页面处理未激活的状态下,requestAnimationFrame 会停止执行;当页面后面再转为激活时,requestAnimationFrame 又会接着上次的地方继续执行。
针对 Generator ,其实 React 团队为此做过一些努力
针对 Webworkers , React 团队同样做过一些分析和讨论
关于在 React 中引入 Webworkers 的讨论,我这里仅贴一下在 issues 中看到的部分,因为没有深入去研究来龙去脉,暂不做翻译
For now I can see the following solutions for this problem:
So yeah, for now I don’t see this working without a build tool. My preference would go to the first one.
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.
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 模式:
Scheduler task
// 一个 scheduler 的任务
var newTask = {
id: taskIdCounter++, // 任务id,在 react 中是一个全局变量,每次新增 task 会自增+1
callback: callback, // 在调度过程中被执行的回调函数
priorityLevel: priorityLevel, // 通过 Scheduler 和 React Lanes 优先级融合过的任务优先级
startTime: startTime, // 任务开始时间
expirationTime: expirationTime, // 任务过期时间
sortIndex: -1 // 排序索引, 全等于过期时间. 保证过期时间越小, 越紧急的任务排在最前面
};</
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。