一篇就夠·通關Event Loop執行順序

先說明,本文針對的是node.js運行時,由uv實現的event loop

所有理論依據來源于 node.js源碼。(版本略)

0x00 總有面試官要刁難朕

我們不妨看一下這樣的題目

console.log(1)
setTimeout(() => {
  console.log(2)
}, 0)
Promise.resolve().then(() => {
    console.log(3)
}).then(() => {
    console.log(4)
})
console.log(5)
請問上面代碼的打印結果?
▇▇▇▇▇▇▇▇▇▇  
<p>對吧,無數次被這種裝X面試題惡心。</p>
<blockquote>
<p>小聲嗶嗶:誰項目里會這樣寫代碼?</p>
</blockquote>
<p><strong>不過惡心歸惡心,不管有沒有實用性,透過這些題目來弄清楚技術的真相,是沒有壞處的。</strong></p>
<p><strong>我們的目標是: 以后還有類似的題目,不管千變萬化,直接通關</strong></p>
<p/>
![](https://user-gold-cdn.xitu.io/2020/3/23/17105a2f24e1fa70)
<p/>
<h3 class="heading" data-id="heading-1">0x01 沒有銀彈,還是要拿源碼說話</h3>
<p>為了證明不是胡說八道,先貼出關鍵源碼</p>

// 來自 deps/uv/src/unix/core.c while (r != 0 && loop->stop_flag == 0) { uv__update_time(loop); uv__run_timers(loop); // ⭐️ timer ran_pending = uv__run_pending(loop); // ⭐️ 上一個循環一些沒來得及做完的事 uv__run_idle(loop); // ⭐️ 底層用,暫時不懂 uv__run_prepare(loop); // ⭐️ 底層用,暫時不懂

uv__io_poll(loop, timeout); // ⭐️io, network or file system 等等
uv__run_check(loop); // ⭐️ setImmediate
uv__run_closing_handles(loop); // ⭐️ event on('close')
if (mode == UV_RUN_ONCE) {
 // 這里不重要
}
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
 // 這里不重要

}

<p>然后我們開始逐個去了解</p>
<ul>
<li>
<p>timer</p>
<p>這部分主要是檢查有沒有可以執行的定時器,包括但不限于<code>setTimeout``setInterval</code>。</p>
<p>這里的的具體實現在<code>deps/uv/src/unix/timer.c</code>,簡單說就是使用一個最小堆(小頂堆), 把時間最接近的一個取出來,判斷當前時間是否可以執行。</p>
</li>
<li>
<p>pending</p>
<p>這個階段是執行 上一個循環<code>poll階段</code>還沒來得及處理的callback。</p>
<p>這句話,在下面介紹<code>poll階段</code>的時候才回過頭來理解。</p>
</li>
<li>
<p>idle + prepare</p>
<p>按文檔說是底層預留的,暫時我還沒研究清楚。請忽略。</p>
</li>
<li>
<p>poll</p>
<p>**關鍵!**這個階段處理的,就是我們比較熟悉的<code>network``fs</code>之類的異步操作回調。就是說你去請求一個遠程的接口,那么回調函數會在<code>poll</code>階段執行。</p>
<p>然后就是跟上面<code>pending</code>的關聯。</p>
<p>由于<code>uv__io_poll</code>代碼有點長就不貼了,有興趣自己去看。</p>
<p>一般來說注册送28体验金的游戏平台,我們的每一個階段,都會處理完已經就緒的所有callback,如果<code>poll</code>階段觸發大量的 callback,就會占用很多的時間。</p>
<p>我們的<code>uv</code>當然是不會設計成這樣的,所以,它會從<code>timer</code>里拿到最小的(未來最快到達的)一個定時器的時間,作為<code>poll</code>階段的 <code>timeout</code>。</p>
<p>如果<code>timeout</code>到了,還有callback沒開始執行的,對不起,請到<code>pending</code>隊列里。</p>
<blockquote>
<p>可能是<code>uv</code>認為,<code>poll</code>階段的callback,相對來說對“準時”不太敏感,所以通過這樣盡量確保<code>timer</code>的執行不會誤差太多。</p>
</blockquote>
</li>
<li>
<p>check</p>
<p>為什么叫做<code>check</code>我也不清楚。</p>
<p>但是這個階段將會運行我們 <code>setImmediate</code>注冊的回調。</p>
<p>很震驚吧,<code>setImmediate</code>完全就不是<code>timer</code>那一族的~~~~</p>
</li>
<li>
<p>closing_handles</p>
<p>執行<code>close</code>事件注冊的回調,放在循環的最后一個階段,也是合情合理。</p>
</li>
</ul>
<h3 class="heading" data-id="heading-2">0x03 那么我們練習一下</h3>
<blockquote>
<p>關于process.nextTick</p>
<p>nextTick 是個復雜的實現,需要另外開一篇來講解。</p>
<p>為了方便下面的練習,我暫時先把結論放出來。</p>
<p>nextTick會直接追加在每一個階段末尾,就是說,如果<code>timer</code>階段的回調里有<code>process.nextTick</code>,通過這個來注冊的回調,會在緊接著的<code>pending</code>之前就執行。</p>
</blockquote>
<p>✏️ 題目一</p>

