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

列表代码如下,页面第一次创建的时候设置定时器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;
const data = { ...fieldsValue, enable_integrity_check: switcheValue, }; 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会更好,设置定时器和清除定时器可以写在一起,不用分开,可维护性更好。