1. 程式人生 > 程式設計 >JavaScript 關於事件迴圈機制的刨析

JavaScript 關於事件迴圈機制的刨析

目錄
  • 前言:
  • 一、事件迴圈和任務佇列產生的原因:
  • 二、事件迴圈機制:
  • 三、任務佇列:
    • 3.1 任務佇列的型別:
    • 3.2 兩者區別:
    • 3.3 更細緻的事件迴圈過程
  • 四、強大的非同步專家 process.nextTick()
    • 4.1 process.nextTick()在何時呼叫?

前言:

這次主要整理一下自己對 事件迴圈機制,同步,非同步任務,巨集任務,微任務的理解,大概率暫時還有些偏差或者錯誤。如果有,十分歡迎各位糾正我的錯誤!

一、事件迴圈和任務佇列產生的原因:

首先,JS是單執行緒,這樣設計也是具有合理性的,試想如果一邊進行dom的刪除,另一邊又進行dom的新增,瀏覽器該如何處理?

引用:

單執行緒即任務是序列的,後一個任務需要等待前一個任務的執行,這就可能出現長時間的等待。但由於類似ajax網路請求、setTimeout時間延遲、DOM事件的使用者互動等,這些任務並不消耗 CPU,是一種空等,資源浪費,因此出現了非同步。通過將任務交給相應的非同步模組去處理,主執行緒的效率大大提升,可以並行的去處理其他的操作。當非同步處理完成,主執行緒空閒時,主執行緒讀取相應的callback,進行後續的操作,最大程度的利用CPU。此時出現了同步執行和非同步執行的概念,同步執行是主執行緒按照順序,序列執行任務;非同步執行就是cpu跳過等待,先處理後續的任務(CPU與網路模組、timer等並行進行任務)。由此產生了任務佇列與事件迴圈,來協調主執行緒與非同步模組之間的工作。

“”

二、事件迴圈機制:

圖解:

在這裡插入圖片描述

首先把JS執行程式碼操作 分為主執行緒任務佇列,任何一段js程式碼的執行都可以分為以下幾個步驟:

步驟一: 主執行緒讀取JS程式碼,此時為同步環境,形成相應的堆和執行棧;
步驟二: 當主執行緒遇到非同步操作的時候,將非同步操作交給對應的API進行處理;
步驟三: 當非同步操作處理完成,推入任務佇列中
步驟四: 主執行緒執行完畢後,查詢任務佇列,取出一個任務,並推入主執行緒進行處理
步驟五: 重複步驟二、三、四

其中常見的非同步操作有:ajax請求,setTimeout,還有類似onclik事件等

三、任務佇列:

同步和非同步任務分別進入不同的執行環境,同步的進入主執行緒,即主執行棧,非同步的進入任務佇列

首先,顧名思義,既然是一個佇列,那麼就遵循FIFO原則

如上示意圖,任務佇列存在多個,它們的執行順序:

同一任務佇列內,按佇列順序被主執行緒取走;
不同任務佇列之間,存在著優先順序,優先順序高的優先獲取(如使用者I/O)

3.1 任務佇列的型別:

任務佇列分為 巨集任務(macrotask queue)微任務(microtask queue)

巨集任務主要包含:script( 整體程式碼)、setTimeout、setInterval、I/O、UI 互動http://www.cppcns.com事件、setImmediate(Node.js 環境)

微任務主要包含:Promise、MutaionObserver、process.nextTick(Node.js 環境)

3.2 兩者區別:

微任務microtask queue:

(1) 唯一,整個事件迴圈當中,僅存在一個;
(2) 執行為同步,同一個事件迴圈中的microtask會按佇列順序,序列執行完畢;

PS:所以利用microtask queue可以形成一個同步執行的環境

巨集任務macrotask queue:

(1) 不唯一,存在一定的優先順序(使用者I/O部分優先順序更高)
(2) 非同步執行,同一事件迴圈中,只執行一個

3.3 更細緻的事件迴圈過程

  • 一、二、三、步同上
  • 主執行緒查詢任務佇列,執行microtask queue,將其按序執行,全部執行完畢;
  • 主執行緒查詢任務佇列,執行macrotask queue,取隊首任務執行,執行完畢;
  • 重複四、五步驟;

先用一個簡單的例子加深一下理解:

console.log('1,time = ' + new Date().toString()) // 1.進入主執行緒,執行同步任務,輸出1
setTimeout(macroCallback,0)// 2. 加入巨集任務佇列 // 7.開始執行此定時器巨集任務,呼叫macroCallback,輸出4
new Promise(function (resolve,reject) {//3.加入微任務佇列
  console.log('2,time = ' + new Date().toString())//4.執行此微任務中的同步程式碼,輸出2
  resolve()
  console.log('3,time = ' + new Date().toString())//5.輸出3
}).then(microCallback)// 6.執行then微任務,呼叫microCallback,輸出5

