Js異步機制
JavaScript
是一門單線程語言,所謂單線程,就是指一次只能完成一件任務,如果有多個任務,就必須排隊,前面一個任務完成,再執行后面一個任務,以此類推。這種模式的好處是實現起來比較簡單,執行環境相對單純,壞處是只要有一個任務耗時很長,后面的任務都必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應也就是假死狀態,往往就是因為某一段Javascript
代碼長時間運行比如死循環,導致整個頁面卡在這個地方,其他任務無法執行。
執行機制
為了解決上述問題,Javascript
將任務的執行模式分為兩種:同步Synchronous
與異步Asynchronous
,同步或非同步,表明着是否需要將整個流程按順序地完成,阻塞或非阻塞,意味着你調用的函數會不會立刻告訴你結果。
同步
同步模式就是同步阻塞,后一個任務等待前一個任務結束,然后再執行,程序的執行順序與任務的排列順序是一致的、同步的。
var i = 100;
while(--i) { console.log(i); }
console.log("while 執行完畢我才能執行");
異步
異步執行就是非阻塞模式執行,每一個任務有一個或多個回調函數callback
,前一個任務結束后,不是執行后一個任務,而是執行回調函數,后一個任務則是不等前一個任務結束就執行,所以程序的執行順序與任務的排列順序是不一致的、異步的。瀏覽器對於每個Tab
只分配了一個Js
線程,主要任務是與用戶交互以及操作DOM
等,而這也就決定它只能為單線程,否則會帶來很復雜的同步問題,例如假定JavaScript
同時有兩個線程,一個線程在某個DOM
節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器無法確定以哪個線程的操作為准。
setTimeout(() => console.log("我后執行"), 0);
// 注意:W3C在HTML標准中規定,規定要求setTimeout中低於4ms的時間間隔算為4ms,此外這與瀏覽器設定、主線程以及任務隊列也有關系,執行時間可能大於4ms,例如老版本的瀏覽器都將最短間隔設為10毫秒。另外,對於那些DOM的變動尤其是涉及頁面重新渲染的部分,通常不會立即執行,而是每16毫秒執行一次。這時使用requestAnimationFrame()的效果要好於setTimeout()。
console.log("我先執行");
異步機制
首先來看一個例子,與上文一樣來測試一個異步執行的操作
setTimeout(() => console.log("我在很長時間之后才執行"), 0);
var i = 3000000000;
while(--i) { }
console.log("循環執行完畢");
本地測試,設置的setTimeout
回調函數大約在30s
之后才執行,遠遠大於4ms
,我在主線程設置了一個非常大的循環來阻塞Js
主線程,注意我並沒有設置一個死循環,假如我在此處設置死循環來阻塞主線程,那么設置的setTimeout
回調函數將永遠不會執行,此外由於渲染線程與JS
引擎線程是互斥的,Js
線程在處理任務時渲染線程會被掛起,整個頁面都將被阻塞,無法刷新甚至無法關閉,只能通過使用任務管理器結束Tab
進程的方式關閉頁面。
Js
實現異步是通過一個執行棧與一個任務隊列來完成異步操作的,所有同步任務都是在主線程上執行的,形成執行棧,任務隊列中存放各種事件回調(也可以稱作消息),當執行棧中的任務處理完成后,主線程就開始讀取任務隊列中的任務並執行,不斷往復循環。
例如上例中的setTimeout
完成后的事件回調就存在任務隊列中,這里需要說明的是瀏覽器定時計數器並不是由JavaScript
引擎計數的,因為JavaScript
引擎是單線程的,如果線程處於阻塞狀態就會影響記計時的准確,計數是由瀏覽器線程進行計數的,當計數完畢,就將事件回調加入任務隊列,同樣HTTP
請求在瀏覽器中也存在單獨的線程,也是執行完畢后將事件回調置入任務隊列。通過這個流程,就能夠解釋為什么上例中setTimeout
的回調一直無法執行,是由於主線程也就是執行棧中的代碼沒有完成,不會去讀取任務隊列中的事件回調來執行,即使這個事件回調早已在任務隊列中。
Event Loop
主線程從任務隊列中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱為Event Loop
,Event Loop
是一個執行模型,在不同的地方有不同的實現,瀏覽器和NodeJS
基於不同的技術實現了各自的Event Loop
。瀏覽器的Event Loop
是在HTML5
的規范中明確定義,NodeJS
的Event Loop
是基於libuv
實現的。
在瀏覽器中的Event Loop
由執行棧Execution Stack
、后台線程Background Threads
、宏隊列Macrotask Queue
、微隊列Microtask Queue
組成。
- 執行棧就是在主線程執行同步任務的數據結構,函數調用形成了一個由若干幀組成的棧。
- 后台線程就是瀏覽器實現對於
setTimeout
、setInterval
、XMLHttpRequest
等等的執行線程。 - 宏隊列,一些異步任務的回調會依次進入宏隊列,等待后續被調用,包括
setTimeout
、setInterval
、setImmediate(Node)
、requestAnimationFrame
、UI rendering
、I/O
等操作 - 微隊列,另一些異步任務的回調會依次進入微隊列,等待后續調用,包括
Promise
、process.nextTick(Node)
、Object.observe
、MutationObserver
等操作
當Js
執行時,進行如下流程
- 首先將執行棧中代碼同步執行,將這些代碼中異步任務加入后台線程中
- 執行棧中的同步代碼執行完畢后,執行棧清空,並開始掃描微隊列
- 取出微隊列隊首任務,放入執行棧中執行,此時微隊列是進行了出隊操作
- 當執行棧執行完成后,繼續出隊微隊列任務並執行,直到微隊列任務全部執行完畢
- 最后一個微隊列任務出隊並進入執行棧后微隊列中任務為空,當執行棧任務完成后,開始掃面微隊列為空,繼續掃描宏隊列任務,宏隊列出隊,放入執行棧中執行,執行完畢后繼續掃描微隊列為空則掃描宏隊列,出隊執行
- 不斷往復...
實例
// Step 1
console.log(1);
// Step 2
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3);
});
}, 0);
// Step 3
new Promise((resolve, reject) => {
console.log(4);
resolve();
}).then(() => {
console.log(5);
})
// Step 4
setTimeout(() => {
console.log(6);
}, 0);
// Step 5
console.log(7);
// Step N
// ...
// Result
/*
1
4
7
5
2
3
6
*/
Step 1
// 執行棧 console
// 微隊列 []
// 宏隊列 []
console.log(1); // 1
Step 2
// 執行棧 setTimeout
// 微隊列 []
// 宏隊列 [setTimeout1]
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3);
});
}, 0);
Step 3
// 執行棧 Promise
// 微隊列 [then1]
// 宏隊列 [setTimeout1]
new Promise((resolve, reject) => {
console.log(4); // 4 // Promise是個函數對象,此處是同步執行的 // 執行棧 Promise console
resolve();
}).then(() => {
console.log(5);
})
Step 4
// 執行棧 setTimeout
// 微隊列 [then1]
// 宏隊列 [setTimeout1 setTimeout2]
setTimeout(() => {
console.log(6);
}, 0);
Step 5
// 執行棧 console
// 微隊列 [then1]
// 宏隊列 [setTimeout1 setTimeout2]
console.log(7); // 7
Step 6
// 執行棧 then1
// 微隊列 []
// 宏隊列 [setTimeout1 setTimeout2]
console.log(5); // 5
Step 7
// 執行棧 setTimeout1
// 微隊列 [then2]
// 宏隊列 [setTimeout2]
console.log(2); // 2
Promise.resolve().then(() => {
console.log(3);
});
Step 8
// 執行棧 then2
// 微隊列 []
// 宏隊列 [setTimeout2]
console.log(3); // 3
Step 9
// 執行棧 setTimeout2
// 微隊列 []
// 宏隊列 []
console.log(6); // 6
每日一題
https://github.com/WindrunnerMax/EveryDay
參考
https://www.jianshu.com/p/1a35857c78e5
https://segmentfault.com/a/1190000016278115
https://segmentfault.com/a/1190000012925872
https://www.cnblogs.com/sunidol/p/11301808.html
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop