一次弄懂event loop

Event Loop 是 JavaScript 異步編程的核心思想,也是前端進階必須跨越的一關。同時,它又是面試的必考點,特別是在 Promise 出現以後,各類各樣的面試題層出不窮,花樣百出。這篇文章從現實生活中的例子入手,讓你完全理解 Event Loop 的原理和機制,並能遊刃有餘的解決此類面試題。

先來一道面試題鎮樓html

async function async1() { 
    console.log('async1 start'); 
await async2(); 
    console.log('async1 end'); 
} 
async function async2() { 
    console.log('async2'); 
} 
console.log('script start'); 
setTimeout(function() { 
    console.log('setTimeout'); 
}, 0); 
async1(); 
new Promise(function(resolve) { 
    console.log('promise1'); 
    resolve(); 
}).then(function() { 
    console.log('promise2'); 
}); 
console.log('script end');`

你是否有見過此類面試題?接下來讓咱們一步一步搞懂他!前端

先從js提及

首先明確一點,js是一門單線程語言。也就是說同一時間只能作一件事。
JavaScript爲何是單線程?與它的用途有關。做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?java

因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。
但單線程容易引發阻塞,好比:面試

alert(1);
console.log(2);
console.log(3);
console.log(4);

alert彈框只要不點擊肯定那就永遠不會打印出2,3,4。
爲了防止主線程堵塞,javaScript有了同步和異步的概念。編程

接下來咱們說說同步、異步

同步:同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務。
異步:異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。這也就是定時器並不能精確在指定時間後輸出回調函數結果的緣由。promise

具體來講,異步執行的運行機制以下。(同步執行也是如此,由於它能夠被視爲沒有異步任務的異步執行。)瀏覽器

(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。數據結構

(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。異步

(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。async

(4)主線程不斷重複上面的第三步。

上文提到了「執行棧、任務隊列」,下面咱們來嘮嘮他倆是啥
執行棧

當咱們調用一個方法的時候,JavaScript 會生成一個與這個方法對應的執行環境,又叫執行上下文(context)。這個執行環境中保存着該方法的私有做用域、上層做用域(做用域鏈)、方法的參數,以及這個做用域中定義的變量和 this 的指向,而當一系列方法被依次調用的時候。因爲 JavaScript 是單線程的,這些方法就會按順序被排列在一個單獨的地方,這個地方就是所謂執行棧。

任務隊列

"任務隊列"是一個事件的隊列(也能夠理解成消息的隊列),IO設備完成一項任務,就在"任務隊列"中添加一個事件,表示相關的異步任務能夠進入"執行棧"了。主線程讀取"任務隊列",就是讀取裏面有哪些事件。

"任務隊列"中的事件,除了IO設備的事件之外,還包括一些用戶產生的事件(好比鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入"任務隊列",等待主線程讀取。

所謂"回調函數"(callback),就是那些會被主線程掛起來的代碼。異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數。

"任務隊列"是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。可是,因爲存在後文提到的"定時器"功能,主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程。

瞭解了前面的知識後,咱們再來看啥是事件循環(event loop)

咱們注意到,在異步代碼完成後仍有可能要在一旁等待,由於此時程序可能在作其餘的事情,等到程序空閒下來纔有時間去看哪些異步已經完成了。因此 JavaScript 有一套機制去處理同步和異步操做,那就是事件循環 (Event Loop)。
示意圖以下:

宏任務和微任務

以去銀行辦業務爲例,當 5 號窗口櫃員處理完當前客戶後,開始叫號來接待下一位客戶,咱們將每一個客戶比做 宏任務接待下一位客戶 的過程也就是讓下一個 宏任務 進入到執行棧。

因此該窗口全部的客戶都被放入了一個 任務隊列 中。任務隊列中的都是 已經完成的異步操做的,而不是註冊一個異步任務就會被放在這個任務隊列中(它會被放到 Task Table 中)。就像在銀行中排號,若是叫到你的時候你不在,那麼你當前的號牌就做廢了,櫃員會選擇直接跳過進行下一個客戶的業務處理,等你回來之後還須要從新取號。

在執行宏任務時,是能夠穿插一些微任務進去。好比你大爺在辦完業務以後,順便問了下櫃員:「最近 P2P 暴雷很嚴重啊,有沒有其餘穩妥的投資方式」。櫃員暗爽:「又有傻子上鉤了」,而後嘰裏咕嚕說了一堆。

咱們分析一下這個過程,雖然大爺已經辦完正常的業務,但又諮詢了一下理財信息,這時候櫃員確定不能說:「您再上後邊取個號去,從新排隊」。因此只要是櫃員可以處理的,都會在響應下一個宏任務以前來作,咱們能夠把這些任務理解成是 微任務

大爺聽罷,揚起 45 度微笑,說:「我就問問。」

櫃員 OS:「艹...」

這個例子就說明了:你大爺永遠是你大爺 在當前微任務沒有執行完成時,是不會執行下一個宏任務的!

總結一下,異步任務分爲 宏任務(macrotask)微任務 (microtask)。宏任務會進入一個隊列,而微任務會進入到另外一個不一樣的隊列,且微任務要優於宏任務執行。

常見的宏任務和微任務

宏任務:script(總體代碼)、setTimeout、setInterval、I/O、事件、postMessage、 MessageChannel、setImmediate (Node.js)

微任務:Promise.then、 MutaionObserver、process.nextTick (Node.js)

來幾道題試試?
setTimeout(() => {
        console.log('A');
    }, 0);
    var obj = {
    func: function() {
        setTimeout(function() {
        console.log('B');
        }, 0);
        return new Promise(function(resolve) {
        console.log('C');
        resolve();
        });
    },
    };
    obj.func().then(function() {
        console.log('D');
    });
    console.log('E');

先把打印結果呈上
image.png
再把解釋呈上:

  • 第一個 setTimeout 放到宏任務隊列,此時宏任務隊列爲 ['A']
  • 接着執行 obj 的 func 方法,將 setTimeout 放到宏任務隊列,此時宏任務隊列爲 ['A', 'B']
  • 函數返回一個 Promise,由於這是一個同步操做,因此先打印出 'C'
  • 接着將 then 放到微任務隊列,此時微任務隊列爲 ['D']
  • 接着執行同步任務 console.log('E');,打印出 'E'
  • 由於微任務優先執行,因此先輸出 'D'
  • 最後依次輸出 'A''B'

再來一個?

let p = new Promise(resolve => {
        resolve(1);
        Promise.resolve().then(() => console.log(2));
        console.log(4);
    }).then(t => console.log(t));
    console.log(3);

打印結果:
image.png

  • 首先將 Promise.resolve() 的 then() 方法放到微任務隊列,此時微任務隊列爲 ['2']
  • 而後打印出同步任務 4
  • 接着將 p 的 then() 方法放到微任務隊列,此時微任務隊列爲 ['2', '1']
  • 打印出同步任務 3
  • 最後依次打印微任務 21
當 Event Loop 遇到 async/await

async/await 僅僅是生成器的語法糖,因此不要怕,只要把它轉換成 Promise 的形式便可。下面這段代碼是 async/await 函數的經典形式。

async function foo() {
    // await 前面的代碼
    await bar();
    // await 後面的代碼
   }
    async function bar() {
    // do something...
    }
    foo();

其中 await 前面的代碼 是同步的,調用此函數時會直接執行;而 await bar(); 這句能夠被轉換成 Promise.resolve(bar())await 後面的代碼 則會被放到 Promise 的 then() 方法裏。所以上面的代碼能夠被轉換成以下形式,這樣是否是就很清晰了?

function foo() {
        // await 前面的代碼
        Promise.resolve(bar()).then(() => {
            // await 後面的代碼
        });
    }
    function bar() {
        // do something...
    }
    foo();

最後咱們回到開篇那個題目

function async1() {
  console.log('async1 start'); // 2

  Promise.resolve(async2()).then(() => {
    console.log('async1 end'); // 6
  });
}

function async2() {
  console.log('async2'); // 3
}

console.log('script start'); // 1

setTimeout(function() {
  console.log('settimeout'); // 8
}, 0);

async1();

new Promise(function(resolve) {
  console.log('promise1'); // 4
  resolve();
}).then(function() {
  console.log('promise2'); // 7
});
console.log('script end'); // 5
  • 首先打印出 script start
  • 接着將 settimeout 添加到宏任務隊列,此時宏任務隊列爲 ['settimeout']
  • 而後執行函數 async1,先打印出 async1 start,又由於 Promise.resolve(async2()) 是同步任務,因此打印出 async2,接着將 async1 end 添加到微任務隊列,,此時微任務隊列爲 ['async1 end']
  • 接着打印出 promise1,將 promise2 添加到微任務隊列,,此時微任務隊列爲 ['async1 end', promise2]
  • 打印出 script end
  • 由於微任務優先級高於宏任務,因此先依次打印出 async1 endpromise2
  • 最後打印出宏任務 settimeout
Node.js 與 瀏覽器環境下事件循環的區別

Node.js 在升級到 11.x 後,Event Loop 運行原理髮生了變化,一旦執行一個階段裏的一個宏任務(setTimeout,setInterval 和 setImmediate) 就馬上執行微任務隊列,這點就跟瀏覽器端一致。

案例
案例1
const p1 = new Promise((resolve, reject) => {
        console.log('promise1');
        resolve();
    })
   .then(() => {
        console.log('then11');
        new Promise((resolve, reject) => {
            console.log('promise2');
            resolve();
        })
        .then(() => {
            console.log('then21');
        })
        .then(() => {
            console.log('then23');
        });
  })
  .then(() => {
    console.log('then12');
  });

    const p2 = new Promise((resolve, reject) => {
    console.log('promise3');
        resolve();
    }).then(() => {
        console.log('then31');
    });
  • 首先打印出 promise1
  • 接着將 then11promise2 添加到微任務隊列,此時微任務隊列爲 ['then11', 'promise2']
  • 打印出 promise3,將 then31 添加到微任務隊列,此時微任務隊列爲 ['then11', 'promise2', 'then31']
  • 依次打印出 then11promise2then31,此時微任務隊列爲空
  • then21then12 添加到微任務隊列,此時微任務隊列爲 ['then21', 'then12'](由於then21和then12都是第二層then)
  • 依次打印出 then21then12,此時微任務隊列爲空
  • then23 添加到微任務隊列,此時微任務隊列爲 ['then23']
  • 打印出 then23
案例2

這道題實際在考察 Promise 的用法,當在 then() 方法中返回一個 Promise,p1 的第二個完成處理函數就會掛在返回的這個 Promise 的 then() 方法下,所以輸出順序以下。

const p1 = new Promise((resolve, reject) => {
        console.log('promise1'); // 1
        resolve();
    })
    .then(() => {
        console.log('then11'); // 2
        return new Promise((resolve, reject) => {
            console.log('promise2'); // 3
            resolve();
        })
        .then(() => {
            console.log('then21'); // 4
        })
        .then(() => {
            console.log('then23'); // 5
        });
    })
    .then(() => {
        console.log('then12'); //6
    });

將不斷更新完善,歡迎批評指正!

參考

http://www.ruanyifeng.com/blo...
https://juejin.im/post/5cbc0a...

相關文章
相關標籤/搜索