一个定时器引发的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>;
}
}