前几天刚体验过 React 18
,感觉非常 Nice,没有看到的小伙伴不要错过:
是不是惊叹于 React 团队的更新速度?React 17
没什么存在感, React 18
就来了?实际上 React 17
本身就是一个过渡版本,它的主要目的是帮助我们进行渐进式升级。
但是没有啥存在感的 React 17
也做了很多非常棒的优化,比如我们今天聊的 useEffect
清理机制的变更。
当组件卸载时,React 会执行清理。比如,如果你在 useEffect
方法中返回一个函数,它就会在组件卸载时执行。
useEffect(() => {
// This is the effect itself.
return () => {
// This is its cleanup.
};
});
在 React 17
之前,useEffect
的清理函数会在 commit
阶段执行 。这意味着当组件卸载时,React 先会执行清理函数,然后才会更新屏幕。它类似于 componentWillUnmount
这个生命周期的行为。
commit
阶段是什么不记得了?我们先来回顾一下
React Fiber
引入了异步渲染,有了异步渲染之后,React
组件的渲染过程是分时间片的,不是一口气从头到尾把子组件全部渲染完,而是每个时间片渲染一点,然后每个时间片的间隔都可去看看有没有更紧急的任务(比如用户按键),如果有,就去处理紧急任务,如果没有那就继续照常渲染。
根据 React Fiber
的设计,一个组件的渲染被分为两个阶段:
render
阶段)是可以被 React
打断的,一旦被打断,这阶段所做的所有事情都被废弃,当 React
处理完紧急的事情回来,依然会重新渲染这个组件,这时候第一阶段的工作会重做一遍;commit
阶段,一旦开始就不能中断,也就是说第二个阶段的工作会稳稳当当地做到这个组件的渲染结束。两个阶段的分界点,就是 render 函数。render 函数之前的所有生命周期函数(包括 render)都属于第一阶段,之后的都属于第二阶段。
回到刚刚的问题,每次组件卸载都需要先运行一次清理函数才更新屏幕,这对于较大的应用程序,是会有一些性能影响的。比如在切换标签页的时候,可能会感到卡顿。
在 React 17 之后,useEffect
的清理函数会延迟到 commit
阶段完成之后才会执行。换句话说, useEffect
清理函数被更改为异步执行,比如组卸载时,清理函数会在屏幕更新后执行。
我们可以使用 Profiler API
来测试一下这个改变会不会提升我们的组件性能。
Profiler API
可以测试 React 组件的渲染性能,如果我们要测试一个组件,可以把它放到 Profiler
组件中,组件接收一个 onRender
函数,当组件每次 commit
更新时,函数都会执行:
render(
<App>
<Profiler id="Navigation" onRender={callback}>
<Navigation {...props} />
</Profiler>
<Main {...props} />
</App>
);
onRender
中的下面两个参数我们可能会用到:
phase
: "mount" | "update" :确定组件是第一次挂载还是更新commitTime
:组件 commit
更新时的时间戳下面我们来看一个简单的例子,当我们点击 Show users
按钮时,它会通过 API 获取用户列表并渲染用户列表。如果点击 Hide users
,UserInfo
这个组件会被卸载。
//App.jsx
export default function App() {
const callback = (phase, actualTime, baseTime, commitTime) => {
console.group(phase);
console.table({
commitTime,
});
console.groupEnd();
}
return (
<Profiler id="users" onRender={callback}>
<div className="App">
<section>
<h2>Users:</h2>
<Button title="Users">
<Users />
</Button>
</section>
</div>
</Profiler>
);
}
//Button.jsx
export default function Button({ title, children }) {
let [toggle, setToggle] = useState(false);
return (
<section>
<button className="primary" onClick={() => setToggle(!toggle)}>
{toggle ? `HIDE ${title}` : `SHOW ${title}`}
</button>
{toggle && children}
</section>
);
}
//Users.jsx
export default function UserInfo() {
const [user, setUser] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const signal = abortController.signal;
fetch(USERS_API, { signal: signal })
.then(results => results.json())
.then(data => {
setUser(data);
});
return function cleanup() {
console.log('I am in cleanup function');
abortController.abort();
};
}, []);
return (
<div>
<h4>Users</h4>
{user === null ? (
<p>Loading User Data ...</p>
) : (
<pre>{JSON.stringify(user, null, 4)}</pre>
)}
</div>
);
}
在 React 17
之前,先回执行清理函数,然后屏幕才会被更新,这会增加 commit
时间。
在 React 17
之后,清理函数会在在屏幕更新后异步执行,这会减少 commit
时间。
嗯,就是这样一个小的优化,提升了组件卸载时 10%
的渲染性能,不要小看它,正是这些大大小小的优化让 React
应用程序的体验变得越来越好。
不得不佩服 React 团队,真的是致力于极致的用户体验!