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

编辑注:在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下面的代码,非常巧妙。有空再写写。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 浅析javascript异步执行函数导致的变量变化问题解决思路

    浅析javascript异步执行函数导致的变量变化问题解决思路 for(var i=0;i<3;i++) { setTimeout(function(){ console.log(i) },0); } 控制台输出: 3 3 3 这是因为执行方法的时候for循环已经执行完成每次执行的时候取得都是3 而不是1-2-3这时我们可以使用立即执行函数为每一次循环创建一个变量副本来供定时器调用解决这个问题 for (var i = 0; i < 3; i++) { setTimeout( (functio

  • 自己封装的javascript事件队列函数版

    背景 javascript中使用addEventListener()或attachEvent()绑定事件时会有几个小问题: 一.使用addEventListener()或attachEvent()添加的匿名函数无法移除. 复制代码 代码如下: var oBtn = document.getElementById('btn'); oBtn.addEventListener('click',function(){    alert('button is clicked')},false) oBtn.

  • 浅谈js的异步执行

    1.Javascript语言的执行环境是"单线程"(single thread): 优点:实现起来比较简单,执行环境相对单纯: 缺点:只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行.常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行. 为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous). 2.&

  • 浅谈js文件引用方式及其同步执行与异步执行

    任何以appendChild(scriptNode) 的方式引入的js文件都是异步执行的 (scriptNode 需要插入document中,只创建节点和设置 src 是不会加载 js 文件的,这跟 img 的与加载不同 ) html文件中的<script>标签中的代码或src引用的js文件中的代码是同步加载和执行的 html文件中的<script>标签中的代码使用document.write()方式引入的js文件是异步执行的 html文件中的<script>标签src

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

    编辑注:在Review别人的JavaScript代码时曾看到过类似的队列函数,不太理解,原来这个是为了保证函数按顺序调用.读了这篇文章之后,发现还可以用在异步执行等. 假设你有几个函数fn1.fn2和fn3需要按顺序调用,最简单的方式当然是: fn1(); fn2(); fn3(); 但有时候这些函数是运行时一个个添加进来的,调用的时候并不知道都有些什么函数:这个时候可以预先定义一个数组,添加函数的时候把函数push 进去,需要的时候从数组中按顺序一个个取出来,依次调用: var stack =

  • JavaScript箭头函数中的this详解

    前言 箭头函数极大地简化了this的取值规则. 普通函数与箭头函数 普通函数指的是用function定义的函数: var hello = function () { console.log("Hello, Fundebug!"); } 箭头函数指的是用=>定义的函数: var hello = () => { console.log("Hello, Fundebug!"); } JavaScript箭头函数与普通函数不只是写法上的区别,它们还有一些微妙的不

  • javaScript的函数对象的声明详解

    写作缘由: 平时再用js写函数的时候,一般都是以惯例 function fn () {} 的方式来声明一个函数,在阅读一些优秀插件的时候又不免见到 var fn = function () {} 这种函数的创建,究竟他们用起来有什么区别呢,今天就本着打破砂锅问到底的精神,好好来说说这个让人神魂颠倒的--函数声明. 函数声明 函数声明示例代码 复制代码 代码如下: function fn () {     console.log('fn 函数执行..');     // code.. } 这样我们

  • Javascript异步流程控制之串行执行详解

    这篇文章主要讲一下如何串行执行一组异步任务,例如有下面几个任务,在这里我们用setTimeout模拟一个异步任务: let taskA = () => setTimeout(() => console.log('run task A'), 100); let taskB = () => setTimeout(() => console.log('run task B'), 50); let taskC = () => setTimeout(() => console.l

  • Python 异步协程函数原理及实例详解

    这篇文章主要介绍了Python 异步协程函数原理及实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一. asyncio 1.python3.4开始引入标准库之中,内置对异步io的支持 2.asyncio本身是一个消息循环 3.步骤: (1)创建消息循环 (2)把协程导入 (3)关闭 4.举例: import threading # 引入异步io包 import asyncio # 使用协程 @ asyncio.coroutine def

  • JavaScript函数封装的示例详解

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,

  • JavaScript箭头函数与普通函数的区别示例详解

    目录 箭头函数与普通函数的区别 箭头函数的理解 箭头函数里的this指向 总结 箭头函数与普通函数的区别 要讨论箭头函数和普通函数的区别,首先来看看两者的基本格式 普通函数和箭头共同点就是圆括号和大括号,圆括号里面一般放置参数,大括号一般放置函数主体,很明显箭头函数不需要写那么长,举个例子,有一个数组,使用map方法为数组的每个元素增加字符 let arr=['昨天','今天','明天'] let newarr=arr.map(function(item){ return item+='放假'

  • JavaScript设计模式之职责链模式详解

    目录 职责链模式 1. 现实中的职责链模式 2. 实际开发中的职责链模式 3. 用职责链模式重构代码 4. 灵活可拆分的职责链节点 5. 异步的职责链 6. 职责链模式的优缺点 7. 用 AOP 实现职责链 8. 小结 职责链模式 职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止. 职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇

  • JavaScript浏览器对象之一Window对象详解

    JavaScript提供了一组以window为核心的对象,实现了对浏览器窗口的访问控制.JavaScript中定义了6种重要的对象: window对象 表示浏览器中打开的窗口: document对象 表示浏览器中加载页面的文档对象: location对象包含了浏览器当前的URL信息: navigation对象 包含了浏览器本身的信息: screen对象 包含了客户端屏幕及渲染能力的信息: history对象 包含了浏览器访问网页的历史信息. 除了window对象之外,其他的5个对象都是windo

  • Javascript类型系统之String字符串类型详解

    javascript没有表示单个字符的字符型,只有字符串String类型,字符型相当于仅包含一个字符的字符串 字符串String是javascript基本数据类型,同时javascript也支持String对象,它是一个原始值的包装对象.在需要时,javascript会自动在原始形式和对象形式之间转换.本文将介绍字符串String原始类型及String包装对象 定义 字符串String类型是由引号括起来的一组由16位Unicode字符组成的字符序列 字符串类型常被用于表示文本数据,此时字符串中的

随机推荐