美文网首页
异步函数执行顺序问题

异步函数执行顺序问题

作者: Max_Law | 来源:发表于2019-06-28 11:19 被阅读0次

大家做笔试题的时候一定遇到过判断异步函数执行顺序的题目,包含setTimeout,setInterval,Promise,Async等等,来一道基础题,抛砖引玉一下:

for (var i = 1;i < 6;i ++) {
  setTimeout(function fn() {
      console.log(i)
  },i * 1000)
}

对于萌新来说,发现在控制台打印的是连续的5个6可能会大吃一惊,老鸟可能会知道正确答案,然后说这个用闭包(即IIFE)解决或者用es6的局部变量就可以解决了(当然还有其他很多方法,我们最后再总结),但具体为什么会产生这样的情况,大多数人都说不清楚。

for (var i = 1;i < 6;i ++) { // 闭包
  (function(arg){
    setTimeout(function fn() {
        console.log(arg)
    },arg * 1000)
   })(i);//调用时参数
} 
for (let i = 1;i < 6;i ++) { // es6
  setTimeout(function fn() {
      console.log(i)
  },i * 1000)
}

导致这个问题的原因还得从JavaScript的底层性质说起,总所周知JavaScript的一大特点就是单线程,那我们是如何处理多线程和异步的情况呢?利用的就是任务队列这一思想,学过node都知道的事件循环机制。

任务队列

任务队列

在JavaScript中我们拥有两个任务队列,分别为宏任务队列(macro-task)和微任务队列(micro-task),新标准中又被称为 taskjobs

宏任务 微任务
script(整体代码) process.nextTick(只在node环境中可用)
XHR回调/Async Promise.then
事件回调(鼠标键盘事件) Object.observe(已废弃)
setImmediate(只在node环境中可用) MutationObserver(html5新特性,dom变动观察器)
setTimeout -
setInterval -
I/O -
UI rendering -

process.nextTick() 指在当前"执行栈"的尾部之后,下一次Event Loop(主线程读取"任务队列")之前 ,触发process指定的回调函数。也就是说,它指定的任务总是发生在所有异步任务之前,当前主线程的末尾。(nextTick虽然也会异步执行,但是不会给其他io事件执行的任何机会)

整个事件循环是从整体代码执行为第一个宏任务开始的,然后再执行其中的微任务,完成第一次循环,再找到接下来的宏任务执行,再执行其微任务,这样一层层的循环直到执行完毕。

接下来我们分析一下上面那道问题,首先执行第一次宏任务,也就是 for 循环,执行5遍,分别得到:

setTimeout(function fn() {
      console.log(i)  // 异步里面的函数是未执行状态,所以i是现在是不确定的
  },1000);

setTimeout(function fn() {
      console.log(i)
  },2000)

... // 5个待执行函数

上诉五个待执行函数将排进宏队列中,第一遍宏队列执行完毕,由于没有微队列,我们接下来继续执行其宏队列。

一秒后,将执行第一次 console.log(i) ,根据JavaScript中作用域的定义,首先我们会在当前执行作用域寻找 i 的值,发现不存在,这时候向上寻找在全局作用域中,我们找到了 i 这时候 i 已经是6了,所以控制台将打印出6,后续相同,隔一秒打印出一个6。

现在明白了为什么会出现5个6了吧,也应该明白了,为什么能通过闭包解决这个问题,因为相当于给每个宏任务都添加上 i 的值,就不需要向上查找全局的 i 了。

拓展

我们根据上表来做做练习,看看你是否真的掌握了。

// 请判断下列代码的执行顺序
console.log("start");

// 因为setImmediate只在node环境中可用,所以这里用setTimeout(0)代替
setTimeout(function() {
  console.log("setImmediate");
}, 0);

setTimeout(function() {
  console.log("timeout1");
  new Promise(function(resolve) {
    resolve();
    console.log("timeout1_promise");
  }).then(function() {
    console.log("timeout1_then");
  });
}, 2000);

for (var i = 1; i < 6; i++) {
  setTimeout(function() {
    console.log("loop" + i);
  }, i * 1000);
  console.log(i);
}

function xhr() {
  // 用async模拟XHR
  console.log("xhr");
  return "async";
}

async function asyncFn() {
  // 用async模拟
  const tag = await xhr();
  return tag;
}

