React useEffect Hook的对象 & 数组依赖
核心精神
前言
useEffect
可以说是使用React
Hook
时最常用的hook,可以用于实现一些生命周期操作和对变量的监听。
本文是对Object & array dependencies in the React useEffect Hook的翻译,帮助自己更好地理解useEffect
的同时,也希望帮助到大家。
useEffect
useEffect
在没有设置第二个参数的时候,会在每次渲染的时候执行其回调:
1
|
const Example = () => {
|
然而,我们一般很少这么用,因为通常我们并不需要在每次渲染的时候都执行回调,这样会执行不必要次数导致性能下降。
useEffect
有第二个参数,称为依赖数组,只有当依赖数组内的元素发生变化的时候,才会执行useEffect
的回调。这么做就能够优化effect
执行的次数。
1
|
const Example = () => {
|
这种做法在数组元素类型为基本数据类型时可以起到作用。但对于复杂数据类型如:对象,数组和函数来说,React
会使用referential equality
来对比前后是否有不同。
React
会检查当前渲染下的这个对象和上一次渲染下的对象的内存地址是否一致。两个对象必须是同一个对象useEffect
才会跳过执行effect
。所以,即使内容完全相同,内存地址不同的话,useEffect
还是会执行effect
。
Option 1 - 依赖于对象属性
在这个例子中,当前组件会从props
获取一个对象,并在useEffect
的依赖数组中使用这个对象:
1
|
import React, { useState, useEffect } from 'react'
|
理想情况下,props
传入的team
的内容是一样的话,其内存地址也会相同。但这其实是无法保证的。
所以,为了解决这个问题,我们可以只使用team
对象里的一些属性,而不是使用整个对象。
1
|
import React, { useState, useEffect } from 'react'
|
假设team
对象中的id
和active
属性都是基本数据类型,effect
就只会在id
或者active
属性发生变化的时候执行(注意是或,不是且)。
team
对象如果是在组件内被创建的话也能够起到作用:
1
|
import React, { useState, useEffect } from 'react'
|
所以,即使team
对象在每次渲染过程中都重新被创建,也不会导致effect
每次渲染都会执行,因为useEffect
只依赖于id
和active
属性。
需要注意的是,这个方案不适用于依赖元素为数组的情况。
Option2 - 在内部创建对象
在上一个例子中,如果effect
不是使用对象里的元素,而是以整个对象作为依赖会发生什么呢?
1
|
import React, { useState, useEffect } from 'react'
|
team
对象是在每次渲染下重新创建的,所以useEffect
在每次重新渲染时都会执行。
幸运的是,react-hooks/exhaustive-deps
ESLint rule提示道:
1
|
The 'team' object makes the dependencies of useEffect Hook
|
在我们使用ESLint推荐的使用useMemo
Hook之前,我们可以尝试更加简单的操作。我们可以尝试两次创建team
对象,一个用于传递给Player
子组件,一个用于useEffect
内部。
1
|
import React, { useState, useEffect } from 'react'
|
现在team
对象是在useEffect
内部被创建,并且只在effect
要被执行的时候被创建。id
、name
、active
作为依赖,只有当这些值发生变化的时候effect
才会执行。
创建对象相对来说开销是比较小的,所以在useEffect中重新创建一个team
对象是可以接受的。
优化useEffect
所带来的性能提升远远大于创建两个对象所带来的性能损耗。
Option3 - 记忆对象
然而,如果创建对象或数组的开销是昂贵的,那重复创建对象就会比执行多次effect
更糟糕。在这种情况下,我们需要“缓存”创建的对象或数组,这样当其中的数据没有发生变化时,这个对象或数组就不会在渲染过程中发生变化。这个过程称为“记忆”(memoization),我们通过useMemo
来实现。
1
|
import React, { useState, useEffect, useMemo } from 'react'
|
假设这个createTeam()
方法开销昂贵,那我们自然会希望它执行的次数越少越好。useMemo
Hook可以实现只有在id
、name
或者active
在渲染过程中发生变化时,才会再次创建team
。但如果在Team
组件重新渲染过程中,以上属性没有一个发生变化,team
对象就会是同一个对象。因为是同一个对象,我们就可以放心地使用useEffect
,不用担心会执行不必要的次数。
Option 4 - 自己创建
如果以上方案都无法解决问题怎么办?比如从props
中传入了对象或者数组,这个对象或数组会成为useEffect
的依赖数组元素,而且我们并不知道创建一个新的team
对象所需的属性。但我们还是想要实现在组件渲染过程中“缓存”这个对象的值。
在这种情况下,我们可以使用useRef
Hook替代useMemo
Hook。
1
|
import React, { useState, useEffect, useRef } from 'react'
|
这里使用了isDeepEqual
来判断teamRef.current
和team
的值是否一致,而不是比较两者的内存地址。所以,即使在每次渲染过程中team
对象是一个新的对象,如果它的内容是一致的,isDeepEqual()
也会返回true
。
所以当两者在做深比较的时候,如果内容一致,isDeepEqual
会返回true
,teamRef.current
会继续指向原本team
对象的内存地址。那依赖数组里的元素teamRef.current
就没有发生变化,useEffect
也不会再执行。如果isDeepEqual
返回false
,teamRef.current
就会被赋予新的team
值,effect
就会执行。
如果你发现自己遇到了Option4这样的情况,我建议安装react-use
npm包,使用其中的useDeepCompareEffect
Hook来解决问题,还能够避免react-hooks/exhaustive-deps
lint rule报错。
总结
大多数情况下,我能用Option1来解决问题。如果Option1无法解决,我就会使用react-use
包里的helper Hook来解决问题。