JavaScript單線程事件循環(Event Loop)那些事

1.概述

本篇主要介紹JavaScript的運行機制:單線程事件循環(Event Loop). html

結論先: 在JavaScript中, 利用運行至完成和非阻塞IO 完成單線程下異步任務的處理. 就是先處理主模塊(主線程)上的同步任務, 再處理異步任務. 異步任務使用事件循環機制完成調度.node

涉及的內容有: 單線程, 事件循環, 同步執行, 異步執行, 定時器, nodeJS的事件循環ajax

開始以前, 先看下面的代碼, 給出結果:chrome

// 當前時間
console.log('A: ' + new Date());

// 1秒(1000毫秒)後執行的定時器
// 異步執行的代碼
setTimeout(function() {
    console.log('B: ' + new Date());
}, 1000);

// 循環3秒(3000毫秒)
var end = Date.now() + 3000;
while(Date.now() < end) {
}

// 當前時間
console.log('C: ' + new Date());

在瀏覽器中的結果爲(chrome-50.0.2661.102):瀏覽器

A: Thu May 25 2017 13:48:26 GMT+0800 (中國標準時間)
C: Thu May 25 2017 13:48:29 GMT+0800 (中國標準時間) 
B: Thu May 25 2017 13:48:29 GMT+0800 (中國標準時間)

在NodeJS(v7.7.2 win-x64)中的結果爲:服務器

>node scripts\async.js
A: Thu May 25 2017 13:50:55 GMT+0800 (中國標準時間)
C: Thu May 25 2017 13:50:58 GMT+0800 (中國標準時間)
B: Thu May 25 2017 13:50:58 GMT+0800 (中國標準時間)

tip: 瀏覽器下和NodeJS結果一致.微信

分析上面的代碼與結果, 注意的要點:網絡

  • 雖然設置的定時器爲1秒後執行, 但實際的執行時間在3秒之後, 看結果中B:的輸出, 在A:的3秒後.多線程

  • B:的輸出在C:的輸出以後. 可見, 雖然在while循環後, 時間已經到了定時器代碼須要執行的時間, 但並無當即執行, 而是等到了console.log('C: ')執行完, 再執行的定時器的代碼.併發

本篇就是說明爲何會出現以上的現象. 下面請一步步的看.

2.單線程

單線程, 指的是JavaScript在一個時間僅處理一個任務. 就是JavaScript在執行時, 存在一個執行隊列, 依次執行隊列中的任務, 不能同時執行多個任務.
單線程的優點, 也是JavaScript選擇單線程的緣由是:
1, 下降處理複雜性, 簡化開發. 例如不用考慮死鎖, 競爭機制等.
2, 做爲用於處理與用戶互動的腳本語言, 能夠更加容易地處理狀態同步的問題(想一想考慮用戶操做的不肯定性).
3, JavaScript核心維護人員自身的設計與理解.
4, 越簡單越容易推廣, 快速上手.

除了優點, 單線程有明顯的劣勢, 就是併發處理能力, 由於單線程處理下全部的任務就要排隊處理. 可是若是排在前面的任務處理很耗時, 那就致使後面的任務一直處於等待狀態. 若是前面的任務出處於滿載運行狀態還能夠, 可是若是前面的任務處於IO等待狀態呢? 就會致使CPU處理資源的浪費.
思考, 前面的是AJAX任務, 後邊是其餘任務. AJAX任務須要等待網絡請求響應結束, 才能處理, 此時前面的AJAX任務就處於IO等待狀態. 從而致使後面的任務也執行不了, 形成了單線程下的資源浪費. (CPU沒有辦法高速運轉, 處於空閒狀態).

在此狀況下, 徹底能夠掛起前面的AJAX任務(掛起等待AJAX的響應結果), 先執行後面的任務. 等後面的任務處理完畢後, 再看前面的AJAX任務是否獲得了IO結果, 若是有結果了, 在翻回來處理便可. 這種處理方式, 就是異步方式.

3.同步任務和異步任務

