從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裏查看是否有可執行的任務。bash

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)
wait(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的回調。 (第一輪事件循環結束了,咱們開始第二輪循環。)
  6. 從宏任務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宏任務開始:
  7. 首先輸出2。接下來遇到了process.nextTick(),一樣將其分發到微任務Event Queue中,記爲process2。
  8. new Promise當即執行輸出4,then也分發到微任務Event Queue中,記爲then2。
  9. 如今開始執行微任務,咱們發現有process2和then2兩個微任務能夠執行輸出3,5。 第二輪事件循環結束,第二輪輸出2,4,3,5。第三輪事件循環從setTimeout2宏任務開始:
  10. 直接輸出9,將process.nextTick()分發到微任務Event Queue中。記爲process3。
  11. 直接執行new Promise,輸出11。將then分發到微任務Event Queue中,記爲then3。
  12. 執行兩個微任務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。 第一輪事件循環完成了
  5. 如今執行宏任務二輸出4,後面的setTimeout做爲宏任務五排入宏任務隊列。 第二輪事件循環完成了
  6. 執行宏任務三輸出5,執行宏任務四輸出6,宏任務四里面的setTimeout做爲宏任務六。
  7. 執行宏任務五輸出7,8。then回調做爲宏任務五的微任務排入宏任務五的微任務隊列。
  8. 輸出同步代碼9,宏任務五的同步代碼執行完了,如今執行宏任務五的微任務。
  9. 輸出10,後面的setTimeout做爲宏任務七排入宏任務的隊列。 宏任務五執行完成了,當前已是第五輪事件循環了。
  10. 執行宏任務六輸出11,執行宏任務七輸出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 表達式的運算結果。
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。
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。
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回調進入微任務棧。
  5. 如今宏任務執行完了,執行微任務輸出6。
  6. 而後回到async1函數接着往下執行輸出7。
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。
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
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

案例有點多主要爲了之後回顧,若是你們以爲個人理解有誤差歡迎指正。

緩緩先...

-^-

相關文章
相關標籤/搜索