Event Loop

簡介

我們都知道 JS 是一門單線程執(zhí)行語言,單線程意味著每次只能處理一件事隘梨,意味著阻塞程癌。JS 提供了很多異步代碼,Event Loop(事件環(huán)轴猎,又稱事件輪詢)就是 JS 處理異步的一種機(jī)制∏独颍現(xiàn)在我們用一種比較形象的方式去解釋這種機(jī)制

先備知識

在了解 Event Loop 之前,我們先要了解一些知識:

  1. 什么是(JS)異步捻脖?

在表現(xiàn)上來說锐峭,異步表現(xiàn)為代碼的執(zhí)行順序和你書寫的順序不一樣了。具體來說可婶,是與同步相對沿癞,異步處理不用阻塞當(dāng)前線程來等待處理完成,而是允許后續(xù)操作矛渴,直至其它線程將處理完成椎扬,并回調(diào)通知此線程。

  1. 什么是回調(diào)函數(shù)具温?

異步編程往往需要回調(diào)函數(shù)輔助(但有回調(diào)函數(shù)并不一定是異步編程)蚕涤。結(jié)合上面提到的異步,舉個(gè)例子铣猩,你去商店買東西揖铜,店員說沒貨,于是你登記自己的電話號碼要求店員有貨的時(shí)候打電話剂习,然后你就可以繼續(xù)做自己的事蛮位,隨后進(jìn)貨后店員打電話通知你取貨较沪。在這個(gè)例子里鳞绕,你的電話號碼就叫回調(diào)函數(shù)失仁,你把電話留給店員就叫登記回調(diào)函數(shù),店里后來有貨了叫做觸發(fā)了回調(diào)關(guān)聯(lián)的事件们何,店員給你打電話叫做調(diào)用回調(diào)函數(shù)萄焦,你到店里去取貨叫做響應(yīng)回調(diào)事件

// 下面這個(gè)并非異步
function call(cb) {
    cb();
}
call(() => {});
// 異步
fetch('http://example.com/movies.json')
.then(function(response) {
    return response.json();
})
setTimeout(param => {
    console.log(param)
}, 1000, 'param')
  1. 進(jìn)程和線程?

這兩個(gè)概念都會在下面用到

進(jìn)程: 進(jìn)程(Process)是計(jì)算機(jī)中的程序關(guān)于某數(shù)據(jù)集合上的一次運(yùn)行活動(dòng)冤竹,是系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位拂封,是操作系統(tǒng)結(jié)構(gòu)的基礎(chǔ),在現(xiàn)代系統(tǒng)中鹦蠕,進(jìn)程基本都是線程的容器冒签。(百度百科)

線程: 是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位,它被包含在進(jìn)程之中钟病,是進(jìn)程中的實(shí)際運(yùn)作單位萧恕,一條線程指的是進(jìn)程中一個(gè)單一順序的控制流(由此可見線程同一時(shí)間基本只能做一件事情)。(百度百科)

說白了肠阱,如果把我們的身體比喻成一個(gè)進(jìn)程票唆,那么線程就是我們身體的各個(gè)部件,因?yàn)榫€程同一時(shí)間基本只能做一件事情屹徘,所以我們腳走路的時(shí)候不能踢毽子走趋,踢毽子的時(shí)候不能走路。

瀏覽器每打開一個(gè) Tab 頁噪伊,即為一個(gè)進(jìn)程簿煌,其中又包括許多線程,比如渲染線程鉴吹、JS 引擎線程啦吧、HTTP 請求線程、計(jì)時(shí)器等等拙寡。我們可以在 Chrome > 更多工具 > 任務(wù)管理器 中看到我們?yōu)g覽器中的所有進(jìn)程授滓。

瀏覽器進(jìn)程
  1. 棧和隊(duì)列?

棧(stack)和隊(duì)列(queue)都是一個(gè)運(yùn)算受限的線向表肆糕。

棧遵循的是“后進(jìn)先出”原則般堆,棧的模式就像我們擠地鐵,最后進(jìn)去的人(最外面的人诚啃,即棧頂)出去之后淮摔,先進(jìn)去的人(最里面的人,即棧底)才能出來始赎。

隊(duì)列遵循的是“先進(jìn)先出”原則和橙,隊(duì)列很語義化仔燕,和我們排隊(duì)一樣,前面的人先辦業(yè)務(wù)魔招,后面的人才能繼續(xù)晰搀。

瀏覽器中的 Event Loop

首先我們引入執(zhí)行棧的概念,JS 代碼在執(zhí)行時(shí)办斑,每次遇到一個(gè)函數(shù)外恕,便會在執(zhí)行棧中壓入這個(gè)函數(shù),函數(shù)執(zhí)行完會被移出執(zhí)行棧乡翅,直到執(zhí)行棧為空鳞疲。

