Skip to content

实现noop-renderer

到目前为止我们实现的模块: 核心模块:Reconciler 公用方法:React 浏览器宿主环境:ReactDOM 当前项目的问题:测试用例太单薄,无法照顾到项目的边界情况,但课程时长有限,无法覆盖 所有用例 解决办法:构建成熟的React 测试环境,实现测试工具,学员按需跑通用例 为了测试Reconciler ,我们需要构建「宿主环境无关的渲染器」,这就是react-noop- renderer 以下是使用noop-renderer 的一个用例packages/react-

jsx
reconciler/src/__tests__/ReactEffectOrdering-test.js :
/**
* Copyright (c) Facebook, Inc. and its affiliates.

*
* This source code is licensed under the MIT license found in the

* LICENSE file in the root directory of this source tree.

*
@emails react-core

* @jest-environment node

*/
/* eslint-disable no-func-assign */
'use strict';
let React;

let ReactNoop;

let Scheduler;

let act;

let useEffect;
describe('ReactHooksWithNoopRenderer', () => {

beforeEach(() => {

jest.resetModules();

jest.useFakeTimers();
React = require('react');

act = require('jest-react').act;

Scheduler = require('scheduler');

ReactNoop = require('react-noop-renderer');
useEffect = React.useEffect;

});
test('passive unmounts on deletion are fired in parent -
> child order', async () => {

const root = ReactNoop.createRoot();

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 />);

});
expect(root).toMatchRenderedOutput('Child');

await act(async () => {

root.render(null);

});

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

});

});

Noop-Renderer的实现

包括两部分:

hostConfig

工厂函数(类似ReactDOM ) 在ReactDOM 宿主环境的原生节点是DOM 节点,在Noop-Renderer 宿主环境包括三类节 点:

Instance (HostComponent )
const instance = {

id: instanceCounter++,

type: type,

children: [],

parent: -1,

props

};
TextInstance (HostText )
const textInstance = {

text: content,

id: instanceCounter++,

parent: -1

};
Container (HostRoot )
const container = {

rootID: idCounter++,

children: []

};

对于如下组件:

jsx
function App() {
return (

<>
<Child />

<div>hello world</div>

</>

);

}
function Child() {

return 'Child';

}

经由Noop-Renderer 渲染后得到的树状结构如下(对标DOM 树):

json
{
"rootID"0,

"children": [

{

"text""Child",

"id"0,

"parent"0

},

{

"id"2,

"type""div",

"children": [

{

"text""hello world",

"id"1,

"parent"2

}

],

"parent"0,

"props": {

"children""hello world"

}

}

]

}

除此以外,还需实现「以ReactElement的形式导出树状结构」。

完善Reconciler测试环境

需要思考的问题:如何在并发环境测试运行结果?比如: 如何控制异步执行的时间?使用mock timer 如何记录并发情况下预期的执行顺序? 完善并发测试环境 安装并发的测试上下文环境:

bash
pnpm i -D -w jest-react

安装matchers

js
reactTestMatchers.js
'use strict';
const JestReact = require('jest-react');

