React团队如何详尽解析并发的复杂特性以优化应用性能?

2026-04-01 13:592阅读0评论SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1983个文字,预计阅读时间需要8分钟。

React团队如何详尽解析并发的复杂特性以优化应用性能?

目录引言遇到的困境

1.如何表达渲染结果?

2.如何测试并开发环境?

React的应对策略实现一个渲染器如何测试并开发环境?总结引言React18进入大家庭已有一段时间了,不知道大家对它有哪些了解?下面将简要探讨一些相关内容。

遇到的困境

1.如何表达渲染结果?

2.如何测试并开发环境?

React团队如何详尽解析并发的复杂特性以优化应用性能?

React的应对策略

实现一个渲染器如何测试并开发环境?

总结

目录
  • 引言
  • 遇到的困境
    • 1. 如何表达渲染结果?
    • 2. 如何测试并发环境?
  • 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团队测试并发特性的资料请关注易盾网络其它相关文章!

    本文共计1983个文字,预计阅读时间需要8分钟。

    React团队如何详尽解析并发的复杂特性以优化应用性能?

    目录引言遇到的困境

    1.如何表达渲染结果?

    2.如何测试并开发环境?

    React的应对策略实现一个渲染器如何测试并开发环境?总结引言React18进入大家庭已有一段时间了,不知道大家对它有哪些了解?下面将简要探讨一些相关内容。

    遇到的困境

    1.如何表达渲染结果?

    2.如何测试并开发环境?

    React团队如何详尽解析并发的复杂特性以优化应用性能?

    React的应对策略

    实现一个渲染器如何测试并开发环境?

    总结

    目录
    • 引言
    • 遇到的困境
      • 1. 如何表达渲染结果?
      • 2. 如何测试并发环境?
    • 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团队测试并发特性的资料请关注易盾网络其它相关文章!