从event loop到async await来了解事件循环机制

JS为何是单线程的?

最初设计JS是用来在浏览器验证表单操控DOM元素的是一门脚本语言,若是js是多线程的那么两个线程同时对一个DOM元素进行了相互冲突的操做,那么浏览器的解析器是没法执行的。前端

JS为何须要异步?

若是JS中不存在异步,只能自上而下执行,若是上一行解析时间很长,那么下面的代码就会被阻塞。对于用户而言,阻塞就意味着"卡死",这样就致使了不好的用户体验。好比在进行ajax请求的时候若是没有返回数据后面的代码就没办法执行。node

JS单线程又是如何实现异步的呢?

js中的异步以及多线程均可以理解成为一种“假象”,就拿h5的WebWorker来讲,子线程有诸多限制,不能控制DOM元素、不能修改全局对象 等等,一般只用来作计算作数据处理。这些限制并无违背咱们以前的观点,因此说是“假象”。JS异步的执行机制其实就是事件循环(eventloop),理解了eventloop机制,就理解了JS异步的执行机制。ajax

JS的事件循环(eventloop)是怎么运做的?

事件循环、eventloop、运行机制 这三个术语其实说的是同一个东西,在写这篇文章以前我一直觉得事件循环简单的很,就是先执行同步操做,而后把异步操做排在事件队列里,等同步操做都运行完了(运行栈空闲),按顺序运行事件队列里的内容。可是远不止这么肤浅,咱们接下来一步一步的深刻来了解。promise

“先执行同步操做异步操做排在事件队列里”这样的理解其实也没有任何问题但若是深刻的话会引出来不少其余概念,好比event table和event queue,咱们来看运行过程:浏览器

  1. 首先判断JS是同步仍是异步,同步就进入主线程运行,异步就进入event table。
  2. 异步任务在event table中注册事件,当知足触发条件后(触发条件多是延时也多是ajax回调),被推入event queue。
  3. 同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,若是有就推入主线程中。
setTimeout(() => {
  console.log('2秒到了')
}, 2000)
复制代码

咱们用上面的第二条来分析一下这段脚本,setTimeout是异步操做首先进入event table,注册的事件就是他的回调,触发条件就是2秒以后,当知足条件回调被推入event queue,当主线程空闲时会去event queue里查看是否有可执行的任务。markdown

console.log(1) // 同步任务进入主线程
setTimeout(fun(),0)   // 异步任务,被放入event table, 0秒以后被推入event queue里
console.log(3) // 同步任务进入主线程
复制代码

一、3是同步任务立刻会被执行,执行完成以后主线程空闲去event queue(事件队列)里查看是否有任务在等待执行,这就是为何setTimeout的延迟时间是0毫秒却在最后执行的缘由。多线程

关于setTimeout有一点要注意延时的时间有时候并非那么准确。异步

setTimeout(() => {
  console.log('2秒到了')
}, 2000)
sleep(9999999999)
复制代码

分析运行过程:async

  1. console进入Event Table并注册,计时开始。
  2. 执行sleep函数,sleep方法虽然是同步任务但sleep方法进行了大量的逻辑运算,耗时超过了2秒。
  3. 2秒到了,计时事件timeout完成,console进入Event Queue,可是sleep还没执行完,主线程还被占用,只能等着。
  4. sleep终于执行完了,console终于从Event Queue进入了主线程执行,这个时候已经远远超过了2秒。

其实延迟2秒只是表示2秒后,setTimeout里的函数被会推入event queue,而event queue(事件队列)里的任务,只有在主线程空闲时才会执行。上述的流程走完,咱们知道setTimeout这个函数,是通过指定时间后,把要执行的任务(本例中为console)加入到Event Queue中,又由于是单线程任务要一个一个执行,若是前面的任务须要的时间过久,那么只能等着,致使真正的延迟时间远远大于2秒。 咱们还常常遇到setTimeout(fn,0)这样的代码,它的含义是,指定某个任务在主线程最先的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务所有执行完成,栈为空就立刻执行。可是即使主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。函数

关于setInterval: 以setInterval(fn,ms)为例,setInterval是循环执行的,setInterval会每隔指定的时间将注册的函数置入Event Queue,不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。须要注意的一点是,一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就彻底看不出来有时间间隔了。

上面的概念很基础也很容易理解但不幸的消息是上面讲的一切都不是绝对的正确,由于涉及到Promise、async/await、process.nextTick(node)因此要对任务有更精细的定义:

