異步編程之事件循環機制

JavaScript 是一門單線程語言,咱們能夠經過異步編程的方式來實現實現相似於多線程語言的併發操做。javascript

本文着重講解經過事件循環機制來實現多個異步操做的有序執行、併發執行;經過事件隊列實現同級多個併發操做的前後執行順序,經過微任務和宏任務的概念來說解不一樣階段任務執行的前後順序,最後經過將瀏覽器和 Node 下的事件循環機制進行對比,對比其事件循環機制的不一樣之處,以及在 Node 端經過libuv引擎來實現多個異步任務的併發執行。java

1、前言

咱們知道JavaScript 是一門單線程語言,對於大多數人而言,單線程最大的好處是不用像多線程那樣到處在乎狀態的同步問題,這裏沒有死鎖的存在,也沒有像多線程之間來回切換帶來性能上的開銷。一樣,單線程也存在自身的弱點,主要表如今如下幾個方面:node

  1. 沒法利用多核cpu,一個簡單的例子,在一個位置從同一臺服務器拉取不一樣的資源,若是採用單線程同步的方式去拉取,代碼大體以下:編程

    getData(‘from_db’),//耗時爲M,
    getData(‘from_db_api’),//耗時爲N,
    若是採用同步單線程的方式總共耗時爲:M+N
  2. js代碼錯誤或者耗時過長會阻塞後面代碼的執行,例如頁面在進行dom渲染時,若是頁面的js代碼報錯會引發整個頁面白屏的現象。api

  3. 大量計算佔用CPU致使沒法繼續調用異步I/O。
    後來HTML5定製了Web Workers可以建立多線程來進行計算,可是使用Web Workers技術開的多線程有着諸多的限制,例如:全部新線程都受主線程的徹底控制,不能獨立執行。這意味着這些「線程」 實際上應屬於主線程的子線程。另外,這些子線程並無執行I/O操做的權限,只能爲主線程分擔一些簡單的計算任務。因此嚴格來說這些線程並無完整的功能,也所以這項技術並不是改變了 JavaScript 語言的單線程本質。瀏覽器

    因此咱們能夠預見,將來的 JavaScript 依然會是一門單線程語言,所以JavaScript採用異步編程方式實現程序「非阻塞」的特色,那麼咱們如何實現這一特徵了,答案就是咱們今天要講的——event loop(事件循環)。服務器

2、瀏覽器下的事件循環機制

一、執行棧

JavaScript變量主要存儲在堆和棧兩個位置,其中,堆裏主要存儲對象,棧主要存儲基本類型的變量以及指針變量。當咱們調用一個方法時,JS 會生成一個與這個方法對應的執行環境,又叫執行上下文,當一系列方法被調用時,因爲咱們的js是單線程的,因此這些方法會被單獨排在一個地方,這個地方叫作執行棧。
當一個腳本第一次執行的時候,JS  引擎會解析這段代碼,並將其中的同步代碼按照執行順序加入執行棧中,而後從頭開始執行。若是當前執行的是一個方法,那麼 JS 會向執行棧中添加這個方法的執行環境,而後進入這個執行環境繼續執行其中的代碼。當這個執行環境中的代碼 執行完畢並返回結果後,JS 會退出這個執行環境並把這個執行環境銷燬,回到上一個方法的執行環境。這個過程反覆進行,直到執行棧中的代碼所有執行完畢。網絡

二、事件隊列

以上說的都是 JS 同步代碼的執行,那麼當程序執行異步代碼後會如何進行呢?咱們前面提到過 JS 最大的特色是非阻塞,下面咱們說一下實現這一點的關鍵在於這項機制——事件隊列。多線程

當js引擎遇到一個異步事件後不會一直等待返回結果,這個事件會先掛起,繼續執行執行棧中的其餘任務,直到這個異步事件的結果返回,JS 引擎會將這個事件放入與當前執行棧不一樣的一個隊列中,咱們稱之爲事件隊列。併發

被放入事件隊列不會馬上執行其回調,而是等待當前執行棧中的全部任務都執行完畢, 主線程處於閒置狀態時,主線程會去查找事件隊列是否有任務。若是有,那麼主線程會從中取出排在第一位的事件,並把這個事件對應的回調放入執行棧中,而後執行其中的同步代碼...,如此反覆,這樣就造成了一個無限的循環。這就是這個過程被稱爲「事件循環(Event Loop)」的緣由。

異步編程之事件循環機制

(圖片來源:網絡)

三、微任務和宏任務

關於微任務和宏任務咱們能夠用一張圖來講明:

異步編程之事件循環機制

(圖片來源:網絡)

在一個事件循環中,異步事件返回結果後會被放到一個任務隊列中。然而,根據這個異步事件的類型,這個事件實際上會被對應的宏任務隊列或者微任務隊列中去。而且在當前執行棧爲空的時候,主線程會 查看微任務隊列是否有事件存在。若是不存在,那麼再去宏任務隊列中取出一個事件並把對應的回調加入當前執行棧;若是存在,則會依次執行隊列中事件對應的回調,直到微任務隊列爲空,而後去宏任務隊列中取出最前面的一個事件,把對應的回調加入當前執行棧...如此反覆,進入循環。

宏任務主要包含:script( 總體代碼)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 環境)

微任務主要包含:Promise、MutaionObserver、process.nextTick(Node.js 環境)

3、Node環境下的事件循環模型

與瀏覽器有何異同?

在 Node 中,事件循環表現出的狀態與瀏覽器中大體相同。不一樣的是 Node  中有一套本身的模型。Node  中事件循環的實現是依靠的libuv引擎。咱們知道 Node  選擇Chrome V8引擎做爲js解釋器,V8引擎將js代碼分析後去調用對應的Node   api,而這些api最後則由libuv引擎驅動,執行對應的任務,並把不一樣的事件放在不一樣的隊列中等待主線程執行。所以實際上 Node  中的事件循環存在於libuv引擎中。

異步編程之事件循環機制

(圖片來源:網絡)

從上面這個模型中,咱們能夠大體分析出 Node  中的事件循環的順序:

外部輸入數據-->輪詢階段(poll)-->檢查階段(check)-->關閉事件回調階段(close callback)-->定時器檢測階段(timer)-->I/O事件回調階段(I/O callbacks)-->閒置階段(idle, prepare)-->輪詢階段...

以上各階段的名稱是根據我我的理解的翻譯,爲了不錯誤和歧義,下面解釋的時候會用英文來表示這些階段。這些階段大體的功能以下:

timers: 這個階段執行定時器隊列中的回調如 setTimeout() 和 setInterval()。

  • I/O callbacks: 這個階段執行幾乎全部的回調。可是不包括close事件,定時器和setImmediate()的回調。
  • idle, prepare: 這個階段僅在內部使用,能夠沒必要理會。
  • poll: 等待新的I/O事件,node在一些特殊狀況下會阻塞在這裏。
  • check: setImmediate()的回調會在這個階段執行。
  • close callbacks: 例如socket.on('close', ...)這種close事件的回調。

4、小結

JavaScript事件循環是很是重要的一個基礎概念,咱們能夠經過這種機制實現異步編程,解決JavaScript同步單線程沒法實現併發操做的問題,可使咱們對一段異步代碼的執行順序有一個清晰的認識,從而減小代碼運行的不肯定性。合理的使用各類延遲事件的方法,有助於代碼更好的按照其優先級去執行。

做者:Liu Gang

相關文章
相關標籤/搜索