自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

React團(tuán)隊(duì)是如何測(cè)試并發(fā)特性的

開(kāi)發(fā)
本文來(lái)聊聊React團(tuán)隊(duì)如何測(cè)試并發(fā)特性。

大家好,我卡頌。

React18進(jìn)入大家視野已經(jīng)有一段時(shí)間了,不知道各位有沒(méi)有嘗試「并發(fā)特性」呢?

當(dāng)啟用「并發(fā)特性」后,React會(huì)從「同步更新」變?yōu)椤府惒健?yōu)先級(jí)、可中斷的更新」。

這也為編寫(xiě)單元測(cè)試帶來(lái)了一些難度。

本文來(lái)聊聊React團(tuán)隊(duì)如何測(cè)試并發(fā)特性。

遇到的困境

主要有兩個(gè)問(wèn)題需要面對(duì)。

1. 如何表達(dá)渲染結(jié)果?

React可以對(duì)接不同宿主環(huán)境的渲染器,大家最熟悉的渲染器想必是ReactDOM,用于對(duì)接「瀏覽器」與「Node環(huán)境」(SSR)。

對(duì)于一些場(chǎng)景,可以用ReactDOM的輸出結(jié)果做測(cè)試。

比如,下面是使用ReactDOM的輸出結(jié)果測(cè)試「無(wú)狀態(tài)組件的渲染結(jié)果是否符合預(yù)期」(測(cè)試框架是jest):

it('should render stateless component', () => {
const el = document.createElement('div');
ReactDOM.render(<FunctionComponent name="A" />, el);
expect(el.textContent).toBe('A');
});

這里有個(gè)不方便的地方 —— 這個(gè)用例依賴(lài)瀏覽器環(huán)境與DOM API(比如用到document.createElement)。

對(duì)于測(cè)試「React內(nèi)部運(yùn)行機(jī)制」這樣的場(chǎng)景,摻雜了宿主環(huán)境相關(guān)信息顯然會(huì)讓測(cè)試用例編寫(xiě)起來(lái)更繁瑣。

2. 如何測(cè)試并發(fā)環(huán)境?

如果將上文的用例中ReactDOM.render改為ReactDOM.createRoot,那么用例就會(huì)失?。?/p>

// 之前
ReactDOM.render(<FunctionComponent name="A" />, el);
expect(el.textContent).toBe('A');
// 之后
ReactDOM.createRoot(el).render(<FunctionComponent name="A" />);
expect(el.textContent).toBe('A');

這是因?yàn)樵谛碌募軜?gòu)下,很多「同步更新」變成了「并發(fā)更新」,當(dāng)render執(zhí)行后,頁(yè)面還沒(méi)完成渲染。

要讓上述用例成功,最簡(jiǎn)單的修改方式是:

ReactDOM.createRoot(el).render(<FunctionComponent name="A" />);
setTimeout(() => {
// 異步獲取結(jié)果
expect(el.textContent).toBe('A');
})

如何優(yōu)雅的應(yīng)對(duì)這種變化?

React的應(yīng)對(duì)策略

接下來(lái)我們來(lái)看React團(tuán)隊(duì)的應(yīng)對(duì)方式。

首先來(lái)看第一個(gè)問(wèn)題 —— 如何表達(dá)渲染結(jié)果?

既然ReactDOM渲染器對(duì)應(yīng)瀏覽器、Node環(huán)境,ReactNative渲染器對(duì)應(yīng)Native環(huán)境。

那能不能為測(cè)試「內(nèi)部運(yùn)行流程」專(zhuān)門(mén)開(kāi)發(fā)一個(gè)渲染器呢?

答案是肯定的。

這個(gè)渲染器叫React-Noop-Renderer。

簡(jiǎn)單的說(shuō),這個(gè)渲染器會(huì)渲染出純JS對(duì)象。

實(shí)現(xiàn)一個(gè)渲染器

React內(nèi)部有個(gè)叫Reconciler的包,他會(huì)引用一些「操作宿主環(huán)境」的API。

比如如下方法用于「向容器中插入節(jié)點(diǎn)」:

function appendChildToContainer(child, container) {
// 具體實(shí)現(xiàn)
}

對(duì)于瀏覽器環(huán)境(ReactDOM),使用appendChild方法實(shí)現(xiàn)即可:

function appendChildToContainer(child, container) {
// 使用appendChild方法
container.appendChild(child);
}

打包工具(rollup)將Reconciler包與上述這類(lèi)「針對(duì)瀏覽器環(huán)境的API」打包起來(lái),就是ReactDOM包。

在React-Noop-Renderer中,與ReactDOM中的DOM節(jié)點(diǎn)對(duì)標(biāo)的是如下數(shù)據(jù)結(jié)構(gòu):

const instance = {
id: instanceCounter++,
type: type,
children: [],
parent: -1,
props
};

注意其中的children字段,用于保存子節(jié)點(diǎn)。