單線程的JavaScript爲了更好利用CPU的性能, 將執行的任務設計爲: 同步任務和異步任務, 兩類.

  • 同步任務(synchronous task), 就是須要一個個順序執行的任務, 不能跳過, 執行完前一個才能執行後一個. 咱們稱之爲在主模塊(主線程)執行的任務.

  • 異步任務(asynchronous task), 指的是被掛起執行的任務, 在系統內部處於等待IO處理結果狀態, 一旦處理完畢, 記錄下來, 等待後續處理. 須要事件循環處理的任務. 上面示例中的AJAX任務就是異步任務.

你應該會想, JavaScript不是單線程麼, 怎麼還能異步處理呢?
是這樣的, JavaScript的單線程, 指的是在JavaScript語言(語法)層面是單線程的. 而內部的執行, 仍是能夠利用處處理器多線程和操做系統的任務調度的, 在後臺處理咱們的異步任務. 當操做在後臺被處理完成後(例如ajax接收完畢了服務器的響應), 操做系統將結果告知給JavaScript, 並最終被JavaScript執行.

JavaScript是如何調度這些同步任務和異步任務的呢?
就涉及到了, 本文的重點: 任務隊列 和 事件循環, 執行棧.

4.事件循環模型

如圖(邏輯概述圖)所示:
圖片描述
執行以下:

  • step1, 同步任務直接放入到主模塊(主線程)任務隊列執行. 異步任務掛起後臺執行, 等待IO事件完成或行爲事件被觸發.

  • step2, 系統後臺執行異步任務, 若是某個異步任務事件發生(或者是行爲事件被觸發), 則將該任務push到任務隊列中, 每一個任務會對應一個回調函數進行處理. 這個步驟在後臺一直執行, 由於就不斷有事件被觸發, IO不斷完成, 任務被不斷的加入到任務隊列中.

  • step3, 執行任務隊列中的任務. 任務的具體執行是在執行棧中完成的. 當運行棧中一個任務的基本運行單元(稱之爲Frame, 楨)所有執行完畢後, 去讀取任務隊列中的下一個任務, 繼續執行. 是一個循環的過程. 處理一個任務隊列中的任務, 稱之爲一個tick.

注意, step3, 是一個循環的過程, 這就是事件循環. 循環執行任務隊列中已經發生的事件對應的任務.

再參考開始的代碼, 咱們能夠知道:

// A:當前時間 
// 同步代碼, 直接進入任務隊列
console.log('A: ' + new Date());

// B:1秒(1000毫秒)後執行的定時器
// 異步代碼, 等待到時事件發生, 纔會進入任務隊列
setTimeout(function() {
    console.log('B: ' + new Date());
}, 1000);

// 循環3秒(3000毫秒), 
// 同步代碼, 直接進入任務隊列
var end = Date.now() + 3000;
// 同步代碼, 直接進入任務隊列
while(Date.now() < end) {
}

// C:當前時間
// 同步代碼, 直接進入任務隊列
console.log('C: ' + new Date());

也就意味着, 此時, log(A), while, log(C) 三個任務, 已經進入到了任務隊列中. 而setTimeout是異步任務(與AJAX一致)在等待事件發生(到時事件). 於此同時, JavaScript開始處理任務隊列. 隊列是先進先出, 須要依次處理. 因此, 即時當前已經到1s了, 事件發生, 也僅僅是將該任務push入任務隊列而已(並無當即執行回調函數). 當將setTimeout入隊列時, log(C)已經在隊列中了, 所以, setTimeout的log(B), 會在log(B)後執行. 這就是輸出了: A, C, B的緣由. 以下圖(邏輯概述圖)所示:
圖片描述
由任務隊列可知, 輸出爲: A, C, B順序.

5.定時器函數

JavaScript提供了能夠操做定時器的函數, setTimeout()和setInterval. 在NodeJS中還有setImmediate().

setTimeout(), 定時執行

setTimeout(callback, timer), 多久(毫秒計)後執行, 常規用法已經演示.
須要提醒你們的是, setTimetout()是延時觸發, 而不是即時觸發. 指的是, 在有機會處理計時器事件時, 優先處理最早到時的計時器程序. 而不是時間到當即處理. 由於是單線程, 須要先處理當前的任務, 例如主模塊中的任務(同步任務).