//函式定義
function macroCallback() {
  console.log('4,time = ' + new Date().toString())
}

function microCallback() {
  console.log('5,time = ' + new Date().toString())
}

執行結果:

請新增圖片描述

四、強大的非同步專家 process.nextTick()

第一次看見這東西,有點眼熟啊,想了一下好像之前專案中 用過 this.$nextTick(callback) 當時說的是 當頁面上元素被重新渲染之後 才會執行回撥函式中的程式碼
,不是很理解,暫時記住吧

請新增圖片描述

4.1 process.nextTick()在何時呼叫?

任何時候在給定的階段中呼叫 process.nextTick(),www.cppcns.com所有傳遞到 process.nextTick() 的回撥將在事件迴圈繼續之前解析

在事件迴圈中,每進行一次迴圈操作稱為tick,知道了這個之後,對理解這個方法什麼時候呼叫瞬間明白了一些!

再借用別人的例子,加深一下對事件迴圈的理解吧:

var flag = false // 1. 變數宣告

Promise.resolve().then(() => {
  // 2. 將 then 任務分發到本輪迴圈微任務佇列中去
  console.log('then1') // 8. 執行 then 微任務, 列印 then1,flag 此時是 true 了
  flag = true
})
new Promise(resolve => {
  // 3. 執行 Promise 裡 同步程式碼
  console.log('promise')
  resolve()
  setTimeout(() => { // 4. 將定時器裡的任務放到巨集任務佇列中
    console.log('timeout2') // 11. 執行定時器巨集任務 這邊指定了 10 的等待時長,因此在另一個定時器任務之後執行了
  },10)
}).then(function () {
  // 5. 將 then 任務分發到本輪迴圈微任務佇列中去
  console.log('then2') // 9. 執行 then 微任務, 列印 then2,至此本輪 tick 結束
})
function f1(f) {
  // 1. 函式宣告
  f()
}
function f2(f) {
  // 1. 函式宣告
  setTimeout(f) //  7. 把`setTimeout`中的`f`放到巨集任務佇列中,等本輪`tick`執行完,下一次事件迴圈再執行
}
f1(() => console.log('f為:',flag ? '非同步' : '同步')) // 6. 列印 `f為:同步`
f2(() => {
  console.log('timeout1,','f為:',flag ? '非同步' : '同步') // 10. 執行定時器巨集任務
})

console.log('本輪巨集任務執行完') // 7. 列印

執行結果:

請新增圖片描述

process.nextTick 中的回撥是在當前tick執行完之後,下一個巨集任務執行之前呼叫的。

官方的例子:

let bar;

// 這個方法用的是一個非同步簽名,但其實它是同步方式呼叫回撥的
function someAsyncApiCall(callback) { callback(); }

// 回撥函式在`someAsyncApiCall`完成之前被呼叫
someAsyncApiCall(() => {
  // 由於`someAsyncApiCall`已經完成,bar沒有被分配任何值
  console.log('bar',bar); // undefined
});

bar = 1;

使用 process.nextTick:

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar',bar); // 1
});

bar = 1;

再看一個含有 process.nextTick的例子:

console.log('1'); // 1.壓入主執行緒執行棧,輸出1

setTimeout(function () { //2.它的回撥函式被加入 巨集任務佇列中
	//7.目前微任務佇列為空,所以取出 巨集任務佇列首項,執行此任務
    console.log('2'); // 輸出2
    process.nextTick(function () { // 16.上一次迴圈結束,在下一次巨集任務開始之前呼叫,輸出3
        console.log('3'); 
    })
  PrCmeBq  new Promise(function (resolve) {
    	//8.執行 此promise的同步任務,輸出4,狀態變為resolve
        console.log('4');
        resolve();
    }).then(function () {//9.檢測到非同步方法then,將其回撥函式加入 微任務佇列中
        console.log('5'); // 10. 取出微任務佇列首項,也就是這個then的回撥,執行,輸出5
    })
})

process.nextTick(function () { // 11.一次事件迴圈結束,執行nextTick()的回撥,輸出6
    console.log('6');
})
new Promise(function (resolve) { 
	//3.執行promise中的同步任務 輸出7,狀態變為resolve
    console.log('7');
    resolve();
}).then(function () { //4.檢測到非同步方法then,將其回撥函式加入 微任務佇列中
    console.log('8'); //6. 主執行緒執行完畢,取出微任務佇列中首項,將其回撥函式壓入執行棧,輸出8
})