所以appendChildToContainer方法在React-Noop-Renderer中可以實(shí)現(xiàn)的很簡(jiǎn)單:

function appendChildToContainer(child, container) {
const index = container.children.indexOf(child);
if (index !== -1) {
container.children.splice(index, 1);
}
container.children.push(child);
};

打包工具將Reconciler包與上述這類(lèi)「針對(duì)React-Noop的API」打包起來(lái),就是React-Noop-Renderer包。

基于React-Noop-Renderer,可以完全脫離正常的宿主環(huán)境,測(cè)試Reconciler內(nèi)部的邏輯。

接下來(lái)來(lái)看第二個(gè)問(wèn)題。

如何測(cè)試并發(fā)環(huán)境?

「并發(fā)特性」再?gòu)?fù)雜,說(shuō)到底也只是「各種異步執(zhí)行代碼的策略」,最終執(zhí)行策略的API不外乎setTimeout、setInterval、Promise等。

在jest中,可以模擬這些異步API,控制他們的執(zhí)行時(shí)機(jī)。

比如上面的異步代碼,在React中的測(cè)試用例會(huì)這么寫(xiě):

// 測(cè)試用例修改后:
await act(() => {
ReactDOM.createRoot(el).render(<FunctionComponent name="A" />);
})
expect(el.textContent).toBe('A');

act方法來(lái)自jest-react包,他的內(nèi)部會(huì)執(zhí)行jest.runOnlyPendingTimers方法,讓所有等待中的計(jì)時(shí)器觸發(fā)回調(diào)。

比如如下代碼:

setTimeout(() => {
console.log('執(zhí)行')
}, 9999999)

執(zhí)行jest.runOnlyPendingTimers后會(huì)立刻打印「執(zhí)行」。

通過(guò)這種方式,人為控制React并發(fā)更新的速度,同時(shí)對(duì)框架代碼0侵入。

除此之外,用于驅(qū)動(dòng)并發(fā)更新的Scheduler(調(diào)度器)模塊,本身也有一個(gè)針對(duì)測(cè)試的版本。

在這個(gè)版本中,開(kāi)發(fā)者可以手動(dòng)控制Scheduler的輸入、輸出。

比如,我想測(cè)試組件卸載時(shí)useEffect回調(diào)的執(zhí)行順序。

如下面代碼所示,其中Parent為掛載的「被測(cè)試組件」:

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

根據(jù)yieldValue的插入順序是否符合預(yù)期,就能確定useEffect的邏輯是否符合預(yù)期:

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

總結(jié)

React中測(cè)試用例的編寫(xiě)策略為:

可以用ReactDOM測(cè)的用例,一般結(jié)合ReactDOM與ReactTestUtils(瀏覽器環(huán)境的輔助方法)完成

需要把控中間過(guò)程的用例,使用Scheduler的測(cè)試包,用Scheduler.unstable_yieldValue記錄過(guò)程信息

脫離宿主環(huán)境,單獨(dú)測(cè)試React內(nèi)部運(yùn)行流程的,使用React-Noop-Renderer

測(cè)試并發(fā)下的場(chǎng)景,需要結(jié)合上述工具與jest-react一起使用

如果想深入學(xué)習(xí)下React中與測(cè)試相關(guān)的技巧,可以看下司徒正美老師的作品anu[1]。

這是個(gè)類(lèi)React框架,但能跑通800+的React用例。里面實(shí)現(xiàn)了ReactTestUtils、React-Noop-Renderer的簡(jiǎn)化版。

責(zé)任編輯:張燕妮 來(lái)源: 魔術(shù)師卡頌
相關(guān)推薦

2021-03-12 18:25:09

開(kāi)發(fā)前端React

2022-02-04 22:18:28

React路由應(yīng)用

2020-01-07 15:40:43

React前端技術(shù)準(zhǔn)則

2024-02-07 12:35:00

React并發(fā)模式concurrent

2023-02-09 14:55:43

旋轉(zhuǎn)門(mén)React團(tuán)隊(duì)

2016-11-21 15:08:38

Leader工程師團(tuán)隊(duì)管理

2011-09-23 09:46:09

軟件項(xiàng)目

2023-04-20 10:15:57

React組件Render

2022-05-10 09:14:15

React 并發(fā)渲染

2022-10-26 15:22:31

React組件User組件

2023-02-02 08:41:14

React團(tuán)隊(duì)Vite

2021-07-15 23:18:48

Go語(yǔ)言并發(fā)

2022-04-14 09:01:39

React源碼Flow

2020-10-12 10:06:26

技術(shù)React代數(shù)

2021-04-25 08:43:30

管理前端后端

2009-01-16 10:01:57

MySQL復(fù)制特性測(cè)試

2015-10-10 16:02:36

React NativAndroid

2022-04-14 08:00:00

Cypress測(cè)試開(kāi)發(fā)

2022-12-23 08:34:30

HookReact
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)