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>;
  }
}
相关推荐
复苏季风2 分钟前
v-for什么时候使用index,什么是时候用uuid当key
前端·vue.js
小高0073 分钟前
🔍Vue 隐藏神技巧:99% 开发者没用过,却能让代码简洁 50%
前端·javascript·vue.js
用户50093768390393 分钟前
使用无头浏览器Playwright解决Web打印生成PDF的问题
前端
uhakadotcom4 分钟前
开源一个AI导航站工具-jobleap4u
前端·面试·github
阿聪_5 分钟前
tsconfig.json strict属性配置false会有什么响
前端
艾小码5 分钟前
React Hooks时代:抛弃Class,拥抱函数式组件与状态管理
前端·javascript·react.js
用户50093768390395 分钟前
前端无预览静默打印实现:web-print-pdf npm包与打印专家的完美协作
前端
wycode6 分钟前
Vue2实践(1)之用Vue.extend做一个函数调用的Dialog
前端·vue.js
鹏多多7 分钟前
js使用History.replaceState实现不刷新修改浏览器url
前端·javascript·浏览器
AliciaIr7 分钟前
深入理解CSS居中:面试必备的布局技巧与底层原理(下)
前端·css