setTimeout(() => { console.log('A') }, 0)

setImmediate(() => { console.log('B') })

<p>答案</p>

AB 或 BA

<p>解釋:</p>
<blockquote>
<p>首先這里的第一個知識點,是timer的第二個參數,取值范圍是 [1, 2^31 - 1]。也就是說,這個 0 會被當成 1 處理。</p>
<p>然后根據運行環境的差異,如果進入到當前循環前注册送28体验金的游戏平台,已經過去了 1ms ,那就打印 AB。</p>
<p>否則,如果在 1ms 內就開始了本次循環,那<code>timer</code>還沒準備后,就會在下一次循環觸發,自然就打印 BA。</p>
</blockquote>
<ul>
<li/>
</ul>
<p>✏️ 題目二</p>

const fs = require('fs')

fs.readFile(__filename, () => { setTimeout(() => { console.log('A') }, 0)

setImmediate(() => { console.log('B') }) })

<p>答案:</p>

BA

<p>解釋</p>
<blockquote>
<p>知識點在于<code>fs.readFile</code>,這個是 io操作,它的整個回調會在<code>poll</code>階段執行。
而<code>poll</code>之后馬上進入<code>check</code>,所以正好先執行了剛注冊的<code>setImmediate</code>。</p>
<p><code>setTimeout</code>自然就要等到下一個循環的<code>timer</code>階段。</p>
</blockquote>
<ul>
<li/>
</ul>
<p>✏️ 題目三,這個劃重點</p>

setImmediate(() => { console.log('1') setImmediate(() => { console.log('2') }) process.nextTick(() => { console.log('nextTick') }) })

setImmediate(() => { console.log('3') })

<p>答案:</p>

1 3 nextTick 2

<p>解釋:</p>
<blockquote>
<p>首先,最外層的兩個<code>setImmediate</code>會順序注冊到同一個<code>check</code>階段,而上面提到<code>nextTick</code>會直接追加到當前階段末尾,所以是<code>1 3 nextTick</code>而不是<code>1 nextTick 3</code> 。</p>
<p>而內層的<code>setImmediate</code>會注冊到下一次循環的<code>check</code>階段,所以 <code>2</code>最后打印。</p>
<p>請細品。</p>
</blockquote>
<h3 class="heading" data-id="heading-3">0x04 繼續練習之前,講講 promise</h3>
<p>和 <code>process.nextTick</code>類似,<code>promise</code>的回調也是在當前階段的末尾追加。</p>
<p>不過有意思的是,<code>process.nextTick</code>擁有更高的優先級。</p>
<p>這個實現細節,也是需要另外一篇文章來講解(挖坑+1)。。。。</p>
<h3 class="heading" data-id="heading-4">0x05 繼續練習吧</h3>
<p>✏️ 題目四</p>

const promise = Promise.resolve()

promise.then(() => { console.log('A') })

process.nextTick(() => { console.log('B') })

<p>答案:</p>

BA

<p>解釋</p>
<blockquote>
<p>無需解釋,先記住二者的優先級。</p>
</blockquote>
<ul>
<li/>
</ul>
<p>✏️ 題目五</p>

setTimeout(() => { console.log(1) }, 0)

new Promise((resolve, reject) => { console.log(2) for (let i = 0; i < 10000; i++) { i === 9999 && resolve() } console.log(3) }).then(() => { console.log(4) }) console.log(5)

<p>答案</p>

2 3 5 4 1

