React团队是如何测试并发特性的

更新日期: 2022-08-22阅读: 854标签: 并发

大家好,我卡颂。

react18 进入大家视野已经有一段时间了,不知道各位有没有尝试 「并发特性」 呢?

当启用 「并发特性」 后, React 会从 「同步更新」 变为 「异步、带优先级、可中断的更新」 。

这也为编写单元测试带来了一些难度。

本文来聊聊 React 团队如何测试并发特性。

遇到的困境

主要有两个问题需要面对。

1. 如何表达渲染结果?

React 可以对接不同宿主环境的渲染器,大家最熟悉的渲染器想必是 Reactdom ,用于对接 浏览器 与 「Node环境」 (SSR)。

对于一些场景,可以用 ReactDOM 的输出结果做测试。

比如,下面是使用 ReactDOM 的输出结果测试 「无状态组件的渲染结果是否符合预期」 (测试框架是 jest ):

 it('should render stateless component', () => {
const el = document.createElement('div');
ReactDOM.render(<FunctionComponent name="A" />, el);
expect(el.textContent).toBe('A');
});

这里有个不方便的地方 —— 这个用例依赖 浏览器环境 与 DOM api (比如用到 document.createElement )。

对于测试 「React内部运行机制」 这样的场景,掺杂了宿主环境相关信息显然会让测试用例编写起来更繁琐。

2. 如何测试并发环境?

如果将上文的用例中 ReactDOM.render 改为 ReactDOM.createRoot ,那么用例就会失败:

// 之前
ReactDOM.render(<FunctionComponent name="A" />, el);
expect(el.textContent).toBe('A');

// 之后
ReactDOM.createRoot(el).render(<FunctionComponent name="A" />);
expect(el.textContent).toBe('A');

这是因为在新的架构下,很多 「同步更新」 变成了 「并发更新」 ,当 render 执行后,页面还没完成渲染。

要让上述用例成功,最简单的修改方式是:

ReactDOM.createRoot(el).render(<FunctionComponent name="A" />);

setTimeout(() => {
// 异步获取结果
expect(el.textContent).toBe('A');
})

如何优雅的应对这种变化?

React的应对策略

接下来我们来看 React 团队的应对方式。

首先来看第一个问题 —— 如何表达渲染结果?

既然 ReactDOM 渲染器对应浏览器、 Node 环境, ReactNative 渲染器对应 Native 环境。

那能不能为测试 「内部运行流程」 专门开发一个渲染器呢?

答案是肯定的。

这个渲染器叫 React-Noop-Renderer 。

简单的说,这个渲染器会渲染出纯 JS 对象。

实现一个渲染器

React 内部有个叫 Reconciler 的包,他会引用一些 「操作宿主环境」 的 API 。

比如如下方法用于 「向容器中插入节点」 :

function appendChildToContainer(child, container) {
// 具体实现
}

对于浏览器环境( ReactDOM ),使用 appendChild 方法实现即可:

function appendChildToContainer(child, container) {
// 使用appendChild方法
container.appendChild(child);
}

打包工具( rollup )将 Reconciler 包与上述这类 「针对浏览器环境的API」 打包起来,就是 ReactDOM 包。

在 React-Noop-Renderer 中,与 ReactDOM 中的 DOM 节点对标的是如下数据结构:

const instance = {
id: instanceCounter++,
type: type,
children: [],
parent: -1,
props
};

注意其中的 children 字段,用于保存子节点。

所以 appendChildToContainer 方法在 React-Noop-Renderer 中可以实现的很简单:

function appendChildToContainer(child, container) {
const index = container.children.indexOf(child);
if (index !== -1) {
container.children.splice(index, 1);
}
container.children.push(child);
};

打包工具将 Reconciler 包与上述这类 「针对React-Noop的API」 打包起来,就是 React-Noop-Renderer 包。

基于 React-Noop-Renderer ,可以完全脱离正常的宿主环境,测试 Reconciler 内部的逻辑。

接下来来看第二个问题。

如何测试并发环境?

「并发特性」再复杂,说到底也只是 「各种异步执行代码的策略」 ,最终执行策略的 API 不外乎 setTimeout 、 setInterval 、 Promise 等。

在 jest 中,可以模拟这些异步 API ,控制他们的执行时机。

比如上面的异步代码,在 React 中的测试用例会这么写:

// 测试用例修改后:
await act(() => {
ReactDOM.createRoot(el).render(<FunctionComponent name="A" />);
})
expect(el.textContent).toBe('A');

