React useEffect Hook的对象 & 数组依赖 useEffect监控复杂对象

React useEffect Hook的对象 & 数组依赖

核心精神

useEffect(() => {
$('#micro-container').animate({ scrollTop: '0px' }, 200);
}, [datasource.pagenation.count,datasource.pagenation.current])

前言

useEffect可以说是使用React Hook时最常用的hook,可以用于实现一些生命周期操作和对变量的监听。

本文是对Object & array dependencies in the React useEffect Hook的翻译,帮助自己更好地理解useEffect的同时,也希望帮助到大家。

useEffect

useEffect在没有设置第二个参数的时候,会在每次渲染的时候执行其回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Example = () => {
const [count, setCount] = useState(0)

useEffect(() => {
document.title = `You clicked ${count} times`
})

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}

然而,我们一般很少这么用,因为通常我们并不需要在每次渲染的时候都执行回调,这样会执行不必要次数导致性能下降。

useEffect有第二个参数,称为依赖数组,只有当依赖数组内的元素发生变化的时候,才会执行useEffect的回调。这么做就能够优化effect执行的次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Example = () => {
const [count, setCount] = useState(0)

useEffect(() => {
document.title = `You clicked ${count} times`
}, [count]) // Only re-run the effect if count changes

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}

这种做法在数组元素类型为基本数据类型时可以起到作用。但对于复杂数据类型如:对象,数组和函数来说,React会使用referential equality来对比前后是否有不同。

React会检查当前渲染下的这个对象和上一次渲染下的对象的内存地址是否一致。两个对象必须是同一个对象useEffect才会跳过执行effect。所以,即使内容完全相同,内存地址不同的话,useEffect还是会执行effect

Option 1 - 依赖于对象属性

在这个例子中,当前组件会从props获取一个对象,并在useEffect的依赖数组中使用这个对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState, useEffect } from 'react'
import { getPlayers } from '../api'
import Players from '../components/Players'

const Team = ({ team }) => {
const [players, setPlayers] = useState([])

useEffect(() => {
if (team.active) {
getPlayers(team.id).then(setPlayers)
}
}, [team])

return <Players team={team} players={players} />
}

理想情况下,props传入的team的内容是一样的话,其内存地址也会相同。但这其实是无法保证的。

所以,为了解决这个问题,我们可以只使用team对象里的一些属性,而不是使用整个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState, useEffect } from 'react'
import { getPlayers } from '../api'
import Players from '../components/Players'

const Team = ({ team }) => {
const [players, setPlayers] = useState([])

useEffect(() => {
if (team.active) {
getPlayers(team.id).then(setPlayers)
}
}, [team.id, team.active])

return <Players team={team} players={players} />
}

假设team对象中的idactive属性都是基本数据类型,effect就只会在id或者active属性发生变化的时候执行(注意是或,不是且)。

team对象如果是在组件内被创建的话也能够起到作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState, useEffect } from 'react'
import { getPlayers } from '../api'
import Players from '../components/Players'

const Team = ({ id, name, active }) => {
// construct the object from props and/or state
const team = { id, name, active }
const [players, setPlayers] = useState([])

useEffect(() => {
if (team.active) {
getPlayers(team.id).then(setPlayers)
}
}, [team.id, team.active])

return <Players team={team} players={players} />
}

所以,即使team对象在每次渲染过程中都重新被创建,也不会导致effect每次渲染都会执行,因为useEffect只依赖于idactive属性。

需要注意的是,这个方案不适用于依赖元素为数组的情况。

Option2 - 在内部创建对象

在上一个例子中,如果effect不是使用对象里的元素,而是以整个对象作为依赖会发生什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState, useEffect } from 'react'
import { getPlayers } from '../api'
import Players from '../components/Players'

const Team = ({ id, name, active }) => {
// construct the object from props/state
const team = { id, name, active }
const [players, setPlayers] = useState([])

useEffect(() => {
if (team.active) {
getPlayers(team).then(setPlayers)
}
}, [team])

return <Players team={team} players={players} />
}

team对象是在每次渲染下重新创建的,所以useEffect在每次重新渲染时都会执行。

幸运的是,react-hooks/exhaustive-depsESLint rule提示道:

1
2
3
The 'team' object makes the dependencies of useEffect Hook
change on every render. To fix this, wrap the initialization
of 'team' in its own useMemo() Hook.

