LOGO
| 做生意,没那么难

微信小程序中基于数组实现任务队列的3步核心编码与异步调度实战

在微信小程序里实现队列,听起来像是个数据结构理论题,但实际开发中你会碰到很多“不得不排队”的场景——比如图片上传需要按顺序一张张发、WebSocket消息需要防止并发乱序、甚至用户点击按钮的防抖节流在某些业务下也得靠队列来保证严格顺序。网上很多文章只会给你贴一个class Queue的代码,然后说“看,这就是队列”,但真正写小程序时你会发现,队列的生命周期、页面销毁时的任务丢失、以及setData与异步任务的配合才是坑点。咱们今天就把这些坑一个一个填上。

先明确一点:队列的核心是先进先出(FIFO)。微信小程序本身没有内置队列,但我们可以用数组模拟。不过单纯用数组push和shift会有一个性能隐患——shift操作会导致数组元素向前移动,数据量大时会有性能损耗。更好的做法是用两个指针(head和tail)模拟环形队列,或者直接用链表结构。考虑到小程序的运行环境是JavaScript,而且队列中的任务通常是异步操作(比如网络请求),我推荐用“数组+索引”的方式,既避免shift性能问题,又方便调试时打印队列状态。

第一步:设计队列类,解决“任务丢失”和“并发控制”
写一个Queue类,内部维护一个数组tasks和两个索引head、tail。入队时往tasks[tail]赋值,tail++;出队时读取tasks[head],然后head++,当head和tail相等时说明队列空了。这样不用删除数组元素,只移动指针,性能稳定。但要注意:如果队列长期使用,tasks数组会越来越大,因为旧任务占用的位置没有被回收。所以在出队后,当head超过一定阈值(比如1000)时,可以执行一次tasks.splice(0, head)并重置head和tail。这个阈值根据你的业务频率来定,低频操作甚至不用管。

代码实现大致如下(注意这是核心逻辑,后面会结合小程序场景细化):

class Queue {
constructor() {
this.tasks = [];
this.head = 0;
this.tail = 0;
}
enqueue(item) {
this.tasks[this.tail++] = item;
}
dequeue() {
if (this.tail === this.head) return null;
const item = this.tasks[this.head];
delete this.tasks[this.head]; // 释放引用,避免内存泄漏
this.head++;
if (this.head > 1000 && this.head === this.tail) {
this.tasks = [];
this.head = 0;
this.tail = 0;
}
return item;
}
get size() {
return this.tail - this.head;
}
}

但这样还不够。在小程序里,队列中的任务往往是异步的,比如上传图片需要等待上一张上传完成才能继续下一张。如果只是简单用dequeue取任务然后执行,异步回调还没回来时,下一个任务可能已经被取走了——这就变成并发执行了,失去了队列的串行意义。所以需要加一个“是否正在执行”的锁标志,以及一个“执行完成”的回调来触发下一个任务。

第二步:结合小程序生命周期,防止页面卸载时任务丢失
很多开发者会遇到一个问题:在页面A中启动了一个队列任务(比如批量上传10张图片),用户突然切换到页面B,小程序可能销毁页面A的onUnload,队列对象也被回收,未完成的任务就丢了。解决方法有两个:要么把队列挂载到全局App上,要么在页面卸载时把未完成的任务持久化到Storage。推荐前者,因为队列通常是临时性任务,不需要跨会话持久化。具体做法:在app.js中定义一个全局队列管理器:

App({
globalQueue: {
uploadQueue: new Queue(),
messageQueue: new Queue()
}
})

然后在页面中用getApp().globalQueue.uploadQueue来操作。这样即使页面销毁,队列依然存活。但要注意:任务中如果引用了页面的this(比如setData),页面销毁后回调执行会报错。所以任务最好设计成纯数据传递,或者用回调函数时判断页面是否还存在。可以给每个任务加一个pageId标识,执行回调前检查一下当前页面栈。

第三步:用队列解决“图片按序上传”的真实案例
假设用户选择了5张图片,需要按顺序上传到服务器,且每张上传完成后更新进度条。常见错误写法是直接用for循环调用wx.uploadFile,结果5个请求同时发出,顺序完全不可控。用队列改造:

1. 用户选择图片后,把图片路径数组依次enqueue进全局队列。
2. 启动队列的“消费循环”:定义一个process函数,先检查锁(是否正在执行),如果空闲则dequeue一个任务,设置锁为true,执行上传。
3. 上传成功或失败后,锁设为false,然后递归调用process(或者用while循环检查,但要注意避免同步死循环)。
4. 每完成一个任务,通过回调更新页面的进度数据。

