浅析JavaScript的事件循环机制

本文为我的看法,若是发现文章有错误的地方,欢迎你们指正,感谢感谢~~前端

转载请标明出处node

本文针对于Chrome浏览器环境下的事件循环机制,node环境下尚未进行试验,之后再试验下~~

前言

众所周知,JavaScript的一大特色就是单线程,也就是会按顺序执行代码,同一时间只能作一件事。web

为何JavaScript会被设计成单线程?

JavaScript的诞生,一开始是为了解决浏览器用户交互的问题,以及用来操做DOM,基于这个缘由,JavaScript被设计成单线程,不然会带来复杂的同步问题。ajax

为何JavaScript须要异步?

单线程意味着全部任务都要排队进行,若是存在一个任务执行时间过长,后面的任务都会被阻塞,对于用户而言就意味着“卡死”。vim

单线程的JavaScript是怎么执行异步代码的呢?api

这就涉及到JavaScript的事件循环机制(event loop)了。promise

事件循环机制(event loop)

这里先推荐去看看Philip Roberts的演讲《Help, I’m stuck in an event-loop》,虽然内容没有涉及到任务队列的细分,可是对函数调用栈(call stack)的分析仍是挺不错的浏览器

列举几个概念:执行上下文函数调用栈(call stack), 任务队列(task queue)bash

  • 执行上下文(之后有空应该会再写一篇文章分析一下哈哈):
    • 全局环境:JavaScript代码运行起来会首先进入该环境
    • 函数环境:当函数被调用执行时,会进入当前函数中执行代码
    • eval(不建议使用,可忽略)
  • 函数调用栈(call stack)是决定了js代码的运行机制,遇到函数时,会生成一个新的函数上下文,而且入栈,执行完毕后出栈
  • JavaScript中的任务分为macro-task(宏任务)与micro-task(微任务)
  • macro-task包括:script(一段代码),setTimeout,setInterval,setImmediate,requestAnimationFrame,I/O,UI rendering
  • micro-task包括:process.nextTick, Promise, Object.observe, MutationObserver

  1. 当JavaScript代码开始执行时,首先将全局环境压入函数调用栈(栈底永远都是全局上下文,除非线程结束,在浏览器上表现为窗口关闭),以后,每遇到一个函数,建立一个新的函数上下文,而且入栈。异步

  2. 执行过程当中,遇到了macro-task或者micro-task,都会将其交给对应的web api去处理,好比setTimeout交给timer模块,ajax请求交给network模块,DOM操做交给DOM对应模块处理,处理完成后,会将对应的回调函数放入对应的队列中(macro-task队列以及micro-task队列)

  3. 每当函数调用栈中的上下文都执行完毕时(全局环境仍然存在),主进程会去查询micro-task队列,若是micro-task队列为空,会取macro-task队列第一个task放入调用栈执行,不然,取micro-task队列的第一个task放入调用栈执行,若是在处理task期间,若是有新添加的microtasks或者macro-task,也会被添加到相应队列的末尾

  4. 一直循环第3步,直至全部任务执行完毕,这就是事件循环

按照个人思路大概画了个流程图


来实践一下,想象如下代码片断的控制台输出

console.log('start')

setTimeout(function setTimeout1() {
    console.log('setTimeout1')
    setTimeout(function setTimeout3() {
        console.log('setTimeout3')
        new Promise(function promise4(resolve, reject) {
            console.log('promise4')
            resolve('then')
        }).then(function then4() {
            console.log('promise4 then')
        })
    }, 0)

    new Promise(function promise3(resolve, reject) {
        console.log('promise3')
        setTimeout(function setTimeout4() {
            resolve('then')
        }, 0)
        console.log('after resolve')
    }).then(function then3() {
        console.log('promise3 then')
    })

}, 0)

new Promise(function promise1(resolve, reject) {
    console.log('promise1')
    resolve('then')
}).then(function then1() {
    console.log('promise1 then')
    new Promise(function promise2(resolve, reject) {
        console.log('promise2')
        resolve('then')
    }).then(function then2() {
        console.log('promise2 then')
    })
})

setTimeout(function setTimeout2() {
    console.log('setTimeout2')
}, 0)



console.log('end')


/* 
控制台输出
start
promise1
end
promise1 then
promise2
promise2 then
setTimeout1
promise3
after resolve
setTimeout2
setTimeout3
promise3 then
*/

复制代码

细化步骤还挺多的,因此作了个gif~~

第一步

全局上下文global进栈

全局上下文global进栈

第二步

console.log('start')
复制代码

遇到console.log,函数进栈,调用web api的console接口,运行完成后出栈

第三步

setTimeout(function setTimeout1() {
 //....
}, 0)
复制代码

遇到setTimeout,交给timer模块执行,setTimeout出栈,timer执行完该定时器后(0秒后),将回调函数setTimeout1放入macro-task队尾。划重点!!这是一个很容易产生误解的地方,不少同窗下意识都以为定时器就是到了设定时间后当即执行,实际上是到了时间后,将回调函数放入macro-task队列,等待执行

第四步

new Promise(function promise1(resolve, reject) {
    console.log('promise1')
    resolve('then')
}).then(function then1() {
    //...
})
复制代码

