JavaScript队列函数与异步执行详解
本文详解JavaScript中如何构建按序执行的函数队列。针对同步与异步混合场景,分析了简单数组调用的局限性,并逐步推导通过next回调机制解决异步函数执行顺序问题的方案,最终引出Node.js Connect框架中间件队列的实现原理。
编辑注:审查他人JavaScript代码时,曾遇类似队列函数,起初不解其意,后知旨在确保函数按序调用。研读本文后,发现该机制亦适用于异步执行场景。
同步函数队列的基本实现
若需按序调用fn1、fn2及fn3,最简方式如下:
fn1();
fn2();
fn3();
然若函数于运行时动态添加,调用前未知具体列表;此时可预定义数组,添加时将函数push入内,需时从数组依序取出并调用:
var stack = [];
// 执行其他操作,定义fn1
stack.push(fn1);
// 执行其他操作,定义fn2、fn3
stack.push(fn2, fn3);
// 调用的时候
stack.forEach(function(fn) { fn() });
函数有无名称无关紧要,直接传入匿名函数亦可。测试如下:
var stack = [];
function fn1() {
console.log('第一个调用');
}
stack.push(fn1);
function fn2() {
console.log('第二个调用');
}
stack.push(fn2, function() { console.log('第三个调用') });
stack.forEach(function(fn) { fn() }); // 按顺序输出'第一个调用'、'第二个调用'、'第三个调用'
异步函数调用的挑战
此实现目前运行正常,但忽略了一种情况:异步函数的调用。异步是JavaScript不可回避的话题,此处不探讨相关术语概念,请读者自行查阅(如某篇著名评注)。若知下述代码输出1、3、2,请继续:
console.log(1);
setTimeout(function() {
console.log(2);
}, 0);
console.log(3);
若stack队列中包含类似异步函数,我们的实现将陷入混乱:
var stack = [];
function fn1() { console.log('第一个调用') };
stack.push(fn1);
function fn2() {
setTimeout(function fn2Timeout() {
console.log('第二个调用');
}, 0);
}
stack.push(fn2, function() { console.log('第三个调用') });
stack.forEach(function(fn) { fn() }); // 输出'第一个调用'、'第三个调用'、'第二个调用'
问题显著:fn2确按序调用,但setTimeout内的function fn2Timeout() { console.log(‘第二个调用') }并非立即执行(即便timeout设为0);fn2调用后立即返回,接着执行fn3,待fn3执行完毕,才真正轮到fn2Timeout。
引入next回调机制
如何解决?分析可知,关键在于fn2Timeout,我们必须等待其真正执行完毕再调用fn3,理想情况大致如下:
function fn2() {
setTimeout(function() {
fn2Timeout();
fn3();
}, 0);
}
此举相当于移除原fn2Timeout,替换为新函数,并将原fn2Timeout与fn3插入。这种动态修改原函数的写法有专门名词叫Monkey Patch。按程序员口头禅:“做肯定是能做”,但编写略显别扭,且易致逻辑混乱。有无更佳方案? 退一步,不强求等fn2Timeout完全执行完才执行fn3,而在fn2Timeout函数体最后一行调用:
function fn2() {
setTimeout(function fn2Timeout() {
console.log('第二个调用');
fn3(); // 注{1}
}, 0);
}
如此看似改善,但定义fn2时尚无fn3,此fn3从何而来?
另一问题:既然fn2需调用fn3,便不能通过stack.forEach调用fn3,否则fn3将被重复调用两次。
不可将fn3硬编码于fn2内。反之,只需在fn2Timeout末尾找出stack中fn2的下一个函数,再调用:
function fn2() {
setTimeout(function fn2Timeout() {
console.log('第二个调用');
next();
}, 0);
}
此next函数负责找出stack中的下一个函数并执行。现实现next:
var index = 0;
function next() {
var fn = stack[index];
index = index + 1; // 其实也可以用shift 把fn 拿出来
if (typeof fn === 'function') fn();
}
next通过stack[index]获取stack中的函数,每调用next一次index加1,从而取出下一个函数。 next使用方式如下:
var stack = [];
// 定义index 和next
function fn1() {
console.log('第一个调用');
next(); // stack 中每一个函数都必须调用`next`
};
stack.push(fn1);
function fn2() {
setTimeout(function fn2Timeout() {
console.log('第二个调用');
next(); // 调用`next`
}, 0);
}
stack.push(fn2, function() {
console.log('第三个调用');
next(); // 最后一个可以不调用,调用也没用。
});
next(); // 调用next,最终按顺序输出'第一个调用'、'第二个调用'、'第三个调用'。
现stack.forEach一行已删除,我们自行调用一次next,next找出stack中首个函数fn1执行,fn1内调用next,找出下一个函数fn2并执行,fn2内再调用next,依此类推。 每个函数内必须调用next,若某函数未写,执行完该函数后程序将直接结束,无任何机制继续。
面试题与中间件模式应用
理解函数队列此实现后,你应能解决以下面试题:
// 实现一个LazyMan,可以按照以下方式调用:
LazyMan(“Hank”)
/* 输出:
Hi! This is Hank!
*/
LazyMan(“Hank”).sleep(10).eat(“dinner”)输出
/* 输出:
Hi! This is Hank!
// 等待10秒..
Wake up after 10
Eat dinner~
*/
LazyMan(“Hank”).eat(“dinner”).eat(“supper”)
/* 输出:
Hi This is Hank!
Eat dinner~
Eat supper~
*/
LazyMan(“Hank”).sleepFirst(5).eat(“supper”)
/* 等待5秒,输出
Wake up after 5
Hi This is Hank!
Eat supper
*/
// 以此类推。
Node.js中大名鼎鼎的connect框架正是如此实现中间件队列。有兴趣可查阅其源码或此篇解读《何为 connect 中间件》。
细心的你可能看出,此next暂只能置于函数末尾,若置于中间,原问题重现:
function fn() {
console.log(1);
next();
console.log(2); // next()如果调用了异步函数,console.log(2)就会先执行
}
redux与koa通过不同实现,允许next置于函数中间,执行完后续函数后再折返执行next下方代码,十分巧妙。有空再述。
以上为本文全部内容,望对各位学习有所助益,亦盼大家多多支持本站。
JavaScript闭包与类(Class)的区别及适用场景对比
2026-11-05 15:48:57
JavaScript入门要学ES6吗?let与箭头函数等新特性解析
2026-11-05 14:49:16
JavaScript 8种数据类型详解:基本类型与引用类型的区别
2026-11-05 14:25:15
怎么在JavaScript中使用async/await优化Ajax异步流程
2026-11-05 13:56:15
JavaScript replace回调函数实现动态正则替换逻辑
2026-11-05 13:52:13
