JavaScript 事件循環詳解(翻譯)

最近在搜索更詳細的關於JS事件處理的資料。發現國內的大部分blog都是相互抄襲。而MDM對於這裏的解釋也並很少。翻閱了部分文章,發現這篇文章頗有價值。故譯之。雖然文章寫於2013年,可是依然具備很高參考價值javascript

原文:The JavaScript Event Loop: Explained java

這篇文章是講什麼的?

對於目前Web瀏覽器上最流行的腳本語言JavaScript。這篇文章爲你提供了該語言基本的事件驅動模型的講解,它與那些典型的有求必應的語言好比Ruby, Python, Java不一樣。在這篇文章中,我會爲你解釋這些JavaScript中併發模型的核心概念,包括事件循環,消息隊列來幫助你提升對這門已經使用過,可是尚未完全理解的語言更深刻的理解。瀏覽器

誰適合讀這篇文章?

這篇文章面向那些已經開始彷佛用JavaScript語言從事Web開發的工程師,或者是計劃從事這項工做的人員。若是你已經很是熟悉JavaScript的事件循環機制,那麼你會以爲這篇文章的內容對於你來講已經再熟悉不過了。對於那些沒有對事件循環充分了解的人,我但願這篇文章可以幫助到你,這樣才能讓你更好的理解你天天面對的代碼。ruby

非阻塞 I/O(Non-blocking I/O)

JavaScript幾乎全部的I/O都是非阻塞的。包括HTTP請求,數據訪問,讀寫磁盤。一個單線程在運行時去處理這些操做,提供一個回調函數,而後接着去作其它的事情。當操做完成了,這個回調函數提供的消息會被推送到隊列中。在某個時間點,消息從隊列中被移除,緊接着回調函數就被觸發了。服務器

雖然這個交互模型對於不少開發者來講已經很是熟悉了 -- 好比 mousedownclick 事件的處理,- 可是這與那種典型服務器端同步的請求處理不一樣。閉包

讓咱們對比一下向 www.google.com 發出請求後將返回的代碼輸出到控制檯。首先,Ruby 的話:併發

response = Faraday.get 'http://www.google.com'
    puts response
    puts 'Done!'
複製代碼

執行路徑大概是這個樣子的:異步

  1. get 方法被執行,而後執行線程開始等待,直到收到響應
  2. Google 收到響應而且返回到回調並存儲到一個變量中
  3. 變量的值(返回的響應結果)輸出到控制檯中
  4. Done 被輸出到控制檯

讓咱們利用 Node.js 中完成同樣的事情看看:函數

request('http://www.google.com', function(error, response, body) {
    console.log(body);
    });
     
    console.log('Done!');
複製代碼

看起來一個顯著的不一樣和不一樣的行爲:oop

  1. 請求函數被執行,傳遞一個匿名函數做爲一個回調執行函數,在收到響應後執行
  2. Done 被立刻輸出到控制套
  3. 有時候,響應返回了,咱們的回調函數也執行力,響應的主體被輸出到了控制檯

事件循環

將請求的響應以回調函數的方式處理,容許 JavaScript在等待異步操做成功返回並執行回調函數以前能夠作一些其餘的事情。可是,在內存中怎麼執行這些回調的呢?執行順序是什麼樣的呢?什麼致使他們被調用的呢?

JavaScript 的運行環境有一個用於存儲消息和用於關聯回調函數的消息隊列。這些消息以事件被註冊的順序進行排列(好比鼠標點擊事件或者是 HTTP 請求響應事件)。好比用戶點擊一個按鈕,若是沒有該事件的回調函數被註冊,那麼就沒有消息加入隊列。

在循環中,隊列輪詢下一個消息(每一次輪詢被看成是一個 tick),若是有消息,那麼就執行消息對應的回調。

回調函數的調用做爲調用堆棧的初始幀,因爲JavaScript是單線程的,消息輪詢和處理會被中止,直到堆棧內的回調函數所有返回。後續函數調用(同步)向堆棧添加新的調用(例如初始化顏色)。

