JS前端宏任務微任務及Event Loop使用詳解

前言

首先我們要瞭解javascript是一個單線程的腳本語言,也就是說我們在執行代碼的過程中不會出現同時進行兩個進程(執行兩段代碼)。

Javascript語言將任務的執行模式分成兩種:同步(Synchronous)和異步(Asynchronous)。

同步:一個進程在執行某個請求的時候,若該請求需要一段時間才能返回信息,那麼這個進程將會一直等待下去,直到收到返回信息才繼續執行下去。

異步:進程不需要一直等下去,而是繼續執行下面的操作,不管其他進程的狀態。當有消息返回時系統會通知進程進行處理,這樣可以提高執行的效率。

進程:狹義上,就是正在運行的程序的實例。廣義上,進程是一個具有一定獨立功能的程序關於某個數據集合的一次運行活動。描述的是CPU在運行指令及加載和保存上下文所需要的時間。

線程:是程序中一個單一的順序控制流程。進程內一個相對獨立的、可調度的執行單元,是系統獨立調度和分派CPU的基本單位。指運行中的程序的調度單位。

執行棧:V8(谷歌瀏覽器引擎)內部維護出來的一個用來存放函數的執行上下文環境的一個棧結構。

相信有不少人和我有著一樣的疑惑,既然javascript是單線程的腳本語言,那麼他有什麼優勢呢,為什麼不寫成多進程的呢?

  • 單線程不僅可以節省內存同時也可以節省上下文切換時間;
  • 不會和渲染線程沖突(如果JS是雙線程的,當頁面渲染線程還在執行時,JS已經將渲染頁面的參數修改,就會導致頁面渲染出現問題)

宏任務

(macro)task,可以理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)。

宏任務隊列

  • I/O
  • UI-rendering (頁面渲染)
  • script
  • setTimeout
  • setInterval
  • setImmediate (node環境下是,而瀏覽器環境下不是)
  • requestAnimationFrame (在瀏覽器環境是,而node環境不是)

requestAnimationFrame在MDN的定義為,下次頁面重繪前所執行的操作,而重繪也是作為宏任務的一個步驟來存在的,且該步驟晚於微任務的執行。

微任務

microtask,可以理解是在當前 task 執行結束後立即執行的任務。也就是說,在當前task任務後,下一個task之前,在渲染之前。

所以它的響應速度相比setTimeout(setTimeout是task)會更快,因為無需等渲染。也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的所有microtask都執行完畢(在渲染前)。

微任務隊列

  • process.nextTick (node環境下是,而瀏覽器環境下不是)
  • promise.then
  • MutationObserver (在瀏覽器環境是,而node環境不是)

Event-Loop

上面我們講瞭半天宏任務、微任務等各種任務的執行那麼vent-Loop到底是個啥?

javascript是一個單進程的語言,同一時間不能處理多個任務,所以什麼時候執行宏任務什麼時候執行微任務呢?於是乎我們需要Event Loop-事件循環機制(計算機系統的一種運作機制)這樣一個判斷邏輯存在。

同步代碼: js為腳本語言,對於同步代碼來說,自上而下進行解釋執行。

異步代碼: 對於一個任務,分為多個片段來進行執行, 先執行一段,如果碰到比較耗時間的操作,比如本地或者網絡io請求。 第一段開始進行io之後,把執行權交由其他任務,當io完成後再來執行後半段的任務(io操作比較耗時,在系統進行io時,主線程是空閑的)

執行順序

1. 首先執行同步代碼,這屬於宏任務;

2. 當執行完所有的同步代碼後,執行棧為空,檢查是否有異步代碼要執行;

3. 執行微任務;

4. 執行完微任務後,有必要的情況下會渲染頁面;

5. 開啟下一輪 Event Loop,執行宏任務中的代碼;

代碼在執行過程中,遇到異步代碼,會將異步代碼用隊列裝起來(掛起)

實戰出真理,我們用一個🌰感受一下
console.log('start');          // 1
function foo(){
   console.log('foo');           
}
foo()                          // 2
setTimeout(function() {        //異步代碼中的宏任務,先掛起
   console.log('setTimeout'); // 7
},1000)
new Promise(resolve =>{
   console.log('promise');    // 3
   resolve()
})
   .then(function() {        //異步代碼中的微任務,先掛起
       console.log('promise1');// 5
   })
   .then(function() {        //異步代碼中的微任務,先掛起
       console.log('promise2');// 6
   })
console.log('end');            // 4

我們跟著實行順序來分析,第一步首先執行同步代碼,所以首先打印出來‘start’,調用函數foo打印出來‘foo’,執行Promise函數打印出來‘promise’,最後打印出來‘end’同步代碼就執行完畢,再檢查是否有異步代碼要執行,第三步執行微任務,所以先打印出第一微任務列表中的‘promise1’,緊接著打印第二微任務列表中的‘promise2’純js不用渲染頁面,最後開啟下一輪的Event-Loop,執行宏任務中的代碼,而setTimeout就是異步代碼中的宏任務所以最終打印出來‘setTimeout’,最後我們來看看在谷歌瀏覽器中的打印效果。

這裡有個小細節,如果將定時器setTimeout的時間設置為0呢?結果其實還是一樣的,setTimeout不會因為時間而改變執行順序,因為它仍然是異步代碼中的宏任務,不會因為延遲執行的時間而改變。

結語

以上就是我對於宏任務、微任務以及事件循環機制(Event Loop)的一些淺顯的認識與理解,更多關於JS宏任務微任務Event Loop的資料請關註WalkonNet其它相關文章!

推薦閱讀: