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这些我们下次再讲。