麻煩把JS的事件環給我安排一下!!!

上次你們跟我吃飽喝足又擼了一遍PromiseA+,想必你們確定滿腦子想的都是西瓜可樂......node

什麼西瓜可樂!明明是Promise!面試

呃,清醒一下,今天你們搬個小板凳,聽我說說JS中比較有意思的事件環,在瞭解事件環以前呢,咱們先來了解幾個基本概念。api

棧(Stack)

棧是一種遵循後進先出(LIFO)的數據集合,新添加或待刪除的元素都保存在棧的末尾,稱做棧頂,另外一端稱做棧底。在棧裏,新元素都靠近棧頂,舊元素都接近棧底promise

感受提及來並非很好理解,咱們舉個例子,好比有一個乒乓球盒,咱們不停的向球盒中放進乒乓球,那麼最早放進去的乒乓球必定是在最下面,最後放進去的必定是在最上面,那麼若是咱們想要把這些球取出來是否是就必須依次從上到下才能拿出來,這個模型就是後進先出,就是咱們後進入球盒的球反而最早出來。瀏覽器

棧的概念其實在咱們js中十分的重要,你們都知道咱們js是一個單線程語言,那麼他單線程在哪裏呢,就在他的主工做線程,也就是咱們常說的執行上下文,這個執行上下文就是棧空間,咱們來看一段代碼:bash

console.log('1');
 function a(){
    console.log('2');
    function b(){
        console.log('3')
    }
    b()
 }
 a()
 
複製代碼

咱們知道函數執行的時候會將這個函數放入到咱們的執行上下文中,當函數執行完畢以後會彈出執行棧,那麼根據這個原理咱們就能知道這段代碼的運行過程是socket

  1. 首先咱們代碼執行的時候會有一個全局上下文,此時代碼運行,全局上下文進行執行棧,處在棧底的位置
  2. 咱們遇到console.log('1'),這個函數在調用的時候進入執行棧,當這句話執行完畢也就是到了下一行的時候咱們console這個函數就會出棧,此時棧中仍然只有全局上下文
  3. 接着運行代碼,這裏注意的是咱們遇到的函數聲明都不會進入執行棧,只有當咱們的函數被調用被執行的時候纔會進入,這個原理和咱們執行棧的名字也就如出一轍,接着咱們遇到了a();這句代碼這個時候咱們的a函數就進入了執行棧,而後進入到咱們a的函數內部中,此時咱們的函數執行棧應該是 全局上下文 —— a
  4. 接着我運行console.log('2'),執行棧變成 全局上下文——a——console,接着咱們的console運行完畢,咱們執行棧恢復成全局上下文 —— a
  5. 接着咱們遇到了b();那麼b進入咱們的執行棧,全局上下文——a——b,
  6. 接着進入b函數的內部,執行console.log('3')的時候執行棧爲全局上下文——a——b——console,執行完畢以後回覆成全局上下文——a——b
  7. 而後咱們的b函數就執行完畢,而後就被彈出執行棧,那麼執行棧就變成全局上下文——a
  8. 而後咱們的a函數就執行完畢,而後就被彈出執行棧,那麼執行棧就變成全局上下文
  9. 而後咱們的全局上下文會在咱們的瀏覽器關閉的時候出棧

咱們的執行上下文的執行過程就是這樣,是否是清楚了不少~函數

經過上面的執行上下文咱們能夠發現幾個特色:oop

  • 執行上下文是單線程
  • 執行上下文是同步執行代碼
  • 當有函數被調用的時候,這個函數會進入執行上下文
  • 代碼運行會產生一個全局的上下文,只有當瀏覽器關閉纔會出棧

隊列(Queue)

隊列是一種遵循先進先出(FIFO)的數據集合,新的條目會被加到隊列的末尾,舊的條目會從隊列的頭部被移出。ui

這裏咱們能夠看到隊列和棧不一樣的地方是棧是後進先出相似於乒乓球盒,而隊列是先進先出,也就是說最早進入的會最早出去。 一樣咱們舉個例子,隊列就比如是咱們排隊過安檢,最早來到的人排在隊伍的首位,後來的人接着排在隊伍的後面,而後安檢員會從隊伍的首端進行安檢,檢完一我的就放行一我的,是否是這樣的一個隊伍就是先進先出的一個過程。

隊列這裏咱們就要提到兩個概念,宏任務(macro task),微任務(micro task)。

任務隊列

Js的事件執行分爲宏仁務和微任務

  • 宏仁務主要是由script(全局任務),setTimeoutsetIntervalsetImmediate ,I/O ,UI rendering
  • 微任務主要是process.nextTick, Promise.then, Object.observer, MutationObserver.

瀏覽器事件環

js執行代碼的過程當中若是遇到了上述的任務代碼以後,會先把這些代碼的回調放入對應的任務隊列中去,而後繼續執行主線程的代碼知道執行上下文中的函數所有執行完畢了以後,會先去微任務隊列中執行相關的任務,微任務隊列清空以後,在從宏仁務隊列中拿出任務放到執行上下文中,而後繼續循環。

  1. 執行代碼,遇到宏仁務放入宏仁務隊列,遇到微任務放入微任務隊列,執行其餘函數的時候放入執行上下文
  2. 執行上下文中所有執行完畢後,執行微任務隊列
  3. 微任務隊列執行完畢後,再到宏仁務隊列中取出第一項放入執行上下文中執行
  4. 接着就不停循環1-3的步驟,這就是瀏覽器環境中的js事件環
//學了上面的事件環 咱們來看一道面試題
    setTimeout(function () {
      console.log(1);
    }, 0);
    
    Promise.resolve(function () {
      console.log(2);
    })
    
    new Promise(function (resolve) {
      console.log(3);
    });
    
    console.log(4);
    
    //上述代碼的輸出結果是什麼???
