乐趣区

关于前端: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 的简化版。

正美老师 R.I.P

退出移动版