React16.9 Unsafe 生命周期 支持组件性能评估
React团队在2019年8月9日发布了最新的16.9版本,该版本的关键变更主要有以下几点:
- React 新增
<React.Profiler>
API,对组件进行性能评估 - React DOM Unsafe 生命周期
- React DOM 废弃
javascript:URLs
- React DOM 废弃
Factory
组件 - 用于测试的
act()
方法正式支持异步
1、新增<React.Profiler> API,支持组件进行性能评估
<React.Profiler>
提供了一种通过编程的方式来收集测量代码的方式,一般较小的应用不会使用它,通常用于大型应用中。
<Profiler>
可以作为一个节点添加到 React 项目中的任意一个子树上,来评估该子树的渲染的频率及渲染 “成本”。可以在多处使用,也可以嵌套使用。其旨在帮助标识应用程序中渲染缓慢的部分,并可能会更有利于进行 memoization
等优化。它接受两个参数id和onRender。会在React更新的commit
阶段调用它。
render(
<App>
<Profiler id="app" onRender={callback}>
<Navigation {...props} />
</Profiler>
<Main {...props} />
</App>
);
onRender
触发时,会返回关于本次更新的性能参数:
-
id
,用于区分多个Pofiler,由props传入 -
phase
,值为 "mount" 或者 "update" ,表示当前组件树是第一次挂载(mount)还是处于更新周期(update) -
actualDuration
,当前组件树更新所花费的时间,使用了一些组件的缓存方法。例如React.memo可以看到较为明显的减少 -
baseDuration
,初始挂载组件树的时间,可以理解为没有任何优化情况下的渲染所花费的时间 -
startTime
,本次更新的初始时间戳 -
commitTime
,本次更新的结束时间戳(到达commit阶段截止) -
interactions
,本次更新的调度堆栈
注意:
Profiling 会增加一些额外的开销,不推荐在生产环境使用。
2、Unsafe 生命周期
在16.3版本时,React团队就讨论过这三个生命周期潜在的问题,并且在16.3版本中将加入UNSAFE_前缀作为他们的别名,按照当时定下的计划,将会在16.9中抛出warning,并且在17.0的大版本中彻底移除。
componentWillMount → UNSAFE_componentWillMount
componentWillReceiveProps → UNSAFE_componentWillReceiveProps
componentWillUpdate → UNSAFE_componentWillUpdate
React v16.9 不包含破坏性更改,而且旧的生命周期方法在此版本依然沿用。但是,当你在新版本中使用旧的生命周期方法时,会提示如下警告:

官方保证即便在17.0中,使用UNSAFE_
的生命周期也可以正常使用,也只是生命周期函数名字变更了而已。想要在老项目升级时避免抛出warning,可以手动变更函数名。当然也有更好的方案,运行一个自动重命名的 codemod
脚本实现一键变更:
cd your_project
npx react-codemod rename-unsafe-lifecycles
开发团队也可以在项目中加入严格模式(Strict Mode)<React.StrictMode>
来禁止使用这类有潜在风险的生命周期。
3、废弃 javascript:URLs
a
标签的href
如果使用javascript:
的写法很容易遭受攻击,因为它很容易意外在标签中(<a href>)引入未经处理的输出,造成安全漏洞。在16.9版本中继续使用这种写法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
的写法来创建组件,该组件使用 render 方法返回一个对象
function FactoryComponent() {
return { render() { return <div />; } }
}
这种模式令人困惑,因为它看起来像函数组件 ,然而它并不是。
React支持它会导致库变大、变慢。因此,在 16.9 中正式弃用此模式,并在遇到警告时输出警告。如果项目中依赖了此组件,可以通过添加 FactoryComponent.prototype = React.Component.prototype
来做兼容。
5、用于测试的 act()方法正式支持异步
React 16.8 引入了名为 act()
的新测试实用程序,来帮助你编写更匹配浏览器行为的测试代码。例如,对单个 act()
中的多个状态更新进行批处理。这与 React 已有的处理真实浏览器事件时的工作方式相匹配,并有助于为将来 React 组件更频繁地批处理更新做准备。
然而,React v16.8 中的 act()
仅支持同步函数,在act()
中写异步代码(异步状态更新)将会抛出如下警告,并无法轻易修复:
An update to SomeComponent inside a test was not wrapped in act(...).
在 React 16.9 中 act()
支持异步函数 ,你可以在调用它时,使用 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来减少样板代码。它的许多方法已经通过 act() 进行了实现