宏任务(macro-task):包括总体代码script、setTimeout、setInterval、MessageChannel、postMessage、setImmediate。
微任务(micro-task):Promise、process.nextTick、MutationObsever。

在划分宏任务、微任务的时候并无提到async/await由于async/await的本质就是Promise。

事件循环机制究竟是怎么样的? 不一样类型的任务会进入对应的Event Queue,好比setTimeout和setInterval会进入相同(宏任务)的Event Queue。而Promise和process.nextTick会进入相同(微任务)的Event Queue。

  1. 「宏任务」、「微任务」都是队列,一段代码执行时,会先执行宏任务中的同步代码。
  2. 进行第一轮事件循环的时候会把所有的js脚本当成一个宏任务来运行。
  3. 若是执行中遇到setTimeout之类宏任务,那么就把这个setTimeout内部的函数推入「宏任务的队列」中,下一轮宏任务执行时调用。
  4. 若是执行中遇到 promise.then() 之类的微任务,就会推入到「当前宏任务的微任务队列」中,在本轮宏任务的同步代码都执行完成后,依次执行全部的微任务。
  5. 第一轮事件循环中当执行彻底部的同步脚本以及微任务队列中的事件,这一轮事件循环就结束了,开始第二轮事件循环。
  6. 第二轮事件循环同理先执行同步脚本,遇到其余宏任务代码块继续追加到「宏任务的队列」中,遇到微任务,就会推入到「当前宏任务的微任务队列」中,在本轮宏任务的同步代码执行都完成后,依次执行当前全部的微任务。
  7. 开始第三轮,循环往复...

下面用代码来深刻理解上面的机制:

setTimeout(function() {
    console.log('4')
})

new Promise(function(resolve) {
    console.log('1') // 同步任务
    resolve()
}).then(function() {
    console.log('3')
})
console.log('2')
复制代码
  1. 这段代码做为宏任务,进入主线程。
  2. 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。
  3. 接下来遇到了Promise,new Promise当即执行,then函数分发到微任务Event Queue。
  4. 遇到console.log(),当即执行。
  5. 总体代码script做为第一个宏任务执行结束。查看当前有没有可执行的微任务,执行then的回调。

第一轮事件循环结束了,咱们开始第二轮循环。

  1. 从宏任务Event Queue开始。咱们发现了宏任务Event Queue中setTimeout对应的回调函数,当即执行。

执行结果:1 - 2 - 3 - 4

console.log('1')
setTimeout(function() {
    console.log('2')
    process.nextTick(function() {
        console.log('3')
    })
    new Promise(function(resolve) {
        console.log('4')
        resolve()
    }).then(function() {
        console.log('5')
    })
})

process.nextTick(function() {
    console.log('6')
})

new Promise(function(resolve) {
    console.log('7')
    resolve()
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9')
    process.nextTick(function() {
        console.log('10')
    })
    new Promise(function(resolve) {
        console.log('11')
        resolve()
    }).then(function() {
        console.log('12')
    })
})
复制代码
  1. 总体script做为第一个宏任务进入主线程,遇到console.log(1)输出1。
  2. 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。咱们暂且记为setTimeout1。
  3. 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。咱们记为process1。
  4. 遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。咱们记为then1。
  5. 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,咱们记为setTimeout2。
  6. 如今开始执行微任务,咱们发现了process1和then1两个微任务,执行process1,输出6。执行then1,输出8。

第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮事件循环从setTimeout1宏任务开始:

  1. 首先输出2。接下来遇到了process.nextTick(),一样将其分发到微任务Event Queue中,记为process2。
  2. new Promise当即执行输出4,then也分发到微任务Event Queue中,记为then2。
  3. 如今开始执行微任务,咱们发现有process2和then2两个微任务能够执行输出3,5。

第二轮事件循环结束,第二轮输出2,4,3,5。第三轮事件循环从setTimeout2宏任务开始:

  1. 直接输出9,将process.nextTick()分发到微任务Event Queue中。记为process3。
  2. 直接执行new Promise,输出11。将then分发到微任务Event Queue中,记为then3。
  3. 执行两个微任务process3和then3。输出10。输出12。

第三轮事件循环结束,第三轮输出9,11,10,12。 整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。 (请注意,node环境下的事件监听依赖libuv与前端环境不彻底相同,输出顺序可能会有偏差)

