理解 JavaScript 的事件循环

作者: _月光临海 | 来源:发表于2018-04-09 23:12 被阅读30次
    随便看技术文章时发现关于事件循环(Event Loop)方面的知识还是很混乱,于是多方查阅了相关资料,在此做下笔记,如若理解有误还望各位看官指出

    首先我们需要了解几个名词:Event Loop执行栈任务队列宏任务(macro)微任务(micro)

    • Event Loop你可以近似的理解为:<script> 标签内的代码自上而下执行一遍为一个循环 Event Loop。异步任务会被推到下一个 Event Loop 去执行。
    • 执行栈:当前执行 Event Loop 的任务顺序
    • 任务队列:全部 Event Loop 的任务执行顺序,包括宏任务队列和微任务队列
    • 宏任务:当前 Event Loop 中按顺序执行的任务,可以有多个宏任务队列
    • 微任务:当前 Event Loop 中所有宏任务执行之后再执行,只能有一个微任务队列

    下面是一个 Event Loop 执行过程的概述,这里暂不考虑定时器的延时操作:
    1. 开始执行 script 中代码,即进入第一轮 Event Loop
    2. 遇到同步的 macro_1,直接执行,比如单独的打印语句 console.log()
    3. 遇到 micro_1,推到本轮 Event Loop 末尾,比如 Promise 的 then 中的任务
    4. 遇到异步 macro_2,推到第二轮 Event Loop
    5. 遇到异步 micro_2,推到第二轮 Event Loop ,macro_2 之后
    6. 遇到同步的 macro_3,直接执行
    7. 遇到 micro_3,推到本轮 Event Loop 末尾,在 micro_1 之后
    8. 遇到异步 macro_4,推到第三轮 Event Loop

    这么说看着有些晕,我们不妨举个栗子:

    new Promise(function(resolve) {
        console.error("macro_1");    // 第一轮宏任务 1
        resolve()
    }).then(function() {
        console.log("micro_1");      // 第一轮微任务 1
    })
    
    setTimeout(function() {
        new Promise(function(resolve) {
            console.error("macro_2");// 第二轮宏任务
            resolve()
        }).then(function() {
            console.log("micro_2");  // 第二轮微任务
        })
    })
    
    new Promise(function(resolve) {
        console.error("macro_3");    // 第一轮宏任务 2
        resolve()
    }).then(function() {
        console.log("micro_3");      // 第一轮微任务 2
    })
    
    setTimeout(function() {
        new Promise(function(resolve) {
            console.error("macro_4");  // 第三轮宏任务
            resolve()
        }).then(function() {
            console.log("micro_4");    // 第三轮微任务
        })
    })
    

    结果:


    结果

    上面的例子执行过程就与之前阐述的基本一致:

    • 第一轮:宏1 - 宏2 - 微1 - 微2
    • 第二轮:宏 - 微
    • 第三轮:宏 - 微

    有了上面的基础,我们来看一个大一点的栗子:
    console.error('macro-start')
    
    setTimeout(function() {
        console.log('timeout1')
        new Promise(function(resolve) {
            console.log('timeout1_promise')
            resolve()
        }).then(function() {
            console.log('timeout1_then')
        })
    }, 2000)
    
    for(let i = 0; i <= 5; i++) {
        setTimeout(function() {
            console.log("inner_" + i)
        }, i * 1000)
        console.log(i)
    }
    
    new Promise(function(resolve) {
        console.log('promise1')
        resolve()
    }).then(function() {
        console.warn("micro-start")
        const delay = +new Date();
        while(+new Date() - delay < 1000);
        console.log('then1')
    })
    
    setTimeout(function() {
        console.log('timeout2')
        new Promise(function(resolve) {
            console.log('timeout2_promise1')
            resolve()
        }).then(function() {
            console.log('timeout2_then1')
        })
        new Promise(function(resolve) {
            console.log('timeout2_promise2')
            resolve()
        }).then(function() {
            console.log('timeout2_then2')
        })
    }, 1000)
    
    new Promise(function(resolve) {
        console.log('promise2')
        resolve()
    }).then(function() {
        console.log('then2')
        console.warn("micro-end")
    })
    
    console.error("macro-end")
    
    让我们一步步的分析:

    开始 Event Loop:

    第一个打印:没啥说的,直接打印
    console.error('macro-start')
    
    • 接着是个 2ssetTimeout,异步操作,2s 后推入任务队列,当前事件循环没完事,先不管
    • for 循环,包括了 setTimeoutconsole.log() 两个语句,setTimeout 没有设置时间参数,直接推入任务队列,别的先不管,因此
    第二个打印:for 循环中定时器外的 console.log()
     0,1,2,3,4,5
    
    • 再往下读,遇到一个 Promise,宏任务,直接打印
    第三个打印:Promise 中的 console.log()
    console.log('promise1')
    
    • Promise 后有个 then,微任务,先不管
    • 接着又是一个 1ssetTimeout1s 后推入任务队列,同样不管
    • 然后是 Promise
    第四个打印:Promise 中的 console.log()
    console.log('promise2')
    
    • 之后的 then 同样道理,微任务,Pass,往后看
    第五个打印:全局的 console.error()
    console.error("macro-end")
    
    • 至此,本次 Event Loop 的所有 宏任务 已经执行完毕,正如之前所说,微任务是同一轮宏任务执行完之后才开始执行,而本轮共有两个微任务,先看第一个 then
    第六个打印:then 中的 console.warn()
    console.warn("micro-start")
    
    • 之后是一个阻塞 1s 语句,即 1s 后进行后面的操作:
    第七个打印:while 阻塞语句之后的 console.log()
    # 1s later
    console.log('then1')
    
    • 然后是 Event Loop 的第二个微任务 then
    第八个打印:then 中的 console.log()
    console.log('then2')
    
    第九个打印:then 中的 console.warn()
    console.warn("micro-end")
    
    • 此时,Event Loop 已全部执行完毕,将开启新一轮 Event Loop,回到顶部再来一次。
    • 从头再读,先是那个 2ssetTimeout,在第一轮读到这个语句时已经开始计时,2s 后将任务推入任务队列,而第一轮中有个 while1s 阻塞,实际上再过 1s 就应该执行其中的语句了,先记着这个,我们继续往下看
    • 又回到了 for 循环,这其中有个时间参数为 0setTimeout,就是第一轮时直接推入任务队列,但由于当时第一轮的宏任务和微任务还没有执行完,因此一直被卡着。现在第一轮任务已经执行完了,因此
    第十个打印:for 中时间参数为 0 的 setTimeout
    console.log("inner_" + i)  //  console.log("inner_0")
    
    • for 循环中还有个时间参数为 1ssetTimeout,注意,正如上面所说,setTimeout 是执行时就开始计时,1s 后推入任务队列。而第一轮的 while 阻塞了 1s,因此这里将不再等待 1s 才打印,而是直接打印。
    第十一个打印:for 中时间参数为 1s 的 setTimeout
    console.log("inner_" + i)  //  console.log("inner_1")
    
    • 接着是 1ssetTimeout
    第十二个打印,1s 的问题跟上面的情况一样,因此不再赘述
    console.log('timeout2')
    
    • 其实你可以将 1s 的这个 setTimeout 内的部分但拿出来看,这样层次清晰一些:
    console.log('timeout2')
    new Promise(function(resolve) {
        console.log('timeout2_promise1')
        resolve()
    }).then(function() {
        console.log('timeout2_then1')
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise2')
        resolve()
    }).then(function() {
        console.log('timeout2_then2')
    })
    
    • 参考第三、四、七、八四个打印可知,这里应该是:
    第十三个打印:
    console.log('timeout2_promise1')
    
    第十四个打印:
    console.log('timeout2_promise2')
    
    第十五个打印:
    console.log('timeout2_then1')
    
    第十六个打印:
    console.log('timeout2_then2')
    
    • 之后没有立即打印的结果出来,记得刚才那个 2ssetTimeout 吗?到它了。因为第一轮时 setTimeout 已经开始计时,而 while 已经阻塞了一秒,因此这里只需要再等 1s 就会执行。
    第十七个打印:
    # 1s later
    console.log('timeout1')
    
    • 然后继续
    第十八个打印:
    console.log('timeout1_promise')
    
    第十九个打印:
    console.log('timeout1_then')
    
    • 之后又到了 for 循环,打印时间参数为 2ssetTimeout 中的内容
    第二十个打印:
    console.log("inner_" + i) //console.log("inner_2")
    
    • 接下来就只剩下 for 循环中的时间参数为 3,4,5 的三个 setTimeout 了,因此最后的打印为
    第二十一个打印:
    # 1s later
    console.log("inner_" + i) //console.log("inner_3")
    # 1s later
    console.log("inner_" + i) //console.log("inner_4")
    # 1s later
    console.log("inner_" + i) //console.log("inner_5")
    

    整理一下,完整的打印顺序如下
    macro-start
    0
    1
    2
    3
    4
    5
    promise1
    promise2
    macro-end
    micro-start
    -------------- 1s later --------------
    then1
    then2
    micro-end
    inner_0
    inner_1
    timeout2
    timeout2_promise1
    timeout2_promise2
    timeout2_then1
    timeout2_then2
    -------------- 1s later --------------
    timeout1
    timeout1_promise
    timeout1_then
    inner_2
    -------------- 1s later --------------
    inner_3
    -------------- 1s later --------------
    inner_4
    -------------- 1s later --------------
    inner_5
    
    您还可以尝试调整 while 阻塞语句的位置及阻塞时间,以便加深理解

    相关文章

      网友评论

        本文标题:理解 JavaScript 的事件循环

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