當(dāng)遇到異步代碼時(shí)(HTTP 請求),回調(diào)函數(shù)(會被掛起蠕蚜,由其他線程處理(比如網(wǎng)絡(luò)線程)尚洽,當(dāng)處理完畢會把回調(diào)函數(shù)加入到 Task 隊(duì)列中。一旦執(zhí)行棧為空靶累,Event Loop 就會從隊(duì)列中取出一個(gè)函數(shù)放入執(zhí)行棧中執(zhí)行腺毫。所以本質(zhì)上來說 JS 中的異步還是同步行為。

JS 中的 Task 隊(duì)列分別兩種尺铣,不同的任務(wù)源會被分配到不同的隊(duì)列中拴曲。任務(wù)源可以分為微任務(wù)(microtask)宏任務(wù)(macrotask)。在 ES6 規(guī)范中凛忿,microtask 稱為 jobs澈灼,macrotask 稱為 task〉暌纾看下圖:

  • 微任務(wù)包括 process.nextTick 叁熔,promise ,MutationObserver床牧,其中 process.nextTick 為 Node 獨(dú)有荣回。
  • 宏任務(wù)包括 script , setTimeout 戈咳,setInterval 心软,setImmediate ,I/O 著蛙,UI rendering删铃。

[圖片上傳失敗...(image-1e0115-1555772297164)]

Event Loop 的執(zhí)行順序如下:

  • 執(zhí)行最舊的 macrotask(一次);
  • 檢查是否存在 microtask踏堡,然后不停執(zhí)行猎唁,直到清空隊(duì)列(多次);
  • 執(zhí)行render顷蟆;
  • 然后開始下一輪 Event Loop诫隅;

這里有一個(gè)關(guān)鍵點(diǎn)腐魂,對于 macrotask,每次只會拉出一個(gè)來執(zhí)行逐纬;對于 microtask蛔屹,是把整體隊(duì)列拉出來執(zhí)行空。從圖中我們也可以看到這一點(diǎn)

我們來看一下例子

console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    }).then(() => {
        console.log(5.1)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
}).then(() => {
    console.log(8.1)
})

setTimeout(() => {
    console.log(9)
    new Promise(resolve => {
        console.log(11)
        resolve()
    }).then(() => {
        console.log(12)
    }).then(() => {
        console.log(12.1)
    })
})
  1. 執(zhí)行同步代碼(macrotask)风题,打印出 1 7判导;
  2. 執(zhí)行完所有微任務(wù)嫉父,打印出 8 8.1卖鲤;
  3. 執(zhí)行一次宏任務(wù)第晰,打印出 2 4;
  4. 執(zhí)行完所有微任務(wù),打印出 5 5.1不傅;
  5. 執(zhí)行一次宏任務(wù),打印出 9 11咸作;
  6. 執(zhí)行完所有微任務(wù)宪迟,打印出 12 12.1;

Node Event Loop

Node 中的 Event Loop 與瀏覽器大有不同树碱,這是因?yàn)?Node 中的異步操作更多更復(fù)雜肯适,除了網(wǎng)絡(luò),定時(shí)器外成榜,還有文件讀寫框舔,數(shù)據(jù)庫操作等等... 所以 Node 中 Event Loop 分為 6 個(gè)階段,每個(gè)階段都有一個(gè)回調(diào)隊(duì)列等待執(zhí)行赎婚,它們會按照順序反復(fù)運(yùn)行刘绣。每當(dāng)進(jìn)入某一個(gè)階段的時(shí)候,都會從對應(yīng)的回調(diào)隊(duì)列中取出函數(shù)去執(zhí)行挣输。當(dāng)隊(duì)列為空或者執(zhí)行的回調(diào)函數(shù)數(shù)量到達(dá)系統(tǒng)設(shè)定的閾值纬凤,就會進(jìn)入下一階段。

6個(gè)階段

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

Node 會盡可能地保障定時(shí)器任務(wù)的準(zhǔn)時(shí)可靠撩嚼,所以有好幾個(gè)階段都是在處理定時(shí)器停士。我們分別來解釋一下這 6 個(gè)階段:

1. timers

這個(gè)階段執(zhí)行 setTimeout 和 setInterval 設(shè)定的回調(diào)。我們的定時(shí)器都會設(shè)定一個(gè)時(shí)間閾值完丽,但這個(gè)值并不意味著回調(diào)會精確在這個(gè)時(shí)刻執(zhí)行恋技,而是時(shí)間超過閾值之后,回調(diào)會被盡早的執(zhí)行舰涌。因?yàn)椴僮飨到y(tǒng)對其他回調(diào)的調(diào)度可能會延遲他們的執(zhí)行猖任。

