这次做的一个需求:类似于探探的卡片推送,每次滑动之后需要再向后台请求新的卡片,浏览过的卡片不再出现。
为了保证前端滑动的流畅度,通常在首次请求的时候,会请求多张卡片,然后前端每消费一张,就向后台请求一张。因为每次看过的卡片会上报给后台并记录,每次请求卡片的时候带上页面上已有的但未浏览的卡片id(我们字段命名为cacheId)。这样就可以保证浏览过的卡片不再出现了。
但是现实中出现一个问题,就是当卡片滑动过快时,上一次请求卡片的结果还未返回,下一次请求又发送到了后台,这样就会可能造成两次连续请求的卡片是相同的(因为第二次请求的cacheId不包含上次请求返回的id)。这时候就需要保证第二次请求必须要等到第一次请求完全结束之后才能发出。
这是一个典型的队列问题。
我们可以假设一个队列,专门存储这些请求。每次请求结束之后再执行下一次请求。
代码大致如下:
/**
* AsyncQueue
* 异步队列,会按顺序执行传入的异步函数
*
*/
type Task = {
fn: any,
args: any[]
}
class AsyncQueue {
queue: Task[] = []
status: 'busy' | 'free' = 'free'
exec() {
this.status = 'busy'
const { fn, args } = this.queue.shift() as Task
fn(...args).then(() => {
if(!this.queue.length) {
this.status = 'free'
} else {
this.exec()
}
})
}
push(task: Task) {
this.queue.push(task)
if(this.status === 'free') {
this.exec()
}
}
quit() {
this.queue = []
}
}
export default AsyncQueue
因为javascript是单线程的,我们没有办法用一个线程来专门执行这个队列里面的任务,所以需要用一个状态来记录当前队列的执行状态free|busy,当free的时候,在push操作之后会立即触发这个队列的执行器,当busy的时候,由上次任务触发下次任务的执行。
我们可以通过下面代码来测试:
function asyncTest(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('resolve exec', id)
resolve()
}, 1000)
})
}
const asyncQueue = new AsyncQueue()
asyncQueue.push({fn: asyncTest, args: [1]})
asyncQueue.push({fn: asyncTest, args: [2]})
asyncQueue.push({fn: asyncTest, args: [3]})
asyncQueue.push({fn: asyncTest, args: [4]})
asyncQueue.push({fn: asyncTest, args: [5]})
asyncQueue.push({fn: asyncTest, args: [6]})
asyncQueue.push({fn: asyncTest, args: [7]})
setTimeout(() => {
asyncQueue.push({fn: asyncTest, args: [8]})
}, 2200);
我们可以看到控制台会按顺序输出resolve exec: 1-8
这种方式有个好处,就是我们开始时并不需要知道有多少任务需要我们执行。我们在调用的时候只需要调用这个队列的push方法。