微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

JavaScript 中如何实现函数队列?(一)

2017-02-23 20:18 前端大全

(点击上方公众号,可快速关注)


作者:lijsh

segmentfault.com/a/1190000008320677

如有好文章投稿,请点击 → 这里了解详情


假设你有几个函数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 中无法避免的一个话题,这里不打算探讨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。

怎么解决?我们分析下,这里的关键在于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 框架和库 移动端 h5开发相关内容总结——CSS篇 趣图:调 CSS 的真实写照 趣图:调 CSS 的真实写照 promises 很酷,但很多人并没有理解就在用了
猜您喜欢 一个很棒的 Android APP框架 iOS 面试题(五):weak 的内部实现原理 案例丨中国移动一级业务支撑系统网状网PaaS之路 Spark Streaming Crash 如何保证Exactly Once Semantics 1000赞的程序员高薪职业建议