function init() {
    var link = document.getElementById("foo");
     
    link.addEventListener("click", function changeColor() {
    this.style.color = "burlywood";
    });
    }
     
    init();
複製代碼

在這個例子中,當用戶點擊頁面元素,而後一個onclick 事件被觸發,一個消息被壓入隊列中。當消息被壓入隊列,他的回調函數changeColor被執行。當changeColor返回的時候(也多是拋出異常),事件循環就繼續執行。只要changeColor被指定爲onclick的回調函數,後面在該元素的點擊會致使更多的消息(以及相關changeColor回調的消息)被壓入隊列。

隊列中的額外消息

若是函數在你的代碼中被異步調用(好比 setTimeout),在以後的事件循環中,回調函數會以另外一個消息隊列的一部分被執行。好比:

function f() {
        console.log("foo");
        setTimeout(g, 0);
        console.log("baz");
        h();
    }
     
    function g() {
        console.log("bar");
    }
     
    function h() {
        console.log("blix");
    }
     
    f();

複製代碼

因爲setTimeout是非阻塞的,它會在0毫秒後被執行,而且並非做爲此消息的一部分被處理。在這個例子中,setTimeout被調用,傳入一個回調函數 g 和 一個超時事件 0 毫秒。當時間到了之後,一個以g爲回調函數的消息將會被壓入隊列中。控制檯會輸出相似: foo, baz, blix 而後在下一次事件循環輸出: bar。若是在同一個調用幀中(譯者注:就是一個函數內)執行了兩次 setTimeout,傳入相同的值(譯者注: 時間間隔)。他們會按照前後順序執行。

Web Workers

Web Worker 容許你將昂貴的操做轉入到獨立到線程中執行,節約主要線程去作其它事情。worker具備獨立的消息隊列,事件循環,和獨立的內存空間。worker 與主線程經過消息來完成通信,這看起來有點像以前的事件處理那樣。

首先,咱們的 worker:

// our worker, which does some CPU-intensive operation
    var reportResult = function(e) {
        pi = SomeLib.computePiToSpecifiedDecimals(e.data);
        postMessage(pi);
    };
     
    onmessage = reportResult;
複製代碼

而後是咱們的js代碼:

// our main code, in a <script>-tag in our HTML page
    var piWorker = new Worker("pi_calculator.js");
    var logResult = function(e) {
        console.log("PI: " + e.data);
    };
     
    piWorker.addEventListener("message", logResult, false);
    piWorker.postMessage(100000);
複製代碼

在這個例子中,主線程衍生並啓動一個worker,而後並將logResult這個回調加入到事件循環。在worker中,reportResult被註冊到本身的message事件中。當worker從主線程接收到消息,worker就會返回一個消息,所以就會致使reportResult被執行。

當進行壓棧的時候,一個消息會被推送到主線程,並被壓入消息堆棧(而後執行回調函數)。經過這個方式,開發人員能夠將cpu密集型操做委託給獨立的線程,釋放主線程去繼續處理消息和事件。

關於閉包

JavaScript支持在回調函數中使用閉包,該回調在執行時維持對建立它們的環境的訪問,即便回調執行完建立了新的調用堆棧。對於知道回調是以不一樣的消息被執行的要比知道回調被建立更有趣。思考下面的代碼:

function changeHeaderDeferred() {
        var header = document.getElementById("header");
     
        setTimeout(function changeHeader() {
            header.style.color = "red";
     
            return false;
        }, 100);
     
        return false;
    }
     
    changeHeaderDeferred();
複製代碼

這個例子中,changeHeaderDeferred執行的時候包含了header變量。setTimeout被執行,100毫秒後一個消息被添加到消息隊列中。changeHeaderDeferred函數返回了false,結束了此次處理 - 可是回調函數中依然保留着header的引用,因此沒有被垃圾回收。當第二個消息被處理的時候,函數體外(changeHeaderDeferred)它依然保持這對header的聲明。第二次處理完後,header才被垃圾回收處理。

相關文章
相關標籤/搜索