在講 Event Loop (事件循環)之前,我們來了解點 node 的東西,來幫助我們更加明白事件循環是幹什麼的
Node 解決了什麼
Web 服務器的瓶頸在於併發的用戶量。Node 的首要目標是提供一種簡單的,用於創建高性能服務器的開發工具。
Node在處理高併發,I/O 密集場景有明顯的性能優勢
- 高併發,是指在同一時間併發訪問服務器
- I/O 密集指的是文件操作、網絡操作、數據庫
- 相對的有 CPU 密集,CPU 密集指的是邏輯處理運算、壓縮、解壓、加密、解密
Web 主要場景就是接收客戶端的請求讀取靜態資源和渲染界面,所以 Node 非常適合 Web 應用的開發。
進程與線程
進程是操作系統分配資源和調度任務的基本單位,線程是建立在進程上的一次程序運行單位,一個進程上可以有多個線程。
- 瀏覽器線程
- 用戶界面-包括地址欄、前進/後退按鈕、書籤菜單等
- 瀏覽器引擎-在用戶界面和呈現引擎之間傳送指令(瀏覽器的主進程)
- 渲染引擎,也被稱爲瀏覽器內核(瀏覽器渲染進程)
- 一個插件對應一個進程(第三方插件進程)
- GPU提高網頁瀏覽的體驗( GPU 進程)
- 瀏覽器渲染引擎
- 渲染引擎內部是多線程的,內部包含 ui 線程和 js 線程
- js 線程 ui 線程 這兩個線程互斥的,目的就是爲了保證不產生衝突。
- ui 線程會把更改的放到隊列中,當 js 線程空閒下來的時候,ui 線程再繼續渲染
- js 單線程
- js 是單線程,爲什麼呢?如果多個線程同時操作 DOM ,那頁面不會很混亂?這裏所謂的單線程指的是主線程是單線程的,所以在 Node 中主線程依舊是單線程的。
- 因爲是單線程,所以所有任務都需要排隊,前一個任務結束,後一個任務才能執行,如果前一個任務花費時間較長,後一個任務等待時間也隨之變長。
- js可以做到先把等待中的任務先放一邊晾着,去處理後面的任務
- 於是所有任務可以分爲兩種,一種是同步任務,另一種是異步任務:同步任務很簡單,前面的任務完成後面才能執行,一個接一個地執行任務。異步任務不佔用主線程,直接進入“任務隊列”中,等任務隊列通知主線程,某個任務可以執行了,纔會進入主線程執行。
- webworker 多線程
- 它和 js 主線程不是平級的,主線程可以控制 webworker,但是 webworker不能操作 DOM,不能獲取 document,window
- 其他線程
- 瀏覽器事件觸發線程(用來控制事件循環,存放 setTimeout、瀏覽器事件、ajax 的回調函數)
- 定時觸發器線程(setTimeout 定時器所在線程)
- 異步 HTTP 請求線程(ajax 請求線程)
單線程特點是節約了內存,並且不需要再切換執行上下文。
異步執行的運行機制
- 所有同步任務都在主線程上執行,形成一個執行棧。
- 主線程之外,還存在一個“任務隊列”。只要異步任務有了運行結果,就在“任務隊列”中放置一個事件。
- 一旦執行棧中的所有同步任務都執行完畢,就會去“任務隊列”中讀取新的任務放到執行棧中,再依次執行任務。
- 只要主線程空了,就會讀取任務隊列,這就是js的運行機制。這個過程會不斷地重複。
再說說事件和回調函數
- 任務隊列其實存放的是事件的隊列,主程序讀取任務隊列,其實就是在讀有哪些事件罷了
- 只要指定過回調函數,這些事件發生時就會進入任務隊列中,等待主線程讀取
- 異步任務必須指定回調函數,當主線程執行異步任務時,其實就是在執行對應的回調函數
- 任務隊列是一個先進先出的數據結構,排在前面的先執行。當調用棧中的任務空了後,主線程會自動調用任務隊列裏的任務執行
來看看Event Loop
- 主線程從任務隊列中讀取任務,這個過程是不斷重複的,所以被稱爲Event Loop(事件循環),從字面意思就清楚了
- 再來看一張圖
上圖中,主線程產生了heap(堆)和stack(棧),棧中的代碼調用各種api,然後在任務隊列中加入click,load,done等事件,當棧中的任務都執行完後就去調用任務隊列中的事件並依次執行。
Node
如圖(圖片是借鑑的):
NodeJs的運行機制:
- V8引擎解析js代碼
- 代碼中可能會調用node API,node會交給LIBUV庫處理
- LIBUV通過阻塞I/O和多線程實現了異步I\O
- 將任務的執行結果返回給V8引擎,V8引擎再將結果返回給用戶
Node中的Event Loop
在LIBUV內部有這樣一個事件環機制,在node啓動時會初始化事件環
- 這裏每一個階段都對應一個事件隊列,當Event Loop執行到某一階段的時候會將該階段對應的事件依次執行。
- 當隊列執行完畢or執行的數量超過上限的時候,會自動轉入下一階段。 這裏我們重點關注一下poll階段
poll階段
同步,異步 阻塞和非阻塞
- 阻塞和非阻塞指的是調用者的狀態,關注的是程序在等待調用結果時的狀態
- 同步和異步指的是被調用者是如何通知的,關注的是消息通知機制
宏任務和微任務
- macro-task(宏任務):
- setTimeout, setInterval, setImmediate, I/O
- micro-task(微任務):
- process.nextTick,
- 原生 Promise (有些實現的promise 將 then 方法放到了宏任務中,瀏覽器默認放到了微任務),
- Object.observe (已廢棄),
- MutationObserver(不兼容,已廢棄)
- MessageChannel(vue中 nextClick 實現原理)
同步代碼先執行,執行是在棧中執行的,微任務大於宏任務,微任務會先執行(棧),宏任務後執行(隊列)
敲幾行代碼來理解知識點
《1》宏任務,微任務在瀏覽器和 node 環境執行順序不同
// 這個列子裏面,包含了宏任務,微任務,分別看看瀏覽器和node 打印的結果
console.log(1)
// 棧
setTimeout(function(){
console.log(2)
// 微任務
Promise.resolve(100).then(function(){
console.log('promise')
})
}) // 如果不寫時間,默認是4ms
// 棧
let promise = new Promise(function(resolve, reject){
console.log(7)
resolve(100)
}).then(function(data){
// 微任務
console.log(data)
})
// 棧
setTimeout(function(){
console.log(3)
})
console.log(5)
// 瀏覽器結果:1 7 5 100 2 promise 3
// node 結果: 1 7 5 100 2 3 promise
瀏覽器和 node 環境執行順序不同,瀏覽器是先把一個棧以及棧中的微任務走完,纔會走下一個棧。node 環境裏面是把所以棧走完,才走微任務
《2》nextTick 和 then 都屬於微任務,誰優先執行呢?
process.nextTick(function(){
console.log('nextTick')
})
Promise.resolve().then(function(){
console.log('then')
})
// 結果打印:nextTick then
// 再加一個宏任務呢
setImmediate(function(){
console.log('setImmediate')
})
// 結果打印:nextTick then setImmediate
nextTick 會比 其他微任務、宏任務執行快
《3》i/o 文件操作(宏任務),搭配微任務,誰優先執行呢?
let fs = require('fs');
fs.readFile('./1/log',function(){
console.log('fs')
})
process.nextTick(function(){
console.log('text')
})
// 結果打印:text fs
i/o 文件操作(宏任務), 如果有微任務,先執行微任務,再執行文件讀取
《4》
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
}, 500);
}, 1000);
setTimeout(() => {
console.log(3)
setTimeout(() => {
console.log(4)
}, 1000);
}, 500)
//3 1 2 4
// 個人觀點:從上往下,所以上面的定時器先註冊,時間一樣所以執行2再執行4