【React架构 - Scheduler中的MessageChannel】

前序

我们都知道JS代码是在浏览器5个进程(下面有介绍)中渲染进程中的Js引擎线程执行的,其他还有GUI渲染线程、定时器线程等,而页面的布局和绘制是在GUI线程中完成的,这些线程之间是互斥的,所以在执行Js的同时会阻塞页面的渲染绘制。

60帧我们是认识标准帧率,所以我们本文都是以60帧来进行说明,即16ms。

所以我们需要在16ms之内完成Js解析执行、样式布局、页面绘制这三个步骤,如果Js执行太长时间到站页面不能及时绘制就会导致卡顿。而在React15及之前都是同步执行的,当组件过多会很容易导致卡顿,这和React设计理念快速响应不符,所以在React16之后调整架构,自实现了Scheduler来通过时间分片结合可中断的方式来进行异步渲染,以及在此基础上新增了优先级来进行优先调度。

本文主要介绍了以下两个问题:

  • MessageChannel是什么及其简单应用
  • 为什么要自己实现Scheduler而不使用现有浏览器API

MessageChannel是什么?

MessageChannel能帮助我们构建一条通道,以DOM Event的形式发送信息,通道两端的端口发送消息实现通信,是一个宏任务。MessageChannel 实例有两个只读属性:

  • port1: 消息通道的第一个端口,连接源上下文通道。
  • port2: 消息通道的第二个端口,连接目标上下文通道。

port1、port2统称为MessagePort。

MessageChannel 可以通过调用 MessagePort 的 postMessage 函数相互发送消息,并通过监听 MessagePort 的 message 事件获取对方端口发送的消息内容。

const { port1, port2 } = new MessageChannel();

port1.onmessage = (e) => {
  console.log(`port1 接收来自 port2 的消息:${e.data}`)
  port1.postMessage('hello port2')
  port1.close()
}

port2.onmessage = (e) => {
  console.log(`port2 接收来自 port1 的消息:${e.data}`)
  port2.postMessage('hello, are you ok?') // 由于port1发送消息之后关闭了连接,所以这个不会没有地方接收
}