new Promise(function (resolve) { 
    console.log('1')// 宏任务一
    resolve()
}).then(function () {
    console.log('3') // 宏任务一的微任务
})
setTimeout(function () { // 宏任务二
    console.log('4')
    setTimeout(function () { // 宏任务五
        console.log('7')
        new Promise(function (resolve) {
            console.log('8')
            resolve()
        }).then(function () {
            console.log('10')
            setTimeout(function () {  // 宏任务七
                console.log('12')
            })
        })
        console.log('9')
    })
})
setTimeout(function () { // 宏任务三
    console.log('5')
})
setTimeout(function () {  // 宏任务四
    console.log('6')
    setTimeout(function () { // 宏任务六
        console.log('11')
    })
})
console.log('2') // 宏任务一
复制代码
  1. 所有的代码做为第一个宏任务进入主线程执行。
  2. 首先输出1,是同步代码。then回调做为微任务进入到宏任务一的微任务队列。
  3. 下面最外层的三个setTimeout分别是宏任务2、宏任务3、宏任务四按序排入宏任务队列。
  4. 输出2,如今宏任务一的同步代码都执行完成了接下来执行宏任务一的微任务输出3。

第一轮事件循环完成了

  1. 如今执行宏任务二输出4,后面的setTimeout做为宏任务五排入宏任务队列。

第二轮事件循环完成了

  1. 执行宏任务三输出5,执行宏任务四输出6,宏任务四里面的setTimeout做为宏任务六。
  2. 执行宏任务五输出7,8。then回调做为宏任务五的微任务排入宏任务五的微任务队列。
  3. 输出同步代码9,宏任务五的同步代码执行完了,如今执行宏任务五的微任务。
  4. 输出10,后面的setTimeout做为宏任务七排入宏任务的队列。

宏任务五执行完成了,当前已是第五轮事件循环了。

  1. 执行宏任务六输出11,执行宏任务七输出12。

结果:一、二、三、四、五、六、七、八、九、十、十一、12

-^-,这个案例是有点恶心,目的是搞明白各宏任务之间执行的顺序以及宏任务和微任务的执行关系。

初步总结:宏任务是一个队列按先入先执行的原则,微任务也是一个队列也是先入先执行。 可是每一个宏任务都对应会有一个微任务队列,宏任务在执行过程当中会先执行同步代码再执行微任务队列。

上面的案例只是用setTimeout和Promise模拟了一些场景来帮助理解,并无用到async/await下面咱们从什么是async/await开始讲起。

async/await是什么?

咱们建立了 promise 但不能同步等待它执行完成。咱们只能经过 then 传一个回调函数这样很容易再次陷入 promise 的回调地狱。实际上,async/await 在底层转换成了 promise 和 then 回调函数。也就是说,这是 promise 的语法糖。每次咱们使用 await, 解释器都建立一个 promise 对象,而后把剩下的 async 函数中的操做放到 then 回调函数中。async/await 的实现,离不开 Promise。从字面意思来理解,async 是“异步”的简写,而 await 是 async wait 的简写能够认为是等待异步方法执行完成。

async/await用来干什么?

用来优化 promise 的回调问题,被称做是异步的终极解决方案。

async/await内部作了什么?

async 函数会返回一个 Promise 对象,若是在函数中 return 一个直接量(普通变量),async 会把这个直接量经过 Promise.resolve() 封装成 Promise 对象。若是你返回了promise那就以你返回的promise为准。 await 是在等待,等待运行的结果也就是返回值。await后面一般是一个异步操做(promise),可是这不表明 await 后面只能跟异步操做 await 后面实际是能够接普通函数调用或者直接量的。

await的等待机制?

若是 await 后面跟的不是一个 Promise,那 await 后面表达式的运算结果就是它等到的东西;若是 await 后面跟的是一个 Promise 对象,await 它会“阻塞”后面的代码,等着 Promise 对象 resolve,而后获得 resolve 的值做为 await 表达式的运算结果。可是此“阻塞”非彼“阻塞”这就是 await 必须用在 async 函数中的缘由。async 函数调用不会形成“阻塞”,它内部全部的“阻塞”都被封装在一个 Promise 对象中异步执行。(这里的阻塞理解成异步等待更合理)

async/await在使用过程当中有什么规定?

每一个 async 方法都返回一个 promise 对象。await 只能出如今 async 函数中。

async/await 在什么场景使用?

单一的 Promise 链并不能发现 async/await 的优点,可是若是须要处理由多个 Promise 组成的 then 链的时候,优点就能体现出来了(Promise 经过 then 链来解决多层回调的问题,如今又用 async/await 来进一步优化它)。

