React Hooks 系列之2 useEffect
Pin Young Lv9
  • content
    {:toc}

掌握 React Hooks api 将更好的帮助你在工作中使用,对 React 的掌握更上一层楼。本系列将使用大量实例代码和效果展示,非常易于初学者和复习使用。

今天我们讲讲 useEffect 的使用方法。

为什么使用 useEffect

生命周期中写逻辑的问题

react 中旧的生命周期可能会有副作用,比如页面的 title 要展示点击次数时,代码如下:

1
2
3
4
5
6
componentDidMount() {
document.title = `${this.state.count} times`
}
componentDidUpdate() {
document.title = `${this.state.count} times`
}

在 componentDidMount 和 componentDidUpdate 中都写了同样的代码,我们不能在组件的生命周期中挂载一次,这就导致了代码重复的问题。

另一个例子,页面中包含了倒计时,并且在页面销毁时要清除倒计时 timer

1
2
3
4
5
6
componentDidMount() {
this.interval = setInterval(this.tick, 1000)
}
componentWillUnmount() {
clearInterval(this.interval)
}

如果这个组件比较复杂,同时包含了上述的两种逻辑,那么代码如下:

1
2
3
4
5
6
7
8
9
10
componentDidMount() {
document.title = `${this.state.count} times`
this.interval = setInterval(this.tick, 1000)
}
componentDidUpdate() {
document.title = `${this.state.count} times`
}
componentWillUnmount() {
clearInterval(this.interval)
}

我们看到2个问题

  1. 代码重复。设置标题的代码重复了1遍
  2. 代码分散。逻辑看起来就分散在了组件生命周期的各个地方

因此,我们需要更好的方法解决

useEffect 解决的问题

  • EffectHook 用于函数式组件中副作用,执行一些相关的操作,解决上述的问题
  • 可以认为是 componentDidMount, componentDidUpdate, componentWillUnmount 的替代品

接下来学习如何使用 useEffect。

useEffect After Render 的使用

举个例子,通过一个点击按钮,修改页面 title 的例子来说明

Class 组件的写法示例

7ClassCounter.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, { Component } from 'react'

class ClassCounter extends Component {

state = {
count: 0
}

componentDidMount() {
document.title = `${this.state.count} times`
}
componentDidUpdate() {
document.title = `${this.state.count} times`
}

render() {
return (
<div>
<button onClick={() => {
this.setState({
count: this.state.count + 1
})
}}>
Clicked {this.state.count} times
</button>

</div>
)
}
}

export default ClassCounter

App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react'

import './App.css'

import ClassCounter from './components/7ClassCounter'

const App = () => {
return (
<div className="App">
<ClassCounter />
</div>
)
}

export default App

效果如下

image

使用 useEffect 改写上述示例

接下来使用函数时组件实现上述的例子

7HookCounter.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState, useEffect } from 'react'

function HookCounter() {
const [count, setCount] = useState(0)

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

return (
<div>
<button onClick={() => {
setCount(prevCount => prevCount + 1)
}} >Clicked {count} times</button>
</div>
)
}

export default HookCounter

效果和 Class 组件相同

image

可以看到 useEffect 的第一个入参是一个匿名函数,它会在每次 render 后调用。在第一次 render 和后续的更新 render 都会被调用。

另外,useEffect 写在函数式组件内,这样就可以直接拿到 props 和 state 的值,不用写 this 之类的代码。

有条件地执行 useEffect

上一节了解到 useEffect 会在每次 render 后执行里面的函数,这可能会有一些性能问题,接下来就讲一讲如何有条件地执行 useEffect 中的匿名函数。

在上一节的示例上进行扩展一个输入 name 的功能,通过判断只执行 count 变化带来的逻辑。

Class 组件的写法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React, { Component } from 'react'

interface stateType {
count: number
name: string
}

class ClassCounter extends Component {

state = {
count: 0,
name: '',
}

componentDidMount() {
document.title = `${this.state.count} times`
}

componentDidUpdate(prevProps: any, prevState: stateType) {
if (prevState.count !== this.state.count) {
console.log('Update title')
document.title = `${this.state.count} times`
}
}

render() {
return (
<div>
<input
type="text"
value={this.state.name}
onChange={(e) => {
this.setState({
name: e.target.value
})
}}
/>
<button onClick={() => {
this.setState({
count: this.state.count + 1
})
}}>
Clicked {this.state.count} times
</button>
</div>
)
}
}

export default ClassCounter

image

为了更好的性能,注意代码中判断了 prevState

useEffect 的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { useState, useEffect } from 'react'

function HookCounter() {
const [count, setCount] = useState(0)
const [name, setName] = useState('')

useEffect(() => {
console.log('useEffect - update title')
document.title = `You clicked ${count} times`
}, [count])

return (
<div>
<input
type="text"
value={name}
onChange={(e) => {
setName(e.target.value)
}}
/>
<button onClick={() => {
setCount(prevCount => prevCount + 1)
}} >Clicked {count} times</button>
</div>
)
}

export default HookCounter

image

注意到 useEffect 的第二个参数 [count],这个参数是一个数组,元素是要被观察的 state 或 props,只有指定的这个变量发生变化时,才会触发 useEffect 中的第一个参数匿名函数的执行。这有利于性能的保证。

只执行1次 useEffect

本节通过一个记录鼠标坐标的示例研究一下如何只执行一次 useEffect

记录鼠标位置示例 Class 写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { Component } from 'react'

class RunEffectsOnlyOnce extends Component {
state = {
x: 0,
y: 0
}

logMousePos = (e: MouseEvent) => {
this.setState({
x: e.clientX,
y: e.clientY
})
}

componentDidMount() {
document.addEventListener('mousemove', this.logMousePos)
}

render() {
return (
<div>
Y - {this.state.y}, X - {this.state.x}
</div>
)
}
}

export default RunEffectsOnlyOnce

image

这里只在 componentDidMount 中做了事件绑定,只执行了一次事件绑定

useEffect 中记录鼠标坐标

上述效果改造为函数式组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { useState, useEffect } from 'react'

function RunEffectsOnlyOnce() {

const [x, setX] = useState(0)
const [y, setY] = useState(0)

const logMousePos = (e: MouseEvent) => {
setX(e.clientX)
setY(e.clientY)
}

useEffect(() => {
console.log('addEventListener')
document.addEventListener('mousemove', logMousePos)
}, [])

return (
<div>
Y - {y}, X - {x}
</div>
)
}

export default RunEffectsOnlyOnce

注意到 useEffect 方法的第二个参数传入一个空数组,有效的避免了多次调用的问题。

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。

如果你传入了一个空数组([]),effect 内部的 props 和 state 就会一直拥有其初始值。尽管传入 [] 作为第二个参数更接近大家更熟悉的 componentDidMountcomponentWillUnmount 思维模式,但我们有更好的方式来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得额外操作很方便。

需要清除的 Effect

本节研究如何实现 willUnmount 这个生命周期,实现组件销毁时,清除 effect 逻辑。

在上一个demo中增加一个逻辑,点击按钮展示或隐藏鼠标的坐标组件。

显示与移除组件

共三个文件,结构如下

image