在网页中做一套剧情动画,比如引导页里的小人先走到屏幕中央、点头打个招呼、说一句台词再飘走,这种连续的场景切换靠单纯的链式animate往往力不从心。原因在于链式调用只是把动画依次排队,你很难在中间插入判断逻辑、等待用户点击或者动态决定下一段剧情。jQuery其实内置了一套队列调度机制,核心就是queue、dequeue和clearQueue三个方法,其中dequeue可以让开发者手动推进队列,把动画的控制权真正拿到自己手里。本文围绕dequeue展开,从原理讲到实战,最后完成一个可暂停、可跳过的剧情动画编排器。

一、先弄懂jQuery队列机制的底层原理
jQuery为每个元素维护一个名为fx的默认动画队列,当你连续调用多次animate时,这些动画并不会并行执行,而是被依次推入队列,前一个动画完成后才会触发下一个。这套机制的关键在于:队列里的每一项本质上只是一个函数,jQuery执行完动画后内部会自动调用一次$el.dequeue(),把下一个函数取出来执行。理解了这一点就明白了,dequeue并不是什么黑魔法,它只是手动触发了那个本来由jQuery自动完成的推进动作。
可以用一个直观的比喻来理解:队列就像排队买票的一群人,animate只是把人排进队伍,而dequeue就是喊一声“下一位”。平时售票员(jQuery内部逻辑)会自动喊号,但当你想控制节奏——比如让某个人先等等、插个人进来、或者直接清空队伍——就需要自己拿起话筒。看一个最基础的例子,先用queue放入三个手动执行的步骤,再用dequeue逐个触发:
// 定义一个队列,注意第三个步骤里没有再调用dequeue,队列到此结束
$('#box').queue('story', [
function(next) {
console.log('第一幕:主角登场');
next(); // 手动调用next等价于手动dequeue,推进到下一个函数
},
function(next) {
console.log('第二幕:冲突出现');
setTimeout(next, 1000); // 停顿一秒再进入下一幕
},
function() {
console.log('第三幕:落幕');
}
]);
// 队列放入后不会自动执行,需要手动启动
$('#box').dequeue('story');这段代码里有几个细节值得注意。queue的自定义队列名可以随便取,这里叫story,与默认的fx队列互不干扰;队列函数接收的next参数就是推进器,调用它相当于执行$(this).dequeue('story');如果某个函数里既不调用next也不调用dequeue,队列就会停在这一步,这正是实现“等待用户交互”的关键特性。
二、dequeue与animate、delay的配合技巧
自定义队列最强大的地方在于可以把原生动画函数也塞进去混合编排。animate执行完毕后,如果它发现自己运行在自定义队列里,同样会调用dequeue推进。下面演示一个把移动、停顿、变色、自定义逻辑混排的例子:
var $actor = $('#actor');
$actor.queue('plot', [
function(next) {
// 向右移动600像素,耗时1.5秒,动画结束后animate会自动调用next
$actor.animate({ left: '600px' }, 1500, 'swing', next);
},
function(next) {
// delay本质也是往队列里塞一个延时函数
$actor.delay(800, 'plot').dequeue('plot');
// 上一行delay后手动dequeue,等价于直接用setTimeout(next, 800)
},
function(next) {
$actor.find('.bubble').fadeIn(300, next); // 台词气泡淡入
},
function(next) {
// 纯逻辑步骤,不需要动画
if (!window.userClicked) {
// 暂停队列,等待外部某个事件再dequeue继续
window.resumePlot = next;
return; // 不调用next,队列在这里挂起
}
next();
}
]);
$actor.dequeue('plot');
// 某个按钮点击后恢复剧情
$('#btn').on('click', function() {
if (window.resumePlot) {
var resume = window.resumePlot;
window.resumePlot = null;
resume(); // 手动推进被挂起的队列
}
});这个例子体现了dequeue的三种典型用法:一是作为动画回调传给animate,让动画完成后自动推进;二是配合delay实现队列内的定时停顿;三是刻意不调用next,把推进权交给外部事件,实现“剧情暂停点”。第三种用法在剧情动画里尤其常用,比如等待用户点击“继续”按钮、等待一段语音播放完毕,都可以通过把next存起来、稍后再调用的方式实现。
另外要提一下clearQueue。当用户点了“跳过剧情”按钮时,直接$actor.clearQueue('plot')就能清空尚未执行的步骤,再配合$actor.stop(true)终止当前正在跑的动画,就能干净利落地跳出剧情。这两个方法一组合,暂停、继续、跳过三件套就齐了。
三、实战:实现一个完整的剧情动画编排器
把前面的技巧整合起来,可以封装一个小型编排器,用声明式的写法定义剧情节点,内部自动转换为队列函数。这样剧情策划只需要关心“发生什么”,不用关心“怎么调度”。完整代码如下:
(function($) {
function Plot(el) {
this.$el = $(el);
this.queueName = 'plot_' + ($.guid++);
}
Plot.prototype.act = function(steps) {
var self = this, qn = this.queueName;
this.$el.queue(qn, $.map(steps, function(step) {
return function(next) {
if (step.type === 'move') {
self.$el.animate({ left: step.x, top: step.y }, step.dur || 800, next);
} else if (step.type === 'say') {
self.$el.find('.bubble').text(step.text).fadeIn(300, function() {
setTimeout(function() {
self.$el.find('.bubble').fadeOut(200, next);
}, step.hold || 1500);
});
} else if (step.type === 'wait') {
// 等待外部信号,返回恢复函数供外部调用
self.pending = next;
if (step.autoMs) {
setTimeout(function() { self.resume(); }, step.autoMs);
}
}
};
}));
return this;
};
Plot.prototype.start = function() {
this.$el.dequeue(this.queueName);
return this;
};
Plot.prototype.resume = function() {
if (this.pending) {
var n = this.pending;
this.pending = null;
n();
}
return this;
};
Plot.prototype.skip = function() {
this.pending = null;
this.$el.clearQueue(this.queueName).stop(true);
return this;
};
window.Plot = Plot;
})(jQuery);
// 使用方式:声明式定义剧情
var plot = new Plot('#actor');
plot.act([
{ type: 'move', x: '300px', y: '200px', dur: 1200 },
{ type: 'say', text: '欢迎来到冒险世界!', hold: 1800 },
{ type: 'move', x: '600px', y: '100px', dur: 1000 },
{ type: 'wait', autoMs: 5000 }, // 等玩家点继续,最多等5秒自动推进
{ type: 'say', text: '我们出发吧!' },
{ type: 'move', x: '1000px', y: '400px', dur: 1500 }
]).start();
$('#skipBtn').on('click', function() { plot.skip(); });
$('#nextBtn').on('click', function() { plot.resume(); });这个编排器有几个设计要点。第一,每个Plot实例用唯一的队列名,避免多个角色共用一个元素时队列互相污染;第二,wait类型的步骤把next暂存在实例的pending属性上,既可以由按钮点击触发resume,也可以通过autoMs设置超时自动推进,兼顾了交互性与兜底体验;第三,skip方法先清队列再stop当前动画,确保跳过后不会残留半截动画状态。
如果要更进一步,还可以在这个基础上扩展出并行剧情(多个元素各自持有队列,用一个总控dequeue协调)、条件分支(在步骤函数里根据状态决定把哪段子队列动态queue进去)等玩法。核心思路始终不变:把每个剧情节点封装成调用next收尾的函数,把控制权收敛到dequeue这一个入口上。
四、容易踩的坑与注意事项
第一个坑是忘记推进队列。队列函数里如果既没调用next也没调用dequeue,而且内部也没有任何会触发dequeue的逻辑,整条剧情就会无声无息地卡死,而且控制台不会有任何报错。建议在开发阶段给每个步骤函数加上日志,确认步骤确实被逐个执行。
第二个坑是队列名混用。对fx默认队列调用dequeue('fx')和调用无参的dequeue()效果一样,但如果你的自定义队列忘了传名字,操作就会落到fx队列上,可能意外打断正在执行的CSS动画。养成习惯:自定义队列相关的所有queue、dequeue、clearQueue调用都显式传队列名。
第三个坑是重复start。对已经开始执行的队列再次dequeue,会把同一个函数再执行一遍,可能造成动画重叠。可以在编排器的start方法里加一个started标志位做保护。掌握了这些细节之后,dequeue加queue的组合足以应付绝大多数需要精确时序控制的动画场景,比引入完整的动画引擎轻量得多。
jQuery.dequeue动画队列jquery动画修改时间:2026-09-13 14:10:44