自动售货机内部的纸币器和硬币器通常通过串口或USB与主控板通信,驱动层会以事件回调的形式向上层应用报告设备状态。例如硬币器每识别一枚硬币会触发一次计数事件,纸币器在进币、识别、暂存、入钞、退钞等不同阶段也会产生独立回调。这些回调带有异步特征,到达时机不可预测,而且不同设备之间完全独立。如果直接把余额更新、找零计算等逻辑写在回调函数里,多个回调可能在极短时间内先后触发,导致状态修改穿插进行。jQuery提供的Deferred与队列机制恰好可以把这些离散的硬件事件变成有序的异步任务,让支付流程保持稳定。

支付硬件回调的异步性与竞态挑战
自动售货机常用的MDB或ICP协议中,纸币器和硬币器并不是每次只上报一个简单结果。以硬币器为例,当用户投入一枚硬币时,传感器会检测硬币的直径、材质和通过速度,可能先后产生多个脉冲信号,驱动层可能把这些信号合并成一个投币事件,也可能分别上报。纸币器更复杂,从纸币进入通道到最终确认入钞,会经历接收开始、纸币识别中、纸币暂存、入钞完成等多个状态,每个状态都可能触发回调。这些回调之间的顺序在理想情况下是固定的,但在实际硬件通信中,中断、缓冲区刷新和轮询延迟都可能造成顺序颠倒或事件重叠。
如果采用传统写法,直接在回调中更新全局余额变量,代码大致如下:
// 不安全的回调写法
var totalCredit = 0;
function onCoinCredit(amount) {
totalCredit += amount;
if (totalCredit >= price) {
makeChange(totalCredit - price);
totalCredit = 0;
}
}
function onBillCredit(amount) {
totalCredit += amount;
checkEnoughCredit();
}
上面代码的隐患在于,当硬币器和纸币器的事件几乎同时到达时,JavaScript虽然单线程,但两个回调可能交叉执行。例如硬币回调执行到一半,纸币回调被触发,读取到的totalCredit可能已经加上了硬币金额,但找零逻辑还没执行完,导致重复计算或找零金额错误。更麻烦的是,找零过程中用户可能继续投币,新事件再次修改余额,造成状态机混乱。
要解决这个问题,不能依赖回调的原始顺序,必须引入一个串行化的队列,让每个支付相关事件按到达时间依次处理。同时需要一种机制让当前处理流程可以等待异步操作,比如等待找零完成后再处理下一个投币事件。jQuery的Deferred和队列操作正好提供了这两项能力。
基于jQuery Deferred的回调封装与队列设计
jQuery的Deferred对象表示一个尚未完成的异步操作,它可以通过resolve或reject改变状态,并触发done、fail等回调。Deferred还支持pipe或then,允许把多个异步步骤串联起来。在支付场景中,一个完整的硬币处理流程可以抽象为一个Deferred任务:先更新余额,再检查是否达到售价,如果需要找零则等待找零完成,最后结束任务。通过Deferred的promise方法,可以只暴露状态监听接口,避免外部随意改变任务状态。
队列方面,jQuery提供了queue与dequeue函数,它们通常用于动画队列,但也可以用来管理自定义函数序列。不过在实际项目中,更灵活的做法是维护一个数组作为任务队列,并配合一个正在处理的标志位。每次硬件回调触发时,将事件包装成任务并放入队列,然后尝试从队列中取出一个任务执行。如果当前已经有任务在执行,就等待其完成后再继续。这样可以保证同一时间只有一个支付事件在改变状态,避免竞态。当然,jQuery.Callbacks也能作为回调列表使用,不过Deferred在表达异步任务完成状态时更加直观。
下面是一个基于Deferred的支付队列封装示例:
function PaymentQueue() {
this.queue = [];
this.processing = false;
}
PaymentQueue.prototype.enqueue = function (taskFn) {
var self = this;
var deferred = $.Deferred();
this.queue.push({
taskFn: taskFn,
deferred: deferred
});
this.processNext();
return deferred.promise();
};
PaymentQueue.prototype.processNext = function () {
var self = this;
if (this.processing || this.queue.length === 0) {
return;
}
this.processing = true;
var item = this.queue.shift();
$.when(item.taskFn()).done(function (result) {
item.deferred.resolve(result);
}).fail(function (err) {
item.deferred.reject(err);
}).always(function () {
self.processing = false;
self.processNext();
});
};
上面的PaymentQueue把每个硬件事件对应的处理函数作为任务入队,并返回一个promise。processNext在执行完当前任务后会自动取出下一个任务。这样所有支付事件都会按进入队列的顺序串行处理。即使硬币器和纸币器同时上报,也只会一个接一个地更新余额和状态,不会出现交叉修改。
实战:纸币器与硬币器状态机的队列处理
有了队列基础,接下来需要把纸币器与硬币器的具体回调接入队列。以常见的支付状态机为例,可以定义以下状态:空闲、等待支付、纸币暂存、硬币累计、缴款确认、找零中、交易完成。硬币器回调主要包括投币成功、硬币器管存状态变化、找零完成;纸币器回调主要包括纸币接收中、纸币暂存、纸币入钞、纸币退回。每种回调都对应一个处理函数,这些函数统一通过队列执行。
以下代码展示了如何将硬币器和纸币器事件包装后加入队列:
var payQueue = new PaymentQueue();
function onCoinInserted(amount) {
payQueue.enqueue(function () {
var deferred = $.Deferred();
// 模拟异步更新余额
setTimeout(function () {
currentCredit += amount;
console.log('硬币入账:' + amount);
deferred.resolve();
}, 50);
return deferred.promise();
});
}
function onBillEscrowed(amount) {
payQueue.enqueue(function () {
var deferred = $.Deferred();
// 纸币暂存,等待确认
setTimeout(function () {
escrowAmount = amount;
console.log('纸币暂存:' + amount);
deferred.resolve();
}, 80);
return deferred.promise();
});
}
function onBillStacked() {
payQueue.enqueue(function () {
var deferred = $.Deferred();
setTimeout(function () {
currentCredit += escrowAmount;
escrowAmount = 0;
checkPriceAndChange();
deferred.resolve();
}, 30);
return deferred.promise();
});
}
在上述实现中,onCoinInserted和onBillEscrowed分别把硬币入账和纸币暂存操作放入同一个payQueue。由于队列是串行的,如果硬币事件先入队,随后纸币暂存事件到达,即使纸币暂存的实际硬件动作更快完成,处理顺序仍然以事件入队顺序为准。这样可以避免用户同时投入硬币和纸币时余额被重复计算或漏算。checkPriceAndChange函数内部也可以使用Deferred,当需要找零时触发找零流程,并等待找零完成后再结束任务,从而保证后续事件不会在找零过程中干扰余额。
队列还支持错误隔离。如果在处理某个任务时抛出异常或返回的Deferred被拒绝,当前任务会失败,但队列会继续处理下一个任务。这符合支付设备维护的需求,不能因为一次设备故障就阻塞所有后续支付事件。
异常处理、超时与调试技巧
硬件回调存在超时风险。比如纸币器上报了纸币暂存,但后续入钞确认始终没有到来,此时任务不能一直等待,否则整个队列都会被卡住。可以用Deferred配合setTimeout实现超时机制。当某个任务超过预设时间未完成时,主动reject该Deferred,并执行相应的恢复逻辑,比如命令纸币器退回纸币、复位设备状态。
以下是一个带超时的任务包装示例:
function runWithTimeout(taskFn, timeoutMs) {
var deferred = $.Deferred();
var timer = setTimeout(function () {
deferred.reject('timeout');
}, timeoutMs);
$.when(taskFn()).done(function () {
clearTimeout(timer);
deferred.resolve();
}).fail(function (err) {
clearTimeout(timer);
deferred.reject(err);
});
return deferred.promise();
}
payQueue.enqueue(function () {
return runWithTimeout(function () {
var d = $.Deferred();
// 等待纸币器确认入钞
setTimeout(function () {
d.resolve();
}, 200);
return d.promise();
}, 1000);
});
调试时建议在队列关键位置输出状态信息,比如当前队列长度、处理状态、任务耗时。jQuery的Deferred可以通过state方法查询状态,判断任务是在等待、已解决还是已拒绝。但不要在每个回调中大量使用console.log,这会拖慢处理速度,影响支付实时性。可以采用分级日志,只在异常或状态切换时输出。还可以为队列增加统计指标,例如最大等待时间、失败任务数,用于评估硬件通信质量。
自动售货机的支付控制虽然看似简单,但纸币器与硬币器回调的异步性容易引发难以复现的故障。借助jQuery的Deferred和自建队列,能够在不引入复杂状态管理库的前提下,把硬件事件变成可控的串行任务流,保证每一次投币、找零都按正确顺序执行。该方案也可扩展到多个支付设备并存的场景,只需为每类设备维护独立队列或共享队列即可。
jQuery异步队列自动售货机硬件回调纸币器硬币器状态处理修改时间:2026-08-26 06:10:02