1. 程式人生 > 程式設計 >深入理解JavaScript的事件執行機制

深入理解JavaScript的事件執行機制

目錄
  • 前言
  • 瀏覽器 非同步執行的原理
  • 瀏覽器中的事件迴圈
    • 執行棧與任務佇列
    • 巨集任務和微任務
  • Async/await的執行順序
    • 特點
    • 示例
    • 個人分析

前言

熟悉事件迴圈,瞭解瀏覽器執行機制將對我們理解 的執行過程和排查執行問題有很大幫助。以下是總結的一些瀏覽器事件迴圈的一些原理和示例。

瀏覽器 JS 非同步執行的原理

  • JS 是單執行緒的,也就是同一個時刻只能做一件事情,那麼,為什麼瀏覽器可以同時執行非同步任務呢?
  • 因為瀏覽器是多執行緒的,當 JS 需要執行非同步任務時,瀏覽器會另外啟動一個執行緒去執行該任務。
  • 也就是說,JS 是單執行緒的指的是 執行JS 程式碼的執行緒只有一個,是瀏覽器提供的 JS 引擎執行緒(主執行緒)。瀏覽器中還有定時器執行緒和 HTTP 請求執行緒等,這些執行緒主要不是來跑 http://www.cppcns.com
    JS 程式碼的。
  • 比如主執行緒中需要發一個 AJAX 請求,就把這個任務交給另一個瀏覽器執行緒(HTTP 請求執行緒)去真正傳送請求,待請求回來了,再將 callback 裡需要執行的 JS 回撥交給 JS 引擎執行緒去執行。
  • 即瀏覽器才是真正執行傳送請求這個任務的角色,而 JS 只是負責執行最後的回撥處理。所以這裡的非同步不是 JS 自身實現的,其實是瀏覽器為其提供的能力。

深入理解JavaScript的事件執行機制

瀏覽器中的事件迴圈

執行棧與任務佇列

JS 在解析一段程式碼時,會將同步程式碼按順序排在某個地方,即執行棧,然後依次執行裡面的函式。
遇到非同步任務時就交給其他執行緒處理,待當前執行棧所有同步程式碼執行完成後,會從一個佇列中去取出已完成的非同步任務的回撥加入執行棧繼續執行。

遇到非同步任務時又交給其他執行緒,.....,如此迴圈往復。
而其他非同步任務完成後,將回調放入任務佇列中待執行棧來取出執行。

深入理解JavaScript的事件執行機制

巨集任務和微任務

根據任務的種類不同,可以分為微任務(micro task)佇列和巨集任務(macro task)佇列。
事件迴圈的過程中,執行棧在同步程式碼執行完成後,優先檢查微任務佇列是否有任務需要執行,如果沒有,再去巨集任務佇列檢查是否有任務執行,如此往復。
微任務一般在當前迴圈就會優先執行,而巨集任務會等到下一次迴圈,因此,微任務一般比巨集任務先執行,並且微任務NAbrjK佇列只有一個,巨集任務佇列可能有多個。另外我們常見的點選和鍵盤等事件也屬於巨集任務。

常見巨集任務:

  • setTimeout()
  • setInterval()
  • UI互動事件
  • postMessage
  • setImmediate() -- nodeJs

常見微任務:

  • promise.then()、promise.catch()
  • new MutaionObserver()
  • process.nextTick() -- nodeJs

如下示例:

console.log('同步程式碼1');
setTimeout(() => {
    console.log('setTimeout')
},0)

new Promise((resolve) => {
  console.log('同步程式碼2')
  resolve()
}).then(() => {
    console.log('promise.then')
})
console.log('同步程式碼3');

// 最終輸出"同步程式碼1"、"同步程式碼2"、"同步程式碼3"、"promise.then"、"setTimeout"

具體分析如下:

  • setTimeout 回撥和 promise.then 都是非同步執行的,將在所有同步程式碼之後執行;
  • 雖然 promise.then 寫在後面,但是執行順序卻比 setTimeout 優先,因為它是微任務;
  • new Promise 是同步執行的,promise.then 裡面的回撥才是非同步的。

注意: 在瀏覽器中 setTimeout 的延時設客棧置為 0 的話,會預設為 4ms,NodeJS 為 1ms。
微任務和巨集任務的本質區別:

  • 巨集任務特徵:有明確的非同步任務需要執行和回撥;需要其他非同步執行緒支援。
  • 微任務特徵:沒有明確的非同步任務需要執行,只有回撥;不需要其他非同步執行緒支援。

Async/await的執行順序

特點

  • async宣告的函式只是把該函式的return包裝了,使得無論如何都會返回promise物件(非promise會轉化為promise{resolve})。
  • await宣告只能用在async函式中。
    • 當執行async函式時,遇到await宣告,會先將await後面的內容按照'平常的執行規則'執行完(此處注意,當執行函式內容中又遇到await宣告,此時接著執行該await宣告內容)。
    • 執行完後立馬跳出async函式,去執行主執行緒其他內容,等到主執行緒執行完再回到await處繼續執行後面的內容。

示例

const a = async () => {
  console.log("a");
  await b();
  await e();
};

const b = async () => {
  console.log("b start");
  await c();
  console.log("b end");
};

const c = async () => {
  console.log("c start");
  await d();
  console.log("c end");
};

const d = async () => {
  console.log("d");
};

const e = async () => {
  console.log("e");
};

console.log('start');
a();
console.log('end');

執行結果

深入理解JavaScript的事件執行機制

個人分析

  • 在當前同步環境中,先執行console.log('start');輸出'start'。
  • 遇到同步函式a(),執行a()。
  • a()是sync/await構造,函式內遇到console.log("a") 輸出 'a',遇到await宣告 await b(),為非同步函式,進入函式b()內執行。(類似的操作,我自己想的)並將 await b()後的內容推入微任務佇列中。我們可以記做[await b()後]。
  • b()是sync/await構造,順序執行遇到console.log("c start") 輸出 'c start',遇到await宣告 await c(),為非同步函式,進入函式c()內執行。並將 await c()後的內容推入微任務佇列中。我們可以記做[await c()後,await b()後]。
  • c()是sync/await構造,順序執行遇到console.log("b start") 輸出 'b start',遇到await宣告 await d(),為非同步函式,進入函式d()內執行。並將 await d()後的內容推入微任務佇列中。我們可以記做[await d()後,await c()後,await b()後]。
  • d()中,順序執行遇到console.log("") 輸出 'd',d()函式執行結束。
  • 這是執行完 d()後,無可執行非同步函式,此時進入同步環境,執行 a()後的內容。
  • 遇到console.log("end") 輸出 'end'。此時同步環境中主執行緒執行完,檢查微任務佇列是否有微任務。
  • 微任務佇列中[await d()後,await b()後]有微任務,執行await d()後的內容。
  • wait d()後的內容是,console.log("c end"),輸出 'c end'。此時內容執行完畢,再從微任務佇列中[await c()後,await b()後]檢查,執行await c()後的內容。
  • 執行await c()後的內容,console.log("b end");,輸出 'b end'。此時內容執行完畢,再從微任務佇列中[await b()後]檢查,執行await b()後的內容。
  • await d()後的內容是,await e(),遇到await宣告,執行e()。並判斷並將 await e()後無執行程式碼,不用入微任務佇列。
  • 執行e(),順序執行console.log("e");,輸出 'e'。此時函式結束。
  • 微任務佇列中[]無微任務,執行結束。進入同步環境。

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