JavaScript队列函数和异步执行详解


Posted in Javascript onJune 19, 2017

编辑注:在Review别人的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 中无法避免的一个话题,这里不打算探讨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 相关文章推荐
IE 当eval遇上function的处理
Aug 09 Javascript
javascript中不提供sleep功能如何实现这个功能
May 27 Javascript
JS Array.slice 截取数组的实现方法
Jan 02 Javascript
angularjs在ng-repeat中使用ng-model遇到的问题
Jan 21 Javascript
js防阻塞加载的实现方法
Sep 09 Javascript
JavaScript Date 知识浅析
Jan 29 Javascript
Vue服务端渲染和Vue浏览器端渲染的性能对比(实例PK )
Mar 31 Javascript
vue.js声明式渲染和条件与循环基础知识
Jul 31 Javascript
js定时器实现倒计时效果
Nov 05 Javascript
vuejs点击class变化的实例
Sep 05 Javascript
javascript动态创建对象的属性详解
Nov 07 Javascript
深入了解JS之作用域和闭包
Jun 16 Javascript
JS检测是否可以访问公网服务器功能代码
Jun 19 #Javascript
详解angularJS动态生成的页面中ng-click无效解决办法
Jun 19 #Javascript
详解AngularJS脏检查机制及$timeout的妙用
Jun 19 #Javascript
深入理解AngularJs-scope的脏检查(一)
Jun 19 #Javascript
jQuery 实现双击编辑表格功能
Jun 19 #jQuery
Web制作验证码功能实例代码
Jun 19 #Javascript
angularjs+bootstrap实现自定义分页的实例代码
Jun 19 #Javascript
You might like
一次编写,随处运行
2006/10/09 PHP
php 方便水印和缩略图的图形类
2009/05/21 PHP
解析PHP留言本模块主要功能的函数说明(代码可实现)
2013/06/25 PHP
php判断文件夹是否存在不存在则创建
2015/04/09 PHP
CSS中简写属性要注意TRouBLe的顺序问题(避免踩坑)
2021/03/09 HTML / CSS
javascript针对DOM的应用分析(四)
2012/04/15 Javascript
innerText和textContent对比及使用介绍
2013/02/27 Javascript
jQuery拖拽插件gridster使用指南
2015/04/21 Javascript
JQuery实现动态添加删除评论的方法
2015/05/18 Javascript
浅谈Javascript线程及定时机制
2015/07/02 Javascript
javascript先序遍历DOM树的方法
2016/02/27 Javascript
JS实现关闭当前页而不弹出提示框的方法
2016/06/22 Javascript
vue.js国际化 vue-i18n插件的使用详解
2017/07/07 Javascript
ES6扩展运算符用法实例分析
2017/10/31 Javascript
使用nvm管理不同版本的node与npm的方法
2017/10/31 Javascript
15 分钟掌握vue-next响应式原理
2019/10/13 Javascript
[46:40]VGJ.T vs Winstrike 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/20 DOTA
[01:04:09]DOTA2-DPC中国联赛 正赛 iG vs VG BO3 第二场 2月2日
2021/03/11 DOTA
python列表list保留顺序去重的实例
2018/12/14 Python
Python运行DLL文件的方法
2020/01/17 Python
Pytorch高阶OP操作where,gather原理
2020/04/30 Python
Django 解决阿里云部署同步数据库报错的问题
2020/05/14 Python
如何用python爬取微博热搜数据并保存
2021/02/20 Python
CSS3 animation ? steps 函数详解
2019/08/30 HTML / CSS
详解HTML5将footer置于页面最底部的方法(CSS+JS)
2018/10/11 HTML / CSS
Clarks英国官方网站:全球领军鞋履品牌
2016/11/26 全球购物
Ray-Ban雷朋西班牙官网:全球领先的太阳眼镜品牌
2018/11/28 全球购物
Java中的类包括什么内容?设计时要注意哪些方面
2012/05/23 面试题
行政总监岗位职责
2013/12/05 职场文书
国培远程培训感言
2014/03/08 职场文书
演讲稿格式
2014/04/30 职场文书
护士医德医风自我评价
2014/09/15 职场文书
2015年教育实习工作总结
2015/04/24 职场文书
第一节英语课开场白
2015/06/01 职场文书
python实现批量提取指定文件夹下同类型文件
2021/04/05 Python
springboot用户数据修改的详细实现
2022/04/06 Java/Android