美文网首页
对JS事件循环(Event Loop)的一些个人理解

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

作者: xsic | 来源:发表于2019-01-03 03:18 被阅读23次

    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新特性)

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

    • 代码从宏任务(script整体代码)开始执行
    • 如果遇到同步操作,则直接执行;
    • 如果遇到异步操作,则将它里面的异步任务分发到对应的任务队列中(可以有多个队列,例如说:promise队列、settimeout队列、process队列等)。
    • 接着继续执行直到 执行栈 为空
    • 紧接着开始执行微任务中的任务队列(Promise队列、async/await队列等)
    • 直到将微任务中所有的任务队列全部执行完毕
    • 然后开始又返回去执行宏任务中的任务队列,就这样形成一个环状循环

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

    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

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

    开始之前

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

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

    因此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
    

    结语

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

    相关文章

      网友评论

          本文标题:对JS事件循环(Event Loop)的一些个人理解

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