关于前端:React团队是如何测试并发特性的

32次阅读

共计 3308 个字符,预计需要花费 9 分钟才能阅读完成。

大家好,我卡颂。

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

正文完
 0