6.React|6.React Hook之 useEffect

一:描述 ffect Hook 可以让你能够在 Function 组件中执行副作用.

import React, { useState,useEffect}from 'react'; import './App.css'; function App(){ //声明一个名为count的新状态变量 const [count,setCount] = useState(0); //把文档标题设置为包含点击次数的自定义消息 useEffect(()=>{ document.title = `You clicked ${count} times `; }) return (You clicked {count} times
) }export default App;

上面代码看过很多次了,是 React 文档中 Hook 部分一直使用的计数器示例,但是多了个新的功能:把文档标题设置为包含点击次数的自定义消息。而这就是一个副作用。
数据获取,设置订阅或者手动直接更改 React 组件中的 DOM 都属于副作用。有的人习惯成这种行为为 effects,我是比较习惯叫 side effects 也就是副作用的, 这是个概念,需要在 React 必须习惯的概念。
如果熟悉 React 类声明周期方法,可以把 useEffect Hook 视作 componentDidMount、componentDidUpdate 和 componentWillUnmount 的组合体。
React 组件中有两种常见的副作用:
需要清理的副作用
不需要清理的副作用。
二:需要清理的副作用 1、在 class 组件中
在 React class 组件中, render 方法本身不应该进行副作用操作,但是我们通常是期望在 React 更新 DOM 之后执行一些有必要的副作用。
这就是为什么在 React class 中,会把副作用放在 componentDidMount 和 componentDidUpdate 中。回到计数器的示例中,如果要在 class 计数器组件中实现上面的功能,则代码如下:
import React, { Component }from 'react'; import './App.css'; // function App(){ ////声明一个名为count的新状态变量 //const [count,setCount] = useState(0); // //把文档标题设置为包含点击次数的自定义消息 // useEffect(()=>{ //document.title = `You clicked ${count} times `; // }) //return ( // //You clicked {count} times
// // //) // }class App extends 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 click {this.state.count} times
) } }export default App;

上面代码很明显,class 组件中,两个生命周期中有相同的代码(虽然 componentDidUpdate 中的内容也可以放在 click 的事件 handler 中)
这是因为在多数情况下,我们希望执行相同的副作用,无论是组件刚 mount 还是 update 之后。而从概念上来讲,我们希望他在每次 render 之后发生,但是 React 类组件是没有这种生命周期的。虽然可以把 document.title = 'You clicked' + this.state.count + ' times'; 这个操作封装到一个方法中,但是还是需要在 componentDidMount 和 componentDidUpdate 中调用两次。
经过上面两个实例的比较,我们知道useEffect比较方便,那么我们具体分析一下useEffect。
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 在运行时副作用已经更新。
4、详细代码拆分说明
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 与 componentDidMountcomponentDidUpdate 不同,使用 useEffect 调度的副作用不会阻塞浏览器更新屏幕。这使得 application 感觉上具有响应式。大多数副作用不需要同步发生。而如果需要同步进行,(比如测量布局),有一个单独的 useLayoutEffect Hook, API 和 useEffect 相同。
三、需要清理的副作用 上面都是不需要清理的副作用,然而,有些副作用是需要去清理的。比如,希望设置对某些外部数据源的 subscription。而在这种情况下,清理订阅是非常重要的,这样不会引入内存泄露。
1、使用 class 组件示例: 在 React class 中,通常会在 componentDidMount 中设置,而在 componentWillUnmount 中清除它。比如有一个 ChatAPI 模块,可以订阅好友的在线状态,在 class 组件中可能如下所示:
import React, { Component }from 'react'; import './App.css'; class App extends Component{ constructor(props){ super(props); this.state = { isOnline:null } }//设置 componentDidMount(){ chatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ) }//清除 componentWillUnmount(){ chatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ) }handleStatusChange=(status)=>{ this.setState({ isOnline:status.isOnline }) } render(){ if(this.state.isOnline === null){ return "Loading..."; } return this.state.isOnline?'Online':'Offline'; } } export default App;

2、使用 Hooks 的示例 一开始,可能会认为需要单独的 effect 去清理,但是添加订阅和删除订阅的代码联系非常紧密,因此 useEffect 旨在将它保持在一起。如果你的副作用返回一个方法,则 React 则在清理时运行:
import React, { useState ,useEffect }from 'react'; import './App.css'; functionApp(props){ const [isOnline,setIsOnline] = useState(null); functionhandleStatusChange(status){ setIsOnline(status.isOnline); } useEffect(()=>{ ChatAPI.subscribeToFriendStatus(props.friend.id,handleStatusChange); return function cleanup(){ ChatAPI.unsubscribeFromFriendStatus(props.friend.id,handleStatusChange); } }) if(isOnline === null){ return 'Loading...'; }return isOnline?'Online':'Offline'; } export default App;

1、为什么从 effect 中返回一个 function?
这是 effect 可选的清理机制。每个 effect 都可以返回一个在它之后清理的 function。这使得我们能够保持添加订阅和删除订阅彼此接近的订阅的逻辑。这同样是 effect 的一部分。
2、React 在什么时候清理?
当组件卸载的时候,React 会执行清理工作。
然而,effect 会针对每个 render 运行而不仅仅是一次,这就是 React 在下次运行 effect 之前还清除前一个 render effect 的原因。
四、总结 我们已经了解了 useEffect 能够在组件 render 之后进行不同类型的副作用。某些 effect 可能需要清理,因此可以在 effect 中返回一个 function:
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });

