JavaScript 運行機制詳解:再談Event Loop

一年前,我寫了一篇《什麼是 Event Loop?》,談了我對Event Loop的理解。html

  上個月,我偶然看到了Philip Roberts的演講《Help, I'm stuck in an event-loop》。這才尷尬地發現,本身的理解是錯的。我決定重寫這個題目,詳細、完整、正確地描述JavaScript引擎的內部運行機制。下面就是個人重寫。node

  進入正文以前,插播一條消息。個人新書《ECMAScript 6入門》出版了(版權頁內頁1內頁2),銅版紙全綵印刷,很是精美,還附有索引(固然價格也比同類書籍略貴一點點)。預覽和購買點擊這裏git

cover

 1、爲何JavaScript是單線程?

  JavaScript語言的一大特色就是單線程,也就是說,同一個時間只能作一件事。那麼,爲何JavaScript不能有多個線程呢?這樣能提升效率啊。程序員

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

  因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。github

  爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。web

 2、任務隊列

  單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就不得不一直等着。vim

  若是排隊是由於計算量大,CPU忙不過來,倒也算了,可是不少時候CPU是閒着的,由於IO設備(輸入輸出設備)很慢(好比Ajax操做從網絡讀取數據),不得不等着結果出來,再往下執行。瀏覽器

  JavaScript語言的設計者意識到,這時CPU徹底能夠無論IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回告終果,再回過頭,把掛起的任務繼續執行下去。服務器

  因而,JavaScript就有了兩種執行方式:一種是CPU按順序執行,前一個任務結束,再執行下一個任務,這叫作同步執行;另外一種是CPU跳過等待時間長的任務,先處理後面的任務,這叫作異步執行。程序員自主選擇,採用哪一種執行方式。

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

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

(2)主線程以外,還存在一個"任務隊列"(task queue)。系統把異步任務放到"任務隊列"之中,而後繼續執行後續的任務。

(3)一旦"執行棧"中的全部任務執行完畢,系統就會讀取"任務隊列"。若是這個時候,異步任務已經結束了等待狀態,就會從"任務隊列"進入執行棧,恢復執行。

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

  下圖就是主線程和任務隊列的示意圖。

任務隊列

  只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會不斷重複。

 3、事件和回調函數

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

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

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

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

 4、Event Loop

  主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)。

  爲了更好地理解Event Loop,請看下圖(轉引自Philip Roberts的演講《Help, I'm stuck in an event-loop》)。

Event Loop

  上圖中,主線程運行的時候,產生堆(heap)和棧(stack),棧中的代碼調用各類外部API,它們在"任務隊列"中加入各類事件(click,load,done)。只要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",依次執行那些事件所對應的回調函數。

  執行棧中的代碼,老是在讀取"任務隊列"以前執行。請看下面這個例子。

    var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.onload = function (){};    
    req.onerror = function (){};    
    req.send();

  上面代碼中的req.send方法是Ajax操做向服務器發送數據,它是一個異步任務,意味着只有當前腳本的全部代碼執行完,系統纔會去讀取"任務隊列"。因此,它與下面的寫法等價。

    var req = new XMLHttpRequest();
    req.open('GET', url);
    req.send();
    req.onload = function (){};    
    req.onerror = function (){};   

  也就是說,指定回調函數的部分(onload和onerror),在send()方法的前面或後面可有可無,由於它們屬於執行棧的一部分,系統老是執行完它們,纔會去讀取"任務隊列"。

 5、定時器

  除了放置異步任務,"任務隊列"還有一個做用,就是能夠放置定時事件,即指定某些代碼在多少時間以後執行。這叫作"定時器"(timer)功能,也就是定時執行的代碼。

  定時器功能主要由setTimeout()和setInterval()這兩個函數來完成,它們的內部運行機制徹底同樣,區別在於前者指定的代碼是一次性執行,後者則爲反覆執行。如下主要討論setTimeout()。

  setTimeout()接受兩個參數,第一個是回調函數,第二個是推遲執行的毫秒數。

console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);

  上面代碼的執行結果是1,3,2,由於setTimeout()將第二行推遲到1000毫秒以後執行。

  若是將setTimeout()的第二個參數設爲0,就表示當前代碼執行完(執行棧清空)之後,當即執行(0毫秒間隔)指定的回調函數。

setTimeout(function(){console.log(1);}, 0);
console.log(2);

  上面代碼的執行結果老是2,1,由於只有在執行完第二行之後,系統纔會去執行"任務隊列"中的回調函數。

  HTML5標準規定了setTimeout()的第二個參數的最小值(最短間隔),不得低於4毫秒,若是低於這個值,就會自動增長。在此以前,老版本的瀏覽器都將最短間隔設爲10毫秒。

  另外,對於那些DOM的變更(尤爲是涉及頁面從新渲染的部分),一般不會當即執行,而是每16毫秒執行一次。這時使用requestAnimFrame()的效果要好於setTimeout()。

  須要注意的是,setTimeout()只是將事件插入了"任務隊列",必須等到當前代碼(執行棧)執行完,主線程纔會去執行它指定的回調函數。要是當前代碼耗時很長,有可能要等好久,因此並無辦法保證,回調函數必定會在setTimeout()指定的時間執行。

 6、Node.js的Event Loop

  Node.js也是單線程的Event Loop,可是它的運行機制不一樣於瀏覽器環境。

  請看下面的示意圖(做者@BusyRich)。

Node.js

  根據上圖,Node.js的運行機制以下。

(1)V8引擎解析JavaScript腳本。

(2)解析後的代碼,調用Node API。

(3)libuv庫負責Node API的執行。它將不一樣的任務分配給不一樣的線程,造成一個Event Loop(事件循環),以異步的方式將任務的執行結果返回給V8引擎。

(4)V8引擎再將結果返回給用戶。

  Node.js有一個process.nextTick()方法,能夠將指定事件推遲到Event Loop的下一次執行,或者說放到"任務隊列"的頭部,也就是當前的執行棧清空以後當即執行。

function foo() {
    console.error(1);
}

process.nextTick(foo);
console.log(2);
// 2
// 1

  process.nextTick(foo)的做用,與setTimeout(foo, 0)很類似,可是執行效率高得多

  做者:阮一峯

相關文章
相關標籤/搜索