问题
setInterval 是间隔调用,与之类似的还有 setTimeout。这两个 API 通常用来做 ajax 短连接轮询数据。
比如有一个 logs.vue 是用来展示某个正在执行的进程产生的日志:
<template>
<div>
<p v-for="item in logList" :key="item.time">
<span>{{"[" + item.time + "]"}}</span>
<span>{{ item.log }}</span>
</p>
</div>
</template>
<script>
import { Component, Vue, Watch, Prop, Emit } from 'vue-property-decorator'
import { getLogList } from './api'
@Component({})
export default class extends Vue {
logList = []
timer = null
mounted(){
this.getData()
}
async getData(){
let r = await getLogList()
if(r && r.logList){
this.logList = r.logList
}
this.timer = setTimeout(()=>{
console.log(this.timer);
this.getData()
}, 1000)
}
beforeDestory(){
clearTimeout(this.timer)
this.timer = null;
}
}
</script>
这段代码看上去没啥问题,但是测试的时候你会发现,有时候路由已经跳转了,获取进程日志的接口依然在不断调用,甚至,有时候接口调用速度非常快,一秒可能有好几个请求。
分析
beforeDestory 是组件销毁前的生命周期的钩子,这个钩子函数一定会调用,但是能不能彻底销毁 setTimeout 呢?答案是不能。
打开控制台就能看到不断打印出来的 id
这是因为,每次使用 clearTimeout 清除掉的是上一次的 id, 而不是本次正要执行的,这种情况,对于使用 setInterval 也是一样的。
根本原因在于,每次调用 getData, this.timer 是在不断的被赋予新的值,而不是一成不变的。
在以前的原生 js 中,我们通常这样写:
var timer = null
function init(){
timer = setInterval(function(){
getData()
})
}
function getData(){}
window.onload = init
window.onunload = function(){
clearInterval(timer)
}
由于上面的 timer 始终保持一个值,所以这里的清除是有效的
解决
vue 提供了 程序化的事件侦听器 来处理这类边界情况
按照文档的说法,我们的代码可以这样来更改
<script>
import { Component, Vue, Watch, Prop, Emit } from 'vue-property-decorator'
import { getLogList } from './api'
@Component({})
export default class extends Vue {
logList = []
// timer = null
mounted(){
this.getData()
}
async getData(){
let r = await getLogList()
if(r && r.logList){
this.logList = r.logList
}
const timer = setTimeout(()=>{
this.getData()
}, 1000)
this.$once('hook:beforeDestroy', function () {
clearTimeout(timer)
})
}
}
</script>
这样写,还解决了两个潜在问题
- 在组件实例中保存这个 timer,最好只有生命周期钩子有访问它的权限。但是实例中的 timer 会视为杂物
- 如果建立代码独立于清理代码,会使得我们比较难于程序化地清理所建立的东西
结论
我们可以通过 程序化的事件侦听器 来监听销毁我们创建的任何代码示例
除了 setTimeout 和 setInterval ,通常还有一些第三方库的对象示例,如 timePicker,datePicker,echarts图表等。
mounted: function () {
// Pikaday 是一个第三方日期选择器的库
var picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
// 在组件被销毁之前,也销毁这个日期选择器。
this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}