对JS事件循环(Event Loop)的一些个人理解

2019-01-03  本文已影响23人  xsic

2019-1-7更新

之前对async/await的理解出了一点问题,因此我也下了这篇文章后,再去摸索清楚这个ES2017语法的执行顺序,先看完这两个定义之后再往下看文章(定义来源于阮一峰老师的《ECMA script 6 入门》)

async函数返回一个 Promise 对象

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

-------------------------------------------------分割线--------------------------------------------------------------

前言

最近看了一篇讲述了JS事件循环的文章(https://www.jianshu.com/p/12b9f73c5a4f),个人觉得讲得很透彻,看了感觉收益颇多,因此mark下现在的记忆。

基础知识

执行栈:当我们调用一个方法的时候,JS会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为JS是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。

任务队列:JS引擎遇到一个异步任务后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,JS会将这个任务加入与当前执行栈不同的另一个队列,我们称之为任务队列

微任务、宏任务:任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)

按照我自己的理解,是这样的:

下面我们通过一道题目来更浅显的理解它

async function a1() {
    console.log('async1 start');
    await a2();
    console.log('async1 end')
}
async function a2(){
    console.log('async2')
}

console.log('script start')     
setTimeout(function(){
    console.log('setTimeout')
}, 0);
a1();

new Promise((resolve)=>{
    console.log('Promise1');
    resolve();
}).then(()=>{
    console.log('Promise2')
})

执行结果:

script start
async1 start
async2
Promise1
Promise2
async1 end
setTimeout
无标题.png

(图画得粗糙了点,还是强烈推荐看文章开始的那个链接,里面讲得非常详细)

开始之前

根据文章开头的两段定义:

因此await a2()是可以转化成resolve(Promise.resolve())的,也就是resolve里面还嵌套着一个resolve。
我们先把async/await转化成promise对象

async function a1() {
    console.log('async1 start');
    await a2();
    console.log('async1 end')
}
async function a2(){
    console.log('async2')
}

等于

function a1(){
    console.log('async1 start')
    new Promise(resolve=>{
        console.log('async2')  
        resolve(Promise.resolve())     <================重点!!
    }).then(()=>{
        console.log('async1 end')
    })
}

虽然我们在这里已经将async转成promise,可以不去管await了,但是我们还是可以了解一下这段话的

很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面js栈(后面会详述)的代码。

开始

1、首先从console.log('script start')开始,因为这是一个同步任务,所以直接在执行栈执行输出了script start
2、遇到一个异步任务分发器setTimeout(setTimeout本身并不是异步,只是里面的回调函数异步而已),它将这个任务分发到宏任务的setTimeout队列中。
3、往下执行遇到a1(),输出里面的同步任务async1 start,然后进入a1()里面的promise,输出async2,往下遇到resolve(Promise.resolve()),将它添加到微任务的Promise队列中,
4、跳出a1,进入下方那个Promise,输出它里面的同步内容Promise1,然后将resolve()添加进Promise队列中,

至此所有的同步任务都执行完了,让我们来看看此时的任务队列情况
1.png

5、现在开始执行微任务,当进入第一个第一个resolve的时候发现,它里面还是一个resolve,因此将它继续添加到微任务的Promise队列中(这也就是为什么async会比promise晚输出)
6、现在微任务就只剩下两个没有嵌套的resolve()了,依次执行,输出Promise2
async1 end
7、最后回到宏任务去执行setTimeout

最后

大家可以用上面的思路解决这一道题目:


function a1() {                          《=====普通函数
    console.log("执行a1");
    return "a1";
}

async function a2() {                  《=====async函数
    console.log("执行a2");
    return Promise.resolve("a2");
}

async function test() {
    console.log("test start...");
    const v1 = await a1();
    console.log(v1);
    const v2 = await a2();
    console.log(v2);
    console.log(v1, v2);
}

test();

var promise1 = new Promise((resolve)=> { console.log("promise1 start.."); resolve("promise1");});
promise1.then((val)=> console.log(val));

var promise2 = new Promise((resolve)=> { console.log("promise2 start.."); resolve("promise2");});
promise2.then((val)=> console.log(val));
console.log("test end...")

答案(node 10.14):

test start...
执行a1
promise1 start..
promise2 start..
test end...
a1
执行a2
promise1
promise2
a2
a1 a2

结语

我已经尽量将步骤分得细一些。写这篇文章最主要的目的是做个人记录,如果有哪些出错的地方,希望大家能够指出,我将不胜感激~

上一篇下一篇

猜你喜欢

热点阅读