setTimeout(function () { //5.它的回撥函式 加入 巨集任務佇列中
	//12.此刻,微任務佇列為空,開始執行此巨集任務
    console.log('9'); // 輸出9
    process.nextTick(function () { // 17.此刻 微任務和巨集任務佇列都為空了,此次迴圈自動結束,執行此回撥,輸出10
        console.log('10');
    })
    new Promise(function (resolve) {
    	//13. 執行此promise的同步任務,輸出11,狀態改變
        console.log('11');
        resolve();
    }).then(function () {//14.檢測到then非同步方法,加入微任務佇列
        console.log('12');//15.取出微任務佇列首項,執行此then微任務,輸出12
    })

})

執行結果:

請新增圖片描述

此過程步驟詳解:

  • 首先進入主執行緒,檢測到log只是普通函式,壓入執行棧,輸出1;
  • 檢測到setTimeout為特殊的非同步方法(macrotask),將其交由其他核心模組處理,setTimeout的回撥函式被放入巨集任務(macrotask)佇列中;
  • 檢測到promise物件以及其中的resolve是一般的方法,將其同步任務壓入執行棧,輸出7,並且狀態改變為ressolve;
  • 檢測到剛才的promise物件的then方法是非同步方法,將其交由其他核心模組處理,回撥函式被放入微任務(microtask)佇列中;
  • 又檢測到一個setTimeout為特殊的非同步方法,其回撥函式被放入巨集任務(macrotask)佇列中;
  • 此時,主執行緒空了,開始從任務佇列中取,取出 微任務佇列首項,也就是第一個promise的then方法的回撥,執行,輸出8;
  • 檢查此時微任務佇列為空,取出巨集任務佇列首項,也就是第一個setTimeOut,執行其回撥函式,輸出2;
  • 在它的回撥中碰到一個promise,執行其同步任務,輸出4,狀態改變;
  • 然後檢測到then,同上,加入到微任務佇列;
  • 取出微任務佇列首項到主執行緒執行,也就是剛才的then,輸出5;
  • 此次迴圈結束,在下一個巨集任務開始之前,呼叫第一個process.nextTick()的回撥,輸出6;
  • 開始下一個巨集任務,取出巨集任務佇列首項,也就是第二個setTimeout的回撥,將其壓入執行棧,輸出9;
  • 然後將裡面的promise物件的同步任務壓入執行棧,輸出11,狀態改為resolve;
  • 這時又檢測到非同步then方法,同上,將其回撥加入 微任務佇列;
  • 取出微任務佇列首項,也就是剛才的then回撥,輸出12;
  • 此次迴圈結束,在下一次巨集任務開始之前執行,process.nextTick()的回撥,輸出3;
  • 此時發現 任務佇列和主執行緒都空了,此次事件迴圈自動結束,執行最後一個process.nextTick()的回撥,輸出10;

結束!趁著靈光乍現的時候,噼裡啪啦趕緊記錄下來,後面再檢查檢查是否有問題,也歡迎各位指出我的錯誤。

再來分析一個簡單的例子:

console.log('0');
setTimeout(() => {
    console.log('1');
    new Promise(function(resolve) {
        console.log('2');
        resolve();
    }).then(()=>{
        console.log('3');
    })
    new Promise(resolve => {
        console.log('4');
        for(let i=0;i<9;i++){
            i == 7 && resolve();
        }
        console.log('5');
    }).then(() => {
        console.log('6');
    })
})
  • 進入主執行緒,檢測到log為普通函式,壓入執行棧,輸出0;
  • 檢測到setTimeOut是特殊的非同步方法,交給其他模組處理,其回撥函式加入 巨集任務(macrotask)佇列;
  • 此時主執行緒中已經沒有任務,開始從任務佇列中取;
  • 發現為任務佇列為空,則取出巨集任務佇列首項,也就是剛才的定時器的回撥函式;
  • 執行其中的同步任務,輸出1;
  • 檢測到promise及其resolve方法是一般的方法,壓入執行棧,輸出2,狀態改變為resolve;
  • 檢測到這個promise的then方法是非同步方法,將其回撥函式加入 微任務佇列;
  • 緊接著又檢測到一個promise,執行其中的同步任務,輸出4,5,狀態改變為resolve;
  • 然後將它的then非同步方法加入微任務佇列;
  • 執行微任務佇列首項,也就是第一個promise的then,輸出3;
  • 再取出為任務佇列首項,也就是第二個promise的then,輸出6;
  • 此時主執行緒和任務佇列都為空,執行完畢;

程式碼執行結果:

請新增圖片描述

請新增圖片描述

到此這篇關於 關於事件迴圈機制的刨析的文章就介紹到這了,更多相關Script 事件迴圈機制內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!