導讀
javascript是一門單線程語言,一切javascript版的多線程都是用單線程模擬出來的,所以代碼執行還是順序執行的原則,只不過編寫的順序被執行環境重新“編排”了一下而已。
執行過程中,我們把任務分爲同步任務和異步任務,而異步任務又分爲宏任務,微任務。
一般順序:同步->微任務->宏任務
- macro-task(宏任務):包括整體代碼script,setTimeout,setInterval
- micro-task(微任務):Promise,process.nextTick
node環境與瀏覽器環境區別:(這裏先寫出區別,下面做詳細介紹,具體不同環境爲什麼異步輸出不同。)
node環境中,任務全部出隊,並執行。
瀏覽器環境,出隊一個任務,並執行。
任務入隊規則
異步任務隊列分爲宏任務隊列,微任務隊列。
代碼順序執行,碰到異步代碼後放入對應隊列內。第一遍代碼執行完畢後,會取出微任務隊列內的代碼繼續執行,執行完後再取出宏任務隊列內的代碼繼續執行。以此反覆執行直到最後執行完所有任務。
運用遞歸思想描述
function 代碼執行(任務){
任務執行,異步的代碼入隊
代碼執行(微任務出隊)
代碼執行(宏任務出隊)
}
這裏說明一下:由於node環境下的事件監聽依賴libuv與前端環境不完全相同,輸出會有差異。
主要原因:雖然規則相同,但是入隊的順序會有差異,出隊任務數量也有不同,所以輸出會有差異。
實例演示
下面結合實例,將在node端和瀏覽器端分別說明其差異性。
使用代碼:
console.log('1');
setTimeout(function() { //timeout1
console.log('2');
new Promise(function(resolve) {
console.log('4');
setTimeout(function(){//timeout2
console.log('5');
resolve()//promise1
})
}).then(function() {
console.log('6');
})
})
new Promise(function(resolve) {
console.log('8');
resolve();//promise2
}).then(function() {
console.log('9');
})
setTimeout(function() { //timeout3
console.log('10');
new Promise(function(resolve) {
console.log('12');
resolve();//promise3
}).then(function() {
console.log('13');
})
setTimeout(function(){//timeout4
console.log('14')
new Promise(function(resolve){
console.log('15')
resolve() //promise4
}).then(function() {
console.log('16');
})
})
})
瀏覽器端:
這裏建議打開兩個頁面,一邊對照代碼一邊看一下步驟。
第一次執行代碼:輸出1,timeout1壓入宏隊列,輸出8,promise2壓入微隊列,timeout3壓入宏任務隊列。
//在瀏覽器端,邊執行代碼,邊把異步代碼加入對應隊列,這一次執行都是同步代碼。
//宏任務隊列內[timeout1,timeout3],微任務隊列[promise2]
第二次執行代碼(promise2出隊):輸出9
//微任務隊列優先級高,先執行微任務,瀏覽器端只有一個任務出隊並執行。
//宏任務隊列內[timeout1,timeout3],微任務隊列[]
第三次執行代碼(timeout1出隊):輸出2,輸出4,timeout2壓入宏隊。
//宏任務隊列內[timeout3,timeout2],微任務隊列[]
第四次執行代碼(timeout3出隊):輸出10,輸出12,promise3壓入微隊列,timeout4壓入宏隊列。
//宏任務隊列內[timeout2,timeout4],微任務隊列[promise3]
第五次執行代碼(promise3出隊):輸出13。
//宏任務隊列內[timeout2,timeout4],微任務隊列[]
第六次執行代碼(timeout2出隊):輸出5,promise1壓入微隊列。
//宏任務隊列內[timeout4],微任務隊列[promise1]
第七次執行代碼(promise1出隊):輸出6。
//宏任務隊列內[timeout4],微任務隊列[]
第八次執行代碼(timeout4出隊):輸出14,輸出15,promise4壓入微隊列。
//宏任務隊列內[],微任務隊列[promise4]
第九次執行代碼(promise4出隊):輸出16。
//宏任務隊列內[],微任務隊列[]
順序爲:1,8,9,2,4,10,12,13,5,6,14,15,16
node端
第一次執行代碼:輸出1,timeout1壓入宏隊列,輸出8,promise2壓入微隊列,timeout3壓入宏任務隊列。
//第一次執行node端和瀏覽器端二者相同。
//宏任務隊列內[timeout1,timeout3],微任務隊列[promise2]
第二次執行代碼(promise2出隊):輸出9
//二者相同。
//宏任務隊列內[timeout1,timeout3],微任務隊列[]
第三次執行代碼([timeout1,timeout3出隊):輸出2,輸出4,timeout2壓入宏隊列,輸出10,輸出12,promise3壓入微隊列,timeout4壓入宏隊列。
//這裏我們可以看到,node與瀏覽器不同之處,timeout1,timeout3一起出隊,所以內部的同等級(同步代碼)就一起執行了。node環境的這一次執行完成了瀏覽器多次執行步驟。
//宏任務隊列內[timeout2,timeout4],微任務隊列[promise3]
第四次執行代碼(promise3出隊):輸出13。
//宏任務隊列內[timeout2,timeout4],微任務隊列[]
第五次執行代碼(timeout2,timeout4出隊):輸出5,promise1壓入微隊列,輸出14,輸出15,promise4壓入微隊列。
//在這裏輸出數值與瀏覽器端開始有差異。
//宏任務隊列內[],微任務隊列[promise1,promise4]
第六次執行代碼(promise1,promise4出隊):輸出6,輸出16。
//宏任務隊列內[],微任務隊列[]
順序爲:1,8,9,2,4,10,12,13,5,14,15,6,16
總結:
簡單結構的代碼,一般執行順序:同步->微任務->宏任務。複雜結構代碼,微任務優先級大於宏任務優先級。同步代碼邊執行,邊把異步任務入隊,然後系統選擇出隊的任務再一次新一輪執行。
node環境與瀏覽器環境區別:node環境中,任務全部出隊,並執行。瀏覽器環境,出隊一個任務,並執行。
備註1
console.log("11")
new Promise(function(resolve) {
console.log('12');//這裏代碼屬於同步代碼,只有then內代碼纔是異步代碼,同上面“console.log("11")”代碼段一起順序執行。
resolve();
}).then(function() {
console.log('13');
})
備註2
在node環境中process.nextTick()比Promise優先級高,所以優先執行
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
process.nextTick(function() {
console.log('6');
})
//7,6,8
備註3
這一次,徹底弄懂 JavaScript 執行機制:https://juejin.im/post/59e85eebf265da430d571f89