眾所周知,JavaScript 是一門單線程語言,雖然在 html5 中提出了 Web-Worker 系馆,但這并未改變 JavaScript 是單線程這一核心⊥缯眨可看HTML規(guī)范中的這段話:
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.
為了協(xié)調(diào)事件由蘑、用戶交互、腳本代兵、UI 渲染和網(wǎng)絡處理等行為尼酿,用戶引擎必須使用 event loops。Event Loop 包含兩類:一類是基于 Browsing Context 植影,一種是基于 Worker 裳擎,二者是獨立運行的。
而這種考察不論是面試求職思币,還是日常開發(fā)工作句惯,我們經(jīng)常會遇到這樣的情況:給定的幾行代碼,我們需要知道其輸出內(nèi)容和順序支救。因為JavaScript是一門單線程語言,所以我們可以得出結論:
JavaScript是按照語句出現(xiàn)的順序執(zhí)行的
所以我們以為JS都是這樣的:
let a = '1';
console.log(a);
let b = '2';
console.log(b);
結果是1,2
然而實際上JS是這樣的
setTimeout(function(){
console.log('定時器開始')
});
new Promise(function(resolve){
console.log('馬上執(zhí)行for循環(huán)');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('執(zhí)行then函數(shù)')
});
console.log('代碼執(zhí)行結束');
依照JS是按照語句出現(xiàn)的順序執(zhí)行這個理念拷淘,輸出結果:
//"定時器開始"
//"馬上執(zhí)行for循環(huán)"
//"執(zhí)行then函數(shù)"
//"代碼執(zhí)行結束
去chrome上驗證下各墨,結果完全不對,實際上是:
//"馬上執(zhí)行for循環(huán)"
//"代碼執(zhí)行結束"
//"執(zhí)行then函數(shù)"
//"定時器開始"
這就要求我們弄懂JavaScript的執(zhí)行機制了启涯。
1.關于JavaScript
JavaScript是一門單線程語言贬堵,在最新的HTML5中提出了Web-Worker,但JavaScript是單線程這一核心仍未改變结洼。所以一切JavaScript版的”多線程”都是用單線程模擬出來的黎做,一切JavaScript多線程都是紙老虎!
2.JavaScript事件循環(huán)
既然JS是單線程松忍,那就像只有一個窗口的銀行蒸殿,客戶需要排隊一個一個辦理業(yè)務,同理JS任務也要一個一個順序執(zhí)行瞒瘸。如果一個任務耗時過長籍胯,那么后一個任務也必須等著诈铛。那么問題來了,假如我們想瀏覽新聞爬骤,但是新聞包含的超清圖片加載很慢,難道我們的網(wǎng)頁要一直卡著直到圖片完全顯示出來莫换?因此聰明的程序員將任務分為兩類:
同步任務
異步任務
當我們打開網(wǎng)站時霞玄,網(wǎng)頁的渲染過程就是一大堆同步任務骤铃,比如頁面骨架和頁面元素的渲染。而像加載圖片音樂之類占用資源大耗時久的任務坷剧,就是異步任務惰爬。關于這部分有嚴格的文字定義,但本文的目的是用最小的學習成本徹底弄懂執(zhí)行機制听隐,所以我們用導圖來說明:
同步和異步任務分別進入不同的執(zhí)行”場所”补鼻,同步的進入主線程,異步的進入Event Table并注冊函數(shù)雅任。
當指定的事情完成時风范,EventTable會將這個函數(shù)移入Event Queue。
-
主線程內(nèi)的任務執(zhí)行完畢為空沪么,會去Event Queue讀取對應的函數(shù)硼婿,進入主線程執(zhí)行。
上述過程會不斷重復禽车,也就是常說的Event Loop(事件循環(huán))寇漫。
再通俗點就是同步和異步任務分別進入不同的執(zhí)行環(huán)境,同步的進入主線程殉摔,即主執(zhí)行棧州胳,異步的進入Event Queue。主線程內(nèi)的任務執(zhí)行完畢為空逸月,會去Event Queue讀取對應的任務栓撞,推入主線程執(zhí)行。 上述過程的不斷重復就是我們說的Event Loop(事件循環(huán))碗硬。
在事件循環(huán)中瓤湘,每進行一次循環(huán)操作稱為tick,通過閱讀規(guī)范可知恩尾,每一次tick的任務處理模型是比較復雜的弛说,其關鍵的步驟可以總結如下:
在此次tick中選擇最先進入隊列的任務(oldest task),如果有則執(zhí)行(一次)
檢查是否存在Microtasks翰意,如果存在則不停地執(zhí)行木人,直至清空Microtask Queue
更新render
主線程重復執(zhí)行上述步驟
可以用一張圖來說明下流程:
按照上圖這種分類方式通俗點來講就是JS 的執(zhí)行機制是:
執(zhí)行一個宏任務,過程中如果遇到微任務,就將其放到微任務的【事件隊列】里
當前宏任務執(zhí)行完成后,會查看微任務的【事件隊列】,并將里面全部的微任務依次執(zhí)行完
這里相信有人會想問,什么是microtasks?規(guī)范中規(guī)定冀偶,task分為兩大類, 分別是Macro Task (宏任務)和Micro Task(微任務), 并且每個宏任務結束后, 都要清空所有的微任務,這里的Macro Task也是我們常說的task虎囚,有些文章并沒有對其做區(qū)分,后面文章中所提及的task皆看做宏任務(macro task)蔫磨。
macro-task(宏任務)主要包含:script(整體代碼)淘讥、setTimeout、setInterval堤如、I/O蒲列、UI交互事件窒朋、setImmediate(Node.js 環(huán)境)
micro-task(微任務)主要包含:Promise、MutaionObserver蝗岖、process.nextTick(Node.js 環(huán)境)
setTimeout/Promise等API便是任務源侥猩,而進入任務隊列的是由他們指定的具體執(zhí)行任務。來自不同任務源的任務會進入到不同的任務隊列抵赢。其中setTimeout與setInterval是同源的欺劳。
分析示例代碼
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
1、整體script作為第一個宏任務進入主線程铅鲤,遇到console.log划提,輸出script start
2、遇到setTimeout邢享,其回調(diào)函數(shù)被分發(fā)到宏任務Event Queue中
3鹏往、遇到Promise,其then函數(shù)被分到到微任務Event骇塘,Queue中,記為then1伊履,之后又遇到了then函數(shù),將其分到微任務Event Queue中款违,記為then2
4唐瀑、遇到console.log,輸出script end
1插爹、執(zhí)行微任務介褥,首先執(zhí)行then1,輸出promise1,然后執(zhí)行then2递惋,輸出promise2,這樣就清空了所有微任務
2溢陪、執(zhí)行setTimeout任務萍虽,輸出setTimeout 至此,輸出的順序是:script start, script end,promise1, promise2, setTimeout
再來一個例子:
setTimeout(()=>{
console.log("定時器開始執(zhí)行");
})
new Promise(function(resolve){
console.log("準備執(zhí)行for循環(huán)了");
for(var i=0;i<100;i++){
i==22&&resolve();
}
}).then(()=>console.log("執(zhí)行then函數(shù)"))形真;
console.log("代碼執(zhí)行完畢");
首先執(zhí)行script下的宏任務,遇到setTimeout,將其放到宏任務的【隊列】里 遇到 new
Promise直接執(zhí)行,打印"準備執(zhí)行for循環(huán)" 遇到then方法,是微任務,將其放到微任務的【隊列里】 打印 “代碼執(zhí)行完畢”
本輪宏任務執(zhí)行完畢,查看本輪的微任務,發(fā)現(xiàn)有一個then方法里的函數(shù), 打印"執(zhí)行then函數(shù)" 到此,本輪的event loop
全部完成杉编。 下一輪的循環(huán)里,先執(zhí)行一個宏任務,發(fā)現(xiàn)宏任務的【隊列】里有一個 setTimeout里的函數(shù),執(zhí)行打印"定時器開始執(zhí)行"
所以最后的執(zhí)行順序就是:【準備執(zhí)行for循環(huán)–>代碼執(zhí)行完畢–>執(zhí)行then函數(shù)–>定時器開始執(zhí)行】
再來個有難度的例子
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})
console.log('script end');
首先,事件循環(huán)從宏任務(macrotask)隊列開始咆霜,最初始邓馒,宏任務隊列中,只有一個script(整體代碼)任務蛾坯;當遇到任務源(task source)時光酣,則會先分發(fā)任務到對應的任務隊列中去。所以脉课,就和上面例子類似救军,首先遇到了console.log财异,輸出script start; 接著往下走唱遭,遇到setTimeout任務源戳寸,將其分發(fā)到任務隊列中去,記為timeout1拷泽; 接著遇到promise疫鹊,new promise中的代碼立即執(zhí)行,輸出promise1,然后執(zhí)行resolve,遇到setTimeout,將其分發(fā)到任務隊列中去司致,記為timemout2,將其then分發(fā)到微任務隊列中去拆吆,記為then1; 接著遇到console.log代碼蚌吸,直接輸出script end 接著檢查微任務隊列锈拨,發(fā)現(xiàn)有個then1微任務,執(zhí)行羹唠,輸出then1 再檢查微任務隊列奕枢,發(fā)現(xiàn)已經(jīng)清空,則開始檢查宏任務隊列佩微,執(zhí)行timeout1,輸出timeout1缝彬; 接著執(zhí)行timeout2,輸出timeout2 至此哺眯,所有的都隊列都已清空谷浅,執(zhí)行完畢。其輸出的順序依次是:script start, promise1, script end, then1, timeout1, timeout2
流程圖
有個小tip:從規(guī)范來看奶卓,microtask優(yōu)先于task執(zhí)行一疯,所以如果有需要優(yōu)先執(zhí)行的邏輯,放入microtask隊列會比task更早的被執(zhí)行夺姑。
記住墩邀,JavaScript是一門單線程語言,異步操作都是放到事件循環(huán)隊列里面盏浙,等待主執(zhí)行棧來執(zhí)行的眉睹,并沒有專門的異步執(zhí)行線程。