2. pending callbacks

處理一些上一輪循環(huán)中的少數(shù)未執(zhí)行的 I/O 回調(diào)。

3. idle瓷耙,prepare

只在內(nèi)部使用朱躺。這個(gè)階段為一些系統(tǒng)操作執(zhí)行回調(diào)刁赖,例如 TCP 錯(cuò)誤。例如长搀,如果一個(gè) TCP socket 在嘗試連接時(shí)收到 ECONNREFUSED宇弛,一些 *nix 系統(tǒng)會等待要報(bào)告這個(gè)錯(cuò)誤。這會被放入 pending callbacks 階段的隊(duì)列中源请。

4. poll

poll 是一個(gè)至關(guān)重要的階段枪芒,這個(gè)階段會判斷定時(shí)器的存在而更改 Event Loop 流程。

poll 階段有兩個(gè)主要功能:

  1. 計(jì)算需要阻塞多久然后輪詢 I/O谁尸,然后
  2. 處理 poll 隊(duì)列中的事件

當(dāng)事件循環(huán)進(jìn)入 poll 階段舅踪,并且沒有需要執(zhí)行的定時(shí)器,會進(jìn)入下面兩種狀況之一:

  • 如果 poll 隊(duì)列不是空的良蛮,事件循環(huán)會在隊(duì)列上同步迭代直到完成全部隊(duì)列的回調(diào)抽碌,或者系統(tǒng)依賴的限制被達(dá)到。
  • 如果 poll 隊(duì)列是空的决瞳,那么會進(jìn)入下面兩種狀況之一:
    • 如果有代碼被 setImmediate() 設(shè)定货徙,事件循環(huán)會結(jié)束輪詢階段,并且進(jìn)入 check 階段去執(zhí)行設(shè)定的回調(diào)皮胡。
    • 如果代碼沒有被 setImmediate() 設(shè)定痴颊,事件循環(huán)會等待回調(diào)被加入隊(duì)列,然后立即執(zhí)行它們屡贺。

一旦 poll 隊(duì)列是空的蠢棱,事件循環(huán)會檢查超過閾值的定時(shí)器。如果一個(gè)或者多個(gè)定時(shí)器已經(jīng)準(zhǔn)備好烹笔,事件循環(huán)會繞回定時(shí)器階段去執(zhí)行定時(shí)器回調(diào)裳扯。

注意:為了防止 poll 階段耗盡事件循環(huán),libuv(實(shí)現(xiàn)了 Node.js 事件循環(huán)和這個(gè)平臺全部異步行為的 C 語言庫)也有對輪詢事件有一個(gè)最大限制谤职。

5. check

這個(gè)階段允許用戶在 poll 階段完成之后立即執(zhí)行回調(diào)饰豺。如果 poll 階段變的空閑并且代碼被 setImmediate() 放入隊(duì)列,事件循環(huán)就可能進(jìn)入 check 階段而不是繼續(xù)等待允蜈。

事實(shí)上 setImmediate() 是在事件循環(huán)一個(gè)單獨(dú)的階段運(yùn)行的特殊計(jì)時(shí)器冤吨。它使用 libuv 的 API 讓設(shè)定的回調(diào)在 poll 階段完成之后進(jìn)行。

通常饶套,隨著代碼被執(zhí)行漩蟆,事件循環(huán)會最終達(dá)到 poll 階段,在 poll 階段會等待可能到來的連接妓蛮、請求等等怠李。但是,如果一個(gè)回調(diào)被 setImmediate() 設(shè)定并且 poll 階段是空閑的,那么 poll 階段就會結(jié)束捺癞,然后進(jìn)入 check 階段而不是繼續(xù)等待輪詢事件夷蚊。

6. close callbacks

如果一個(gè) socket 或者句柄被突然關(guān)閉(例如 socket.destroy()),close 事件會在這個(gè)階段被發(fā)送髓介,否則它就會被通過 process.nextTick() 發(fā)送惕鼓。

細(xì)節(jié)詳解

setImmediate() vs setTimeout()

setImmediate() 和 setTimeout() 很相似,但是他們的行為卻根據(jù)被調(diào)用的時(shí)機(jī)有差別唐础。

  • setImmediate() 是在當(dāng)前 poll 階段結(jié)束時(shí)立即執(zhí)行箱歧。
  • setTimeout() 是一段代碼在時(shí)間閾值被超過之后盡早執(zhí)行。

