深入了解React useEffect

82 阅读3分钟

1.useEffect中的闭包

我们来看下面的代码

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

  useEffect(() => {
    let timer = setInterval(function() {
      console.log(`Count is: ${count}`);
      setCount(count + 1)
    }, 1000);
    console.log(timer);
    return () => {
      clearInterval(timer);
    }
  }, []);

  return (
    <p>{count}</p>
  );
}

上面的代码运行后,我们期待的结果是Count is (count每秒递增 1) 但是控制台每次打印都是 Count is 0, 并没有出现我们想要的结果。这是为什么呢?

由于useEffect第二个参数:依赖数组为空,说明useEffect只在挂载到DOM上执行一次,之后都不会执行。这就导致了useEffect中定时器只被创建一次,这时的count还是初始化的值 0 ,所以setCount一直在执行setCount(0 + 1)。所以页面展示的count就一直是 1 了。

2.解决闭包问题

那我们要如何解决这个问题呢?

2.1 添加依赖项

如何给useEffect添加依赖项,这个依赖项上一次有讲到,当然我们也可以不传useEffect的第二个参数,也可以解决闭包问题,不过真实的开发情况这样做会造频繁渲染DOM,造成性能的问题。

useEffect(() => {
  let timer = setInterval(function() {
    console.log(`Count is: ${count}`);
    setCount(count + 1)
  }, 1000);
  console.log(timer);
  return () => {
    clearInterval(timer);
  }
}, [count]);    // 关键在这里,useEffect的依赖数组

将count作为依赖项,这样就可以在count发生改变时重新执行effect

这样在useEffect每次被调用的时候,都会”记住”这个数组参数(所以这类的hook也被称为记忆函数),当下一次被调用的时候,会按顺序个比较数组中的元素,看是否和上一次调用的数组元素一模一样,如果一模一样,useEffect第一个参数(回调函数)也就不用被调用了,如果不一样,就重新调用,然后渲染DOM。

2.2 函数式更新state

这种方式,React官网有提到functional-updates。这样就可以避免对外部变量的引用了。

useEffect(() => {
  let timer = setInterval(function() {
    console.log(`Count is: ${count}`);
    setCount(preCount => preCount + 1)
  }, 1000);
  console.log(timer);
  return () => {
    clearInterval(timer);
  }
}, []);

这样打印出来的 count 值虽然依旧是闭包初始化时保存的 0,但 count 不再是在它的初始值上更新,而是在当前 count 值的基础上更新的,所以页面显示的 count 能保持一个最新的值。

不过对于引用类型的数据,这种方法就没有那么好用了,后面会讲到

2.3使用useRef

useEffect、useMemo、useCallback都是自带闭包的。每一次组件的渲染,它们都会捕获当前组件函数上下文中的状态(state, props),所以每一次这三种hooks的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。要解决此问题推荐使用ref

上面这句话是很多文章里说,是React官网给出的原话,但是我并没有找到这句话在哪里。

我们看看这两天做的案例:

这时候,使用函数式更新state就没有作用了,因为需要通过结构或者循环遍历的方式拿到数组中的值。

interface ListItemProps {
    state?: number;
}
export default () => {
    const [list, setList] = useState<ListItemProps[]>([]);
    useEffect(() => {
        layoutEmitter.useSubscription((data) => {
            list.push(data as ListItemProps)
            setList([...list])
        });
    }, [])
    return (
        <div>
            <EventEmitterButton initNumber={11} />
            <button style={{ fontSize: '0.6rem' }} onClick={() => {
                setList([...list, { state: new Date().getTime() }])
                changeData({ state: new Date().getTime() })
            }}>Add List</button>
            <p>list length:{list.length}</p>
            {
                list.map(item => <p key={item.state}>{item.state}</p>)
            }
        </div>
    )
};

运行代码看看有什么问题,当我们点击两次按钮,页面会展示 11 12,点击Add List出现时间戳,再次点击我们发现页面展示11 12 13 ,我的时间戳呢??

产生这个原因是因为useEffect第二个参数为空,只在DOM挂载时运行,当我们第二次点击时,useEffect拿到的是上一次的state(也可以称为陈旧的state),这个问题React官网有给出解决方法,就是通过ref来保存异步回调中读出的最新值

why-am-i-seeing-stale-inside-my-function。 👈可以点击查阅

我们就去看看这个ref可以为我们做些什么

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变,利用这个特性,我们把它用在我们的案例中看看效果

import React, { useEffect, useState, useRef } from 'react';
import { layoutEmitter } from '@/utils/EventEmitter';
import EventEmitterButton from '@/components/EventEmitterButton';
interface ListItemProps {
    state?: number;
}
export default () => {
    const [list, setList] = useState<ListItemProps[]>([]);
    const listRef: any = useRef();

    const changeData = (data: ListItemProps) => {
        list.push(data);
        setList([...list]);
    }
    useEffect(() => {
        layoutEmitter.useSubscription((data) => {
            listRef.current(data)
        });
    }, [])

    useEffect(() => {
        listRef.current = changeData;
        console.log('changeData');
    }, [changeData])

    return (
        <div>
            <EventEmitterButton initNumber={11} />
            <button style={{ fontSize: '0.6rem' }} onClick={() => {
                changeData({ state: new Date().getTime() })
            }}>Add List</button>
            <p>list length:{list.length}</p>
            {
                list.map(item => <p key={item.state}>{item.state}</p>)
            }
        </div>
    )
};

上述解决方法,通过定义第二个useEffect来监听changeData()的变化,并且将 listRef.current = changeData;,当里面的current发生变化的时候并不会引起render,这样ref拿到的就是changeData()方法中list 的最新值,这样就能成功解决闭包问题了,而且这也是官方推荐使用的方式。

使用ref不仅仅可以用useRef,也可以使用creatRef这些我们下次再讲。