0%

js定时器的陷阱

项目遇到一个奇怪的问题,分页查询的页面会自动在两页之间跳变,F5刷新之后问题消失,第一次测试报这个问题的时候,让测试按照最近的操作重试一下,又重现了一次,想着应该是必现问题,后面再看,结果后面再看的时候,怎么都无法重现了。过了一段时间,问题又再次出现,这次一定不能再放过了,F12查看网络调用,发现确实会发送两次调用,两次都是定时刷新触发的,自此心里基本有数应该是定时器导致的。接下来通过分析代码,终于找到问题的根源,是因为出现僵尸定时器,在背后还在一直运行,它里面的状态是不会变的,始终是某一页,这样切换到新的页之后,就会在两页之间自动切换。这个问题还挺隐晦的,特此记录下。

问题分析

页面如下:

image.png

列表代码如下,页面第一次创建的时候设置定时器timerList,当点击新建的时候会clear掉timerList,然后新建的完成或取消的时候再调用openTimer恢复定时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
componentDidMount() {
const { dispatch } = this.props;
dispatch({
type: 'image/getImageListByTid',
});
this.timerList = setInterval(this.timer, 5000);
}

componentWillUnmount() {
clearInterval(this.timerList);
clearInterval(this.timerModal);
}

timer = () => {
const { page } = this.state;
const { dispatch } = this.props;
dispatch({
type: 'image/getImageListByTid',
payload: {
checkschedual: true,
...page,
},
});
};

openTimer = () => {
this.timerModal = setInterval(this.timer, 5000);
};

新建镜像完成的逻辑,回调函数callback中调用openTimer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
okHandle = () => {
const { dispatch, form, refresh, openTimer } = this.props;
const { switcheValue } = this.state;
const callback = () => {
refresh();
openTimer();
};
form.validateFields((err, fieldsValue) => {
if (err) return err;

// console.log('file: ', fieldsValue);

const data = {
...fieldsValue,
enable_integrity_check: switcheValue,
// min_ram:fieldsValue.min_ram*1024, // 后台单位MB
};
dispatch({
type: 'image/createImage',
payload: data,
callback,
});
});
};

新建镜像取消的逻辑,回调会调用openTimer。

1
2
3
4
5
6
7
8
9
10
11
12
cancelHandle = () => {
const { dispatch, openTimer } = this.props;
this.setState({
step: 0,
type: 'imagefile',
});
dispatch({
type: 'image/addModal',
payload: false,
callback: openTimer,
});
};

model代码,成功的时候关闭对话框,失败的时候不关闭对话框。不管成功还是失败都会调用回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 通过镜像地址创建镜像会调用该方法
*createImage({ payload, callback }, { call, put }) {
const formData = new FormData();
formData.append('name', payload.name);
formData.append('disk_format', payload.disk_format);
formData.append('fileurl', payload.fileurl);
formData.append('hypervisor_type', payload.hypervisor_type);
formData.append('min_disk', payload.min_disk);
formData.append('min_ram', payload.min_ram);
formData.append('system', payload.system);
formData.append('version', payload.version);
formData.append('description', payload.description);
formData.append('visibility', payload.visibility);
formData.append('type', payload.type);
formData.append('back', payload.back);
formData.append('enable_integrity_check', payload.enable_integrity_check);
const response = yield call(createImage, formData);
if (isResponseSuccess(response)) {
notification.success({
message: '操作成功!',
});
yield put({
type: 'addModal',
payload: false,
});
} else {
errorMessage(response);
}
if (callback) callback();
},

所以如果新建镜像失败,会调用一次回调函数,设置了定时器。此时对话框还没关闭,不管是取消,还是再次完成,都会再次调用openTimer,而openTimer中没有请除上次的定时器,造成了僵尸定时器。

问题修改

要么model中新建只有成功才调用回调函数,要么openTimer中总是先请除上次的定时器。

问题启示

setInterval和clearInterval必须成对出现,clearInterval的参数是setInterval的返回值。用成员变量记录setInterval的返回值时,如果重复设置setInterval,一定要注意请除上次的定时器,否则就会出现僵尸定时器。

1
2
3
4
5
6
openTimer = () => {
if (this.timerModal) {
clearInterval(this.timerModal);
}
this.timerModal = setInterval(this.timer, 5000);
};

componentDidMount中设置的定时器,注意在componentWillUnmount中要请除,如果用useEffect会更好,设置定时器和清除定时器可以写在一起,不用分开,可维护性更好。

-------------本文结束感谢您的阅读-------------