useEffect使用指南
React项目中如果使用函数式组件进行开发时,如果想在不使用class组件的情况下使用state和其他React特性,Hook将是你的不二选择。
而Effect Hook又是一种比较常见的Hook,可以在函数式组件中执行副作用操作。刚开始可以理解为created、update生命周期。
一、为effect添加依赖
const [count, setCount] = useState(0) useEffect(() => { const fn = setTimeout(() => { console.log(count) setCount(count+1)。 }, 1000) }) // 0 // 1 // 2 useEffect(() => { const fn = setTimeout(() => { console.log(count) setCount(count+1) }, 1000) },[]) // 0
effect的执行机制,是比较两次依赖项是否相同,不同则执行相关effect。
如果不替effect添加依赖,则是比较两次effect。而我们知道在函数式组件中,每次渲染的state、effect都是不同的。只要组件内部状态发生变化就会执行effect。
而将依赖项设置为一个固定值,则effect只会初始化时执行一次。
二、effect内部状态自调用
上面两种情况是比较简单的,在复杂的业务逻辑中,我们往往需要依据某个状态的变化然后再来执行副作用操作,并且还需要为effect设置清除函数。我们看看下面这段代码:
useEffect(() => { const fn = setTimeout(() => { console.log(count) setCount(count+1) }, 1000) return () => clearTimeout(fn) },[count]) // 0 // 1 // 2
我们在effect中添加了清除函数,并且需要将count作为依赖在count变化时执行内部代码。这应该是大家使用Hook Effect比较常见的用法。
但是就这个例子而言,这种设置依赖的写法是有逻辑漏洞的。在effect内部只需要将count做递增,而此时React是知道count的值,所以effect并不要使用count,可以使用setState的函数形式代替传参。
useEffect(() => { const fn = setTimeout(() => { console.log(count) setCount(c => c+1) }, 1000) return () => clearTimeout(fn) }) // 0 // 1 // 2
setCount(c => c+1)就是在方法内部发送更新指令,告诉React去更新state的值,这样就将count从effect依赖中去掉,保持内部逻辑干净无污染。
三、将函数放在effect内部
有时一个effect内部函数代码量过多,我们可以将代码进行分割单独封装成函数,使整个逻辑看起来清晰明了。可能我们为了进一步追求代码规范,需要将函数抽离放在effect外部。
const countPlayer = () => { return setTimeout(() => { setCount(count+10) },1000) } useEffect(() => { const fn = countPlayer() return () => clearTimeout(fn) },[]) console.log(count) // 0 // 10
比如我们要在初始化时通过接口获取数据进行setState赋值然后渲染UI,并且获取数据操作只需要执行一次。上面的代码显示count为0时执行了effect副作用,通过countPlayer函数进行组件state初始化操作。第二次由于依赖没有变更就跳过了直接往后执行。
逻辑上看起来没问题,但是如果当代码量过大并且存在多个函数嵌套依赖的情况,就很难保证准确更新相关effect依赖。并且如果项目安装了eslint-plugin-react-hooks,在项目打包运行时就会出现警告。
这种情况,我们将函数移动到effect内部就能很好地规避这个问题。
四、将函数放在effect外部
如果一个函数在多个effect中被复用,这时我们就必须将函数放在effect外部了。至于逻辑漏洞和代码eslint告警的问题,一般有两种方法解决:
- 将函数转化成纯函数,移动到组件外部;
- 使用useCallback Hook进行包装;
如果一个函数没有使用组件内部任何state、props就可以转化成纯函数,在不同的effect中任意调用。
如果函数内部需要对state等进行setState操作,可以使用useCallback将整个函数包装成一个依赖state的可变值,然后作为effect的依赖使用。
可能有的小伙伴会问,为啥不直接将countPlayer函数作为依赖呢?下面我们看看这样设置后的运行情况。
const countPlayer = () => { return setTimeout(() => { setCount(count+10) },1000) } useEffect(() => { const fn = countPlayer() return () => clearTimeout(fn) },[countPlayer]) console.log(count) // 0 // 10 // 20 // ...
会出现无限循环自调用的bug。之前我们提过,在函数式组件内部,每次获取的函数体、state、effect都不一样,所以会导致effect陷入死循环。下面我们将函数用useCallback进行包装看看效果。
const countPlayer = useCallback(() => { return setTimeout(() => { setCount(c => c+10) },1000) },[]) useEffect(() => { const fn = countPlayer() return () => clearTimeout(fn) },[countPlayer]) console.log(count) // 0 // 10
这样既满足了将函数体抽离出effect的目的;并且通过useCallback包装使函数体本身成为了一个拥有独立依赖的可变值,可以在不同的effect之间复用。
至此,effect Hook一些常用的思路和使用技巧都讲解完毕!