【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 valuelocalSetTimeout(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)来进行通信。

进程和线程

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

android应用开发基础知识,安卓面试2020

第一章:设计思想与代码质量优化 1、设计思想六大原则 2、三大设计模式 3、数据结构 4、算法 第二章:程序性能优化 1、启动速度和执行效率优化 2、布局检测与优化 3、内存优化 4、耗电优化 5、网络传输与数据存储优化 6、APK大小优化 7、屏幕适配 8、…

用Java语言创建的Spring Boot项目中,如何传递List集合呢?

前言: 在上篇文章中,用Java语言创建的Spring Boot项目中,如何传递数组呢??-CSDN博客,我们了解到Spring Boot项目中如何传递数组,但是,对于同类型的List集合,我们又该如何…

Centos7:自动化配置vim | suoders信任列表添加普通用户

Centos7:自动化配置vim | suoders信任列表添加普通用户 vim 配置原理sudoers系统可信任列表中添加普通用户自动化配置vim vim 配置原理 在目录/etc下有一个vimrc文件,该文件是系统中公共的vim配置文件,对所有用户都成立。  而在每个普通用户…

【Kafka系列 06】Kafka Producer源码解析

温馨提示:本文基于 Kafka 2.3.1 版本。 一、Kafka Producer 原理图 生产者的 API 使用还是比较简单,创建一个 ProducerRecord 对象(这个对象包含目标主题和要发送的内容,当然还可以指定键以及分区),然后调…

poi 设置允许西文在单词中间换行

说明本文是CSDN-问答模块,题主提问。问题描述:poi 设置允许西文在单词中间换行 一、问题描述 poi 设置允许西文在单词中间换行? // 创建一个新的文档XWPFDocument document = new XWPFDocument();// 创建段落XWPFParagraph firstParagraph = document.createParagraph();fir…

2022《OpenScene: 3D Scene Understanding with Open Vocabularies》阅读笔记2

A. Implementation Details 3D Distillation. 我们基于PyTorch实现。为了提取,我们使用Adam[26]作为优化器,初始学习率为1e−4,并训练100个epochs。对于MinkowskiNet,我们对ScanNet和Matterport3D实验使用2cm的体素大小,对nuScenes使用5cm的体素尺寸。对于室内数据集,我…

RTSPServer推流服务

RTSPServer推流服务 1.开发原因 由于项目需要使用,虽然有现成的RTSPServer推流服务,由于是闭源代码,无法查看了解内部的逻辑处理流程,所以急需要一套较为稳定并可以使用的推流服务,并且从网上看大部分的RTSPServer推流…

IntelliJ IDEA 常用快捷键和下载链接

下载链接(windows) 下载 IntelliJ IDEA – 领先的 Java 和 Kotlin IDE 编码时: 跳转到引用方法的地方 (有多个引用时会出现下拉列表) ctrl鼠标左键 跳转后回到原来的地方 …

LeetCode 2120.执行所有后缀指令

现有一个 n x n 大小的网格,左上角单元格坐标 (0, 0) ,右下角单元格坐标 (n - 1, n - 1) 。给你整数 n 和一个整数数组 startPos ,其中 startPos [startrow, startcol] 表示机器人最开始在坐标为 (startrow, startcol) 的单元格上。 另给你…

后端程序员入门react笔记(六)- 讲透Promise与Fetch

js声明变量的三种方式 我们通常使用 var let 和const来声明js的变量,但是这三者有什么区别呢? var的变量可以是全局的或者函数作用域,而let和const只能是块级作用域var和let可以重新赋值,而const是不允许重新赋值的,…

【王道数据结构】【chapter7查找】【P285t5】

线性表中各节点的检索概率不等时,可用如下策略提高顺序检索的效率;若找到指定的结点,则将该结点和其前驱结点(若存在)交换,使得经常被访问的结点尽量位于表的前端。试设计在顺序结构和链式结构的线性表盘上…

python中集合(Set)和列表(List)性能比较

文章目录 引言1. Set和List2. 性能对比3. 总结 引言 在当今的软件开发过程中,Python 已经成为了一种极为流行的编程语言,得益于其简洁的语法和强大的库支持。在 Python 中,列表(List)和集合(Set&#xff0…

敏捷开发中如何写好用户故事?

什么是用户故事? 用户故事(user story)是一个用来确认用户和用户需求的简短描述,作为什么用户,希望如何,这样做的目的或者价值何在。用户故事在软件研发中又被描述为需求。用户故事通常的格式为&#xff1…

STL容器之list

​ 1.封装除了对数据的保护、更好地管理数据之外,还有实现了对上层的统一; ​ 2.类模板参数的不同,一方面是为了实例化出来不同的类,另一方面是为了实现类的成员函数的不同; 一、认识list ​ 1.list是一种带头双向循…

java高并发场景面试题,干货来袭

为什么阿里巴巴的持久层抛弃hibernate,采用MyBatis框架? 原因大概有以下4点: 尤其是需要处理大量数据或者大并发情况的网站服务,这也阿里选择MyBatis的原因。 MyBatis整体架构 不多讲,先看目录图 MyBatis源码笔记文档…

捕获在野SMBGhost本地提权攻击样本

前言 从Windows10 v1903/Windows Server v1903开始,微软在协议SMB3.1.1中开启了对数据压缩传输的支持,但是由于SMB没有正确处理压缩的数据包,在客户端/服务端解压数据的时候,没有对COMPRESSIN_TRANSFORM_HEADE结构进行安全校验&a…

【mysql 数据库事务】开启事务操作数据库,写入失败后,不回滚,会有问题么? 这里隐藏着大坑,复试,面试时可以镇住面试老师!!!!

建表字段: CREATE TABLE user (id INT(11) NOT NULL AUTO_INCREMENT,nickname VARCHAR(32) NOT NULL COLLATE utf8mb4_general_ci,email VARCHAR(32) NOT NULL COLLATE utf8mb4_general_ci,status SMALLINT(6) UNSIGNED NULL DEFAULT NULL,password VARCHAR(256) NULL DEFAULT…

Netty入门指南:从零开始的异步网络通信

欢迎来到我的博客,代码的世界里,每一行都是一个故事 Netty入门指南:从零开始的异步网络通信 前言Netty简介由来:发展历程:异步、事件驱动的编程模型: 核心组件解析通信协议高性能特性异步编程范式性能优化与…

C++ stack queue详解以及模拟实现

目录 1.stack的使用 1.1stack的定义 1.2stack的使用 1.3stack的构造 2.stack底层模拟实现 3.queue的使用 3.1queue的定义 3.2queue的使用 4.queue底层模拟实现 1.stack的使用 1.1stack的定义 1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环…

关于 cocos creator 如何打包抖音字节小游戏步骤一

1、cocos creator打开引擎,在顶部选择构建之后,在选择点击构建(ps:具体看项目组的大小,如果是一个简单的不多资源一般不到一分钟,如果项目很大,就至少半个小时以上),之后 成功构建之后如下所示:…