首先让我们做一个小Demo,设置一个名为value的state,并每隔5秒,产生一个随机数,并让value加上这个数.

  1. import React, { useState, useEffect } from 'react';
  2. import './App.css';
  3. function App() {
  4. const [value, setValue] = useState<number>(0);
  5. useEffect(() => {
  6. const timer: NodeJS.Timeout = setInterval(() => {
  7. const random = (Math.random() * 10) | 0;
  8. setValue(value + random);
  9. }, 5000);
  10. return () => {
  11. clearInterval(timer);
  12. };
  13. }, []);
  14. return <div>{value}</div>;
  15. }
  16. export default App;

这里看起来是实现了每隔5秒增加一次值,但

你以为的不是你以为的

如果我们在代码中加入一个定时器数组,用来记录你添加了多少个定时器

  1. function App() {
  2. const [value, setValue] = useState<number>(0);
  3. const [timers, setTimers] = useState<Array<NodeJS.Timeout>>([]);
  4. useEffect(() => {
  5. const timer: NodeJS.Timeout = setInterval(() => {
  6. const random = (Math.random() * 10) | 0;
  7. setValue(value + random);
  8. }, 5000);
  9. timers.push(timer);
  10. setTimers(timers);
  11. console.log(timers);
  12. return () => {
  13. clearInterval(timer);
  14. };
  15. }, [value]);
  16. return <div>{value}</div>;
  17. }

你会惊讶的发现你不仅value发生变化了,而且又多生成了一个定时器。如下图所示。
2214806-20211207180835673-1239672478.png
这对于浏览器性能来说是绝对不可以接受的,那这是为什么产生的呢,产生的原因是因为useEffect 在第一次渲染时获取值为 0 的 value,将不再次执行 effect,所以 setInterval 一直引用第一次渲染时的闭包 value,因此每次都是在0的基础上添加一个随机数,而不是依次累加,最关键的是如果你开启了Vscode的eslint插件,它还会给你自动补齐依赖项, 这里推荐一篇关于react-hooks闭包问题的文章 陈旧闭包问题,解释的比较清楚。

问题不大

为了解决这个问题,我们需要引入react中另外一个hook ,useRef。useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变,利用这个特性,我们把它用在我们的demo中看看效果

  1. function App() {
  2. const [value, setValue] = useState<number>(0);
  3. const [timers, setTimers] = useState<Array<NodeJS.Timeout>>([]);
  4. const saveCallBack: any = useRef();
  5. const callBack = () => {
  6. const random: number = (Math.random() * 10) | 0;
  7. setValue(value + random);
  8. };
  9. useEffect(() => {
  10. saveCallBack.current = callBack;
  11. return () => {};
  12. });
  13. useEffect(() => {
  14. const tick = () => {
  15. saveCallBack.current();
  16. };
  17. const timer: NodeJS.Timeout = setInterval(tick, 5000);
  18. timers.push(timer);
  19. setTimers(timers);
  20. console.log(timers);
  21. return () => {
  22. clearInterval(timer);
  23. };
  24. }, []);
  25. return <div>{value}</div>;
  26. }

使用了useRef后,定时器不会被重复创建,但是value的值变成了依次累加,达到了预期的效果,真是让人神清气爽,so cool!