掌握 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遍
- 代码分散。逻辑看起来就分散在了组件生命周期的各个地方
因此,我们需要更好的方法解决
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
|
效果如下
使用 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 组件相同
可以看到 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
|
为了更好的性能,注意代码中判断了 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
|
注意到 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
|
这里只在 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 就会一直拥有其初始值。尽管传入 [] 作为第二个参数更接近大家更熟悉的 componentDidMount
和 componentWillUnmount
思维模式,但我们有更好的方式来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect
,因此会使得额外操作很方便。
需要清除的 Effect
本节研究如何实现 willUnmount 这个生命周期,实现组件销毁时,清除 effect 逻辑。
在上一个demo中增加一个逻辑,点击按钮展示或隐藏鼠标的坐标组件。
显示与移除组件
共三个文件,结构如下