JavaScript單執行緒起源:
JavaScript作為瀏覽器指令碼語言,JavaScript的主要用途是與使用者互動,以及操作DOM,為了避免複雜性,誕生開始,JavaScript就是單執行緒語言。
比如,假定JavaScript同時有兩個執行緒,一個執行緒在某個DOM節點上新增內容,另一個執行緒刪除了這個節點,這時瀏覽器應該以哪個執行緒為準?所以,為了避免複雜性,從一誕生,JavaScript就是單執行緒。
單執行緒及存在的問題:
單執行緒就意味著,所有任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等著。
問題的解決--同步、非同步
synchronous(同步任務)和asynchronousk(非同步任務)
- 同步任務是呼叫立即得到結果的任務,同步任務在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;
- 非同步任務是呼叫無法立即得到結果的任務,需要額外的操作才能預期結果的任務,非同步任務不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。
macro-task(巨集任務)和micro-task(微任務)
- 巨集任務:macro-task 可以理解是每次執行棧執行的程式碼就是一個巨集任務(包括每次從事件佇列中獲取一個事件回撥並放到執行棧中執行,每一個巨集任務會從頭到尾將這個任務執行完畢,不會執行其它)包括整體程式碼script,setTimeout,setInterval等
微任務:micro-task可以理解是在當前 task 執行結束後立即執行的任務 包括Promise,process.nextTick等
Event Loop事件迴圈
Event Loop即事件迴圈,是指瀏覽器或Node的一種解決javaScript單執行緒執行時不會阻塞的一種機制,其實巨集任務佇列和微任務佇列的執行,就是事件迴圈的一部分。
事件迴圈的具體流程如下:
- 從巨集任務佇列中,按照入隊順序,找到第一個執行的巨集任務,放入呼叫棧,開始執行;
- 執行完該巨集任務下所有同步任務後,即呼叫棧清空後,該巨集任務被推出巨集任務佇列,然後微任務佇列開始按照入隊順序,依次執行其中的微任務,直至微任務佇列清空為止;
- 當微任務佇列清空後,一個事件迴圈結束;
- 接著從巨集任務佇列中,找到下一個執行的巨集任務,開始第二個事件迴圈,直至巨集任務佇列清空為止。
Js執行機制流程圖
注意:
- 當我們第一次執行的時候,直譯器會將整體程式碼script放入巨集任務佇列中,因此事件迴圈是從第一個巨集任務開始的;因此我在流程圖中將主執行緒單獨分離開。
- 如果在執行微任務的過程中,產生新的微任務新增到微任務佇列中,也需要一起清空;微任務佇列沒清空之前,是不會執行下一個巨集任務的。
流程圖解釋:
- 在Js任務入棧後判斷是否為同步任務,同步任務則立即執行;若為非同步任務則判斷微任務或巨集任務加入相應佇列等待處理。
- 在所有同步程式執行結束,立即執行當前微任務佇列中的全部微任務(因為整體的Js程式碼視為一個巨集任務)
- 當微佇列清空後,執行巨集任務佇列中的下一個巨集任務,執行結束後,判斷微任務佇列是否存在微任務,存在的話,則清空微任務佇列,開啟下一個迴圈。
Event Loop練習
console.log("a");
setTimeout(function () {
console.log("b");
}, 0);
new Promise((resolve) => {
console.log("c");
resolve();
})
.then(function () {
console.log("d");
})
.then(function () {
console.log("e");
});
console.log("f");
/**
* 輸出結果:a c f d e b
*/
async function async1() {
console.log("a");
const res = await async2();
console.log("b");
}
async function async2() {
console.log("c");
return 2;
}
console.log("d");
setTimeout(() => {
console.log("e");
}, 0);
async1().then(res => {
console.log("f")
})
new Promise((resolve) => {
console.log("g");
resolve();
}).then(() => {
console.log("h");
});
console.log("i");
/**
* 輸出結果:d a c g i b h f e
*/
這段程式碼中宣告瞭兩個async方法,
- 在整體的Script語句中,首先輸出:d;
setTimeout中的語句加入巨集任務佇列;
巨集任務佇列 微任務佇列 整體的Script setTimeout - async1()執行,輸出:a;後async2()執行,輸出:c;await阻斷執行;console.log("b")暫不執行,而去執行主執行緒任務;
此時async1()未執行結束,.then()方法不會被加入微任務佇列;
巨集任務佇列 微任務佇列 整體的Script setTimeout 執行new Promise輸出:g;.then(() => {console.log("h");})加入微任務佇列;
巨集任務佇列 微任務佇列 整體的Script console.log("h") setTimeout 6.執行console.log("i");語句,輸出:i;此時主執行緒的同步任務已經全部執行結束,返回阻斷處繼續執行;輸出:b;.then(res => {console.log("f")})加入微佇列;
巨集任務佇列 微任務佇列 setTimeout console.log("h") console.log("f") 7.清空微任務佇列後,執行巨集任務佇列的下一個任務,開啟下一個迴圈;輸出h、f、e;
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,7,6,8,2,4,3,5,9,11,10,12(node與瀏覽器可能略有不同)
*/
注:
- 在node中,process.nextTick()優先順序高於Promise.then、catch、finally;
- 在node中,巨集任務佇列:setImmediate()佇列優先順序高於setTimeout()佇列;
- 在node14以後,也是每執行一個巨集任務、則清空即時的微任務佇列。
參考文件: