執行題
setTimeout(function(){
console.log('定時器開始啦')
});
new Promise(function(resolve){
console.log('馬上執行for迴圈啦');
for(var i = 0; i < 10000; i++){
if(i == 99) resolve();
}
}).then(function(){
console.log('執行then函式啦');
});
console.log('程式碼執行結束');
// 馬上執行for迴圈啦
// 程式碼執行結束
// 執行then函式啦
// 定時器開始啦
// 如果沒有執行對,請往下看
複製程式碼
JavaScript本身是一門單執行緒語言
為什麼
JS
是單執行緒的?作為瀏覽器指令碼語言,JavaScript 的主要用途是與使用者互動,以及操作 DOM 。這決定了它只能是單執行緒,否則會帶來很複雜的同步問題。比如,假定 JavaScript 同時有兩個執行緒,一個執行緒在某個DOM
節點上新增內容,另一個執行緒刪除了這個節點,這時瀏覽器應該以哪個執行緒為準?
同步執行和非同步執行
單執行緒就意味著,所有任務都需要排隊,前一個任務結束,才能執行後一個任務。如果前一個任務耗時很長,那麼後一個任務就不得不一直等待 於是乎,
JS
設計者們把所有任務分成兩類,同步和非同步
- 同步:只有前一個任務執行完畢,才能執行後一個任務
- 非同步:當同步任務執行到某個
WebAPI
時,就會觸發非同步操作,此時瀏覽器會單獨開執行緒去處理這些非同步任務。
任務佇列、回撥佇列、事件迴圈
WebAPI
是啥?瀏覽器事件、定時器、ajax
,這些操作不會阻塞 JS 的執行,JS 會跳過當前程式碼,執行後續程式碼
- 任務佇列( Task Queue ):主執行緒執行完畢後所觸發的非同步任務( WebAPIs ),叫任務佇列
- 回撥佇列( Callback Queue ):這些非同步 WebAPI 執行完成後得到的結果,會新增到 callback queue 中
- 事件迴圈( Event Loop ):只要主執行緒的同步任務執行完畢,就會不斷的讀取 "回撥佇列" 中的回撥函式,到主執行緒中執行,這個過程不斷迴圈往復
如何知道主執行緒執行執行完畢?JS引擎存在 monitoring process 程式,會持續不斷的檢查主執行緒執行為空,一旦為空,就會去 callback queue 中檢查是否有等待被呼叫的函式。
說了一堆概念,來一起看看這段程式碼
console.log('1');
setTimeout(function() {
console.log('2');
}, 0);
console.log('3');
複製程式碼
執行結果如下:
- 列印1
- 遇到 WebAPI(
setTimeout
) ,瀏覽器新開定時器執行緒處理,執行完成後把回撥函式存放到回撥佇列中。專業一點的說發:JS
引擎遇到非同步任務後不會一直等待其返回結果,而是將這個任務掛起交給其他瀏覽器執行緒處理,自己繼續執行主執行緒中的其他任務。這個非同步任務執行完畢後,把結果返回給回撥佇列。被放入的程式碼不會被立即執行。而是當主執行緒所有同步任務執行完畢, monitoring process 程式就會把 "回撥佇列" 中的第一個回撥程式碼放入主執行緒。然後主執行緒執行程式碼。如此反覆 - 列印3 非同步
setTimeout
不會阻塞同步程式碼,因此會首先列印3 - 主執行緒執行完畢後,執行 Callback Queue 列印2
macro task 與 micro task
非同步任務的執行優先順序並不相同,它們被分為兩類:微任務( micro task ) 和 巨集任務( macro task ) 根據非同步事件的型別,這些事件實際上會被派發對應的巨集任務和微任務中,在當前主執行緒執行完畢後,
- 會先檢視微任務中是否有事件存在,如果不存在,則再去找巨集任務
- 如果存在,則會依次執行佇列中的引數,直到微任務列表為空,讓後去巨集任務中一次讀取事件到主執行緒中執行,如此反覆 當前主執行緒執行完畢後,會首先處理微任務佇列中的事件,讓後再去讀取巨集任務佇列的事件。在同一次事件迴圈中,微任務永遠在巨集任務之前執行。
- 巨集任務( macro-task ):整體
script
、setTimeout
、setInterval
、UI互動事件
、I/O
- 微任務( micro-task ):
process.nextTick
、Promise
、MutaionObserver
整體script本身就是一次巨集任務
上程式碼
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function (resolve, reject) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()
1. setTimeout:巨集任務:存入巨集任務佇列
2. Promise:函式本身是同步執行的( **Promise** 只有一個引數,預設new的時候就會同步執行), `.then` 是非同步,因此依次列印1、2 `.then` 存入微任務中
3. 列印3( 第一次主執行緒執行完畢 )
4. 執行微任務中的回撥函式:5, 讓後執行巨集任務中的 `setTimeout` 4
// 最終結果1,2,3,5,4
複製程式碼
來點稍微高難度的
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
setTimeout(() => {
console.log(9)
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
第一次同步執行:1,7,8
巨集任務:setTimeout
2,4,5
9,11,12
複製程式碼
應用場景
console.log('先執行這裡');
setTimeout(() => {
console.log('再執行啦');
}, 0);
複製程式碼
- 在工作中,經常會遇到上述的程式碼,含義:只要主執行緒執行完成,就立馬執行
setTimeout
中的回撥程式碼- micro-task 優先於 macro-task 執行,在瀏覽器中高優先順序的程式碼可以在
promise
或setTimeout(()=>{}, 0)
中執行- 認知尚淺,只能想到這些應用場景
總結
這些概念是中級以上前端開發必知必會內容,其實實際的落地場景很少
- 為什麼
new Promise
第一個引數是同步執行的 ?學習Promise && 簡易實現Promisenode
中的 JS 執行機制是什麼樣子的?從一道執行題,瞭解Node中JS執行機制
附:這篇部落格 也許 想表達 瀏覽器環境中JS同步和非同步的執行過程 (⊙﹏⊙)b