而有一些 side effect 可能没有清理的过程,因此不需要返回任何内容。
useEffect(() => { document.title = `You clicked ${count} times`; });

通过 useEffect,能够将之前在两个生命周期中的内容整合到一个 function 中。
五、使用多个 effect 1、Tips:使用多个 effect 来分离问题
使用 Hook 的动机中包括了 class 组件的生命周期将相关的逻辑拆分的问题的解决,而在 Hook 的使用中,也能够把多个 effect 放在 function 组件。
下面是 class 组件的相关代码:
class FriendStatusWithCounter extends React.Component { constructor(props) { super(props); this.state = { count: 0, isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); }componentDidMount() { document.title = `You clicked ${this.state.count} times`; ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); }componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; }componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); }handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); }

请注意设置document.title 的逻辑如何在 componentDidMountcomponentDidUpdate 之前拆分。而订阅的逻辑也在 componentDidMountcomponentDidUpdate 之间传播。componentDidMount 包含两个任务的代码。
使用 Hook 解决这个问题其实就像之前使用 useState 解决问题一样,可以使用多个 effect,然后将不相关的逻辑都拆分成不同的 effect。
function FriendStatusWithCounter(props) { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); const [isOnline, setIsOnline] = useState(null); useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); function handleStatusChange(status) { setIsOnline(status.isOnline); } // ... }

六、跳过 effect 优化性能 在某些情况下,每次 render 后清理或者使用 effect 可能会产生性能问题。在类组件中,可以通过 componentDidUpdate 中编写 prevPropsprevState 的额外比较来解决这个问题:
componentDidUpdate(prevProps, prevState) { if (prevState.count !== this.state.count) { document.title = `You clicked ${this.state.count} times`; } }

这个要求很常见,而这种方式已经被内置到 useEffect Hook 的 API中,如果在重新渲染之间没有更新某些值,则可以告诉 React 跳过 effect,为了实现这种方式,需要将数组作为可选的第二个参数传递给 useEffect
useEffect(() => { document.title = `You clicked ${count} times`; }, [count]);

上面的例子中,// 只有在 count 发生变化的时候才会执行这个 effect, [count] 作为第二个参数传递。如果 count = 5,然后组件如果进行了 re-render,如果 count=5,则 React 会比较前一个 render 和 下一个 render 的值。因为两次 5 === 5,因此React 会跳过这次 effect,这是性能优化。
当 count = 6 的时候,React 会比较 5 !== 6。此时,React 会重新去调用 effect,如果数组中有多个项目,只要有一个的比较值是不相同的, React 也会执行这个 effect。
上面的作用,也同样应用于 cleanup 的 effect:
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }, [props.friend.id]); // 只有 props.friend.id 变化的时候才会调用 effect

【6.React|6.React Hook之 useEffect】未来,第二个参数可能会通过构建的时候,转换自动添加。

    推荐阅读