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 相关文章推荐
JS求平均值的小例子
Nov 29 Javascript
jQuery实现倒计时按钮功能代码分享
Sep 03 Javascript
html的DOM中Event对象onblur事件用法实例
Jan 21 Javascript
完善的jquery处理机制
Feb 21 Javascript
BootStrap Progressbar 实现大文件上传的进度条的实例代码
Jun 27 Javascript
详解Javascript中的原型OOP
Oct 12 Javascript
Node.js引入UIBootstrap的方法示例
May 11 Javascript
详解如何实现Element树形控件Tree在懒加载模式下的动态更新
Apr 25 Javascript
对layui数据表格动态cols(字段)动态变化详解
Oct 25 Javascript
使用vue重构资讯页面的实例代码解析
Nov 26 Javascript
JavaScript判断数据类型有几种方法及区别介绍
Sep 02 Javascript
vite+vue3.0+ts+element-plus快速搭建项目的实现
Jun 24 Vue.js
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
php实现通用的从数据库表读取数据到数组的函数实例
2015/03/21 PHP
利用PHP_XLSXWriter代替PHPExcel的方法示例
2017/07/16 PHP
php简单计算权重的方法示例【适合抽奖类应用】
2019/06/10 PHP
PHP实现文件上传后台处理脚本
2020/03/04 PHP
用js遍历 table的脚本
2008/07/23 Javascript
11个用于提高排版水平的基于jquery的文字效果插件
2012/09/14 Javascript
javascript克隆对象深度介绍
2012/11/20 Javascript
Jquery实现列表(隔行换色,全选,鼠标滑过当前行)效果实例
2013/06/09 Javascript
JavaScript包装对象使用介绍
2013/08/29 Javascript
jQuery之过滤元素操作小结
2013/11/30 Javascript
JSONP获取Twitter和Facebook文章数的具体步骤
2014/02/24 Javascript
js强制把网址设为默认首页
2015/09/29 Javascript
百度坐标(BD09)、国测局坐标(火星坐标,GCJ02)、和WGS84坐标系之间的转换
2016/02/19 Javascript
JavaScript判断变量是否为数组的方法(Array)
2016/02/24 Javascript
基于javascript显示当前时间以及倒计时功能
2016/03/18 Javascript
Javascript仿京东放大镜的效果
2017/03/01 Javascript
Vue的轮播图组件实现方法
2018/03/03 Javascript
vue数据控制视图源码解析
2018/03/28 Javascript
node中IO以及定时器优先级详解
2019/05/10 Javascript
[36:19]2018DOTA2亚洲邀请赛 小组赛 A组加赛 Newbee vs LGD
2018/04/03 DOTA
用Python实现QQ游戏大家来找茬辅助工具
2014/09/14 Python
Python2.x版本中基本的中文编码问题解决
2015/10/12 Python
Win7下搭建python开发环境图文教程(安装Python、pip、解释器)
2016/05/17 Python
Python遍历pandas数据方法总结
2018/02/09 Python
Python操作MySQL模拟银行转账
2018/03/12 Python
python的继承知识点总结
2018/12/10 Python
浅谈Python 列表字典赋值的陷阱
2019/01/20 Python
Python基本socket通信控制操作示例
2019/01/30 Python
Python第三方包之DingDingBot钉钉机器人
2020/04/09 Python
python将YUV420P文件转PNG图片格式的两种方法
2021/01/22 Python
CSS3,线性渐变(linear-gradient)的使用总结
2017/01/09 HTML / CSS
英国领先的豪华时尚家居网上商店:Amara
2019/08/12 全球购物
俄罗斯在线购买飞机票、火车票、巴士票网站:Tutu.ru
2020/03/16 全球购物
英语系本科生个人求职信
2013/09/21 职场文书
大三在校生电子商务求职信
2013/10/29 职场文书
英语辞职信范文
2015/02/28 职场文书