<p>解釋</p>
<blockquote>
<p>這里有個知識點,<code>new Promise</code>的參數是同步執行的。</p>
<p>所以 <code>2``3``5</code>都是同步順序輸出的。</p>
<p>然后 <code>then</code> 在一個同步的for循環后觸發,會追加到本階段末尾,所以<code>4</code>緊接著輸出。</p>
<p>最后是<code>setTimeout</code>,會在下一個循環的<code>timer</code>階段執行,輸出 <code>1</code></p>
</blockquote>
<ul>
<li/>
</ul>
<p><strong>🐸 BOSS戦</strong></p>

setImmediate(() => { console.log(1) setTimeout(() => { console.log(2) }, 100) setImmediate(() => { console.log(3) }) process.nextTick(() => { console.log(4) }) }) process.nextTick(() => { console.log(5) setTimeout(() => { console.log(6) }, 100) setImmediate(() => { console.log(7) }) process.nextTick(() => { console.log(8) }) }) console.log(9)

<p>答案:</p>

9 5 8 1 7 4 3 6 2

```

解釋:

你已經是一個成熟的程序員了,試著用上面的知識自己來解釋吧。

Tips 可以嘗試畫出來,一共經過了多少個循環, 每個循環的每個階段執行了什么。

原文鏈接:

上一篇:Promise 初探
下一篇:vue中的keep-alive緩存

相關推薦

  • 面試題之Event Loop終極篇

    先上一道常見的筆試題 大家可以先配合下面這個圖片思考一下輸出順序及這么運行的原因 圖片描述(https://img.javascriptcn.com/104c69f0f75d4c2b7f366...

    9 個月前
  • 那些年,那些坑--swiper loop:true引發綁定dom的click事件無效及解決方案

    對于 ,只要做過輪播圖的童鞋應該都再熟悉不過了。這是一個很強大的圖片輪播插件,本身無任何第三方庫依賴,即插即用。api 文檔很清晰,所以很快能夠上手。但是,再好的插件也會出現令人不愉快的地方,當然,今...

    2 年前
  • 踩了pointer-events:none的坑

    bug出現: 一前端同事跟我說:你把這段加到全局的css里面 然后,悲劇了,上線后發現我的圖片都不能點擊了。 bug解決: 情急之下,在所有需要點擊的img標簽外邊都包了一層div,把點擊事件...

    2 年前
  • 談談React事件機制和未來(react-events)

    (https://img.javascriptcn.com/cadc1922d817cf3302e243762f169c2d) 當我們在組件上設置事件處理器時,React并不會在該DOM元素上直接綁...

    8 個月前
  • 詳細剖析 event 幾個基本點

    事件流 當在某個DOM元素上觸發事件以后,DOM是如何派發事件并尋找監聽器的呢?這個派發過程就是事件流。 事件從Document(有的瀏覽器是window,總之是最外層根元素)開始,按照物理層級結構,...

    1 個月前
  • 譯文:JS事件循環機制(event loop)之宏任務、微任務

    原文標題:《Tasks, microtasks, queues and schedules》 這是一篇谷歌大神文章,寫得非常精彩。譯者想借這次翻譯深入學習一下,由于水平有限,英文好的同學建議直接閱...

    2 年前
  • 讓在Vue中使用的EventBus也有生命周期

    最近遇到了vue項目中的性能問題,整個項目不斷的進行操作五分鐘左右,頁面已經很卡,查看頁面占用了1.5G內存,經過排查一部分原因,是自己模塊使用的eventBus在離開頁面未進行off掉。

    2 年前
  • 讓事件偵聽器連接到節點使用addEventListener

    CommunityTyilo(https://stackoverflow.com/users/1/community)提出了一個問題:Get event listeners attached to n...

    2 年前
  • 被忽略掉的composition event

    最近在學習別人是如何使用Vue來造輪子,因此選擇在Vue的 UI框架 Element UI。elinput中有這么一段代碼 element在處理輸入框的input事件的時候,首先進行了一次判斷...

    3 個月前
  • 簡潔明了探索瀏覽器event loop

    前段時間我對于瀏覽器Event loop中的MacroTask和MicroTask哪個先執行有所困惑,苦于搜索也沒有發現很明確的答案注册送28体验金的游戏平台,于是決定深入探索瀏覽器Event loop,現有所愚見,想與大家分...

    1 年前

官方社區

掃碼加入 JavaScript 社區