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

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

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

好了,話很少說,接下來讓咱們進入今天的話題。今天咱們來談一談事件環究竟是什麼?javaScript的事件環和Node的事件環有什麼區別?有沒有一種無從下手的感受,別捉急,只要你仔細閱讀本篇文章,相信可以解開心中的疑惑。 java

1、先了解幾組常見概念

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

一、heap(堆)和 stack(棧)

堆棧是在計算機領域不可忽視的概念,若是想要詳細瞭解,請移步《堆棧_百度百科》。在javaScript中,棧中存的是基本數據類型,會自動分配內存空間,自動釋放;堆中存的是引用數據類型,是動態分配的內存,大小不定也不會自動釋放。git

  • heap堆:也能夠叫堆內存;是一種隊列優先,先進先出的數據結構;
  • stack棧:又名'堆棧',也是一種數據結構,不過它是按照先進後出原則存儲數據的。

嘻嘻😝,本身花了半天時間(誇張)畫得,自我感受良好。github

既然咱們大體理解了堆和棧的含義,咱們來看一道面試題,如何用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();
複製代碼

二、線程和進程

首先,咱們應該知道進程比線程要大。一個程序至少要有一個進程,一個進程至少要有一個線程。就拿咱們常常用的瀏覽器爲例吧,爲了更直觀一些,先看下這張圖片:ajax

因而可知,瀏覽器就是多進程的,當一個網頁崩潰時不會影響其餘網頁的正常運行。咱們主要了解下一下幾個方面:

  • 渲染引擎:渲染引擎內部是多線程的,內部包含了兩個最重要的線程ui線程和js線程。這裏要特別注意ui線程和js線程是互斥的,由於JS運行結果會影響到ui線程的結果。ui更新會被保存在隊列中等到js線程空閒時當即被執行。
  • js單線程:JavaScript最大的特色就是單線程的,其實應該說其主線程是單線程的。爲何這麼說呢?你想一下,若是js是多線程的,咱們在頁面中這個線程要刪了那個元素(不順眼),另外一個線程呢我要保留那個元素(我罩着的),這樣豈不是就亂套了。這也是爲何JavaScript執行同步代碼,異步代碼並不會阻塞代碼的運行。
  • 其餘線程:
    • 瀏覽器事件觸發線程(用來控制事件循環,存放setTimeout、瀏覽器事件、ajax的回調函數)
    • 定時觸發器線程(setTimeout定時器所在線程)
    • 異步HTTP請求線程(ajax請求線程)

三、宏任務和微任務

宏任務和微任務能夠說都是異步任務。若是瞭解vue源碼的同窗,應該知道宏任務macrotask和微任務microtask這兩個概念,他們的執行時機是不同的。vue$nextTick的源碼就是經過宏任務和微任務實現的。(能夠去vue的github瞭解一下其實現原理)。數據庫

  • 常見的宏任務macrotask有:setTimeoutsetIntervalsetImmediate(ie瀏覽器才支持,node中本身也實現了)、MessageChannel
  • 常見的微任務microtask有:promise.then()process.nextTick(node的)

2、javaScript的事件環

瀏覽器中,事件環的運行機制是,先會執行棧中的內容,棧中的內容執行後執行微任務,微任務清空後再執行宏任務,先取出一個宏任務,再去執行微任務,而後在取宏任務清微任務這樣不停的循環,咱們能夠看下面這張圖理解一下:數組

從圖中能夠看出,同步任務會進入執行棧,而異步任務會進入任務隊列(callback queue)等待執行。一旦執行棧中的內容執行完畢,就會讀取任務隊列中等待的任務放入執行棧開始執行。(圖中缺乏微任務)promise

那麼,咱們來道面試題檢驗一下,當咱們在瀏覽器中運行下面的代碼,輸出的結果是什麼呢?

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

3、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啓動時會初始化事件環,話很少說,先上圖:

圖中顯示的每一個階段都對應一個事件隊列,當 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、setTimeout二、nextTick、setImmediate一、setImmediate二、setTimeout1,爲何是這樣的順序呢?微任務nextTick的輸出是由於timers隊列切換到check隊列,setImmediate1setImmediate2連續輸出是因只有當前隊列執行完畢後才能進去下一對列。

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

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

相關文章
相關標籤/搜索