假设你有几个函数fn1
、fn2
和fn3
须要按顺序调用,最简单的方式固然是:node
fn1(); fn2(); fn3();
但有时候这些函数是运行时一个个添加进来的,调用的时候并不知道都有些什么函数;这个时候能够预先定义一个数组,添加函数的时候把函数push 进去,须要的时候从数组中按顺序一个个取出来,依次调用:git
var stack = []; // 执行其余操做,定义fn1 stack.push(fn1); // 执行其余操做,定义fn二、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 中没法避免的一个话题,这里不打算探讨JavaScript 中有关异步的各类术语和概念,请读者自行查阅(例如某篇著名的评注)。若是你知道下面代码会输出一、三、2,那请继续往下看:github
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
。
怎么解决?咱们分析下,这里的关键在于fn2Timeout
,咱们必须等到它真正执行完才调用fn3
,理想状况下大概像这样:redux
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
会重复调用两次。less
咱们不能把fn3
写死在fn2
里。相反,咱们只须要在fn2Timeout
末尾里找出stack
中fn2
的下一个函数,再调用:koa
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
下面的代码,很是巧妙。有空再写写。