實操中還有一個setTimeout(callback, 0)的用法, 表示當即加入到任務隊列. 可是注意, 並非在執行setTimeout的時候, 就加入隊列了, 而是當所有的同步任務入隊列後, 當即加入到任務隊列, 也就意味着同步任務以後第一個執行. 但聽說這個值內部執行時有一個最小值, 4ms.
上面的代碼, 將時間改成0, 測試結果仍是A, C, B. 不會由於先執行的setTimeout()而就將任務先執行.

// 異步代碼, 等待到時事件發生, 纔會進入任務隊列
setTimeout(function() {
    console.log('B: ' + new Date());
}, 0);

未發生的定時器, 可使用clearTimeout()方法清除.

setInterval(), 循環執行.

setInterval(callback, timer) 與setTimeout()類似, 不過是在callback執行完畢後, 再次設置了計時器. 再也不贅述.

6.NodeJS中的事件循環

NodeJS的事件循環模型比瀏覽器更爲複雜些.
以下圖所示(引用自NodeJS官方文檔), 事件循環, 按照下圖的順序調用事件.
圖片描述
因爲出現了不一樣的事件循環段, 例如 timer, check, 出現了額外的控制定時器方法.

setImmediate(), 當即執行

邏輯含義上講, 與setTimeout(callback, 0)一致. 都是當即執行. 在NodeJS中setImmediate()存在的主要場景就是, 在異步IO調用中, 若是同時使用setImmediate()和settimeout(), 能夠保證, setImmediate()先於全部的setTimeout()執行.
以下代碼: (引用自NodeJS官方文檔)

var fs = require('fs');
// 異步文件IO
fs.readFile(__filename, () => {

    setTimeout(function timeout () {
      console.log('timeout');
    },0);

    setImmediate(function immediate () {
      console.log('immediate');
    });
});

以上代碼的執行結果, 必定是:

>node scripts\async-node.js
immediate
timeout
這是由於, 根據NodeJS的事件循環處理順序, 處理完IO後, 須要處理check, 而setImmediate()就是check中的事件. 所以先處理.

但上面的代碼若是沒有在異步IO中調用, 在主模塊(主線程)中調用, 則順序不必定, 由操做系統調度決定!

process.nextTick(callback)

tick, 就是一個事件循環週期. 在prcess.nextTick()中設置的異步callback會在當前事件循環週期結束, 下一個事件循環週期開始前執行.
像是一個插入的tick. 生成了一個新的週期. 說白了, 是一個插隊行爲.
所以, 在時間上看, 必定先於settimeout(callback, 0)和setImmediate()執行. 一般用來處理在下一個事件週期(異步任務)前, 必需要處理好的任務. 常見的有, 處理錯誤, 回收資源, 和 從新執行存在錯誤的操做等.

測試一下執行時機:

setTimeout(function timeout () {
  console.log('timeout');
},0);

setImmediate(function immediate () {
  console.log('immediate');
});

process.nextTick(function immediate () {
  console.log('nickTick');
});

結果爲:

>node scripts\async-node.js
nextTick
timeout
immediate

可見, nextTick先發生.

注意, 在NodeJS中, nexttick並非一個特殊的定時器.
注意, 因爲nextTick()會插隊執行, 所以, NodeJS限制了nextTick()遞歸調用的深度. 防止IO處理飢餓.一直在處理nextTick(). 因爲該緣由, 遞歸時, NodeJS建議使用setImmediate()完成.

對比process.nextTick, setImmediate, setTimeout

process.nextTick, 永遠先執行.
setImmediate和setTimeout, 那個先到時那個先執行. 若是同時, 則由系統調度負責.

7.總結

在JavaScript中, 利用運行至完成和非阻塞IO 完成單線程下異步任務的處理. 就是先處理主模塊(主線程)上的同步任務, 再處理異步任務. 異步任務使用事件循環機制完成調度.

參考:

NodeJS文檔, https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
JavaScript 運行機制詳解:再談Event Loop, http://www.ruanyifeng.com/blog/2014/10/event-loop.html
樸靈 深刻淺出Node.js http://www.infoq.com/cn/master-nodejs

8.結語

以上就是本人對事件循環的理解. 一家之言, 歡迎討論拍磚!
更多內容, 能夠關注, 微信公衆號, 小韓說理.
圖片描述

相關文章
相關標籤/搜索