port2.postMessage('hello port1')`

简单来说,MessageChannel就是构建一个信息通道,通过postMessage发布消息,通过onMessage来订阅消息,通过close可以来关闭连接。

MessageChannel的应用场景

MessageChannel除了在Scheduler中进行分片和调度之外还有以下一些应用场景,下面进行简单介绍:

  • 实现两个worker的直接通信
  • window与单个iframe或者多个iframe之间的通信可以使用MessageChannel
  • 可以作为简单的EventEmitter做事件的订阅发布,实现不同脚本之间的通信
  • MessageChannel的消息在发送和接收的过程需要序列化和反序列化。利用这个特性,可以实现深拷贝,类似JSON.parse(JSON.stringify())

简单EventEmitter示例:

// a.js
export default function a(port) {
  port.postMessage({ from: 'a', message: 'ping' });
}

// b.js
export default function b(port) {
  port.onmessage = (e) => {
    console.log(e.data); // {from: 'a', message: 'ping'}
  };
}
// index.js
import a from './a.js';
import b from './b.js';
const { port1, port2 } = new MessageChannel();
b(port2);
a(port1);

MessageChannel实现深拷贝类似JSON.parse(JSON.stringify()),也有一些限制,比如:函数 和 Symbol 等特殊值无法拷贝,执行过程中会报错

MessageChannel的应用场景可以参考下这篇文章的应用场景介绍,其中有代码举例说明,较本文更加详细React 源码中的 MessageChannel 到底是什么

为神马不使用已有API?

针对这块内容,我们先从几个Q&A来了解一下原因,然后再看看MessageChannel在Scheduler中是如何使用的。

Q: 为什么不使用requestIdleCallback?
A: requestIdleCallback虽然是浏览器自带的api,用以在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。在一帧16ms里面如果有富余时间则会执行该函数里面注册的任务,但是其存在一些问题让React选择不使用:

  • 浏览器兼容问题
  • 稳定性差(正常情况下渲染一帧时长控制在16.67ms,requestIdleCallback FPS只有20ms )
  • requestIdleCallback 目的主要是用来处理不重要且不紧急的任务,因为React渲染内容并非是不重要且不紧急;

在这里插入图片描述
从上面可以看出对safari浏览器不友好,详细兼容问题可以去can i use上查询。

Q:为什么不使用 requestAnimationFrame

  • requestAnimationFrame(rFA)执行时机是页面渲染前,将React Task放到rAF中,依然有可能会阻塞渲染
  • 有可能过了几次loop才调用一次rAF,React Task就会被搁置太久(浏览器并没有规定应该何时渲染页面,因此RAF是不稳定的)

Q: 为什么不使用setTimeout
A: 虽然MessageChannel也是宏任务,但其执行时间在setTimeout之前,而且setTimeout最低会有4ms的延迟,并且当浏览器不支持MessageChannel也会降级使用setTimeout。

至于为什么setTimeout会有4ms延迟,请查看这篇文章:为什么 setTimeout 有最小时延 4ms ?

React 源码中 MessageChannel 做了什么
上面解释了为什么React选择自己实现时间分片也不使用已有API来进行处理,下面从源码的角度来看看在React 源码中 MessageChannel 做了什么?

源码位置:packages/scheduler/src/forks/Scheduler.js

let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  schedulePerformWorkUntilDeadline = () => {] nullable value
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

通过以上代码可以看到,React 实现任务调度的方法顺序为:setImmediate -> setTimeout -> MessageChannel ,这几种方法都属于宏任务,决定该顺序的主要原因在于其运行时间的先后。

  • setImmediate当前事件循环的末尾立即执行回调函数
  • MessageChannel会在下一个事件循环的开头执行。 ​
  • setTimeout定时执行,并且会有延迟

至于setImmediate、Promise、setTimeout、MessageChannel的先后执行时间可以在控制台执行下方代码查看,其中setImmediate和setTimeout谁先执行主要看setImmediate的注册时机

// setImmediate111比setTimeout先执行
setImmediate(() => {
	console.log('setImmediate111')
})
setTimeout(() => {
    console.log('setTimeout')
}, 0)
// setImmediate222比setTimeout后执行比MessageChannel先执行
setImmediate(() => {
	console.log('setImmediate222')
})
const { port1, port2 } = new MessageChannel()
port2.onmessage = function () {
    console.log('MessageChannel')
}
port1.postMessage('ping')
requestAnimationFrame(() => {
    console.log('requestAnimationFrame')
})
Promise.resolve().then(() => {
    console.log('Promise')
})
// setImmediate333最后执行
setImmediate(() => {
	console.log('setImmediate333')
})

在旧版本chrome上MessageChannel会先于setTimeout打印,在新版本chrome上则反过来,应该是chrome在某个版本上修改了宏任务优先级的实现。

题外话

由于上面提及到了浏览器进程,在这简单介绍下:仅限于Chrome浏览器
Chrome浏览器是多进程架构,主要是5个进程:

  • 浏览器主进程(1个):主要负责页面显示、用户交互、子进程管理、文件存储等功能。
  • 网络进程(1个):主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,之后独立处理成为一个单独的进程。
  • GPU 进程(1个):负责处理 GPU 相关的任务。网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,使得 GPU 成为浏览器普遍的需求,最后,Chrome 在其多进程架构上也引入了 GPU 进程。
  • 渲染进程(Renderer进程,多个):核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页。排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。渲染进程是多线程的,里面包含了比如GUI、js引擎、定时器等线程,详情可以查看这篇文章:浏览器渲染进程中的线程
  • 插件进程(多个):主要负责控制一个网页用到的所有插件。因为插件容易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

为了避免一个进程崩溃而导致整个页面无法正常工作,不同的进程之前是相互隔离的,如果需要通信则要通过IPC机制(Inter Process Communication)来进行通信。

进程和线程

  • 进程:一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。
  • 线程:线程是依附于进程存在,而进程中使用多线程并行处理能提升运算效率。

进程和线程之间的关系有以下特点:

  • 一个进程可以包含有多个线程;
  • 线程之间可以共享进程中的数据;
  • 进程之间的内容相互隔离,不同进程数据不能共享;
  • 进程中的任意一线程执行出错,都会导致整个进程的崩溃;
  • 当一个进程关闭之后,操作系统会回收进程所占用的内存。
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/416071.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

仿牛客网项目---社区首页的开发实现

从今天开始我们来写一个新项目,这个项目是一个完整的校园论坛的项目。主要功能模块:用户登录注册,帖子发布和热帖排行,点赞关注,发送私信,消息通知,社区搜索等。这篇文章我们先试着写一下用户的…

数据可视化之旅:电商销售看板的制作与心得

作为一名电商工作者,每天都需要与海量的销售数据打交道。在数据海洋中,如何快速准确地找到关键信息,优化销售策略,是摆在我面前的一大挑战。在了解市面上众多可视化产品后,我选择尝试了山海鲸可视化软件,这…

2024年小程序云开发CMS内容管理无法使用,无法同步内容模型到云开发数据库的解决方案,回退老版本CMS内容管理的最新方法

一,问题描述 最近越来越多的同学找石头哥,说cms用不了,其实是小程序官方最近又搞大动作了,偷偷的升级的云开发cms(内容管理)以下都称cms,不升级不要紧,这一升级,就导致我…

【架构之路】糟糕程序员的20个坏习惯,切记要改掉

文章目录 强烈推荐前言:坏习惯:总结:强烈推荐专栏集锦写在最后 强烈推荐 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站:人工智能 前言: 优秀的程序员…

matlab实现不同窗滤波器示例

1 汉明窗低通滤波器 : 在Matlab中使用汉明窗设计低通滤波器可以通过fir1函数实现。汉明窗通常用于设计滤波器,可以提供更突出的频率特性。 下面是一个示例代码,演示如何在Matlab中使用汉明窗设计低通滤波器: % 定义滤波器参数 fs …

Scrapy与分布式开发:框架原生去重机制源码解析与不足分析

框架原生去重机制源码解析与不足分析 导语 在网络爬虫和数据采集领域,去重机制是一个至关重要的环节。随着互联网的迅速发展,数据量呈爆炸式增长,如何在海量数据中高效地筛选出有价值且唯一的信息,成为了一个亟待解决的问题。去重机制正是为了解决这一问题而诞生的。 Sc…

docker中hyperf项目配置虚拟域名

在使用hyperf框架时,直接用了docker环境进行开发 下载镜像运行容器 docker run --name hyperf -v /data/project:/data/project -p 9501:9501 -itd -w /data/project --privileged -u root --entrypoint /bin/sh 镜像ID配置docker-compose.yml version: "3.…

东崎仪表案例-中国新能源汽车产业全面崛起

以下部分数据信息来源:澎湃新闻 1月9日,韩国研究机构SNE Research公布了全球动力电池市场的新一轮统计数据。2023年1—11月,全球登记的电动汽车(EV、PHEV、HEV)电池装车量约为624.4GWh,比2022年同期增长41.…

Qt SQLite的创建和使用

重点: 1.SQLite创建数据库内容方法 链接:SQLite Expert Personal的简单使用-CSDN博客 2.和数据库进行链接方法 QSqlDatabase DB; //数据库连接bool MainWindow::openDatabase(QString aFile) {DBQSqlDatabase::addDatabase("QSQLITE"); /…

高刷显示器 - HKC VG253KM

🔥🔥 今天来给大家揭秘一款电竞神器 - HKC VG253KM 高刷电竞显示器!这款显示器可是有着雄鹰展翅般的设计灵感,背后的大鹏展翅鹰翼图腾让人过目难忘。那么,这款显示器到底有哪些过人之处呢?一起来看看吧&…

vue中使用prettier

前言:prettier是一款有态度的代码格式化工具,它可以集成在IDE中,如VS Code、Web Storm等,也可以安装到我们开发的项目里面。本文主要讲解在Vue中集成prettier的过程,可以便于代码检测和格式化。 prettier官网 从官网的…

使用MyBatisPlus实现向数据库中存储List类型的数据

使用MyBatisPlus实现向数据库中存储List类型的数据 问题描述 建表时,表中的这五个字段为json类型 但是在入库的时候既不能写入数据,也不能查询出数据。 解决方案: 1.首先明确,数据存入的时候是经过了数据类型转化&#xff0c…

ElementUI修改el-tab-pane自定义动态添加class并修改组件样式

参考:ElementUI修改el-tab-pane自定义添加class并修改组件样式_el-tab-pane更换样式-CSDN博客 需求:tab 列表 动态添加class 标识当前版本 1:在调用列表接口的接口里面 初始化调用handleClick()方法 2:tab 点击时 再调用一下…

Mysql索引3--索引优化规则

目录 1、索引失效场景 1、1、不遵循最左前缀法则 ,导致索引失效 1、2、范围查询 ,导致失效 1、3 索引列进行运算,导致失效 ​1、4字符串不加引号,到账失效 1、5头部模糊匹配,导致失效 1、6 or连接条件只有一个有…

10分钟SkyWalking与SpringBoot融合并整合到Linux中

1.依赖配置 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.2.0.RELEASE</version></dependency><dependency><groupId>org.springframe…

CV论文--2024.2.29

1、ShapeLLM: Universal 3D Object Understanding for Embodied Interaction 中文标题&#xff1a;ShapeLLM: 用于具身交互的通用三维物体理解 简介&#xff1a;这篇论文介绍了ShapeLLM&#xff0c;它是专为具体交互设计的首个三维多模态大语言模型&#xff08;LLM&#xff09…

Galaxy基础教程:从列表集合中提取元素标识符

如何从一个列表集合中提取元素标识符&#xff1f; 解决方案 网站&#xff1a;UseGalaxy.CN 工具&#xff1a; Extract element identifiers of a list collection (Galaxy Version 0.0.2) Dataset collection *: 列表集合 讨论 该工具接受一个列表类型的集合作为输入&#xff0…

11.以太网交换机工作原理

目录 一、以太网协议二、以太网交换机原理三、交换机常见问题思考四、同网段数据通信全过程五、跨网段数据通信全过程六、关键知识七、调试命令 前言&#xff1a;在网络中传输数据时需要遵循一些标准&#xff0c;以太网协议定义了数据帧在以太网上的传输标准&#xff0c;了解以…

猜猜心里数字(个人学习笔记黑马学习)

1.定义一个变量&#xff0c;数字类型&#xff0c;内容随意 2.基于input语句输入猜想的数字&#xff0c;通过if和多次elif的组合&#xff0c;判断猜想数字是否和心里数字一致 num5if int(input("请输入第一次猜想的数字&#xff1a;"))5:print("猜对了&#xff0…

JavaEE:多线程(3):案例代码

目录 案例一&#xff1a;单例模式 饿汉模式 懒汉模式 思考&#xff1a;懒汉模式是否线程安全&#xff1f; 案例二&#xff1a;阻塞队列 可以实现生产者消费者模型 削峰填谷 接下来我们自己实现一个阻塞队列 1.先实现一个循环队列 2. 引入锁&#xff0c;实现线程安全 …