一次搞懂Event loop

事件循環

EventLoop

事件循環

事件循環被稱作循環的原因在于,它一直在查找新的事件并且執行。一次循環的執行稱之為 tick, 在這個循環里執行的代碼稱作 task

while (eventLoop.waitForTask()) {
  eventLoop.processNextTask()
}

任務(Tasks)中同步執行的代碼可能會在循環中生成新的任務。一個簡單的生成新任務的編程方式就是 setTimtout(taskFn, deley),當然任務也可以從其他的資源產生,比如用戶的事件、網絡事件或者DOM的繪制。

event-loop-1.png

任務隊列

讓事情變得復雜的情況是,事件循環可能有幾種任務任務隊列。唯一的兩個限制是同一個任務源中的事件必須屬于同一個隊列,并且必須在每個隊列中按插入順序處理任務。除了這些之外,執行環境可以自由地做它所做的事情。例如,它可以決定下一步要處理哪些任務隊列。

while (eventLoop.waitForTask()) {
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }
}

基于這個模型,我們失去了對事件執行時間的控制權。瀏覽器可能決定在執行我們設定的setTimeout之前先清空其他幾個隊列.

event-loop-2.png

Microtask queue

幸運的是,事件循環也有一個單獨的隊列叫做 microtask,microtask 將會在百分百在當前task隊列執行完畢以后執行

while (eventLoop.waitForTask()) {
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }

  const microtaskQueue = eventLoop.microTaskQueue
  while (microtaskQueue.hasNextMicrotask()) {
    microtaskQueue.processNextMicrotask()
  }
}

最簡單的方式生成一個 microtask 任務是 Promise.resolve().then(microtaskFn), Microtasks 的插入執行是按照順序的,而且因為只有一個唯一的 microtask 隊列。執行環境不會再搞錯執行的時間了。
另外,microtask任務 也可以生成新的 microtask任務 并且插入到同樣的隊列中(插入當前microtask)并且在同一個 tick 里執行

event-loop-3.png

渲染

最后一個是關于渲染的任務,不同于其他的任務處理,渲染任務并不是被獨立的后臺任務處理。它可能會是一個獨立運行在每一個tick結束后的算法。執行環境擁有較大的選擇空間,它可能會在每一個任務隊列后執行渲染,也可能執行多個任務隊列而不渲染。
幸運的是這里有一個 requestAnimationFrame(handle)函數,它會正確的在下一次渲染時執行內置的函數

最后這就是我們整個的渲染模型

while (eventLoop.waitForTask()) {
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }

  const microtaskQueue = eventLoop.microTaskQueue
  while (microtaskQueue.hasNextMicrotask()) {
    microtaskQueue.processNextMicrotask()
  }

  if (shouldRender()) {
    applyScrollResizeAndCSS()
    runAnimationFrames()
    render()
  }
}
event-loop-4.png

以上內容翻譯自writing-a-javascript-framework-execution-timing-beyond-settimeout

思考

以上就是對整個event loop的翻譯與解釋,文章解釋比較簡潔明細,但是相信大部分同學可能還是不太明白,那么我們換個思路,如果面試官問什么是event loop,面試官是想知道些什么?我應該怎么回答?

event loop顧名思義就是事件循環,為什么要有事件循環呢?因為V8是單線程的,即同一時間只能干一件事情,但是呢文件的讀取,網絡的IO處理是很緩慢的,并且是不確定的,如果同步等待它們響應,那么用戶就起飛了。于是我們就把這個事件加入到一個 事件隊列里(task),等到事件完成時,event loop再執行一個事件隊列。

值得注意的是,每一種異步事件加入的 事件隊列是不一樣的。唯一的兩個限制是同一個任務源中的事件必須屬于同一個隊列,并且必須在每個隊列中按插入順序處理任務。 也就是說由系統提供的執行task的方法,如 setTimeout setInterval setimmediate 會在一個task,網絡IO會在一個task,用戶的事件會在一個task。event-loop將會按照以下順序執行

  1. update_time
    在事件循環的開頭,這一步的作用實際上是為了獲取一下系統時間,以保證之后的timer有個計時的標準。這個動作會在每次事件循環的時候都發生,確保了之后timer觸發的準確性。(其實也不太準確....)

  2. timers
    事件循環跑到這個階段的時候,要檢查是否有到期的timer,其實也就是setTimeout和setInterval這種類型的timer,到期了,就會執行他們的回調。

  3. I/O callbacks
    處理異步事件的回調,比如網絡I/O,比如文件讀取I/O。當這些I/O動作都結束的時候,在這個階段會觸發它們的回調。

  4. idle, prepare
    這個階段內部做一些動作,與理解事件循環沒啥關系

  5. I/O poll階段
    這個階段相當有意思,也是事件循環設計的一個有趣的點。這個階段是選擇運行的。選擇運行的意思就是不一定會運行。

  6. check
    執行setImmediate操作

  7. close callbacks
    關閉I/O的動作,比如文件描述符的關閉,鏈接斷開,等等等
    (以上參考自方正——Node.js源碼解析:深入Libuv理解事件循環

除了task還有一個microtask,這一個概念是ES6提出Promise以后出現的。這個microtask queue只有一個。并且會在且一定會在每一個task后執行,且執行是按順序的。加入到microtask 的事件類型有Promise.resolve().then(), process.nextTick() 值得注意的是,event loop一定會在執行完micrtask以后才會尋找新的 可執行的task隊列。而microtask事件內部又可以產生新的microtask事件比如

(function microtask() {
  process.nextTick(() => microtask())
})()

這樣就會不斷的在microtask queue添加事件,導致整個eventloop堵塞

最后就是一個渲染的事件隊列,這個隊列只出現在瀏覽器上,并且執行環境會根據情況決定執行與否(可能執行很多task queue也不執行渲染隊列)。它如果執行則一定會在microtask后執行,通過requestAnimationFrame(handle) 方法,能夠保證中間的代碼一定能在下一次執行渲染函數前執行

補充常見的產生microtask和task事件的方法

microtasks:

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

tasks:

  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI渲染

Tips

  1. 我們通過node運行一個js文件,如果沒有可執行事件的事件隊列,進程就會退出,那么怎么不讓它退出呢?

setInterval方法,這貨會一直循環建立新的事件,這樣能夠保證node進程不退出

監聽 beforeExit 事件,通過process.on('beforeExit', handle) 這個事件在node進程退出前會觸發,但是如果這里面的handle包含了一個可以生成異步事件的操作,則node進程也不會退出。手動觸發process.exit(EXIT_CODE)不會觸發該事件

  1. setInterval會導致node進程不能正常退出,但是如果希望即使有setInterval也能正常退出怎么辦(有一些循環并不希望掛起node進程)?

const timer = process.setInterval(handle, deley) 調用setInterval方法會返回一個timer,調用 timer.unref() 則event-loop判斷除它以外,沒有可進行的事件隊列后也會推出

  1. process.on('exit', handle)中,handle里的異步事件不能執行
    exit事件在手動執行process.exit(EXIT_CODE)后,或者event loop中沒有可執行的事件隊列 時觸發。觸發 exit 事件后,執行環境就不會再生成新的 事件隊列了,因此這里面的異步事件都會被強制隊列

最后

以上都是我瞎編的
如果你喜歡我瞎編的文章,歡迎star Github

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容