React团队测试并发特性详解

 更新时间:2022年08月23日 14:55:50   作者:魔术师卡颂  
这篇文章主要为大家介绍了React团队测试并发特性详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

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不外乎setTimeoutsetIntervalPromise等。

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测的用例,一般结合ReactDOMReactTestUtils(浏览器环境的辅助方法)完成
  • 需要把控中间过程的用例,使用Scheduler的测试包,用Scheduler.unstable_yieldValue记录过程信息
  • 脱离宿主环境,单独测试React内部运行流程的,使用React-Noop-Renderer
  • 测试并发下的场景,需要结合上述工具与jest-react一起使用

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

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

以上就是React团队测试并发特性详解的详细内容,更多关于React团队测试并发特性的资料请关注脚本之家其它相关文章!

相关文章

  • react组件中过渡动画的问题解决

    react组件中过渡动画的问题解决

    这篇文章主要为大家介绍了react组件中过渡动画的问题解决,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • React利用scheduler思想实现任务的打断与恢复

    React利用scheduler思想实现任务的打断与恢复

    这篇文章主要为大家详细介绍了React如何利用scheduler思想实现任务的打断与恢复,文中的示例代码讲解详细,感兴趣的小伙伴可以参考一下
    2024-03-03
  • React组件的生命周期详解

    React组件的生命周期详解

    React是用于构建用户界面的JavaScript库。本文详细讲解了React的生命周期,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • react18中react-redux状态管理的实现

    react18中react-redux状态管理的实现

    本文主要介绍了react18中react-redux状态管理的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-05-05
  • React Native使用百度Echarts显示图表的示例代码

    React Native使用百度Echarts显示图表的示例代码

    本篇文章主要介绍了React Native使用百度Echarts显示图表的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-11-11
  • React Native Modal 的封装与使用实例详解

    React Native Modal 的封装与使用实例详解

    这篇文章主要介绍了React Native Modal 的封装与使用,本文通过实例代码图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-09-09
  • React之echarts-for-react源码解读

    React之echarts-for-react源码解读

    这篇文章主要介绍了React之echarts-for-react源码解读,echarts-for-react的源码非常精简,本文将针对主要逻辑分析介绍,需要的朋友可以参考下
    2022-10-10
  • 路由react-router-dom的基本使用教程

    路由react-router-dom的基本使用教程

    在React中,路由是一套映射规则,是URL路径与组件的对应关系。使用React路由,就是配置路径和组件的对应关系,这篇文章主要介绍了路由react-router-dom的使用,需要的朋友可以参考下
    2023-02-02
  • React如何实现浏览器打印部分内容详析

    React如何实现浏览器打印部分内容详析

    这篇文章主要给大家介绍了关于利用React如何实现浏览器打印部分内容的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用React具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-05-05
  • react如何添加less环境配置

    react如何添加less环境配置

    这篇文章主要介绍了react如何添加less环境配置,本文给大家分享遇到问题及解决方案,结合示例代码图文给大家介绍的非常详细,需要的朋友参考下吧
    2022-05-05

最新评论