act 方法来自 jest-react 包,他的内部会执行 jest.runOnlyPendingTimers 方法,让所有等待中的计时器触发回调。

比如如下代码:

setTimeout(() => {
console.log('执行')
}, 9999999)

执行 jest.runOnlyPendingTimers 后会立刻打印 「执行」 。

通过这种方式,人为控制 React 并发更新的速度,同时对框架代码0侵入。

除此之外,用于驱动并发更新的 Scheduler (调度器)模块,本身也有一个针对测试的版本。

在这个版本中,开发者可以手动控制 Scheduler 的输入、输出。

比如,我想测试组件卸载时 useEffect 回调的执行顺序。

如下面代码所示,其中 Parent 为挂载的 「被测试组件」 :

function Parent() {
useEffect(() => {
return () => Scheduler.unstable_yieldValue('Unmount parent');
});
return <Child />;
}

function Child() {
useEffect(() => {
return () => Scheduler.unstable_yieldValue('Unmount child');
});
return 'Child';
}

await act(async () => {
root.render(<Parent />);
});

根据 yieldValue 的插入顺序是否符合预期,就能确定 useEffect 的逻辑是否符合预期:

expect(Scheduler).toHaveYielded(['Unmount parent', 'Unmount child']);

总结

React 中测试用例的编写策略为:

  • 可以用 ReactDOM 测的用例,一般结合 ReactDOM 与 ReactTestUtils (浏览器环境的辅助方法)完成

  • 需要把控中间过程的用例,使用 Scheduler 的测试包,用 Scheduler.unstable_yieldValue 记录过程信息

  • 脱离宿主环境,单独测试 React 内部运行流程的,使用 React-Noop-Renderer

  • 测试并发下的场景,需要结合上述工具与 jest-react 一起使用

如果想深入学习下 React 中与测试相关的技巧,可以看下司徒正美老师的作品 anu 。

这是个类 React 框架,但能跑通800+的 React 用例。里面实现了 ReactTestUtils 、 React-Noop-Renderer 的简化版。

来自:魔术师卡颂

链接: https://fly63.com/article/detial/12030

PHP和Redis实现在高并发下的抢购及秒杀功能示例详解

抢购、秒杀是平常很常见的场景,面试的时候面试官也经常会问到,比如问你淘宝中的抢购秒杀是怎么实现的等等。抢购、秒杀实现很简单,但是有些问题需要解决,主要针对两个问题:

说一说数据库的并发控制

最近在看Elasticsearch时看到了并发控制,由此看到了新的并发控制方式。不得不说Elasticsearch相较于关系型数据库就是两种理论建立的数据存储体系,当然它们在并发控制上也相差甚远,各有千秋。

PHP-高并发和大流量的解决方案

在互联网时代,并发,高并发通常是指并发访问。也就是在某个时间点,有多少个访问同时到来。 高并发架构相关概念QPS (每秒查询率) : 每秒钟请求或者查询的数量,在互联网领域,指每秒响应请求数

处理高并发的一般思路

今天看见有人聊目前系统有2亿的PV,该如何优化?当我看到这个话题的时候,突然在想自己工作中也遇到了不少高并发的场景了,所以即兴发挥,在这里简单总结和分享下,欢迎指正和补充。

nodejs使用 eventproxy 控制并发

很多网站有并发连接数的限制,所以当请求发送太快的时候会导致返回值为空或报错。 安装依赖 express superagent cheerio eventproxy。新建app.js 抓取所有的url

PHP 并发场景的几种解决方案

在秒杀,抢购等并发场景下,可能会出现超卖的现象,在PHP语言中并没有原生提供并发的解决方案,因此就需要借助其他方式来实现并发控制。列出常见的解决方案有:

并发编程三要素:原子性,有序性,可见性

并发编程三要素:原子性: 一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。有序性: 程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

通过Iterator控制Promise.all的并发数

异步是 js 一个非常重要的特性,但很多时候,我们不仅仅想让一系列任务并行执行,还想要控制同时执行的并发数,尤其是在针对操作有限资源的异步任务,比如文件句柄,网络端口等等。

nodejs如何解决高并发?

Node可以在不新增额外线程的情况下,依然可以对任务进行并发处理 —— Node.js是单线程的。它通过事件循环(event loop)来实现并发操作,对此,我们应该要充分利用这一点 —— 尽可能的避免阻塞操作

如何利用 JavaScript 实现并发控制

在开发过程中,有时会遇到需要控制任务并发执行数量的需求。例如一个爬虫程序,可以通过限制其并发任务数量来降低请求频率,从而避免由于请求过于频繁被封禁问题的发生。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!