Event Loop 事件循环机制

127 阅读4分钟
async function async1(){
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}

async function async2(){
    console.log('async2')
}

console.log('script start')

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

async1()

new Promise(function(resolve){
    console.log('promise1') 
    resolve()
}).then(function(){
    console.log('promise2') 
})

console.log('script end')

运行结果:

//script start
//async1 start
//async2
//promise1
//script end
//async1 end
//promise2
//setTimeOut

单线程

同步任务和异步任务

  因为JavaScript是单线程运行的,所有的任务只能在主线程上排队执行;但是如果某个任务特别耗时,比如Ajax请求一个接口,可能1s返回结果,也可能10s才返回,有很多的不确定因素(网络延迟等);如果这些任务也放到主线程中去,那么会阻塞浏览器(用户除了等,不能进行其他操作),于是浏览器就把这些任务分派到异步任务队列中去。

console.log('start')

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

console.log('end')

  当主线程执行到setTimeout的时候,虽然是延迟了0s,但是并不会马上来运行,而是放到异步任务队列中,等下面的同步任务队列执行完了,再来执行异步队列中的任务,所以运行结果是:start、end、setTimeout。

  但如果同步任务中有特别耗时的操作,阻塞了setTimeout的定时执行,那么setTimeout就不会按时来完成。来看下面的例子:

console.log('start')
console.time('now')
let list = []

setTimeout(function() {
    console.timeEnd('now')
}, 1000)


for(let i = 0;i<9999999;i++){
    let now = new Date()
    list.push(i)
}

  虽然我们让setTimeout1s后执行,但是for循环占用了太多的线程资源,实际执行会在2s后。所以事件循环的流程大致如下:

所有任务都在主线程上执行,形成一个执行栈。 主线程发现有异步任务,就在“任务队列”之中加入一个任务事件。 一旦“执行栈”中的所有同步任务执行完毕,系统就会读取“任务队列”(先进先出原则)。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。 主线程不断重复上面的第三步,这样的一个循环称为事件循环。 宏任务与微任务   如果任务队列中有多个异步任务,那么先执行哪个任务呢?于是在异步任务中,也进行了等级划分,分为宏任务(macrotask)和微任务(microtask);不同的API注册的任务会依次进入自身对应的队列中,然后等待事件循环将它们依次压入执行栈中执行。

  宏任务包括:

script(整体代码)
setTimeout, setInterval, setImmediate,
I/O
UI rendering

  微任务包括:

process.nextTick
Promise
Object.observe(已废弃)
MutationObserver(html5新特性)

  我们可以把整体的JS代码也看成是一个宏任务,主线程也是从宏任务开始的。我们把上面事件循环的步骤更新一下:

执行一个宏任务 执行过程中如果遇到微任务就加入微任务队列,遇到宏任务就加入宏任务队列 宏任务执行完毕后,检查当前微任务队列,如果有,就依次执行(一轮事件循环结束) 开始下一个宏任务

console.log('start')

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

new Promise(function(resolve) {
    console.log('promise');
    //注意这边调用resolve
    //不然then方法不会执行
    resolve()
}).then(function() {
    console.log('then');
})

console.log('end');

  分析一下执行流程:

  • 刚开始打印start

  • 遇到setTimeout,放入宏任务中,等待执行

  • 遇到new Promise的回调函数,同步执行,打印promise

  • 当resolve后,then方法会放入微任务,等待执行

  • 打印end,这时整个执行栈清空了,宏任务和微任务队列各有一个回调方法

  • 先执行微任务队列,打印then

  • 执行宏任务,打印timeout   

我们把Promise进行一下改变,看一下下面的例子:   

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
async1()
console.log('script end')

async函数还是基于Promise的一些封装,而Promise是属于微任务的一种;因此会把await async2()后面的所有代码放到Promise的then回调函数中去,因此,如果把上面代码进行如下改写,会好理解很多:

async function async1() {
    console.log('async1 start')
    new Promise(function(resolve){
        console.log('async2')
        resolve()
    }).then(function(){
        console.log('async1 end')
    })
}
async1()
console.log('script end')

  根据上面对微任务的理解,console.log('async1 end')会放到微任务队列中,所以实际执行顺序是:async1 start –> async2 –> script end –> async1 end。

  • 第一轮循环开始

  • 打印script start

  • 发现setTimeout,放入宏任务1

  • 打印async1 start

  • 打印async2

  • 把await async2函数后面的回调放入微任务1

  • 打印promise1

  • 把then中的函数放入微任务2

  • 打印script end

  • 调用栈清空,开始执行微任务1,打印async1 end

  • 执行微任务2,打印promise2

  • 微任务执行完,第一轮循环结束

  • 开始宏任务1,打印setTimeOut

  • 结束.