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>;
  }
}
相关推荐
拉不动的猪1 分钟前
关于scoped样式隔离原理和失效情况&&常见样式隔离方案
前端·javascript·面试
摇滚侠2 分钟前
Vue 项目实战《尚医通》,医院详情菜单与子路由,笔记17
前端·vue.js·笔记
有来技术4 分钟前
vite-plugin-vue-mcp:在 Vue 3 + Vite 中启用 MCP,让 AI 理解并调试你的应用
前端·vue.js·人工智能
fruge9 分钟前
前端本地存储进阶:IndexedDB 封装与离线应用开发
前端
忍者扔飞镖15 分钟前
欧服加载太慢了,咋整
前端·性能优化
鹏北海25 分钟前
Vue 3 超强二维码识别:多区域/多尺度扫描 + 高级图像处理
前端·javascript·vue.js
Jack莱杰25 分钟前
Math.js封装工具库(解决前端因为浮点数导致计算错误)
javascript
Android疑难杂症27 分钟前
一文讲清鸿蒙网络开发
前端·javascript·harmonyos
爱学习的程序媛29 分钟前
【JavaScript基础】Null类型详解
前端·javascript
前端一课33 分钟前
uniapp之WebView容器原理详解
前端