async/await如何使用?

假设一个业务,分多个步骤完成,每一个步骤都是异步的且依赖于上一个步骤的结果。

function myPromise(n) {
    return new Promise(resolve => {
        console.log(n)
        setTimeout(() => resolve(n+1), n)
    })
}
function step1(n) {
    return myPromise(n)
}
function step2(n) {
    return myPromise(n)
}
function step3(n) {
    return myPromise(n)
}

若是用 Promise 实现
step1(1000)
.then(a => step2(a))
.then(b => step3(b))
.then(result => {
    console.log(result)
})

若是用 async/await 来实现呢
async function myResult() {
    const a = await step1(1000)
    const b = await step2(a)
    const result = await step3(b)
    return result
}
myResult().then(result => {
    console.log(result)
}).catch(err => {
    // 若是myResult内部有语法错误会触发catch方法
})
复制代码

看的出来async/await的写法更加优雅一些要比Promise的链式调用更加直观也易于维护。

咱们来看在任务队列中async/await的运行机制,先给出大概方向再经过案例来证实:

  1. async定义的是一个Promise函数和普通函数同样只要不调用就不会进入事件队列。
  2. async内部若是没有主动return Promise,那么async会把函数的返回值用Promise包装。
  3. await关键字必须出如今async函数中,await后面不是必需要跟一个异步操做,也能够是一个普通表达式。
  4. 遇到await关键字,await右边的语句会被当即执行而后await下面的代码进入等待状态,等待await获得结果。

await后面若是不是 promise 对象, await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,做为 await表达式的结果。 await后面若是是 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,而后把 resolve 的参数做为 await 表达式的运算结果。

例1
async function async1() {
  console.log('1')
  await new Promise((resolve) => {
    console.log('2')
    resolve()
  }).then(() => {
    console.log('3')
  })
  console.log('4')
}
async1()
复制代码
  1. new Promise 的函数体是同步脚本因此先执行的是一、2。
  2. 3和4都是微任务,这里由于有await,4要等Promise.then()以后才会执行。

console.log('4')已经被放在await语法糖生成的Promise.then里了,而await的等待必需要等后面Promise.then以后才会结束。

1 -> 2 -> 3 -> 4

例2
setTimeout(function () {
  console.log('6')
}, 0)
console.log('1')
async function async1() {
  console.log('2')
  await async2()
  console.log('5')
}
async function async2() {
  console.log('3')
}
async1()
console.log('4')
复制代码
  1. 6是宏任务在下一轮事件循环执行
  2. 先同步输出1,而后调用了async1(),输出2。
  3. await async2() 会先运行async2(),5进入等待状态。
  4. 输出3,这个时候先执行async函数外的同步代码输出4。
  5. 最后await拿到等待的结果继续往下执行输出5。
  6. 进入第二轮事件循环输出6。

1 -> 2 -> 3 -> 4 -> 5 -> 6

例3
console.log('1')
async function async1() {
  console.log('2')
  await 'await的结果'
  console.log('5')
}

async1()
console.log('3')

new Promise(function (resolve) {
  console.log('4')
  resolve()
}).then(function () {
  console.log('6')
})
复制代码
  1. 首先输出1,而后进入async1()函数,输出2。
  2. await后面虽然是一个直接量,可是仍是会先执行async函数外的同步代码。
  3. 输出3,进入Promise输出4,then回调进入微任务队列。
  4. 如今同步代码执行完了,回到async函数继续执行输出5。
  5. 最后运行微任务输出6。

1 -> 2 -> 3 -> 4 -> 5 -> 6

例4
async function async1() {
  console.log('2')
  await async2()
  console.log('7')
}

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

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

console.log('1')
async1()

new Promise(function (resolve) {
  console.log('4')
  resolve()
}).then(function () {
  console.log('6')
})
console.log('5')
复制代码
  1. 首先输出同步代码1,而后进入async1方法输出2。
  2. 由于遇到await因此先进入async2方法,后面的7被放入微任务队列。
  3. 在async2中输出3,如今跳出async函数先执行外面的同步代码。
  4. 输出4,5。then回调6进入微任务队列。
  5. 如今宏任务执行完了,微任务先入先执行输出七、6。
  6. 第二轮宏任务输出8。

1 -> 2 -> 3 -> 4 -> 5 -> 7 -> 6 -> 8