複製代碼

思考思考思考思考~~~

正確答案是3 4 1,是否是和你想的同樣?咱們來看一下代碼的運行流程

// 遇到setTimeout 將setTimeout回調放入宏仁務隊列中
    setTimeout(function () {
      console.log(1);
    }, 0);
    // 遇到了promise,可是並無then方法回調 因此這句代碼會在執行過程當中進入咱們當前的執行上下文 緊接着就出棧了
    Promise.resolve(function () {
      console.log(2);
    })
    // 遇到了一個 new Promise,不知道你們還記不記得咱們上一篇文章中講到Promise有一個原則就是在初始化Promise的時候Promise內部的構造器函數會當即執行 所以 在這裏會當即輸出一個3,因此這個3是第一個輸入的
    new Promise(function (resolve) {
      console.log(3);
    });
    // 而後輸入第二個輸出4  當代碼執行完畢後回去微任務隊列查找有沒有任務,發現微任務隊列是空的,那麼就去宏仁務隊列中查找,發現有一個咱們剛剛放進去的setTimeout回調函數,那麼就取出這個任務進行執行,因此緊接着輸出1
    console.log(4);
複製代碼

看到上述的講解,你們是否是都明白了,是否是直呼簡單~

那咱們接下來來看看node環境中的事件執行環

NodeJs 事件環

瀏覽器的 Event Loop 遵循的是 HTML5 標準,而 NodeJs 的 Event Loop 遵循的是 libuv標準,所以呢在事件的執行中就會有必定的差別,你們都知道nodejs實際上是js的一種runtime,也就是運行環境,那麼在這種環境中nodejs的api大部分都是經過回調函數,事件發佈訂閱的方式來執行的,那麼在這樣的環境中咱們代碼的執行順序到底是怎麼樣的呢,也就是咱們不一樣的回調函數到底是怎麼分類的而後是按照什麼順序執行的,其實就是由咱們的libuv所決定的。

┌───────────────────────────┐
                ┌─>│           timers          │
                │  └─────────────┬─────────────┘
                │  ┌─────────────┴─────────────┐
                │  │     pending callbacks     │
                │  └─────────────┬─────────────┘
                │  ┌─────────────┴─────────────┐
                │  │       idle, prepare       │
                │  └─────────────┬─────────────┘      ┌───────────────┐
                │  ┌─────────────┴─────────────┐      │   incoming:   │
                │  │           poll            │<─────┤  connections, │
                │  └─────────────┬─────────────┘      │   data, etc.  │
                │  ┌─────────────┴─────────────┐      └───────────────┘
                │  │           check           │
                │  └─────────────┬─────────────┘
                │  ┌─────────────┴─────────────┐
                └──┤      close callbacks      │
                   └───────────────────────────┘
複製代碼

咱們先來看下這六個任務是用來幹什麼的

  • timers: 這個階段執行setTimeout()和setInterval()設定的回調。
  • pending callbacks: 上一輪循環中有少數的 I/O callback會被延遲到這一輪的這一階段執行。
  • idle, prepare: 僅內部使用。
  • poll: 執行 I/O callback,在適當的條件下會阻塞在這個階段
  • check: 執行setImmediate()設定的回調。
  • close callbacks: 執行好比socket.on('close', ...)的回調。

咱們再來看網上找到的一張nodejs執行圖,咱們能看到圖中有六個步驟 ,當代碼執行中若是咱們遇到了這六個步驟中的回調函數,就放入對應的隊列中,而後當咱們同步人物執行完畢的時候就會切換到下一個階段,也就是timer階段,而後timer階段執行過程當中會把這個階段的全部回調函數所有執行了而後再進入下一個階段,須要注意的是咱們在每次階段發生切換的時候都會先執行一次微任務隊列中的全部任務,而後再進入到下一個任務階段中去,因此咱們就能總結出nodejs的事件環順序

  1. 同步代碼執行,清空微任務隊列,執行timer階段的回調函數(也就是setTimeout,setInterval)
  2. 所有執行完畢,清空微任務隊列,執行pending callbacks階段的回調函數
  3. 所有執行完畢,清空微任務隊列,執行idle, prepare階段的回調函數
  4. 所有執行完畢,清空微任務隊列,執行poll階段的回調函數
  5. 所有執行完畢,清空微任務隊列,執行check階段的回調函數(也就是setImmediate)
  6. 所有執行完畢,清空微任務隊列,執行close callbacks階段的回調函數
  7. 而後循環1-6階段

那咱們來練練手~~~

// 咱們來對着咱們的執行階段看看
    let fs = require('fs');
    // 遇到setTimeout 放入timer回調中
    setTimeout(function(){
        Promise.resolve().then(()=>{
            console.log('then1');
        })
    },0);
    // 放入微任務隊列中
    Promise.resolve().then(()=>{
        console.log('then2');
    });
    // i/o操做 放入pending callbacks回調中
    fs.readFile('./text.md',function(){
        // 放入check階段
        setImmediate(()=>{
            console.log('setImmediate')
        });
        // 放入微任務隊列中
        process.nextTick(function(){
            console.log('nextTick')
        })
    });
複製代碼

首先同步代碼執行完畢,咱們先清空微任務,此時輸出then2,而後切換到timer階段,執行timer回調,輸出then1,而後執行i/o操做回調,而後清空微任務隊列,輸出nextTick,接着進入check階段,清空check階段回調輸出setImmediate

全部的規則看着都雲裏霧裏,可是呢只要咱們總結出來了規律,理解了他們的運行機制那麼咱們就掌握了這些規則,好咯,今天又學了這麼多,不說了不說了,趕忙滾去寫業務代碼了.............

相關文章
相關標籤/搜索