本文爲我的看法,若是發現文章有錯誤的地方,歡迎你們指正,感謝感謝~~前端
轉載請標明出處node
衆所周知,JavaScript的一大特色就是單線程,也就是會按順序執行代碼,同一時間只能作一件事。web
JavaScript的誕生,一開始是爲了解決瀏覽器用戶交互的問題,以及用來操做DOM,基於這個緣由,JavaScript被設計成單線程,不然會帶來複雜的同步問題。ajax
單線程意味着全部任務都要排隊進行,若是存在一個任務執行時間過長,後面的任務都會被阻塞,對於用戶而言就意味着「卡死」。vim
單線程的JavaScript是怎麼執行異步代碼的呢?api
這就涉及到JavaScript的事件循環機制(event loop)了。promise
這裏先推薦去看看Philip Roberts的演講《Help, I’m stuck in an event-loop》,雖然內容沒有涉及到任務隊列的細分,可是對函數調用棧(call stack)的分析仍是挺不錯的瀏覽器
列舉幾個概念:執行上下文, 函數調用棧(call stack), 任務隊列(task queue)bash
當JavaScript代碼開始執行時,首先將全局環境壓入函數調用棧(棧底永遠都是全局上下文,除非線程結束,在瀏覽器上表現爲窗口關閉),以後,每遇到一個函數,建立一個新的函數上下文,而且入棧。異步
執行過程當中,遇到了macro-task或者micro-task,都會將其交給對應的web api去處理,好比setTimeout交給timer模塊,ajax請求交給network模塊,DOM操做交給DOM對應模塊處理,處理完成後,會將對應的回調函數放入對應的隊列中(macro-task隊列以及micro-task隊列)
每當函數調用棧中的上下文都執行完畢時(全局環境仍然存在),主進程會去查詢micro-task隊列,若是micro-task隊列爲空,會取macro-task隊列第一個task放入調用棧執行,不然,取micro-task隊列的第一個task放入調用棧執行,若是在處理task期間,若是有新添加的microtasks或者macro-task,也會被添加到相應隊列的末尾
一直循環第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
進棧
第二步
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
隊列,此時promise1
和resolve
都已執行完畢,出棧
第五步
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
隊列,此時then1
、promise2
和resolve
都已執行完畢,出棧
第八步
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
隊尾,此時setTimeout1
和promise3
都已執行完畢,出棧
第十步
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
隊列,此時setTimeout3
和promise4
都已執行完畢,出棧
第十一步
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
隊列,此時setTimeout4
和resolve
已執行完畢,出棧
第十三步
call stack
執行完畢,查詢micro-task
隊列,發現裏面有等待執行的函數,取隊首的函數(也就是then3
)進棧執行,執行完畢後出棧,至此所有代碼執行完畢
呼~終於寫完了
前端萌新一個~~打算常常寫寫文章總結一下知識點,歡迎關注,一塊兒加油啦