这里有个细节:如果上传过程中用户又添加了新图片,可以直接enqueue,process函数会在当前任务完成后自动处理新任务。另外,上传失败的处理策略:可以设计为“失败重试3次再跳过”,或者“失败后暂停整个队列”。我倾向于后者,因为图片顺序上传通常有依赖关系(比如封面图必须先上传),失败后继续传后面的没有意义。实现时可以在任务对象里加一个retryCount字段,达到上限后把锁永久置为false并触发一个错误回调,让用户手动决定是否清空队列继续。

第四步:对比“队列”与“Promise链”的适用场景
有人会说:用Promise链(p.then(()=>next))也能实现顺序执行,何必写个队列类?区别在于:Promise链是静态的,在创建时就已经确定了任务数量和顺序;而队列是动态的,可以在执行过程中随时添加新任务。比如一个聊天页面,接收到的消息需要按顺序显示,但消息是源源不断推送来的,用Promise链就得不断重新构建链条,而队列只需不断enqueue。此外,队列可以方便地查看当前等待数量、清空队列、调整优先级(比如插队到队首),这些都是Promise链做不到的。

再扩展一点:队列还能用来做“请求合并”。比如用户快速点击“点赞”按钮,每次点击都会触发一个请求,但服务器端只需要最后一次的状态。传统做法是防抖,但防抖会丢弃中间状态。如果用队列,可以把每次点击的请求放入队列,然后设置一个定时器(比如500ms),定时器触发时只发送队列中最后一个请求,并把队列清空。这样既保证了最终状态正确,又不会丢失用户的操作记录(可以用于本地日志)。

第五步:注意小程序特有的坑——setData与队列的配合
队列执行过程中往往需要更新UI,比如显示“正在上传第3/5张”。如果在异步回调里直接setData,可能会触发频繁的页面重绘,导致卡顿。我的做法是:在每次任务完成时,不立即setData,而是把更新数据暂存到一个buffer中,然后通过requestAnimationFrame(小程序里用nextTick或setTimeout(fn,0))统一更新。因为队列是串行的,两次任务完成之间有一定时间间隔(网络延迟),所以buffer里通常只有一条数据,setData频率不会太高。但如果任务完成很快(比如本地缓存操作),就需要合并更新。

另外,小程序对setData的大小有限制(单次不超过1MB),如果你的队列任务涉及大量数据传递(比如每张图片的base64),建议把数据放在全局变量或Storage里,setData只传递索引或状态标识。

第六步:调试与测试队列的小技巧
队列在开发时很难直观看到内部状态。我习惯在Queue类里加一个debug模式:通过一个开关控制是否在控制台打印每次enqueue、dequeue、以及当前队列长度。还可以在小程序开发者工具的AppData面板中,把整个队列对象挂载到globalData上,这样就能实时观察tasks数组的变化。注意不要在生产环境开启debug,否则大量日志会消耗性能。

测试时,可以用Mock数据模拟异步任务:用setTimeout代替网络请求,设置不同的延迟时间(比如500ms、2000ms),观察队列是否严格按顺序执行。特别要测试“在队列执行过程中插入新任务”的情况,看看新任务是排在队尾还是插队。如果你需要插队功能,可以给Queue加一个unshift方法(在队首插入),但要小心:如果当前正在执行的任务还没完成,unshift的任务会在当前任务之后执行,因为当前任务已经出队了。所以真正的插队应该是在任务对象上加优先级字段,每次dequeue时遍历查找最高优先级的任务,而不是简单的数组操作。

最后分享一个实际教训:队列中的任务回调如果包含异步操作(比如再次调用队列的process),很容易造成递归栈溢出。解决方案是把递归调用改成用setTimeout(fn,0)触发,让出执行栈。或者用while循环配合Promise,但要注意循环中不能阻塞。

说了这么多,其实队列在小程序里的应用远不止这些。你可以用它来实现WebSocket消息的顺序处理页面的逐步加载(比如先加载首屏,再加载后续内容)、甚至配合蓝牙API实现指令的串行发送(蓝牙一次只能处理一个指令)。核心思想就一个:把并发变成串行,把无序变成有序。当你下次遇到“明明按顺序调用了,结果却乱序返回”的问题时,不妨想想队列——有时候最简单的数据结构,反而是最优雅的解决方案。

上一篇
微信运动点赞总被朋友碾压?手把手教你用小程序轻松刷爆步数赞!
首页
微信咨询
电话联系