React團隊在2019年8月9日發(fā)布了最新的16.9版本,該版本的關(guān)鍵變更主要有以下幾點:
- React 新增
<React.Profiler>API,對組件進行性能評估 - React DOM Unsafe 生命周期
- React DOM 廢棄
javascript:URLs - React DOM 廢棄
Factory組件 - 用于測試的
act()方法正式支持異步
1、新增<React.Profiler> API,支持組件進行性能評估
<React.Profiler>提供了一種通過編程的方式來收集測量代碼的方式,一般較小的應用不會使用它,通常用于大型應用中。
<Profiler>可以作為一個節(jié)點添加到 React 項目中的任意一個子樹上,來評估該子樹的渲染的頻率及渲染 “成本”??梢栽诙嗵幨褂?,也可以嵌套使用。其旨在幫助標識應用程序中渲染緩慢的部分,并可能會更有利于進行 memoization等優(yōu)化。它接受兩個參數(shù)id和onRender。會在React更新的commit階段調(diào)用它。
render(
<App>
<Profiler id="app" onRender={callback}>
<Navigation {...props} />
</Profiler>
<Main {...props} />
</App>
);
onRender觸發(fā)時,會返回關(guān)于本次更新的性能參數(shù):
-
id,用于區(qū)分多個Pofiler,由props傳入 -
phase,值為 "mount" 或者 "update" ,表示當前組件樹是第一次掛載(mount)還是處于更新周期(update) -
actualDuration,當前組件樹更新所花費的時間,使用了一些組件的緩存方法。例如React.memo可以看到較為明顯的減少 -
baseDuration,初始掛載組件樹的時間,可以理解為沒有任何優(yōu)化情況下的渲染所花費的時間 -
startTime,本次更新的初始時間戳 -
commitTime,本次更新的結(jié)束時間戳(到達commit階段截止) -
interactions,本次更新的調(diào)度堆棧
注意:
Profiling 會增加一些額外的開銷,不推薦在生產(chǎn)環(huán)境使用。
2、Unsafe 生命周期
在16.3版本時,React團隊就討論過這三個生命周期潛在的問題,并且在16.3版本中將加入UNSAFE_前綴作為他們的別名,按照當時定下的計劃,將會在16.9中拋出warning,并且在17.0的大版本中徹底移除。
componentWillMount → UNSAFE_componentWillMountcomponentWillReceiveProps → UNSAFE_componentWillReceivePropscomponentWillUpdate → UNSAFE_componentWillUpdate
React v16.9 不包含破壞性更改,而且舊的生命周期方法在此版本依然沿用。但是,當你在新版本中使用舊的生命周期方法時,會提示如下警告:

官方保證即便在17.0中,使用UNSAFE_的生命周期也可以正常使用,也只是生命周期函數(shù)名字變更了而已。想要在老項目升級時避免拋出warning,可以手動變更函數(shù)名。當然也有更好的方案,運行一個自動重命名的 codemod 腳本實現(xiàn)一鍵變更:
cd your_project
npx react-codemod rename-unsafe-lifecycles
開發(fā)團隊也可以在項目中加入嚴格模式(Strict Mode)<React.StrictMode>來禁止使用這類有潛在風險的生命周期。
3、廢棄 javascript:URLs
a標簽的href如果使用javascript:的寫法很容易遭受攻擊,因為它很容易意外在標簽中(<a href>)引入未經(jīng)處理的輸出,造成安全漏洞。在16.9版本中繼續(xù)使用這種寫法React將會拋出警告。
const userProfile = {
website: "javascript: alert('you got hacked')",
};
// This will now warn:
<a href={userProfile.website}>Profile</a>
在未來的主要版本中,如果遇到 javascript:形式的 URL,React 將拋出錯誤。
4、React DOM 廢棄 Factory 組件
在用 Babel 編譯 JavaScript 類流行前,可以在React中采用factory的寫法來創(chuàng)建組件,該組件使用 render 方法返回一個對象
function FactoryComponent() {
return { render() { return <div />; } }
}
這種模式令人困惑,因為它看起來像函數(shù)組件 ,然而它并不是。
React支持它會導致庫變大、變慢。因此,在 16.9 中正式棄用此模式,并在遇到警告時輸出警告。如果項目中依賴了此組件,可以通過添加 FactoryComponent.prototype = React.Component.prototype來做兼容。
5、用于測試的 act()方法正式支持異步
React 16.8 引入了名為 act()的新測試實用程序,來幫助你編寫更匹配瀏覽器行為的測試代碼。例如,對單個 act()中的多個狀態(tài)更新進行批處理。這與 React 已有的處理真實瀏覽器事件時的工作方式相匹配,并有助于為將來 React 組件更頻繁地批處理更新做準備。
然而,React v16.8 中的 act() 僅支持同步函數(shù),在act()中寫異步代碼(異步狀態(tài)更新)將會拋出如下警告,并無法輕易修復:
An update to SomeComponent inside a test was not wrapped in act(...).
在 React 16.9 中 act()支持異步函數(shù) ,你可以在調(diào)用它時,使用 await :
await act(async () => {
// ...
});
React團隊是非常推薦大家為自己組件提供測試用例的,參考Testing Recipes中提供的一些測試技巧和應用場景以及使用act()的地方,也包括對hooks的測試場景,比如測試一個hook的事件:
import React, { useState } from "react";
export default function Toggle(props) {
const [state, setState] = useState(false);
return (
<button
onClick={() => {
setState(previousState => !previousState);
props.onChange(!state);
}}
data-testid="toggle"
>
{state === true ? "Turn off" : "Turn on"}
</button>
);
}
測試用例如下
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import Toggle from "./toggle";
let container = null;
beforeEach(() => {
// setup a DOM element as a render target
container = document.createElement("div");
// container *must* be attached to document so events work correctly.
document.body.appendChild(container);
});
afterEach(() => {
// cleanup on exiting
unmountComponentAtNode(container);
container.remove();
container = null;
});
it("changes value when clicked", () => {
const onChange = jest.fn();
act(() => {
render(<Toggle onChange={onChange} />, container);
});
// get a hold of the button element, and trigger some clicks on it
const button = document.querySelector("[data-testid=toggle]");
expect(button.innerHTML).toBe("Turn off");
act(() => {
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onChange).toHaveBeenCalledTimes(1);
expect(button.innerHTML).toBe("Turn on");
act(() => {
for (let i = 0; i < 5; i++) {
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}
});
expect(onChange).toHaveBeenCalledTimes(6);
expect(button.innerHTML).toBe("Turn on");
});
這些示例采用了原生 DOM API,但也可以使用 React Testing Library來減少樣板代碼。它的許多方法已經(jīng)通過 act() 進行了實現(xiàn)