1. 程式人生 > >前端基礎進階(12):深入核心,詳解事件迴圈機制

前端基礎進階(12):深入核心,詳解事件迴圈機制

Event Loop

JavaScript的學習零散而龐雜,因此很多時候我們學到了一些東西,但是卻沒辦法感受到自己的進步,甚至過了不久,就把學到的東西給忘了。為了解決自己的這個困擾,在學習的過程中,我一直試圖在尋找一條核心的線索,只要我根據這條線索,我就能夠一點一點的進步。

前端基礎進階正是圍繞這條線索慢慢展開,而事件迴圈機制(Event Loop),則是這條線索的最關鍵的知識點。所以,我就馬不停蹄的去深入的學習了事件迴圈機制,並總結出了這篇文章跟大家分享。

事件迴圈機制從整體上的告訴了我們所寫的JavaScript程式碼的執行順序。但是在我學習的過程中,找到的許多國內部落格文章對於它的講解淺嘗輒止,不得其法,很多文章在圖中畫個圈就表示迴圈了,看了之後也沒感覺明白了多少。但是他又如此重要,以致於當我們想要面試中高階崗位時,事件迴圈機制總是繞不開的話題。特別是ES6中正式加入了Promise物件之後,對於新標準中事件迴圈機制的理解就變得更加重要。這就很尷尬了。

最近有兩篇比較火的文章也表達了這個問題的重要性。

但是很遺憾的是,大神們告訴了大家這個知識點很重要,卻並沒有告訴大家為什麼會這樣。所以當我們在面試時遇到這樣的問題時,就算你知道了結果,面試官再進一步問一下,我們依然懵逼。

在學習事件迴圈機制之前,我預設你已經懂得了如下概念,如果仍然有疑問,可以回過頭去看看我以前的文章。

  • 執行上下文(Execution context)
  • 函式呼叫棧(call stack)
  • 佇列資料結構(queue)
  • Promise(我會在下一篇文章專門總結Promise的詳細使用與自定義封裝)

因為chrome瀏覽器中新標準中的事件迴圈機制與nodejs幾乎一樣,因此此處就以整合nodejs一起來理解,其中會介紹到幾個nodejs有,但是瀏覽器中沒有的API,大家只需要瞭解就好,不一定非要知道她是如何使用。比如process.nextTick,setImmediate

OK,那我就先丟擲結論,然後以例子與圖示詳細給大家演示事件迴圈機制。

  • 我們知道JavaScript的一大特點就是單執行緒,而這個執行緒中擁有唯一的一個事件迴圈。

    當然新標準中的web worker涉及到了多執行緒,我對它瞭解也不多,這裡就不討論了。

  • JavaScript程式碼的執行過程中,除了依靠函式呼叫棧來搞定函式的執行順序外,還依靠任務佇列(task queue)來搞定另外一些程式碼的執行。