定時(shí)器被執(zhí)行的順序根據(jù)他們被調(diào)用的上下文會有區(qū)別一膨。如果它們都在主模塊被調(diào)用呀邢,時(shí)機(jī)會和進(jìn)程的性能有關(guān)(可能會被機(jī)器的其他進(jìn)程影響)。

例如汞幢,如果我們運(yùn)行并不在一個(gè)I/O循環(huán)中的腳本(比如在主模塊中)驼鹅,這兩個(gè)定時(shí)器執(zhí)行的順序是不確定的微谓,因?yàn)樗贿M(jìn)程的性能限制森篷。

  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),這是由源碼決定的
  • 進(jìn)入事件循環(huán)也是需要成本的豺型,如果在準(zhǔn)備時(shí)候花費(fèi)了大于 1ms 的時(shí)間仲智,那么在 timer 階段就會直接執(zhí)行 setTimeout 回調(diào)
  • 那么如果準(zhǔn)備時(shí)間花費(fèi)小于 1ms,那么就是 setImmediate 回調(diào)先執(zhí)行了
// timeout_vs_immediate.js
setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

但是姻氨,如果你把兩個(gè)回調(diào)放入一個(gè) I/O 循環(huán)中钓辆,那么 setImmediate() 的回調(diào)總是會被最先執(zhí)行。因?yàn)閮蓚€(gè)代碼寫在 IO 回調(diào)中肴焊,IO 回調(diào)是在 poll 階段執(zhí)行前联,當(dāng)回調(diào)執(zhí)行完畢后隊(duì)列為空,發(fā)現(xiàn)存在 setImmediate 回調(diào)娶眷,所以就直接跳轉(zhuǎn)到 check 階段去執(zhí)行回調(diào)了似嗤。

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout

相比于使用 setTimeout(),使用 setImmediate() 的主要優(yōu)勢是届宠,如果在一個(gè) I/O 循環(huán)中烁落,setImmediate 總會比其他定時(shí)器先執(zhí)行,無論其他定時(shí)器有多少豌注。

microtask

上面介紹的都是 macrotask 的執(zhí)行情況伤塌,對于 microtask 來說,它會在以上每個(gè)階段完成前清空 microtask 隊(duì)列轧铁,這里和瀏覽器類似每聪,microtask 都會在在 macrotask 前面處理

process.nextTick()

你可能注意到 process.nextTick() 在示意圖中沒有展示,盡管它是異步 API 的一部分。這是因?yàn)?process.nextTick() 嚴(yán)格來說并不屬于事件循環(huán)的一部分药薯。實(shí)際上他爸,nextTickQueue 會在每一個(gè)正在進(jìn)行的操作完成后之后執(zhí)行。它有一個(gè)自己的隊(duì)列果善,當(dāng)每個(gè)階段完成后诊笤,如果存在 nextTickQueue,就會清空隊(duì)列中的所有回調(diào)函數(shù)巾陕,并且優(yōu)先于其他 microtask 執(zhí)行讨跟。

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鄙煤,隨后出現(xiàn)的幾起案子晾匠,更是在濱河造成了極大的恐慌,老刑警劉巖梯刚,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凉馆,死亡現(xiàn)場離奇詭異,居然都是意外死亡亡资,警方通過查閱死者的電腦和手機(jī)澜共,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锥腻,“玉大人嗦董,你說我怎么就攤上這事∈莺冢” “怎么了京革?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長幸斥。 經(jīng)常有香客問我匹摇,道長,這世上最難降的妖魔是什么甲葬? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任廊勃,我火速辦了婚禮,結(jié)果婚禮上演顾,老公的妹妹穿的比我還像新娘供搀。我一直安慰自己,他們只是感情好钠至,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布葛虐。 她就那樣靜靜地躺著,像睡著了一般棉钧。 火紅的嫁衣襯著肌膚如雪屿脐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機(jī)與錄音的诵,去河邊找鬼万栅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛西疤,可吹牛的內(nèi)容都是我干的烦粒。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼代赁,長吁一口氣:“原來是場噩夢啊……” “哼扰她!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起芭碍,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤徒役,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后窖壕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體忧勿,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年瞻讽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鸳吸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,716評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡卸夕,死狀恐怖层释,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情快集,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布廉白,位于F島的核電站个初,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏猴蹂。R本人自食惡果不足惜院溺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望磅轻。 院中可真熱鬧珍逸,春花似錦、人聲如沸聋溜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撮躁。三九已至漱病,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背杨帽。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工漓穿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人注盈。 一個(gè)月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓晃危,卻偏偏與公主長得像,于是被迫代替她去往敵國和親老客。 傳聞我的和親對象是個(gè)殘疾皇子山害,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容