在我们使用ESLint推荐的使用useMemo Hook之前,我们可以尝试更加简单的操作。我们可以尝试两次创建team对象,一个用于传递给Player子组件,一个用于useEffect内部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState, useEffect } from 'react'
import { getPlayers } from '../api'
import Players from '../components/Players'

const Team = ({ id, name, active }) => {
const [players, setPlayers] = useState([])

useEffect(() => {
// recreate the `team` object within `useEffect`
// from props/state
const team = { id, name, active }

if (team.active) {
getPlayers(team).then(setPlayers)
}
}, [id, name, active])

const team = { id, name, active }

return <Players team={team} players={players} />
}

现在team对象是在useEffect内部被创建,并且只在effect要被执行的时候被创建。idnameactive作为依赖,只有当这些值发生变化的时候effect才会执行。

创建对象相对来说开销是比较小的,所以在useEffect中重新创建一个team对象是可以接受的。
优化useEffect所带来的性能提升远远大于创建两个对象所带来的性能损耗。

Option3 - 记忆对象

然而,如果创建对象或数组的开销是昂贵的,那重复创建对象就会比执行多次effect更糟糕。在这种情况下,我们需要“缓存”创建的对象或数组,这样当其中的数据没有发生变化时,这个对象或数组就不会在渲染过程中发生变化。这个过程称为“记忆”(memoization),我们通过useMemo来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useState, useEffect, useMemo } from 'react'
import { createTeam } from '../utils'
import { getPlayers } from '../api'
import Players from '../components/Players'

const Team = ({ id, name, active }) => {
// memoize calling `createTeam` because it's
// an expensive operation
const team = useMemo(() => createTeam({ id, name, active }), [
id,
name,
active,
])
const [players, setPlayers] = useState([])

useEffect(() => {
if (team.active) {
getPlayers(team).then(setPlayers)
}
}, [team])

return <Players team={team} players={players} />
}

假设这个createTeam()方法开销昂贵,那我们自然会希望它执行的次数越少越好。useMemo Hook可以实现只有在idname或者active在渲染过程中发生变化时,才会再次创建team。但如果在Team组件重新渲染过程中,以上属性没有一个发生变化,team对象就会是同一个对象。因为是同一个对象,我们就可以放心地使用useEffect,不用担心会执行不必要的次数。

Option 4 - 自己创建

如果以上方案都无法解决问题怎么办?比如从props中传入了对象或者数组,这个对象或数组会成为useEffect的依赖数组元素,而且我们并不知道创建一个新的team对象所需的属性。但我们还是想要实现在组件渲染过程中“缓存”这个对象的值。

在这种情况下,我们可以使用useRef Hook替代useMemo Hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState, useEffect, useRef } from 'react'
import isDeepEqual from 'fast-deep-equal/react'
import { getPlayers } from '../api'
import Players from '../components/Players'

const Team = ({ team }) => {
const [players, setPlayers] = useState([])
const teamRef = useRef(team)

if (!isDeepEqual(teamRef.current, team)) {
teamRef.current = team
}

useEffect(() => {
if (team.active) {
getPlayers(team).then(setPlayers)
}
}, [teamRef.current])

return <Players team={team} players={players} />
}

这里使用了isDeepEqual来判断teamRef.currentteam的值是否一致,而不是比较两者的内存地址。所以,即使在每次渲染过程中team对象是一个新的对象,如果它的内容是一致的,isDeepEqual()也会返回true

所以当两者在做深比较的时候,如果内容一致,isDeepEqual会返回trueteamRef.current会继续指向原本team对象的内存地址。那依赖数组里的元素teamRef.current就没有发生变化,useEffect也不会再执行。如果isDeepEqual返回falseteamRef.current就会被赋予新的team值,effect就会执行。

如果你发现自己遇到了Option4这样的情况,我建议安装react-usenpm包,使用其中的useDeepCompareEffect Hook来解决问题,还能够避免react-hooks/exhaustive-depslint rule报错。

总结

大多数情况下,我能用Option1来解决问题。如果Option1无法解决,我就会使用react-use包里的helper Hook来解决问题。

posted on 2022-04-24 13:32  漫思  阅读(2111)  评论(0编辑  收藏  举报

导航