深入分析React-Scheduler原理

关键词: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 库有用到 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实战视频讲解:进入学习

在 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 模式:

    • 0
      点赞
    • 1
      收藏
      觉得还不错? 一键收藏
    • 0
      评论

    “相关推荐”对你有帮助么?

    • 非常没帮助
    • 没帮助
    • 一般
    • 有帮助
    • 非常有帮助
    提交
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值