瀏覽器事件環和Node事件環不得不說的故事!

亦曾執著過不後悔發表於2018-08-04

瀏覽器事件環和Node事件環不得不說的故事!

瀏覽器事件環和Node事件環不得不說的故事!

進入正文之前,首先要感謝各位大佬對本人第一篇掘金文章《ES6版Promise實現,給你不一樣的體驗》的肯定及指正,可能寫的不盡人意,但是你們的點贊會是我繼續分享的動力之一,只要努力過,結果就不要太在意,因為努力之後的結果會讓你滿意!與諸位共勉!

好了,話不多說,接下來讓我們進入今天的話題。今天我們來談一談事件環到底是什麼?javaScript的事件環和Node的事件環有什麼區別?有沒有一種無從下手的感覺,別捉急,只要你仔細閱讀本篇文章,相信能夠解開心中的疑惑。

瀏覽器事件環和Node事件環不得不說的故事!

一、先了解幾組常見概念

俗話說,工欲善其事必先利其器。在進入瀏覽器事件環和Node事件環情節之前呢,我們有必要了解以下幾組常見的概念。

1、heap(堆)和 stack(棧)

堆疊是在計算機領域不可忽視的概念,如果想要詳細瞭解,請移步《堆疊_百度百科》。在javaScript中,棧中存的是基本資料型別,會自動分配記憶體空間,自動釋放;堆中存的是引用資料型別,是動態分配的記憶體,大小不定也不會自動釋放。

  • heap堆:也可以叫堆記憶體;是一種佇列優先,先進先出的資料結構;
  • stack棧:又名'堆疊',也是一種資料結構,不過它是按照先進後出原則儲存資料的。
    瀏覽器事件環和Node事件環不得不說的故事!

嘻嘻?,自己花了半天時間(誇張)畫得,自我感覺良好。

既然我們大致理解了堆和棧的含義,我們來看一道面試題,如何用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、執行緒和程式

首先,我們應該知道程式比執行緒要大。一個程式至少要有一個程式,一個程式至少要有一個執行緒。就拿我們經常用的瀏覽器為例吧,為了更直觀一些,先看下這張圖片:

瀏覽器事件環和Node事件環不得不說的故事!
由此可見,瀏覽器就是多程式的,當一個網頁崩潰時不會影響其他網頁的正常執行。我們主要了解下一下幾個方面:

  • 渲染引擎:渲染引擎內部是多執行緒的,內部包含了兩個最重要的執行緒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有:setTimeoutsetIntervalsetImmediate(ie瀏覽器才支援,node中自己也實現了)、MessageChannel
  • 常見的微任務microtask有:promise.then()process.nextTick(node的)

二、javaScript的事件環

瀏覽器中,事件環的執行機制是,先會執行棧中的內容,棧中的內容執行後執行微任務,微任務清空後再執行巨集任務,先取出一個巨集任務,再去執行微任務,然後在取巨集任務清微任務這樣不停的迴圈,我們可以看下面這張圖理解一下:

瀏覽器事件環和Node事件環不得不說的故事!

從圖中可以看出,同步任務會進入執行棧,而非同步任務會進入任務佇列(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
複製程式碼
  1. 先執行棧中的內容,也就是同步程式碼,所以2被輸出出來;
  2. 然後清空微任務,所以依次輸出的是 then1 then2
  3. 因程式碼是從上到下執行的,所以1s後 setTimeout1 被執行輸出;
  4. 接著再次清空微任務,then3被輸出;
  5. 最後執行輸出setTimeout2

三、Node的事件環

Node是基於V8引擎的JavaScript執行環境,在處理高併發、I/O密集(檔案操作、網路操作、資料庫操作等)場景有明顯的優勢。Node的事件環機制與瀏覽器的是不太一樣。 在Node執行環境中:

  1. 我們寫的js程式碼會交由V8引擎進行處理
  2. 程式碼中可能會呼叫NodeApi,node會交由libuv處理
  3. libuv通過阻塞I/O和多執行緒實現非同步I/O
  4. 然後通過事件驅動的方式,將結果放到事件佇列中,最終交給我們的應用。

其實本質是在libuv(一個高效能的,事件驅動的I/O庫)內部有這樣一個事件環機制。在Node啟動時會初始化事件環,話不多說,先上圖:

瀏覽器事件環和Node事件環不得不說的故事!
圖中顯示的每個階段都對應一個事件佇列,當event loop執行到某個階段時會將當前階段對應的佇列依次執行。當佇列執行完畢或者執行數量超過上限時,才會轉入下一個階段。node中的微任務在切換佇列時執行。

  • timers計時器:執行setTimeoutsetInterval的回撥函式;
  • 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佇列前面,但是setTimeoutsetImmediate沒有明確的先後順序的,這是由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佇列,setImmediate1setImmediate2連續輸出是因只有當前佇列執行完畢後才能進去下一對列。

總結:今天的話題就先到這裡了。可能本文有表述不清楚或者理解不對的地方,還請各位大佬給予指正,我會繼續努力每週分享,萬分感謝!如果您覺得看了這篇文章有所收穫,請不要忘了動動手指點個小❤️哦!讓我們一起蕩起雙槳,每天進步一點點,在技術的道路上越走越遠!

瀏覽器事件環和Node事件環不得不說的故事!

原創不易,轉載請註明出處!謝謝!

相關文章