无需清除的 effect
有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。
使用 class 的示例
在 React 的 class 组件中,render
函数是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React 更新 DOM 之后才执行我们的操作。
这就是为什么在 React class 中,我们把副作用操作放到 componentDidMount
和 componentDidUpdate
函数中。回到示例中,这是一个 React 计数器的 class 组件。它在 React 对 DOM 进行操作之后,立即更新了 document 的 title 属性
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
注意,在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。
这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。
现在让我们来看看如何使用 useEffect
执行相同的操作。
使用 Hook 的示例
我们在本章节开始时已经看到了这个示例,但让我们再仔细观察它:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
默认情况下,它在第一次渲染之后和每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后(state状态变更都会导致渲染)”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
需要清除的 effect
之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源,需要在组件卸载时清除订阅。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。
使用 Hook 的示例
如何使用 Hook 编写这个组件。
你可能认为需要单独的 effect 来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect
的设计是在同一个地方执行。如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。
小结
了解了 useEffect
可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
其他的 effect 可能不必清除,所以不需要返回。
useEffect(() => {
document.title = `You clicked ${count} times`;
});
effect Hook 使用同一个 API 来满足这两种情况。