遇到promise,构造函数里的promise1会马上进栈而且执行,执行中遇到了resolve函数,进栈,将回调函数then1放入micro-task队列,此时promise1resolve都已执行完毕,出栈

第五步

setTimeout(function setTimeout2() {
    //...
}, 0)
复制代码

遇到setTimeout,交给timer模块执行,setTimeout出栈,timer执行完该定时器后(0秒后),将回调函数setTimeout2放入macro-task队尾。

第六步

console.log('end')
复制代码

第七步

到了很关键的一步,这个时候call stack已经执行完了(只剩下global),主进程会去查询micro-task队列,发现里面有等待执行的函数,取队首的函数(也就是then1)进栈执行

function then1() {
    console.log('promise1 then')
    new Promise(function promise2(resolve, reject) {
        console.log('promise2')
        resolve('then')
    }).then(function then2() {
        //...
    })
}
复制代码

在执行过程当中,又遇到了promise,先执行构造函数里的promise2,执行中遇到了resolve函数,进栈,将回调函数then2放入micro-task队列,此时then1promise2resolve都已执行完毕,出栈

第八步

call stack执行完毕,查询micro-task队列,发现里面有等待执行的函数,取队首的函数(也就是then2)进栈执行

function then2() {
    console.log('promise2 then')
}
复制代码

第九步

call stack执行完毕,查询micro-task队列,发现为空,查询macro-task队列,发现里面有等待执行的函数,取队首的函数(也就是setTimeout1)进栈执行

function setTimeout1() {
    console.log('setTimeout1')
    setTimeout(function setTimeout3() {
      //...
    }, 0)

    new Promise(function promise3(resolve, reject) {
        console.log('promise3')
        setTimeout(function setTimeout4() {
            //...
        }, 0)
        console.log('after resolve')
    }).then(function then3() {
        //...
    })
}
复制代码

执行中遇到setTimeout,交给timer模块执行,setTimeout出栈,timer执行完该定时器后(0秒后),将回调函数setTimeout3放入macro-task队尾。 继续执行,遇到了promise,先执行构造函数里的promise3,又遇到了setTimeout,交给timer模块执行,setTimeout出栈,timer执行完该定时器后(0秒后),将回调函数setTimeout4放入macro-task队尾,此时setTimeout1promise3 都已执行完毕,出栈

第十步

call stack执行完毕,查询micro-task队列,发现为空,查询macro-task队列,发现里面有等待执行的函数,取队首的函数(也就是setTimeout2)进栈执行

function setTimeout2() {
    console.log('setTimeout2')
}
复制代码

第十步

call stack执行完毕,查询micro-task队列,发现为空,查询macro-task队列,发现里面有等待执行的函数,取队首的函数(也就是setTimeout3)进栈执行

function setTimeout3() {
    console.log('setTimeout3')
    new Promise(function promise4(resolve, reject) {
        console.log('promise4')
        resolve('then')
    }).then(function then4() {
        console.log('promise4 then')
    })
}
复制代码

执行中遇到了promise,先执行构造函数里的promise4,遇到了resolve函数,进栈,将回调函数then2放入micro-task队列,此时setTimeout3promise4都已执行完毕,出栈

第十一步

call stack执行完毕,查询micro-task队列,发现里面有等待执行的函数,取队首的函数(也就是then4)进栈执行

function then4() {
    console.log('promise4 then')
}
复制代码

第十二步

call stack执行完毕,查询micro-task队列,发现为空,查询macro-task队列,发现里面有等待执行的函数,取队首的函数(也就是setTimeout4)进栈执行

function setTimeout4() {
    resolve('then')
}
复制代码

执行遇到resolve,将promise的回调函数then3放入micro-task队列,此时setTimeout4resolve已执行完毕,出栈

第十三步

call stack执行完毕,查询micro-task队列,发现里面有等待执行的函数,取队首的函数(也就是then3)进栈执行,执行完毕后出栈,至此所有代码执行完毕

呼~终于写完了

总结

  1. 主进程开始执行代码时,先将全局环境入栈,之后每遇到一个函数,建立一个新的上下文,进栈而且执行,遇到主进程执行不了的函数,交给web api执行,同时出栈
  2. 执行过程当中,遇到了macro-task或者micro-task,都会将其交给对应的web api去处理,好比setTimeout交给timer模块,ajax请求交给network模块,DOM操做交给DOM对应模块处理,处理完成后,会将对应的回调函数放入对应的队列中(macro-task队列以及micro-task队列)
  3. 每当函数调用栈中的上下文都执行完毕时(全局环境仍然存在),主进程会去查询micro-task队列,若是micro-task队列为空,会取macro-task队列第一个task放入调用栈执行,不然,取micro-task队列的第一个task放入调用栈执行,若是在处理task期间,若是有新添加的microtasks或者macro-task,也会被添加到相应队列的末尾
  4. 以上内容只针对于Chrome浏览器环境,node环境尚未具体测试,好像是不太同样的

关于

前端萌新一个~~打算常常写写文章总结一下知识点,欢迎关注,一块儿加油啦

相关文章
相关标签/搜索