标题有点危言耸听了,其实用了也没关系,嘻!

不过对于轮询之类的异步任务,用 setInterval 可能会有如下

问题

异步任务的重复调用

1
setInterval(fetchData, 1000)

假设上面的 fetchData 就是你要轮询的请求,你希望的结果是每隔一秒请求一次。

如果每次请求本身的时间小于 1 秒,那么一切 ok;如果遇到网络状况不好的时候,比如某次请求用了 2 秒,setInterval 并不会关心你的异步任务执行了多久,依然会在 1 秒 后再发起一个请求。

理论上,如果出现了网络卡顿,这种轮询会在短时间内堆积大量的 padding 请求,无论是对于目标服务器和本地网络资源来说都是浪费的。

定时器结束不代表异步任务结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function test() {
return new Promise((resolve) =>
setTimeout(() => {
console.log('嘿嘿嘿!')
resolve()
}, 2000),
)
}

const id = setInterval(test, 1000)
setTimeout(() => {
clearInterval(id)
console.log('我结束定时器了!')
}, 1500)

你启动了一个 1 秒执行一次的定时器来执行一个 2 秒才能执行完毕的异步任务。重复调用就不重复 suo 了(莫名 rap,yo!)这里要说的是你在 1.5 秒的时候结束了定时器,但是控制台依然会在 0.5 秒后对你”嘿嘿嘿!”

换句话说,clearInterval 只是结束了定时器,但它并不知道之前一直调用的那些异步任务都结束了没有。由于异步任务与异步任务之间的执行不是线性的,这很容易导致老的异步任务产生的副作用覆盖新的异步任务

解决方案

setPromiseInterval - 用于定时执行异步任务,调用方式类似原生 setInterval!同时欢迎收看源码,实现的简洁优雅又不失活泼~

没错,广告来的就是这么大胆直白不要脸。

哎,其实也没啥技术含量,基于 Promise 搞来搞去的。之所以还水一篇文章,是因为这两天做了这么一个需求:

实时分页优化

需求就是字面意思,存在不少页面的数据是需要实时获取的,同时本身是分页展示的,原本页面的逻辑是这样的:

  • 页面 mounted 的时候就启动定时器轮询数据
  • 点击分页切换的时候只是改变当前 url.query 的 page 字段

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class PageView {
onLoad() {
this.timer = setInterval(this.onTick, 3000)
}

onTick = () => {
this.fetchData(`/api?page=${this.query.page}`)
}

onPageChange = (page) => {
changeQuery({ page })
}
}

上述实现最大的问题就是页面在分页跳转的时候并不会立刻请求数据,而是傻等定时器轮询,数据的更新时间基本看定时器间隔被设置成了多少。

这会造成分页跳转的时候体验极差:分页器已经显示到了下一页,而数据展示的还是上一页内容,过了一会儿,突然一下子数据变成了当前页,给人的直观感觉就是页面卡顿。

尝试过如下解决方案:

在点击分页的时候展示一个 loading 动画

俗称动画欺骗法。通过此方式至少让用户觉得切换页面的时候页面并没有卡死,它正在请求数据。并顺便把锅甩给后端,因为这种交互看起来就是接口请求慢了!

然而,只是这种程度的优化,如果是基于 setInterval 的轮询,是很难做到的。因为你只能控制 loading 什么时候出现,比如说 onPageChange 的时候,而没法控制它什么时候消失。

你可能会觉得疑惑,在 onTick 里,fetchData 结束的时候调用不就行了?然而事情并不会这么简单,原因开篇已经说了 定时器结束不代表异步任务结束

就是说,结束你 loading 的很可能不是最后一个基于最新 query.page 的请求,而且即使就是最后一个,也有可能之前卡住的某个请求在 loading 关闭之后又请求成功了,这个时候页面展示的最新的正确的数据会被替换成老的错误的数据,然后再等着后面的轮询给纠正回来…

再退一亿步说,即使不考虑上面的问题,该解决方案对于前端体验来说依然是不合格的,因为造成了不必要的等待:假设轮询的时间间隔是 3秒,我点击分页的时候刚好上次请求才结束,于是我要看 3秒钟的 loading 动画,这显然不科学。

在点击分页的时候请求一下最新页的数据

还是 setInterval 的那个老问题,你没法保证你请求的新数据不会被某个卡住的老请求返回的数据给覆盖,从而造成数据鬼畜闪烁。。

在点击分页的时候停止定时器 + 请求最新页的数据

还是 setInterval 的那个老问题,你没法保证你请求的新数据不会被某个卡住的老请求返回的数据给覆盖,从而造成数据鬼畜闪烁。。(人类的本质啊

等一下!我都停止了定时器了,为什么…对!你停止的只是定时器。

最后还是 setPromiseInterval 大法好!

上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PageView {
onLoad() {
this.timer = setInterval(this.onTick, 3000)
}

onTick = () => this.fetchData(`/api?page=${this.query.page}`)

onPageChange = async (page) => {
changeQuery({ page })
await clearPromiseInterval(this.timer) // 先关闭定时器
// 后面的代码可以确保没有请求在执行了
await this.onTick() // 再请求数据
await delay(3000) // 因为 setPromiseInterval 会立刻执行异步任务,所以延时 3 秒再重新启动定时器。不要浪费请求...
this.onLoad()
}
}

当然了,上面的代码只是简单的示例,实际上你可能还得考虑 unmount 阶段定时任务的结束,比如说在上面 delay(3000) 过程中如果切换了页面咋办?
还有连续多次触发 onPageChange 会启动多个定时器的问题,这些本文就不赘述了。拜拜~


 评论