const SchedulerMatchers = require('./schedulerTestMatchers');
function captureAssertion(fn) {

// Trick to use a Jest matcher inside another Jest matcher. `fn` contains an

// assertion; if it throws, we capture the error and return it, so the stack

// trace presented to the user points to the original assertion in the

// test file.

try {

fn();

catch (error) {

return {

pass: false,

message: () => error.message

};

}

return { pass: true };

}

function assertYieldsWereCleared(Scheduler) {

const actualYields = Scheduler.unstable_clearYields();

if (actualYields.length !== 0) {

thrownewError(

'Log of yielded values is not empty. ' +

'Call expect(Scheduler).toHaveYielded(...) first.'

);

}

}

function toMatchRenderedOutput(ReactNoopexpectedJSX) {

if (typeof ReactNoop.getChildrenAsJSX === 'function') {

const Scheduler = ReactNoop._Scheduler;

assertYieldsWereCleared(Scheduler);

return captureAssertion(() => {

expect(ReactNoop.getChildrenAsJSX()).toEqual(expectedJSX);

});

}

return JestReact.unstable_toMatchRenderedOutput(ReactNoop, expectedJSX);

}
module.exports = {
...SchedulerMatchers,

toMatchRenderedOutput

};
schedulerTestMatchers.js
'use strict';
function captureAssertion(fn) {

// Trick to use a Jest matcher inside another Jest matcher. `fn` contains an

// assertion; if it throws, we capture the error and return it, so the stack

// trace presented to the user points to the original assertion in the

// test file.

try {

fn();

catch (error) {

return {

pass: false,

message: () => error.message

};

}

return { pass: true };

}

function assertYieldsWereCleared(Scheduler) {

const actualYields = Scheduler.unstable_clearYields();

if (actualYields.length !== 0) {

thrownewError(

'Log of yielded values is not empty. ' +

'Call expect(Scheduler).toHaveYielded(...) first.'

);

}

}

function toFlushAndYield(SchedulerexpectedYields) {

assertYieldsWereCleared(Scheduler);

Scheduler.unstable_flushAllWithoutAsserting();

const actualYields = Scheduler.unstable_clearYields();

return captureAssertion(() => {

expect(actualYields).toEqual(expectedYields);

});

}

function toFlushAndYieldThrough(SchedulerexpectedYields) {

assertYieldsWereCleared(Scheduler);

Scheduler.unstable_flushNumberOfYields(expectedYields.length);

const actualYields = Scheduler.unstable_clearYields();

return captureAssertion(() => {

expect(actualYields).toEqual(expectedYields);

});

}

function toFlushUntilNextPaint(SchedulerexpectedYields) {
assertYieldsWereCleared(Scheduler);

Scheduler.unstable_flushUntilNextPaint();

const actualYields = Scheduler.unstable_clearYields();

return captureAssertion(() => {

expect(actualYields).toEqual(expectedYields);

});

}

function toFlushWithoutYielding(Scheduler) {

return toFlushAndYield(Scheduler, []);

}

function toFlushExpired(SchedulerexpectedYields) {

assertYieldsWereCleared(Scheduler);

Scheduler.unstable_flushExpired();

const actualYields = Scheduler.unstable_clearYields();

return captureAssertion(() => {
expect(actualYields).toEqual(expectedYields);

});

}

function toHaveYielded(SchedulerexpectedYields) {

return captureAssertion(() => {

const actualYields = Scheduler.unstable_clearYields();

expect(actualYields).toEqual(expectedYields);

});

}

function toFlushAndThrow(Scheduler...rest) {

assertYieldsWereCleared(Scheduler);

return captureAssertion(() => {

expect(() => {

Scheduler.unstable_flushAllWithoutAsserting();

}).toThrow(...rest);
});

}
module.exports = {

toFlushAndYield,

toFlushAndYieldThrough,

toFlushUntilNextPaint,

toFlushWithoutYielding,

toFlushExpired,

toHaveYielded,

toFlushAndThrow
};

更新jest配置:

const { defaults } = require('jest-config');
module.exports = {

...defaults,

modulePathIgnorePatterns: ['<rootDir>/.history'],

moduleDirectories: [

...defaults.moduleDirectories,

'dist/node_modules'

],

testEnvironment: 'jsdom',

moduleNameMapper: {

'^scheduler$': '<rootDir>/node_modules/scheduler/unstable_mock.js'

},

fakeTimers: {

enableGlobally: true,

legacyFakeTimers: true

},

setupFilesAfterEnv: ['./scripts/jest/setupJest.js']

};

当前我们为测试做的准备 针对ReactDOM 宿主环境:ReactTestUtils

针对Reconciler 的测试:React-Noop-Renderer

针对并发环境的测试:jest-reactSchedulerReact-Noop-Renderer 配合使 用

用心学习,用代码说话 💻