useEffect高级用法

2/27/2023 React

React Hook useEffect高级用法

所谓高级用法,只不过是一些深层知识点和使用技巧,也就是对前面知识点的一个巩固和学习。

# 让useEffect只在挂载后和卸载前执行一次

之前useEffect基础用法中 举例2提到的功能。

组件需求:

  1. 若某类组件中有变量a,默认值为0,当组件第一次被挂载后或组件重新渲染后,将网页标题显示为a的值。
  2. 当组件第一次被挂载后执行一个自动累加器 setInterval,每1秒 a 的值+1。为了防止内存泄漏,我们在该组件即将被卸载前清除掉该累加器。

需求分析:
关于自动累加器的操作,只关联"组件挂载后和组件卸载前"这2个生命周期中,那useEffect还包含了每次组件重新渲染后,该咋整?

答:useEffect函数的第2个参数表示该依赖管理,将useEffect的第2个参数,设置为空数组[],即表示告诉React,这个useEffect不依赖任何变量的更新所引发的组件重新渲染,以后此组件在更新也不需要调用此useEffect。

这样就可以实现只在第一次挂载后和卸载前调用此useEffect的目的了。

import React, { useState, useEffect } from 'react';

const InterComponent = () => {
    
    const [a, setA] = useState(0); // 定义变量a,并且默认值为0
    
    // 定义第1个useEffect,专门用来处理自动累加器
    useEffect(()=> {
        
        let timer = setInterval(()=> {setA(a+1)}, 1000); // 这行代码暗藏玄机
        
        return () => {
            clearInterval(timer);
        }
    }, []); // 此处第2个参数为[],告知React以后该组件任何更新引发的重新渲染都跟当前useEffect无关
    
    // 定义第2个useEffect,专门用来处理网页标题更新
    useEffect(() => {
        document.title = `${a} - ${Math.floor(Math.random() * 100)}`;
    }, [a])
    
    return <div> {a} </div>
    
}

export default InterComponent;
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

以上代码看着是没问题的,但是给了一个错误的警告提示:Either include it or remove the dependency array. You can also do a functional update 'setA(a => ...)' if you only need 'a' in the 'setA' call.

该错误警告意思是:如果你确认你传入的第2个参数是数组,那么你可能会用到setA(a => ...) 这种方式来更新a的值。

让我们再看看那行代码:

let timer = setInterval(()=> {setA(a+1)}, 1000); // 这行代码暗藏玄机
1

再看看react给我们的错误警告提示:You can also do a functional update 'setA(a => ...)' if you only need 'a' in the 'setA' call. 你可能会用到setA(a=> ...)这种方式来更新a的值。

那就按照提示,将那行代码修改为:

let timer = setInterval(()=> {setA(a => a+1)}, 1000); 
1

再次执行,错误提示警告没有了,组件也完全按照预期来执行了。

为什么会有这个问题?

关于刚才setInterval中累加 a 的值遇到的问题,React官方文档中其实也有类似示例,只不过他们用的变量count,这里用的变量是a。

看下React官方文档中引用的话:

有时候,你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中的 state,但这通常会引起 Bug,传入空的依赖数组 [],意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在setInterval 的回调中, count 的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包中,且初值为0。每隔1秒,回调就会执行 setCount(0 + 1),因此,count 永远不会超过1。

再次重复一遍:如果useEffect函数第2个参数作为空数组,那么react会将该useEffect的第1个参数 effect 建立一个闭包,该闭包离的变量 a 被永远设定为当初的值,也就是0。尽管setInterval正常工作,每次都"正常执行了",可是 setA(a+1)中 a 的值一直没变化,一直都是当初的0,所以造成 0 + 1 一直都等于1的结果。

而如果修改成setA(a => a+1)的形式,那么就解决了 a 数据异步的问题,每次都是读取最新当前 a 的值。

这个点是使用 useEffect 很容易掉进去的一个坑,所以我现在都是异步更新数据的习惯。

# 性能优化

说下useEffect函数第2个参数提高性能的正确用法吧。

举例:若一个组件中有一个自定义变量obj,obj有两个属性a、b,当a发生变化时,网页标题也会跟着a发生变化。 补充说明:

  1. 为了让a、b都可以发生变化,将在组件中创建2个按钮,点击之后分别可以修改a、b的值;
  2. 为了更加清楚看到每次渲染,我们在网页标题中a的后面再增加一个随机数字;
import React, { useState, useEffect } from 'react';

const docTitleComponent = () => {
    
    const [obj, setObj] = useState({a: 0, b: 0});
    
    useEffect(()=> {
        document.title = `${obj.a} - ${Math.floor(Math.random() * 50)}`;
    }); // 此时并没有给useEffect函数设置第2个参数
    
    return <div>
        {JSON.stringify(obj)}
        <button onClick={() => { setObj({...obj, a: obj.a + 1}) }}>a+1</button>
        <button onClick={() => { setObj({...obj.b, b: obj.b + 1} )}}>b+1</button>
    </div>
    
}

export default docTitleComponent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

由于在网页标题中添加了随机数,因此运行发现即使修改b的值,也会引发网页标题重新"变更一次"。

理由显而易见,修改b的值也会触发组件重新渲染,进而触发useEffect中的逻辑代码。

正确的做法应该是给useEffect添加上第2个参数: [obj.a],明确告诉React,只有obj.a变更引发的重新渲染才执行本条useEffect。

useEffect(()=> {
    document.title = `${obj.a} - ${Math.floor(Math.random() * 50)}`;
}, [obj.a]); // 第2个参数为数组,该数组可以包含多个变量
1
2
3

添加过[obj.a]以后,再次运行,无论obj.b或者其他数据变量引发的组件重新渲染,都不会执行该useEffect,因此达到提高性能的目的。

ok,就这吧,有点累,还是可恶的周一。