关于前端:React如何利用浏览器的事件循环来实现并发特性

3次阅读

共计 2710 个字符,预计需要花费 7 分钟才能阅读完成。

本文只是用于将学习到的常识做一个梳理与总结

浏览器架构

古代浏览器通常采纳多过程架构。每个过程都有独立的内存空间,互相隔离,进步浏览器的稳定性、安全性和性能。

以 Chrome 为例,浏览器的过程蕴含以下几个次要过程:

  • 浏览器主过程: 负责协调整个浏览器的运行,包含用户界面、网络申请、子过程的创立和销毁等。
  • 渲染过程: 将 HTML/CSS/JS 转化为用户能够交互的网页
  • 网络过程: 解决网络申请、响应、DNS 等
  • GPU 过程: 负责解决图形渲染相干的工作,如 2D、3D 绘图等
  • 插件过程: 运行浏览器插件

渲染过程

对于咱们的页面来说,最重要的就是 渲染过程 ,它蕴含了以下的多个 线程:

  1. 主线程:负责解决用户输出、JavaScript 执行和页面布局计算等工作,是渲染过程中最重要的线程之一。
  2. 渲染线程:负责将 HTML、CSS 和 JavaScript 转换为可视化的页面,其中包含页面布局、款式计算、绘制和合成等工作。
  3. 合成线程:负责将页面中的多个图层合成为最终的显示内容,并将其发送到 GPU 进行渲染。
  4. JavaScript 引擎线程:JavaScript 引擎线程负责解析和执行页面中的 JavaScript 代码
  5. 事件线程:负责解决用户输出事件,如鼠标点击、键盘输入等,以及页面中的事件触发和解决
  6. IO 线程: 负责接管其余过程传进来的音讯

在这当中,主线程最为忙碌,既要解决 DOM,又要计算款式,还要解决布局,同时还须要解决 JavaScript 工作以及各种输出事件 。而浏览器则通过在 主线程 中实现 音讯队列 事件循环系统 来调度这么多不同类型的工作。

咱们能够通过上面的图片来理解 主线程 事件循环 音讯队列 和其余线程之间的关系

然而,音讯队列 先进先出 的。主线程 所有执行工作都来自于 音讯队列。会面临以下两个问题

1. 如何解决 高优先级的工作

比方,如何监控 DOM 节点的变动状况(节点的插入、批改、删除等动态变化),而后依据变动来解决相应的业务逻辑。一个通用设计就是利用 js 设计一套监听接口,当变动产生时,渲染引擎同步调用这些接口,这是一个典型的观察者模式。

不过这个模式有个问题,因为 DOM 变动十分频繁,如果每次发生变化的时候,都间接调用相应的 JavaScript 接口,那么这个以后的工作执行工夫会被拉长,从而导致 执行效率的降落。如果将这些 DOM 变动做成异步的音讯事件,增加到音讯队列的尾部,那么又会影响到监控的实时性,因为在增加到音讯队列的过程中,可能后面就有很多工作在排队了。

这也就是说,如果 DOM 发生变化,采纳同步告诉的形式,会影响当前任务的 执行效率 ;如果采纳异步形式,又会影响到 监控的实时性

2. 如何解决单个工作执行时长过久的问题。

从图中能够看到,所有工作都是在单线程中执行的,而因为每一帧的工夫无限,如果某一个 js 工作十分的耗时,那么上面的工作 (DOM 解析、JS 事件、布局计算、用户输出事件等) 就须要期待很长时间。这也就是咱们页面中 卡顿 的由来。

第一个问题就能够通过上面的 微工作 来解决

宏工作,微工作

首先,咱们须要晓得 工作队列 中蕴含有以下两种类型的工作

宏工作

  • 渲染事件(如解析 DOM、计算布局、绘制);
  • 用户交互事件(如鼠标点击、滚动页面、放大放大等);
  • JavaScript 脚本执行事件;
  • 网络申请实现、文件读写实现事件。
  • setTimeout的回调函数属于 宏工作

微工作

  • Promise的回调属于 微工作
  • MutationObserver 的回调函数:当被察看的 DOM 节点发生变化时,MutationObserver 的回调函数会被增加到微工作队列中。
  • queueMicrotask 办法:该办法能够将回调函数增加到微工作队列中,期待执行。该办法是 ES2020 规范中新增的。

宏工作 微工作 的最次要区别在于它们的 执行机会

宏工作 是增加一个新的工作到 音讯队列 中,如果应用 setTimeout 来异步执行一个操作时,工夫距离无奈精准掌控,对于一些 高实时性 的需要不太合乎。比方你在程序中应用 setTimeout 提早 1000ms 去执行某个工作时,可能在这 1000ms 中曾经触发了很多零碎级的工作,它们曾经被插入到了 音讯队列 中。等到过了 1000mssetTimeout才将会它的回调插入到 音讯队列 中,这就须要期待队列前的工作全副执行完了能力到它的回调

微工作 是 在以后 宏工作 完结前再执行 微工作 ,每个 宏工作 都关联了一个 微工作队列 。所以,只有在以后 宏工作 中触发了 微工作 ,所有微工作的回调都会被增加到 微工作队列 中期待执行。这样,你再怎么交互,生成的 宏工作 都会排在以后的 宏工作 之后。这样,实时性 问题就解决了。

React 如何利用浏览器的个性来做“并发”

在理解了后面对于浏览器的个性以及相干问题后。咱们再回到 react 中看 React 为了 并发个性 做了哪些改变。

time slice 与 fiber

在 react16 之前,始终是递归更新。而 16 之后,react 提出了一个新的概念 time slice,便于将工作切分,而后在浏览器的闲暇工夫来执行工作,超出了闲暇工夫则将残余工作往后推。但因为 递归更新中断后无奈再持续 ,所以 react 重构了它的代码,将递归更新改成了 fiber 这种链表构造。这样即便是暂停了,还能从暂停出的链表继续执行。这样就解决了组件 单个执行工作过长 的问题。

异步更新

咱们能够在 react 的 react-reconciler 包中找到 scheduleSyncCallback 办法,所有的 更新操作 都保留到了 syncQueue 队列中,而后通过 scheduleMicrotask 这个办法创立微工作,flushSyncCallbacks就是这个微工作的异步回调,而 flushSyncCallbacks 当中执行的就是所有的 更新操作 。这就解决了组件 更新效率 的问题。

Scheduler 调度器

当初,有了可中断的工作,并且 同步工作 被放到了 微工作 中执行。而且因为个别支流浏览器刷新频率为 60Hz,即每 16.6ms(1000ms / 60Hz)浏览器刷新一次。

所以 react 须要解决的就是如何利用每一帧中预留给 js 线程的工夫来更新组件 (在 scheduler 源码中,react 预留了 5ms)。当超过预留工夫后,react 就会中断更新,期待下一帧的闲暇工夫持续从 被中断的 fiber处执行。这样就尽可能的防止了工作执行工夫过长而呈现 掉帧 卡顿 的景象。

总结

react 利用浏览器的渲染过程主线程的 事件循环 以及 宏工作 微工作 的特点,将原有的数据结构扭转为 Fiber 这种可中断的链表构造。
并且通过将所有的 更新操作 应用 微工作 来执行,解决组件更新的 实时性 问题。而后再实现了 调度器 来实现工作的中断和持续来解决 工作执行工夫过长 的问题。

参考

  • 浏览器工作原理与实际
  • react 技术揭秘
  • 从零实现 React 18
正文完
 0