React Hook:使用 useEffect
一、描述
Effect Hook 可以让你能够在 Function 组件中执行副作用(side effects):
import { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); // Similar to componentDidMount and componentDidUpdate: // 类似于 componentDidMount 和 ComponentDidUpdate useEffect(() => { // Update the document title using the browser API document.title = `You clicked ${count} times`; }); return ( > >You clicked {count} times</p>
上面代码看过很多次了,是 React 文档中 Hook 部分一直使用的计数器示例,但是多了个新的功能:把文档标题设置为包含点击次数的自定义消息。而这就是一个副作用。
数据获取,设置订阅或者手动直接更改 React 组件中的 DOM 都属于副作用。有的人习惯成这种行为为 effects
,我是比较习惯叫 side effects
也就是副作用的, 这是个概念,需要在 React 必须习惯的概念。
如果熟悉 React 类声明周期方法,可以把 useEffect
Hook 视作 componentDidMount
、componentDidUpdate
和 componentWillUnmount
的组合体。
React 组件中有两种常见的副作用:
- 需要清理的副作用
- 不需要清理的副作用。
二、需要清理的副作用
有的时候,我们希望在 React 更新 DOM 之后进行一些额外的操作。网络请求、手动更改 DOM 以及日志记录都是不需要清理的副作用的常见场景。因为运行之后,可以立即被销毁掉。
下面是在 class 组件和 function 组件中分别表示这两种副作用的使用方式:
1、在 class 组件中
在 React class 组件中, render 方法本身不应该进行副作用操作,但是我们通常是期望在 React 更新 DOM 之后执行一些有必要的副作用。
这就是为什么在 React class 中,会把副作用放在 componentDidMount
和 componentDidUpdate
中。回到计数器的示例中,如果要在 class 计数器组件中实现上面的功能,则代码如下:
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 ( > >You clicked {this.state.count} times</p>
上面代码很明显,class 组件中,两个生命周期中有相同的代码(虽然 componentDidUpdate 中的内容也可以放在 click 的事件 handler 中)
这是因为在多数情况下,我们希望执行相同的副作用,无论是组件刚 mount 还是 update 之后。而从概念上来讲,我们希望他在每次 render 之后发生,但是 React 类组件是没有这种生命周期的。虽然可以把 document.title = 'You clicked' + this.state.count + ' times';
这个操作封装到一个方法中,但是还是需要在 componentDidMount
和 componentDidUpdate
中调用两次。
2、使用 effect Hook 的示例
文章最顶部已经写了下面的示例,为了分析代码,单独再拿到这里:
import { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); return ( > >You clicked {count} times</p>
1、useEffect
做了什么?
通过使用这个 Hook,通知 React 组件需要在渲染后执行什么操作。React 将记住传递的 function(把这个 function 成为 “effect”),并在执行 DOM 更新后调用这个 function。在这个效果中,主要的功能仍旧是设置 document.title
,但是也可以执行数据获取,或者是调用其他的命令式的 API。
2、为什么在组件内调用 useEffect
?
在组件内使用 useEffect
是的可以直接从副作用中访问计数器的 count
或者任何的 props。不需要使用特殊的 API 来读取它,它已经在函数的范围内了(通过 useState
)。Hooks 拥抱 Javascript 的闭包,并且避免在 Javascript 已经提供解决方案的情况下在去引入特定的 React API。
3、每次 render 之后都会执行 useEffect 吗?
是的!
这是默认行为,在第一次 render 之后和每次 update 之后都会运行。你可能会更容易的认为副作用发生在 “render 之后”,而不是发生在 “mount” 和 “update” 之后。不过 React 保证 DOM 在运行时副作用已经更新。
(网络请求每次都放在这里面肯定是有问题的,因此需要定制)如果要定制 useEffect
的默认执行行为,可以参考:https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
3、详细代码拆分说明
function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; });
我们通过 useState
声明了 count
state 变量,并且通知 React 需要使用 effect。
然后把一个 funcrion 传递给 useEffect
Hook,而传递的这个 funcrion 就是副作用。
在我们的副作用中,使用 document.title
浏览器 API 设置文档的标题,可以在 effect 中读取最新的 count,因为 count 变量作用域就是在整个 Example function 中。当 React 渲染我们的组件时,会机主我们使用的 effect,然后在更新 DOM 后运行需要的下沟哦。每次渲染都会发生这样的情况,包括第一次 render。
你可能会注意到,传递给 useEffect
的 function 在每次 render 的时候有所不同,这是故意为之的。事实上,这就是让我们在副作用中读取 count 值而不需要担心这个值是旧值。每次在 re-render 的时候,都会有一个不同的副作用,来取代之前的副作用。在某种程度上,这使得副作用更像是 render 结果的一部分——每个副作用都“属于”特殊的 render。文章后面会提到为什么这是有用的。
Tip
与 componentDidMount
和 componentDidUpdate
不同,使用 useEffect
调度的副作用不会阻塞浏览器更新屏幕。这使得 application 感觉上具有响应式。大多数副作用不需要同步发生。而如果需要同步进行,(比如测量布局),有一个单独的 useLayoutEffect Hook, API 和 useEffect
相同。
三、需要清理的副作用
上面都是不需要清理的副作用,然而,有些副作用是需要去清理的。比如,肯呢过希望设置对某些外部数据源的 subscription。而在这种情况下,清理订阅是非常重要的,这样不会引入内存泄露。
1、使用 class 组件示例:
在 React class 中,通常会在 componentDidMount
中设置帝国与,而在 componentWillUnmount
中清楚它。比如有一个 ChatAPI
模块,可以订阅好友的在线状态,在 class 组件中可能如下所示:
class FriendStatus extends React.Component { constructor(props) { super(props); this.state = { isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); } componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) {