最初設計JS是用來在瀏覽器驗證表單操控DOM元素的是一門腳本語言,若是js是多線程的那麼兩個線程同時對一個DOM元素進行了相互衝突的操做,那麼瀏覽器的解析器是沒法執行的。前端
若是JS中不存在異步,只能自上而下執行,若是上一行解析時間很長,那麼下面的代碼就會被阻塞。對於用戶而言,阻塞就意味着"卡死",這樣就致使了不好的用戶體驗。好比在進行ajax請求的時候若是沒有返回數據後面的代碼就沒辦法執行。node
js中的異步以及多線程均可以理解成爲一種「假象」,就拿h5的WebWorker來講,子線程有諸多限制,不能控制DOM元素、不能修改全局對象 等等,一般只用來作計算作數據處理。這些限制並無違背咱們以前的觀點,因此說是「假象」。JS異步的執行機制其實就是事件循環(eventloop),理解了eventloop機制,就理解了JS異步的執行機制。ajax
事件循環、eventloop、運行機制 這三個術語其實說的是同一個東西,在寫這篇文章以前我一直覺得事件循環簡單的很,就是先執行同步操做,而後把異步操做排在事件隊列裏,等同步操做都運行完了(運行棧空閒),按順序運行事件隊列裏的內容。可是遠不止這麼膚淺,咱們接下來一步一步的深刻來了解。promise
「先執行同步操做異步操做排在事件隊列裏」這樣的理解其實也沒有任何問題但若是深刻的話會引出來不少其餘概念,好比event table和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
其實延遲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。
下面用代碼來深刻理解上面的機制:
setTimeout(function() {
console.log('4')
})
new Promise(function(resolve) {
console.log('1') // 同步任務
resolve()
}).then(function() {
console.log('3')
})
console.log('2')
複製代碼
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')
})
})
複製代碼
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') // 宏任務一
複製代碼
-^-,這個案例是有點噁心,目的是讓你們明白各宏任務之間執行的順序以及宏任務和微任務的執行關係。
初步總結: 宏任務是一個棧按先入先執行的原則,微任務也是一個棧也是先入先執行。 可是每一個宏任務都對應會有一個微任務棧,宏任務在執行過程當中會先執行同步代碼再執行微任務棧。
上面的案例只是用setTimeout和Promise模擬了一些場景來幫助理解,並無用到async/await下面咱們從什麼是async/await開始講起。
咱們建立了 promise 但不能同步等待它執行完成。咱們只能經過 then 傳一個回調函數這樣很容易再次陷入 promise 的回調地獄。實際上,async/await 在底層轉換成了 promise 和 then 回調函數。也就是說,這是 promise 的語法糖。每次咱們使用 await, 解釋器都建立一個 promise 對象,而後把剩下的 async 函數中的操做放到 then 回調函數中。async/await 的實現,離不開 Promise。從字面意思來理解,async 是「異步」的簡寫,而 await 是 async wait 的簡寫能夠認爲是等待異步方法執行完成。
用來優化 promise 的回調問題,被稱做是異步的終極解決方案。
async 函數會返回一個 Promise 對象,若是在函數中 return 一個直接量(普通變量),async 會把這個直接量經過 Promise.resolve() 封裝成 Promise 對象。若是你返回了promise那就以你返回的promise爲準。 await 是在等待,等待運行的結果也就是返回值。await後面一般是一個異步操做(promise),可是這不表明 await 後面只能跟異步操做 await 後面實際是能夠接普通函數調用或者直接量的。
若是 await 後面跟的不是一個 Promise,那 await 後面表達式的運算結果就是它等到的東西;若是 await 後面跟的是一個 Promise 對象,await 它會「阻塞」後面的代碼,等着 Promise 對象 resolve,而後獲得 resolve 的值做爲 await 表達式的運算結果。可是此「阻塞」非彼「阻塞」這就是 await 必須用在 async 函數中的緣由。async 函數調用不會形成「阻塞」,它內部全部的「阻塞」都被封裝在一個 Promise 對象中異步執行。(這裏的阻塞理解成異步等待更合理)
每一個 async 方法都返回一個 promise 對象。await 只能出如今 async 函數中。
單一的 Promise 鏈並不能發現 async/await 的優點,可是若是須要處理由多個 Promise 組成的 then 鏈的時候,優點就能體現出來了(Promise 經過 then 鏈來解決多層回調的問題,如今又用 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的運行機制,先給出大概方向再經過案例來證實:
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')
複製代碼
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')
})
複製代碼
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')
複製代碼
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')
複製代碼
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 - 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 - 2 - 3 -4 - 5 - 6 - 7 - async2的結果 - 8
案例有點多主要爲了之後回顧,若是你們以爲個人理解有誤差歡迎指正。
緩緩先...
-^-