Bug随记 —— 一个定时器引发的Bug

一个定时器引发的Bug

今天遇到一个这个样的 bug,我在一个页面使用定时器轮训订单状态,当订单状态改变时,进行路由跳转;在页面销毁时清理掉定时器。代码如下:

ts 复制代码
let timer: ReturnType<typeof setInterval> | null = setInterval(() => {
  queryOrderState();
}, 3000);
onBeforeUnmount(() => {
  timer = null;
  clearInterval(timer);
});

问题:定时器没有被清理掉,然后跳转到其他页面后一直往订单成功页面跳转。

原因:清理定时器时先对timer进行了赋值导致清理时,执行的是clearInterval(null);定时器一直在没有清理掉。

解决:

js 复制代码
onBeforeUnmount(() => {
  if (timer !== null) {
    clearInterval(timer);
    timer = null;
  }
});

其实这个问题仔细一点都可以很快的确认 bug 出现的地方和原因。但我当时手边正好有一个 react 搭建的 demo 项目,我就在 react 中试着复现一下这个 bug,却出现了另一个 bug,代码如下:

tsx 复制代码
const HomePage: FC<HomePageCopm> = () => {
  let timer: any = setInterval(() => {
    console.log('3434');
  }, 1000);
  useEffect(() => {
    return () => {
      clearInterval(timer);
      timer = null;
    };
  }, []);
  return <div></div>;
};

如上代码,当路由跳转后,定时器却没有清理掉,这里我是先把timer = null赋值放在清理定时器之后了,也无法清理掉定时器;我又改变了一下代码结构:

tsx 复制代码
const HomePage: FC<HomePageCopm> = () => {
  useEffect(() => {
    let timer: any = setInterval(() => {
      console.log('3434');
    }, 1000);
    return () => {
      clearInterval(timer);
      timer = null;
    };
  }, []);
  return <div></div>;
};

这样就可以清理掉了,这是为什么呢?

原因:React 组件中每一次渲染或者更新,都是会重新创建整个节点,这样一些引用类型数据(比如:函数、定时器、对象)的地址指向都会改变,这样就相当于每次组件重新更新渲染时,都会对定时器赋值一个新的地址,这样就会导致可能页面清理定时器时只清理掉了一个,还存在其他定时器在执行。

解决:

  • 可以使用useRef, useRef定义的数据在每次组件更新时不会被重新定义/清空。
tsx 复制代码
const HomePage: FC<HomePageCopm> = () => {
  const timerRef = useRef(null);
  timerRef.current = setInterval(() => {
    console.log('3434');
  }, 1000);
  useEffect(() => {
    return () => {
      clearInterval(timer);
      timer = null;
    };
  }, []);
  return <div></div>;
};
  • 或者把 timer 变量定义时和清理定时器放在同一个作用域内,这样也可以清理掉;
ts 复制代码
const HomePage: FC<HomePageCopm> = () => {
  useEffect(() => {
    let timer: any = setInterval(() => {
      console.log('3434');
    }, 1000);
    return () => {
      clearInterval(timer);
      timer = null;
    };
  }, []);
  return <div></div>;
};

反思:为什么 Vue 中可以通过生命周期清理掉定时器,React 中却不行? 答:Vue中可以通过生命周期清理掉定时器,是因为Vue的组件是一个持久存在的实例对象setup()data()中定义的变量在整个组件生命周期内是稳定的,不会因视图更新而重新创建。 当组件被销毁时,相关的定时器变量仍然在作用域中,因此在onBeforeUnmount()中可以准确地清除定时器。

React的函数组件本质上是一个函数,每次渲染都会重新执行该函数,重新创建局部变量 。如果你在组件函数体(useEffect 外)里定义定时器变量,会因为多次 render 而不断创建新的定时器,而useEffect的清理函数只能访问到初始渲染时那个作用域中的定时器变量,无法清除后续 render 创建的那些定时器,导致定时器"清不掉"。

在 React 中使用类组件应该就不会存在这样问题了:

tsx 复制代码
class HomePage extends React.Component {
  timer: any = null;
  componentDidMount(){
      this.timer =  setInterval(() => {
      console.log('tick');
    }, 1000);
  }
  componentWillUnmount(){
    clearInterval(this.timer);
    this.timer = null;
  }
  render() {
    return <div>Home</div>;
  }
}
相关推荐
JosieBook18 分钟前
【SpringBoot】21-Spring Boot中Web页面抽取公共页面的完整实践
前端·spring boot·python
吃饭睡觉打豆豆嘛1 小时前
深入剖析 Promise 实现:从原理到手写完整实现
前端·javascript
前端端1 小时前
claude code 原理分析
前端
GalaxyMeteor1 小时前
Elpis 开发框架搭建第二期 - Webpack5 实现工程化建设
前端
Spider_Man1 小时前
从 “不会迭代” 到 “面试加分”:JS 迭代器现场教学
前端·javascript·面试
我的写法有点潮1 小时前
最全Scss语法,赶紧收藏起来吧
前端·css
小高0071 小时前
🧙‍♂️ 老司机私藏清单:从“记事本”到“旗舰 IDE”,我只装了这 12 个插件
前端·javascript·vue.js
Mo_jon1 小时前
css 遮盖滚动条,鼠标移上显示
前端·css
EveryPossible2 小时前
终止异步操作
前端·javascript·vue.js
Stringzhua2 小时前
setup函数相关【3】
前端·javascript·vue.js