瀏覽器事件環和Node事件環不得不說的故事!
進入正文之前,首先要感謝各位大佬對本人第一篇掘金文章《ES6版Promise實現,給你不一樣的體驗》的肯定及指正,可能寫的不盡人意,但是你們的點贊會是我繼續分享的動力之一,只要努力過,結果就不要太在意,因為努力之後的結果會讓你滿意!與諸位共勉!
好了,話不多說,接下來讓我們進入今天的話題。今天我們來談一談事件環到底是什麼?javaScript
的事件環和Node
的事件環有什麼區別?有沒有一種無從下手的感覺,別捉急,只要你仔細閱讀本篇文章,相信能夠解開心中的疑惑。
一、先了解幾組常見概念
俗話說,工欲善其事必先利其器。在進入瀏覽器事件環和Node事件環情節之前呢,我們有必要了解以下幾組常見的概念。
1、heap(堆)和 stack(棧)
堆疊是在計算機領域不可忽視的概念,如果想要詳細瞭解,請移步《堆疊_百度百科》。在javaScript中,棧中存的是基本資料型別,會自動分配記憶體空間,自動釋放;堆中存的是引用資料型別,是動態分配的記憶體,大小不定也不會自動釋放。
heap
堆:也可以叫堆記憶體;是一種佇列優先,先進先出的資料結構;stack
棧:又名'堆疊',也是一種資料結構,不過它是按照先進後出原則儲存資料的。
嘻嘻?,自己花了半天時間(誇張)畫得,自我感覺良好。
既然我們大致理解了堆和棧的含義,我們來看一道面試題,如何用js程式碼實現佇列和棧的功能呢?其實很簡單啦,就是陣列最基本常用的增刪方法。
- 實現佇列的方法(先進先出)
let arr = new Array();
arr.push(1);
arr.push(2);
arr.shift();
複製程式碼
- 實現棧的方法(先進後出)
let arr = new Array();
arr.push(1);
arr.push(2);
arr.pop();
複製程式碼
2、執行緒和程式
首先,我們應該知道程式比執行緒要大。一個程式至少要有一個程式,一個程式至少要有一個執行緒。就拿我們經常用的瀏覽器為例吧,為了更直觀一些,先看下這張圖片:
由此可見,瀏覽器就是多程式的,當一個網頁崩潰時不會影響其他網頁的正常執行。我們主要了解下一下幾個方面:- 渲染引擎:渲染引擎內部是多執行緒的,內部包含了兩個最重要的執行緒ui執行緒和js執行緒。這裡要特別注意ui執行緒和js執行緒是互斥的,因為JS執行結果會影響到ui執行緒的結果。ui更新會被儲存在佇列中等到js執行緒空閒時立即被執行。
- js單執行緒:
JavaScript
最大的特點就是單執行緒的,其實應該說其主執行緒是單執行緒的。為什麼這麼說呢?你想一下,如果js是多執行緒的,我們在頁面中這個執行緒要刪了那個元素(不順眼),另一個執行緒呢我要保留那個元素(我罩著的),這樣豈不是就亂套了。這也是為什麼JavaScript
執行同步程式碼,非同步程式碼並不會阻塞程式碼的執行。 - 其他執行緒:
- 瀏覽器事件觸發執行緒(用來控制事件迴圈,存放
setTimeout
、瀏覽器事件、ajax
的回撥函式) - 定時觸發器執行緒(
setTimeout
定時器所線上程) - 非同步HTTP請求執行緒(
ajax
請求執行緒)
- 瀏覽器事件觸發執行緒(用來控制事件迴圈,存放
3、巨集任務和微任務
巨集任務和微任務可以說都是非同步任務。如果瞭解vue
原始碼的同學,應該知道巨集任務macrotask
和微任務microtask
這兩個概念,他們的執行時機是不一樣的。vue
的$nextTick
的原始碼就是通過巨集任務和微任務實現的。(可以去vue的github瞭解一下其實現原理)。
- 常見的巨集任務
macrotask
有:setTimeout
、setInterval
、setImmediate
(ie瀏覽器才支援,node中自己也實現了)、MessageChannel
- 常見的微任務
microtask
有:promise.then()
、process.nextTick
(node的)
二、javaScript的事件環
瀏覽器中,事件環的執行機制是,先會執行棧中的內容,棧中的內容執行後執行微任務,微任務清空後再執行巨集任務,先取出一個巨集任務,再去執行微任務,然後在取巨集任務清微任務這樣不停的迴圈,我們可以看下面這張圖理解一下:
從圖中可以看出,同步任務會進入執行棧,而非同步任務會進入任務佇列(callback queue)等待執行。一旦執行棧中的內容執行完畢,就會讀取任務佇列中等待的任務放入執行棧開始執行。(圖中缺少微任務)
那麼,我們來道面試題檢驗一下,當我們在瀏覽器中執行下面的程式碼,輸出的結果是什麼呢?
setTimeout(() => {
console.log('setTimeout1');
Promise.resolve().then(data => {
console.log('then3');
});
},1000);
Promise.resolve().then(data => {
console.log('then1');
});
Promise.resolve().then(data => {
console.log('then2');
setTimeout(() => {
console.log('setTimeout2');
},1000);
});
console.log(2);
// 輸出結果:2 then1 then2 setTimeout1 then3 setTimeout2
複製程式碼
- 先執行棧中的內容,也就是同步程式碼,所以2被輸出出來;
- 然後清空微任務,所以依次輸出的是
then1
then2
;- 因程式碼是從上到下執行的,所以1s後
setTimeout1
被執行輸出;- 接著再次清空微任務,
then3
被輸出;- 最後執行輸出
setTimeout2
三、Node的事件環
Node
是基於V8引擎的JavaScript
執行環境,在處理高併發、I/O密集(檔案操作、網路操作、資料庫操作等)場景有明顯的優勢。Node的事件環機制與瀏覽器的是不太一樣。
在Node執行環境中:
- 我們寫的js程式碼會交由V8引擎進行處理
- 程式碼中可能會呼叫NodeApi,node會交由
libuv
處理 libuv
通過阻塞I/O和多執行緒實現非同步I/O- 然後通過事件驅動的方式,將結果放到事件佇列中,最終交給我們的應用。
其實本質是在libuv
(一個高效能的,事件驅動的I/O庫)內部有這樣一個事件環機制。在Node啟動時會初始化事件環,話不多說,先上圖:
event loop
執行到某個階段時會將當前階段對應的佇列依次執行。當佇列執行完畢或者執行數量超過上限時,才會轉入下一個階段。node中的微任務在切換佇列時執行。
timers
計時器:執行setTimeout
、setInterval
的回撥函式;I/O callbacks
:執行I/O callback
被延遲到下一階段執行;idle, prepare
:佇列的移動,僅內部使用poll
輪詢:檢索新的I/O事件;執行I/O相關的回撥check
:執行setImmediate
回撥close callbacks
:執行close
事件的callback
,例如socket.on("close",func)
好了,接下來我們先來看一道簡單的測試題:
setTimeout(function () {
console.log('setTimeout');
});
setImmediate(function () {
console.log('setImmediate');
});
複製程式碼
這道題中如果你在
node
環境中多執行幾次,就會發現輸出順序是不固定的。也就是說雖然上圖中timers
佇列在check
佇列前面,但是setTimeout
和setImmediate
沒有明確的先後順序的,這是由node
的準備時間(準備工作會浪費一定的時間)導致的。
對應上面這道練習題,我們再來看下面這道題:
let fs = require('fs');
fs.readFile('./1.txt', function () {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
});
複製程式碼
這道題的輸出順序是
setImmediate
然後setTimeout
,無論你執行多少次,結果順序不會發生改變。這是因為fs
檔案操作(I/O操作)屬於屬於poll
階段,poll
階段的下一階段就是check
階段,所以輸出順序是毋庸置疑的。
最後,讓我們來再看一道面試題加深對Node事件環的理解:
setImmediate(() => {
console.log('setImmediate1');
setTimeout(() => {
console.log('setTimeout1')
}, 0);
});
Promise.resolve().then(res=>{
console.log('then');
})
setTimeout(() => {
process.nextTick(() => {
console.log('nextTick');
});
console.log('setTimeout2');
setImmediate(() => {
console.log('setImmediate2');
});
}, 0);
複製程式碼
這道題的輸出順序是:
then、setTimeout2、nextTick、setImmediate1、setImmediate2、setTimeout1
,為什麼是這樣的順序呢?微任務nextTick
的輸出是因為timers
佇列切換到check
佇列,setImmediate1
和setImmediate2
連續輸出是因只有當前佇列執行完畢後才能進去下一對列。
總結:今天的話題就先到這裡了。可能本文有表述不清楚或者理解不對的地方,還請各位大佬給予指正,我會繼續努力每週分享,萬分感謝!如果您覺得看了這篇文章有所收穫,請不要忘了動動手指點個小❤️哦!讓我們一起蕩起雙槳,每天進步一點點,在技術的道路上越走越遠!
原創不易,轉載請註明出處!謝謝!