资讯专栏INFORMATION COLUMN

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

Kyxy / 1013人阅读

摘要:相反,我们只需要在末尾里找出中的下一个函数,再调用第二个调用这个函数负责找出中的下一个函数并执行。我们现在来实现其实也可以用把拿出来通过去获取中的函数,每调用一次会加,从而达到取出下一个函数的目的。中大名鼎鼎的框架正是这样实现中间件队列的。

假设你有几个函数fn1fn2fn3需要按顺序调用,最简单的方式当然是:

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调用之后马上返回,接着执行fn3fn3执行完了然才真正轮到fn2Timeout
怎么解决?我们分析下,这里的关键在于fn2Timeout,我们必须等到它真正执行完才调用fn3,理想情况下大概像这样:

function fn2() {
    setTimeout(function() {
        fn2Timeout();
        fn3();
    }, 0);
}

但这样做相当于把原来的fn2Timeout整个拿掉换成一个新函数,再把原来的fn2Timeoutfn3插进去。这种动态改掉原函数的写法有个专门的名词叫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末尾里找出stackfn2的下一个函数,再调用:

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一行已经删掉了,我们自行调用一次nextnext会找出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下面的代码,非常巧妙。有空再写写。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81491.html

相关文章

  • 谈谈我对js定时器的点理解

    摘要:这两个函数接受定时器的例如我们上面提到的两个函数产生的定时器,并停止对定时器中指定函数的调用。注意,定时器虽然触发了,但是并不会立即执行,它只是把需要延迟执行的函数加入了执行队列,在线程的某一个可用的时间点,这个函数就能够得到执行。 撸了今年阿里、头条和美团的面试,我有一个重要发现....... javascript定时器工作原理是一个重要的基础知识点。因为定时器在单线程中工作,它们表...

    frontoldman 评论0 收藏0
  • 事件循环与任务队列

    摘要:需要注意的是,定时器比较特殊,并没有把回调函数挂在事件循环队列中,它所做的就是设置一个定时器,当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的会被取出执行。 Author: bugall Wechat: bugallF Email: 769088641@qq.com Github: https://github.com/bugall 一...

    SQC 评论0 收藏0
  • JavaScript引擎是如何工作的?从调用栈到Promise你需要知道的

    摘要:最受欢迎的引擎是,在和中使用,用于,以及所使用的。单线程的我们说是单线程的,因为有一个调用栈处理我们的函数。也就是说,如果有其他函数等待执行,函数是不能离开调用栈的。每个异步函数在被送入调用栈之前必须通过回调队列。 翻译:疯狂的技术宅原文:https://www.valentinog.com/bl... 本文首发微信公众号:前端先锋欢迎关注,每天都给你推送新鲜的前端技术文章 sh...

    Simon_Zhou 评论0 收藏0
  • JS 异步的实现

    摘要:由于引擎同一时间只执行一段代码这是由单线程的性质决定的,所以每个代码块阻塞了其它异步事件的进行。这意味着浏览器将等待着一个新的异步事件发生。异步的任务执行的顺序是不固定的,主要看返回的速度。 我们经常说JS是单线程的,比如node.js研讨会上大家都说JS的特色之一是单线程的,这样使JS更简单明了,可是大家真的理解所谓JS的单线程机制吗?单线程时,基于事件的异步机制又该当如何,这些知识...

    sihai 评论0 收藏0
  • 总结javascript基础概念(二):事件队列循环

    摘要:而事件循环是主线程中执行栈里的代码执行完毕之后,才开始执行的。由此产生的异步事件执行会作为任务队列挂在当前循环的末尾执行。在下,观察者基于监听事件的完成情况在下基于多线程创建。 主要问题: 1、JS引擎是单线程,如何完成事件循环的? 2、定时器函数为什么计时不准确? 3、回调与异步,有什么联系和不同? 4、ES6的事件循环有什么变化?Node中呢? 5、异步控制有什么难点?有什么解决方...

    zhkai 评论0 收藏0

发表评论

0条评论

Kyxy

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<