佇列資料結構
  • 一個執行緒中,事件迴圈是唯一的,但是任務佇列可以擁有多個。
  • 任務佇列又分為macro-task(巨集任務)與micro-task(微任務),在最新標準中,它們被分別稱為task與jobs。
  • macro-task大概包括:script(整體程式碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  • micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
  • setTimeout/Promise等我們稱之為任務源。而進入任務佇列的是他們指定的具體執行任務。
    1234 // setTimeout中的回撥函式才是進入任務佇列的任務setTimeout(function(){console.log('xxxx');})
  • 來自不同任務源的任務會進入到不同的任務佇列。其中setTimeout與setInterval是同源的。
  • 事件迴圈的順序,決定了JavaScript程式碼的執行順序。它從script(整體程式碼)開始第一次迴圈。之後全域性上下文進入函式呼叫棧。直到呼叫棧清空(只剩全域性),然後執行所有的micro-task。當所有可執行的micro-task執行完畢之後。迴圈再次從macro-task開始,找到其中一個任務佇列執行完畢,然後再執行所有的micro-task,這樣一直迴圈下去。
  • 其中每一個任務的執行,無論是macro-task還是micro-task,都是藉助函式呼叫棧來完成。

純文字表述確實有點乾澀,因此,這裡我們通過2個例子,來逐步理解事件迴圈的具體順序。

JavaScript
1234567891011121314151617 // demo01  出自於上面我引用文章的一個例子,我們來根據上面的結論,一步一步分析具體的執行過程。// 為了方便理解,我以打印出來的字元作為當前的任務名稱setTimeout(function(){console.log('timeout1');})newPromise(function(resolve){console.log('promise1');for(vari=0;i<1000;i++){i==99&&resolve();}console.log('promise2');}).then(function(){console.log('then1');})console.log('global1');

首先,事件迴圈從巨集任務佇列開始,這個時候,巨集任務佇列中,只有一個script(整體程式碼)任務。每一個任務的執行順序,都依靠函式呼叫棧來搞定,而當遇到任務源時,則會先分發任務到對應的佇列中去,所以,上面例子的第一步執行如下圖所示。

首先script任務開始執行,全域性上下文入棧

第二步:script任務執行時首先遇到了setTimeout,setTimeout為一個巨集任務源,那麼他的作用就是將任務分發到它對應的佇列中。

123 setTimeout(function(){console.log('timeout1');})

巨集任務timeout1進入setTimeout佇列

第三步:script執行時遇到Promise例項。Promise建構函式中的第一個引數,是在new的時候執行,因此不會進入任何其他的佇列,而是直接在當前任務直接執行了,而後續的.then則會被分發到micro-task的Promise佇列中去。

因此,建構函式執行時,裡面的引數進入函式呼叫棧執行。for迴圈不會進入任何佇列,因此程式碼會依次執行,所以這裡的promise1和promise2會依次輸出。

promise1入棧執行,這時promise1被最先輸出

resolve在for迴圈中入棧執行

建構函式執行完畢的過程中,resolve執行完畢出棧,promise2輸出,promise1頁出棧,then執行時,Promise任務then1進入對應佇列

script任務繼續往下執行,最後只有一句輸出了globa1,然後,全域性任務就執行完畢了。

第四步:第一個巨集任務script執行完畢之後,就開始執行所有的可執行的微任務。這個時候,微任務中,只有Promise佇列中的一個任務then1,因此直接執行就行了,執行結果輸出then1,當然,他的執行,也是進入函式呼叫棧中執行的。

執行所有的微任務

第五步:當所有的micro-tast執行完畢之後,表示第一輪的迴圈就結束了。這個時候就得開始第二輪的迴圈。第二輪迴圈仍然從巨集任務macro-task開始。

微任務被清空

這個時候,我們發現巨集任務中,只有在setTimeout佇列中還要一個timeout1的任務等待執行。因此就直接執行即可。

timeout1入棧執行

這個時候巨集任務佇列與微任務佇列中都沒有任務了,所以程式碼就不會再輸出其他東西了。

那麼上面這個例子的輸出結果就顯而易見。大家可以自行嘗試體會。

這個例子比較簡答,涉及到的佇列任務並不多,因此讀懂了它還不能全面的瞭解到事件迴圈機制的全貌。所以我下面弄了一個複製一點的例子,再給大家解析一番,相信讀懂之後,事件迴圈這個問題,再面試中再次被問到就難不倒大家了。

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374 // demo02console.log('golb1');setTimeout(function(){console.log('timeout1');process.nextTick(function(){console.log('timeout1_nextTick');})newPromise(function(resolve){console.log('timeout1_promise');resolve();}).then(function(){console.log('timeout1_then')})})setImmediate(function(){console.log('immediate1');process.nextTick(function(){console.log('immediate1_nextTick');})newPromise(function(resolve){console.log('immediate1_promise');resolve();}).then(function(){console.log('immediate1_then')})})process.nextTick(function(){console.log('glob1_nextTick');})newPromise(function(resolve){console.log('glob1_promise');resolve();}).then(function(){console.log('glob1_then')})setTimeout(function(){console.log('timeout2');process.nextTick(function(){console.log('timeout2_nextTick');})newPromise(function(resolve){console.log('timeout2_promise');resolve();}).then(function(){console.log('timeout2_then')})})process.nextTick(function(){console.log('glob2_nextTick');})newPromise(function(resolve){console.log('glob2_promise');resolve();}).then(function(){console.log('glob2_then')})setImmediate(function(){console.log('immediate2');process.nextTick(function(){console.log('immediate2_nextTick');})newPromise(function(resolve){console.log('immediate2_promise');resolve();}).then(function(){console.log('immediate2_then')})})

這個例子看上去有點複雜,亂七八糟的程式碼一大堆,不過不用擔心,我們一步一步來分析一下。

第一步:巨集任務script首先執行。全域性入棧。glob1輸出。

script首先執行

第二步,執行過程遇到setTimeout。setTimeout作為任務分發器,將任務分發到對應的巨集任務佇列中。

123456789101112 setTimeout(function(){console.log('timeout1');process.nextTick(function(){console.log('timeout1_nextTick');})newPromise(function(resolve){console.log('timeout1_promise');resolve();}).then(function(){console.log('timeout1_then')})})

timeout1進入對應佇列

第三步:執行過程遇到setImmediate。setImmediate也是一個巨集任務分發器,將任務分發到對應的任務佇列中。setImmediate的任務佇列會在setTimeout佇列的後面執行。

123456789101112 setImmediate(function(){console.log('immediate1');process.nextTick(function(){console.log('immediate1_nextTick');})newPromise(function(resolve){console.log('immediate1_promise');resolve();}).then(function(){console.log('immediate1_then')})})

進入setImmediate佇列

第四步:執行遇到nextTick,process.nextTick是一個微任務分發器,它會將任務分發到對應的微任務佇列中去。

123 process.nextTick(function(){console.log('glob1_nextTick');})

nextTick

第五步:執行遇到Promise。Promise的then方法會將任務分發到對應的微任務佇列中,但是它建構函式中的方法會直接執行。因此,glob1_promise會第二個輸出。

123456

相關推薦

前端基礎12深入核心事件迴圈機制

Event Loop JavaScript的學習零散而龐雜,因此很多時候我們學到了一些東西,但是卻沒辦法感受到自己的進步,甚至過了不久,就把學到的東西給忘了。為了解決自己的這個困擾,在學習的過程中,我一直試圖在尋找一條核心的線索,只要我根據這條線索,我就能夠一點一點的進步。 前端基礎進階正是圍繞這條線索

前端基礎十三透徹掌握Promise的使用讀這篇就夠了(轉)

https://www.jianshu.com/p/fe5f173276bd Promise的重要性我認為我沒有必要多講,概括起來說就是必須得掌握,而且還要掌握透徹。這篇文章的開頭,主要跟大家分析一下,為什麼會有Promise出現。 在實際的使用當中,有非常多的應用場景我們不能立即知道應該如

前端基礎圖例那道setTimeout與迴圈閉包的經典面試題

配圖與本文無關 我在詳細圖解作用域鏈與閉包一文中的結尾留下了一個關於setTimeout與迴圈閉包的思考題。 利用閉包,修改下面的程式碼,讓迴圈輸出的結果依次為1, 2, 3, 4, 5 for (var i=1; i<=5; i++) { setTimeout( function ti

【.NET執行緒--】--執行緒方法

        上篇部落格從執行緒的基本概況開始著重討論了執行緒,程序,程式之間的區別,然後討論了執行緒操作的幾個類,並通過例項來說明了執行緒的建立方法。本篇部落格將會帶大家更深入的瞭解執行緒,介紹執行緒的基本方法,並通過一個Demo使用委託來呼叫執行緒之外的物件。

年薪20萬Python工程師7Python資源大全讓你相見恨晚的Python庫 python

我是 環境管理 管理 Python 版本和環境的工具 pyenv – 簡單的 Python 版本管理工具。 Vex – 可以在虛擬環境中執行命令。 virtualenv – 建立獨立 Python 環境的工具。 python程式語言學習 扣群515267276 virtualen

Android 應用程序啟動過程

1.前言 最近一直在看 《Android進階解密》 的一本書,這本書編寫邏輯、流程都非常好,而且很容易看懂,非常推薦大家去看看(沒有收廣告費,單純覺得作者寫的很好)。 今天就將 應用程序啟動過程 總結一下(基於Android 8.0 系統)。 文章中例項&nbs

Android Launcher啟動過程

1.前言 最近一直在看 《Android進階解密》 的一本書,這本書編寫邏輯、流程都非常好,而且很容易看懂,非常推薦大家去看看(沒有收廣告費,單純覺得作者寫的很好)。 今天就將 Launcher 系統啟動過程 總結一下(基於Android 8.0 系統)。 文章

AndroidApplication啟動過程(最詳細&最簡單)

1.前言 最近一直在看 《Android進階解密》 的一本書,這本書編寫邏輯、流程都非常好,而且很容易看懂,非常推薦大家去看看(沒有收廣告費,單純覺得作者寫的很好)。 上一篇簡單的介紹了Android進階(二): 應用程序啟動過程,最終知道了ActivityThrea

AndroidActivity啟動過程(最詳細&最簡單)

1.前言 最近一直在看 《Android進階解密》 的一本書,這本書編寫邏輯、流程都非常好,而且很容易看懂,非常推薦大家去看看(沒有收廣告費,單純覺得作者寫的很好)。 上一篇簡單的介紹了Android進階(三):Application啟動過程(最詳細&最簡單)

Vuewebstorm啟動vue專案配置

使用命令視窗執行 1. npm run mock 2.npm run dev 每次都開啟命令視窗比較麻煩,可以在webstorm內進行配置,從webstorm內啟動 選中run下面的edit configurations,scripts裡面分別選擇mock和

年薪20萬Python工程師7Python資源大全讓你相見恨晚的Python庫 python

我是 環境管理 管理 Python 版本和環境的工具 pyenv – 簡單的 Python 版本管理工具。 Vex – 可以在虛擬環境中執行命令。 virtualenv – 建立獨立 Python 環境的工具。 python程式語言學習 扣群515267276

java泛型

1、泛型簡介 所謂泛型,即通過引數化型別來實現在同一份程式碼上操作多種資料型別,泛型程式設計是一種程式設計正規化,他利用“引數化型別”將型別抽象化,從而實現更為靈活的複用。 先簡單給個例子: //可以想象這裡的T為Integer型別,以便於理解,其實它可以是任何型別 p

年薪20萬Python工程師7Python資源大全讓你相見恨晚的Python庫

我是 環境管理 管理 Python 版本和環境的工具 pyenv – 簡單的 Python 版本管理工具。 Vex – 可以在虛擬環境中執行命令。 virtualenv – 建立獨立 Python 環境的工具。 virtualenvwrappe

python爬蟲分散式系統的高可用與高併發處理

一、應對高併發的基本思路 1、加快單機的速度,例如使用Redis,提高資料訪問頻率;增加CPU的核心數,增大記憶體; 2、增加伺服器的數量,利用叢集。 二、分散式系統的設計 1、無狀態 應用本身沒有狀態,狀態全部通過配置檔案或者叢集的服務端提供並與之同步。比如不同

大資料22個免費的資料視覺化和分析工具推薦

22個免費的資料視覺化和分析工具推薦   本文總結推薦22個免費的資料視覺化和分析工具。列表如下: 資料清理(Data cleaning)   當你分析和視覺化資料前,常需要“清理”工作。比如一些輸入性列表“New York City” ,同時其他人會

爬蟲工程師去重與入庫

資料去重又稱重複資料刪除,是指在一個數字檔案集合中,找出重複的資料並將其刪除,只儲存唯一的資料單元。資料去重可以有效避免資源的浪費,所以資料去重至關重要。資料去重資料去重可以從兩個節點入手:一個是URL去重。即直接篩選掉重複的URL;另一個是資料庫去重。即利用資料庫的一些特性

CSS12—— position:absolute如此高深我當真不懂

  之前在探討float屬性的時候就已經提到了position:absolute的概念,絕對定位和浮動在很多方面都具有相似性,包括“塊狀化”,“包裹性”,“破壞性”等等,在理論層面上兩者是一對兄弟關係。然而在實際場景中,由於絕對定位的“破壞性”通常比float的要強,因此會有人覺得絕對

python爬蟲日誌系統、守護執行緒以及驗證碼處理

一、日誌系統 首先,關日誌系統的設計參考這篇部落格。 1、日誌系統基本用途 (1)多執行緒情況下,debug除錯非常困難 (2)錯誤出現可能有一些隨機性 (3)效能分析 (4)錯誤記錄與分析 (5)執行狀態的實時監測 2、日誌系統設計 (1)錯誤級別:Debug,I

Hibernate 12HQL常用語句彙總

// HQL: Hibernate Query Language. // 特點: // >> 1,與SQL相似,SQL中的語法基本上都可以直接使用。 // >> 2,SQL查詢的是表和表中的列;HQL查詢的是物件與物件中的屬性。 // >>

Python爬蟲入門+學習筆記 3-1 爬蟲工程師HTTP請求分析

Chrome瀏覽器相對於其他的瀏覽器而言,DevTools(開發者工具)非常強大。這節課將為大家介紹怎麼利用Chrome瀏覽器的開發者工具進行HTTP請求分析Chrome瀏覽器講解Chrome 開發者工具是一套內置於Google Chrome中的Web開發和除錯工具,可用來對