js與生俱來的就是單線程無阻塞的腳本語言。 做爲單線程語言,js代碼執行時都只有一個主線程執行任務。
無阻塞的實現依賴於咱們要談的事件循環。eventloop的規範是真的苦澀難懂,僅僅要理解的話,不推薦去硬啃。html
一直在說js是單線程語言。那麼什麼是線程呢,對於大部分前端同窗來講,可能並非那麼清晰。推薦阮大佬的這篇文章,形象生動
首先,計算機的核心是CPU,它承擔了全部的計算任務。它就像一座工廠,時刻在運行。前端
進程就比如工廠的車間,它表明CPU所能處理的單個任務。
任一時刻,CPU老是運行一個進程,其餘進程處於非運行狀態。
即資源分配的最小單位,擁有獨立的堆棧空間和數據存儲空間html5
線程就比如車間裏的工人。車間的空間是工人們共享的,這象徵一個進程的內存空間是共享的,每一個線程均可以使用這些共享內存。
即程序執行的最小單位,一個進程能夠包括多個線程。node
相對於進程來講,線程不涉及數據空間的操做,因此切換更高效,開銷小。web
顯然多進程能夠並行處理,提高cpu的利用率。
可是js初期是做爲腳本出現的,其要與DOM進行交互,以完成對用戶的展現。
若是多進程,同時操做DOM,那麼後果就不可控了。
例如:對於同一個按鈕,不一樣的進程賦予了不一樣的顏色,到底該怎麼展現。chrome
做爲一個腳本語言,若是使用多線程+鎖的話太多複雜了,因此js就是單線程了。api
不過隨着js的發展,承載的能力愈來愈多,侷限於單線程使得js的效率等有所限制。
所以增長了web worker來執行非dom的操做。瀏覽器
不過該線程非主線程有一些限制、例如不能操做DOM等,也就是爲了保證DOM操做的一致性,這裏就先不關注了。bash
咱們主要關注的仍是非阻塞的能力基礎,即事件循環。多線程
說道事件循環就要先說事件隊列。 在主線程運行時,會產生堆(heap)和棧(stack)。
堆中存的是咱們聲明的object類型的數據,棧中存的是基本數據類型以及函數執行時的運行空間。
主線程從任務隊列中讀取事件,這個過程是循環不斷的,因此這種運行機制即Event Loop。
由於異步任務之間並不相同,所以他們的執行優先級也有區別。不一樣的異步任務被分爲兩類:微任務(micro task)和宏任務(macro task)。
setTimeout, setInterval, setImmediate,I/O, UI rendering
Promise,Object.observe(已廢棄),MutationObserver(html5新特性),process.nextTick
執行棧中的代碼(同步任務),老是在讀取"任務隊列"(異步任務)以前執行 當前執行棧執行完畢時會馬上先處理全部微任務隊列中的事件,而後再去宏任務隊列中取出一個事件。同一次事件循環中,微任務永遠在宏任務以前執行
對於不一樣類型的任務執行順序以下:
大概流程圖以下:
不如直接看個栗子:
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})
// 2 3 1
複製代碼
下面來個稍微複雜的:
setTimeout(()=>{
console.log('A');
},0);
var obj={
func:function () {
setTimeout(function () {
console.log('B')
},0);
return new Promise(function (resolve) {
console.log('C');
resolve();
})
}
};
obj.func().then(function () {
console.log('D')
});
console.log('E');
// c,e,d,b,a
複製代碼
你們能夠結合例子本身試下。
在node中,事件循環表現出的狀態與瀏覽器中大體相同。不一樣的是node中有一套本身的模型。
node中事件循環的實現是依靠的libuv引擎。
咱們知道node選擇chrome v8引擎做爲js解釋器,v8引擎將js代碼分析後去調用對應的node api,
而這些api最後則由libuv引擎驅動,執行對應的任務,並把不一樣的事件放在不一樣的隊列中等待主線程執行。
所以實際上node中的事件循環存在於libuv引擎中。
而node 事件分爲下面幾大階段:
值得額外關注的是poll階段
該階段有以下功能:
若是進入 poll 階段,而且沒有 timer 階段加入的任務,將會發生如下狀況
nextTick 比較特殊,它有本身的隊列,而且,獨立於event loop。 它的執行也很是特殊,不管 event loop 處於何種階段,都會在階段結束的時候清空 nextTick 隊列。
直接看例子吧: process.nextTick
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
複製代碼
大概順序以下:
setImmediate
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
複製代碼
這個結果不固定,同一臺機器測試結果也有兩種:
// TIMEOUT FIRED =>1 =>2
或者
// 1=>TIMEOUT FIRED=>2
複製代碼
緣由在於setTimeout 0 node 中至少爲1ms,也就是取決於機器執行至timer時是否到了可執行的時機。
作個對比就比較清楚了:
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setImmediate(function B(){console.log(4);});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 20);
// 1=>2=>TIMEOUT FIRED
複製代碼
此時間隔時間較長,timer階段最後纔會執行,因此會先執行兩次check,出處1,2 下面再看個例子 poll階段任務隊列
var fs = require('fs')
fs.readFile('./yarn.lock', () => {
setImmediate(() => {
console.log('1')
setImmediate(() => {
console.log('2')
})
})
setTimeout(() => {
console.log('TIMEOUT FIRED')
}, 0)
})
// 結果肯定:
// 輸出始終爲1=>TIMEOUT FIRED=>2
複製代碼
瀏覽器的事件循環 瀏覽器比較清晰一些,就是固定的流程,當前宏任務結束,就是執行全部微任務(不必定是所有,可能基於系統能力,會有所剩下),而後再下一個宏任務,微任務這樣交替進行。 node中的事件循環 主要是把握不一樣階段和特殊狀況的處理,特別是poll階段和 process.nextTick任務。
參考文章:
zhuanlan.zhihu.com/p/47152694 html.spec.whatwg.org/multipage/w… www.ruanyifeng.com/blog/2014/1… hackernoon.com/understandi… juejin.im/post/5bac87… 感謝上述參考文章,關於事件循環這裏就總結完畢了,做爲本身的一個學習心得。但願能幫助到有需求的同窗,一塊兒進步。