例5
setTimeout(function () {
  console.log('9')
}, 0)
console.log('1')
async function async1() {
  console.log('2')
  await async2()
  console.log('8')
}
async function async2() {
  return new Promise(function (resolve) {
    console.log('3')
    resolve()
  }).then(function () {
    console.log('6')
  })
}
async1()

new Promise(function (resolve) {
  console.log('4')
  resolve()
}).then(function () {
  console.log('7')
})
console.log('5')
复制代码
  1. 先输出1,2,3。3后面的then进入微任务队列。
  2. 执行外面的同步代码,输出4,5。4后面的then进入微任务队列。
  3. 接下来执行微任务,由于3后面的then先进入,因此按序输出6,7。
  4. 下面回到async1函数,await关键字等到告终果继续往下执行。
  5. 输出8,进行下一轮事件循环也就是宏任务二,输出9。

1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9

例6
async function async1() {
  console.log('2')
  const data = await async2()
  console.log(data)
  console.log('8')
}

async function async2() {
  return new Promise(function (resolve) {
    console.log('3')
    resolve('await的结果')
  }).then(function (data) {
    console.log('6')
    return data
  })
}
console.log('1')

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

async1()

new Promise(function (resolve) {
  console.log('4')
  resolve()
}).then(function () {
  console.log('7')
})
console.log('5')
复制代码
  1. 函数async1和async2只是定义先不去管他,首先输出1。
  2. setTimeout做为宏任务进入宏任务队列等待下一轮事件循环。
  3. 进入async1()函数输出2,await下面的代码进入等待状态。
  4. 进入async2()输出3,then回调进入微任务队列。
  5. 如今执行外面的同步代码,输出4,5,then回调进入微任务队列。
  6. 按序执行微任务,输出6,7。如今回到async1函数。
  7. 输出data,也就是await关键字等到的内容,接着输出8。
  8. 进行下一轮时间循环输出9。

执行结果:1 - 2 - 3 - 4 - 5 - 6 - 7 - await的结果 - 8 - 9

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

async function async1() {
  console.log('1')
  const data = await async2()
  console.log('6')
  return data
}

async function async2() {
  return new Promise(resolve => {
    console.log('2')
    resolve('async2的结果')
  }).then(data => {
    console.log('4')
    return data
  })
}

async1().then(data => {
  console.log('7')
  console.log(data)
})

new Promise(function (resolve) {
  console.log('3')
  resolve()
}).then(function () {
  console.log('5')
})
复制代码
  1. setTimeout做为宏任务进入宏任务队列等待下一轮事件循环。
  2. 先执行async1函数,输出1,6进入等待状态,如今执行async2。
  3. 输出2,then回调进入微任务队列。
  4. 接下来执行外面的同步代码输出3,then回调进入微任务队列。
  5. 按序执行微任务,输出4,5。下面回到async1函数。
  6. 输出了4以后执行了return data,await拿到了内容。
  7. 继续执行输出6,执行了后面的 return data 才触发了async1()的then回调输出7以及data。
  8. 进行第二轮事件循环输出8。

执行结果:1 - 2 - 3 -4 - 5 - 6 - 7 - async2的结果 - 8

例8
async function test() {
  console.log('test start');
  await undefined;
  console.log('await 1');
  await new Promise(r => {
    console.log('promise in async');
    r();
  });
  console.log('await 2');
}

test();
new Promise((r) => {
  console.log('promise');
  r();
}).then(() => {
  console.log(1)
}).then(() => {
  console.log(2)
}).then(() => {
  console.log(3)
}).then(() => {
  console.log(4)
});
复制代码
  1. 从test()开始,先输出 'test start',await 后面并非Promise,不须要等then,这时候后面的 console.log('await 1') await new Promise console.log('await 2') 会当即进入微任务队列。
  2. 而后 console.log('promise') 运行,console.log(1) 进入微任务队列。
  3. 如今开始运行微任务,依据先入先出,console.log('await 1')console.log('promise in async') 被运行。
  4. console.log('promise in async') 所在的Promise前面加了await,因此 console.log('await 2') 做为微任务进入微任务队列。
  5. 刚才第二位进入微任务队列的 console.log(1) 运行,console.log(2) 进入微任务队列,在 console.log(2) 运行以前要先运行 console.log('await 2') 由于await2入队列早,后面的依次 二、三、4。

test start -> promise -> await 1 -> promise in async -> 1 -> await 2 -> 2 -> 3 -> 4

案例有点多主要为了之后回顾,若是你们以为个人理解有误差欢迎指正。

相关文章
相关标签/搜索