问题背景
产品经理:实现一个列表(包含:人员名称、在线状态 等字段),每 10 秒更新一次列表中 在线状态 字段。 开发 SE: 后台实现推送比较复杂,项目要求紧急上线,临时采用前台定时器查询方案。 开发实现:列表查询页面( personList.vue )调用列表查询接口( /queryListApi ),前台查询成功后,判断有数据,则开启 setTimeout 10 秒查询列表接口,更新列表状态数据。
具体代码如下:
<script>
export default {
data() {
return {
personList: [],
timer: null
}
},
created() {
this.queryList()
},
beforeDestory() {
this.clearQueryListTimer()
},
methods: {
queryList() {
axios.get('/queryListApi').then(data => {
if (data && data.length) {
this.personList = [...data]
this.clearQueryListTimer()
this.timer = setTimeout(() => {
this.queryList()
}, 10000)
}
})
},
clearQueryListTimer() {
this.timer && clearTimeout(this.timer)
this.timer = null
}
}
}
</script>
开发自测发现问题:
- 当前页面接口请求 pending 时,切换路由到其他页面,回调中的定时器会仍然执行;
- 快速切换当前页面与其他页面,定时器运行多个,接口频繁多次调用;
- 页面卡顿,内存泄露;
问题分析
- 接口 pending 状态时,切换路由,触发 beforeDestory ,clearTimeout 清理早了,接口请求成功后还是会开启定时器;
- 应该避免暴力切换路由,添加防抖操作;
- 从结果来看,定时器有漏清理问题,导致内存泄露,问题根因指向了 this.timer;
- this.timer 变量会 10 秒修改一次,clearTimeout 清理的 this.timer 指向不明确;
解决方案
- 在定时器回调中,获取当前页面路由,判断是否为 personList.vue 页面,不是则清理定时器;
- 对路由切换添加防抖操作,在接口请求时添加全局 loading 效果;
- 将 this.timer 存到一个数组 timerArr 中,将 timerArr 存到 session 里面;
清理时,在 session 中取出 timerArr,遍历 timerArr ,触发 clearTimeout;
具体代码如下:
<script>
export default {
data() {
return {
personList: [],
timer: null,
loading: false
}
},
created() {
console.log(this.$route.path)
this.queryList()
},
beforeDestory() {
this.clearQueryListTimer()
},
methods: {
queryList() {
this.loading = true
axios.get('/queryListApi').then(data => {
this.loading = false
if (data && data.length) {
this.personList = [...data]
this.clearQueryListTimer()
this.timer = setTimeout(() => {
if (this.$route.path !== '/personList') {
this.clearQueryListTimer()
return
}
this.queryList()
}, 10000)
this.updateTimerArr(this.timer)
}
})
},
updateTimerArr(id) {
let timerArr = JSON.parse(sessionStorage.getItem('timerArr') || '[]')
timerArr.push(id)
sessionStorage.setItem('timerArr', JSON.stringify(timerArr))
},
clearQueryListTimer() {
let timerArr = JSON.parse(sessionStorage.getItem('timerArr') || '[]')
if (timerArr.length) {
for (let i = 0; i < timerArr.length; i++) {
clearTimeout(timerArr[i])
}
sessionStorage.setItem('timerArr', '[]')
}
}
}
}
</script>
定时器清理总结
- 项目中能不用定时器,就不要使用定时器,无法避免的情况下,一定要做好定时器清理;
- 碰到任务状态更新、进度条开发等需求,采用 websocket ,后台推送、前台更新最优,避免使用前台轮询;
- 像 轮播图 等简单的定时器需求,可以在页面 生命周期 中清理定时器;
- 接口 pending 导致 beforeDestory 生命周期被绕过,定时器清理可以搭配 路由判断 一块使用;
- 复杂场景(循环微任务接口查询等),将开启的定时器 timer 拼成数组,存至 session 或全局变量中,循环遍历清理;
- 如果老项目中已有很多定时器书写不规范,临时规避方案:
let timer = setInterval(() => {}, 1000);
for (let i = 0; i < timer + 1; i++) {
clearInterval(i);
}
let timer2 = setTimeout(() => {}, 1000);
for (let j = 0; j < timer2 + 1; j++) {
clearTimeout(j);
}
|