useEffect新手向讲解
useEffect是react官方提供的hook之一,对于初学者来说,理解好这个hook对于更好掌握react是很重要的。小伙伴们可以先看下官方文档中对useEffect的说明:useEffect官方文档传送门
注意:useEffect是在每次函数组件render完成之后再判断是否要执行的
useEffect的参数
setup函数
useEffect(setup, dependencies)
第一个参数:一个回调函数(将其内部的代码称之为Effect中),会在组件渲染完之后执行
● 在开发中,会将那些产生副作用(即会影响到组件的该次渲染)的代码,编写到useEffect传入的函数的函数体中,这样就可以避免这些代码影响到组件的渲染
● 在这个回调函数中,可以指定一个函数作为返回值,我们可以称之为清理函数
● 在这个清理函数中可以做一些工作清除上一次Effect执行所带来的影响,即让旧的Effect不影响新的Effect执行
● 这个清理函数会在下一次执行Effect的时候,比函数体中的代码先执行
● 这个清理函数只会在组件再次渲染后,要再次执行副作用之前执行,也即是在上一个组件销毁前执行。
● 另外再提一点,如果想要副作用在组件unmount之后执行,比如在组件卸载后,移除Effect中监听的事件,就可以返回一个清理函数,在该清理函数中移除对应的事件。useEffect(() => { return () => {} }, [])
依赖项数组
第二个参数(可选):依赖项组成的数组
● 即可以指定useEffect的依赖项,只有当依赖项改变的时候,Effect才会再次触发
● 如果不指定依赖项数组,即每次组件渲染完成后,都会执行对应回调(第一个参数)
● 如果指定了,那么只有在依赖项发生变化的时候,才会执行对应回调(第一个参数)
● 如果指定的是一个空数组,那么,其只会在组件初次渲染的时候执行
● 这个比较是浅比较(也就是比较的是依赖指向的内存空间),官方文档里说的是通过Object.is()进行比较,那么,如果依赖项是一个对象类型的数据的时候,就要特别注意了,即使存储的值不变,但是对象指向的地址发生了变化,即依赖项改变。
● 通常会将Effect中使用的所有变量都设置为依赖项(如果依赖项中的变量是不在回调函数中用到的,那么其实是没有意义的),这样就可以确保当这些值发生变化后,会触发Effect的执行,像setState是由钩子函数useState()生成的,useState会确保组件的每次渲染都取到相同的setState,所以类似于setState可以不写到useEffect的依赖项中
useEffect的返回值
返回值:undefined
useEffect执行流程理解
不过我们最好再来看看官方文档中的描述:
setup:处理 Effect 的函数。setup 函数选择性返回一个 清理(cleanup) 函数。在将组件首次添加到 DOM 之前,React 将运行 setup 函数。在每次依赖项变更重新渲染后,React 将首先使用旧值运行 cleanup 函数(如果你提供了该函数),然后使用新值运行 setup 函数。在组件从 DOM 中移除后,React 将最后一次运行 cleanup 函数。
可选 dependencies:setup 代码中引用的所有响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将验证是否每个响应式值都被正确地指定为一个依赖项。依赖项列表的元素数量必须是固定的,并且必须像 [dep1, dep2, dep3] 这样内联编写。React 将使用 Object.is 来比较每个依赖项和它先前的值。如果省略此参数,则在每次重新渲染组件之后,将重新运行 Effect 函数。如果你想了解更多,请参见 传递依赖数组、空数组和不传递依赖项之间的区别。
可以看出,这里的useEffect能够对应类组件的ComponentDidMount、ComponentDidUpdate、ComponentDidWillUnmounted三个生命周期函数,不过我们并不需要去对应
下面我们通过几个例子,理解一下useEffect的执行机制。
import { useState, useEffect } from "react";x
const App = () => {
console.log('组件被渲染了');
const [count, setCount] = useState(0);
// 这样会触发多次的重渲染,会报错,因为其是在渲染阶段执行的,那么,第一次useCount触发渲染,再次渲染时,count基于的值还是0(上一次渲染还没有完成),然后又会触发渲染,如此往复,成了一个死循环。
```js
// useCount(1);
// 下面这样可以使setState正常
// setTimeout(() => {
// setCount(1);
// }, 0);
// 又或者是这样,可以实现我们上述的需求
useEffect(() => {
console.log('useEffect的回调被执行了');
setCount(1);
}, [count]);
return <div>{count}</div>;
};
export default App;
如果这样,那么控制台输出的是:
● 当第一次组件被渲染后(打印:组件被渲染了)
● 然后会执行一次useEffect的回调(打印:useEffect的回调被执行了)
● 其中setCount更改了组件中的count这个state,又会触发组件的一次渲染(打印:组件被渲染了)
● 而这时,又因为count变成了1(useEffect的依赖项更改),因此useEffect中的回调再一次执行(打印:useEffect的回调被执行了)
● 然后肯定是App函数又被执行了(这里本人还有点疑问),然后检查count没有发生变化,因此不再会执行Effect
如果我们给Effect加上清理函数(即该Effect的返回值,返回一个函数):
useEffect(() => {
setCount(1);
console.log("Effect执行了");
return () => {
console.log("Effect返回的函数执行了");
};
}, [count]);
执行结果:,说明依赖发生变化
再次要执行Effect的时候,Effect的返回值会先于Effect函数体中的代码执行。
如果我们的依赖项没有发生变化:
useEffect(() => {
// setCount(1);
console.log("Effect执行了");
return () => {
console.log("Effect返回的函数执行了");
};
}, [count]);
控制台:
也就是说清理函数只会在依赖项发生变化的时候执行(准确来说只有依赖项发生变化后,才会再次执行Effect,对应的也就是在此Effect执行之前执行对应的返回值)
清理函数用法举例
清理函数用法举例:比如在在Effect中执行的操作,需要对其做一个防抖操作(新手忘了就去查!)的时候(需要用定时器将这次的Effect包裹起来),然后我们可以在Effect的清理函数中执行clearTimeout清除上一个定时器
再举一个例子,比如说我们这时有一个用来显示文章内容的组件,该组件接收一个id prop,当id变化的时候,执行副作用,发送新的网络请求,更新文章内容。
import React, { useState, useEffect } from 'react'
function getArticle ({ id }) {
const [ articleContent, setArticleContent ] = useState(null)
useEffect(() => {
const fetchArticalContentData = async () => {
setArticleContent(null)
const res = fetch(`/.../${id}`)
setArticleContent(res)
}
fetchArticleContent()
}, [id])
const idLoading = ! articleContent
return (
<div>
{ isLoading ? "加载中" : articleContent }
</div>
)
}