asyncFn().then(arg => console.log(arg));

new Promise(function(resolve) {
  console.log("promise1");
  reject();
}).catch(function() {
  console.log("catch1");
});

setTimeout(function() {
  console.log("timeout2");
  new Promise(function(resolve) {
    console.log("timeout2_promise");
    resolve();
  }).then(function() {
    console.log("timeout2_then");
  });
}, 1000);

先自己默默地做一下,再对比控制台中的输出,错了不要紧,我们一步步来看:

第一次事件循环

进入宏任务队列 调用栈执行 进入微任务队列
- console.log("start") -
setImmediate - -
timeout1 - -
setTimeout(i) * 6 - -
- console.log(1) ... console.log(5) -
- console.log("xhr") async.then()
- console.log("promise1") promise1.catch
timeout2 - -
等待第二次事件循环开始 执行微任 微任务队列开始
- asyncFn().then()挂起 -
- console.log("catch1") -
- console.log(async) -

这时候控制台将输出:start1,2,3,4,5,xhr,promise1,catch1,async
解释一下,async为Generator 函数的语法糖(es6新特性),它是一个状态机,封装了多个内部状态,与一般的异步不太一样,虽然都是同时执行微任务,但它会有一个挂起,再.next()的过程,会延迟一点。

第二次事件循环

进入宏任务队列 调用栈执行 进入微任务队列
- console.log("setImmediate") -
- console.log("loop6") -
- console.log("timeout2") -
- console.log("timeout2_promise") timeout2.then()
- console.log("timeout2_then") -
- console.log("timeout1") -
- console.log("timeout1_promise") timeout1.then()
- console.log("timeout1_then") -
- console.log("loop6")*4 -

这时候控制台将输出:setImmediateloop6,timeout2,timeout2_promise,timeout2_then,timeout1,timeout1_promise,timeout1_then,loop6,loop6,loop6,loop6

附:

下面再给出一开始for循环i问题的其他方法

// 利用setTimeout的第三个参数
for (var i=1; i<6; i++) {
  setTimeout( function fn(i) {
    console.log(i);    
   }, i*1000,i );
}
// 利用bind函数
for (var i=1; i<6; i++) {
  setTimeout( function fn(i) {
    console.log(i);
  }.bind(null,i), i*1000 );
}

相关文章

  • 异步函数执行顺序问题

    大家做笔试题的时候一定遇到过判断异步函数执行顺序的题目,包含setTimeout,setInterval,Prom...

  • ECMAScript6--15.Promise用法

    1.Promise(解决异步操作问题的) 什么是异步比如:函数a,执行一个步骤,a执行完要执行b;有一个顺序问题;...

  • 通过JavaScript回调函数:实现任务流程控制

    回调函数和异步并没有什么关系,不过回调函数里面使用异步API,即可触发js异步机制,从而达到控制程序执行顺序的目的。

  • 顺序执行异步函数

    好久没写东西了,感觉自己不是那么的忙,却又没有的时间来,写自己的小心得,今天分享一下,关于顺序执行异步函数的方法,...

  • js 异步执行顺序

    js的执行顺序,先同步后异步异步中任务队列的执行顺序: 先微任务microtask队列,再宏任务macrotask...

  • js 异步执行顺序

    某天中午午饭前,11:48分,钉钉突然闪动了起来。消息如下: 代码如下: 那么打印顺序是咋样的呢?我们来好好分析一...

  • 事件循环机制

    JS 有个全局的函数执行栈,这是执行同步函数的地方 除了函数执行栈,还有一个事件队列,这是执行异步函数的地方 异步...

  • Javascript语法中的重难点

    0. 前言 记录js使用过程中遇到的重难点。 1. 如何解决异步问题 js中函数调用是异步的,即不等待函数执行结束...

  • 回调地域、Promise

    回调函数 异步API无法保证执行顺序,要想按照你的顺序来,需要设计函数嵌套 比如:想要达到a执行完才能执行b,b执...

  • ES6(Promise)

    Promise是异步编程的解决方案。举个例子,有一个函数A,A执行一些步骤,A执行完要执行函数B,这里有个顺序问题...

网友评论

      本文标题: 异步函数执行顺序问题

      本文链接:https://www.haomeiwen.com/subject/stsccctx.html