完全弄懂瀏覽器端的Event-Loop

前言

寫這篇文章的原由是在羣裏看到了各位再討論這部分的內容,這一塊本身也不太懂,一時手癢就寫了這篇文章這一塊不少初學者也是一知半懂,學到一半發現又麻煩又複雜,索性放棄了。 原本打算考完操做系統就寫完的,結果又遇到了 CPU 課設...因此這篇文章斷斷續續寫了不少天javascript


Event Loop

簡單點講 event loop 就是對 JS 代碼執行順序的一個規定(任務調度算法)前端

先看看兩幅圖java

JS enginegit

via: sessionstackgithub

JS runtimeweb

via: sessionstack算法

NOTE:express

一個 web worker 或者一個跨域的 iframe 都有本身的棧,堆和消息隊列。兩個不一樣的運行時只能經過 postMessage 方法進行通訊。若是另外一運行時偵聽 message 事件,則此方法會向其添加消息。跨域

HTML Eventloop瀏覽器

via: livebook.manning.com/#!/book/sec…

這幅圖就是對 whatwg 組織制定 HTML 規範中的 event loop 的可視化

咱們一般在編寫 web 代碼的時候,都是和JS runtime打交道

同步代碼

毫無疑問是按順序執行

console.log(2); // 非異步代碼
console.log(3); // 非異步代碼
複製代碼

顯然結果是 2 3

非阻塞代碼

通常分爲兩種任務,macroTasks 和 microTasks

event loop 裏面有維護了兩個不一樣的異步任務隊列 macroTasks(Tasks) 的隊列 microTasks 的隊列

  • 宏任務包括:setTimeout, setInterval, setImmediate, I/O, UI rendering

  • 微任務包括: 原生 Promise(有些實現的 Promise 將 then 方法放到了宏任務中), Object.observe(已廢棄), MutationObserver, MessageChannel

每次開始執行一段代碼(一個 script 標籤)都是一個 macroTask

一、event-loop start

二、從 macroTasks 隊列抽取一個任務,執行

三、microTasks 清空隊列執行,如有任務不可執行,推入下一輪 microTasks

四、結束 event-loop

值得一提的是,在 HTML 標準中提到了一個 compound microtasks 當它執行時可能會去執行一個 subTask,執行 compound microTasks 是一件很複雜的事情,在 whatwg 我也沒找到這部分具體的執行流程

const p = Promise.resolve();
p.then(() => {
  Promise.resolve().then(() => {
    console.log('subTask');
  });
}).then(() => {
  console.log('compound microTasks');
});
// subTask
// compound microTasks
複製代碼

按理說 p 的兩個 then 先執行,在執行 then 函數回調的時候又發現了 microTask,那應該是下一輪 eventLoop 執行了,可是結果確是相反的

瀏覽器執行代碼的真正過程是下面整個流程,而咱們編寫代碼感知的過程是紅框裏面的(因此之後要是有人再問起你 macroTask 和 microTask 哪一個先執行,可別再說 microTask 了)

例:
setTimeout(() => {
  console.log(123);
});

const p = Promise.resolve(
  new Promise(resolve => {
    setTimeout(() => {
      resolve('p');
      console.log(55);
    }, 1000);
    new Promise(resolve => {
      resolve('p1');
    }).then(r => console.log(r));
  })
);

setTimeout(() => {
  console.log(456);
});

p.then(r => console.log(r));
複製代碼

你們能夠先猜猜這段代碼的執行順序,相信若是沒有上面的介紹,我以爲不少人在這就暈了 不過有了上面的介紹加上我們一步一步的分析,你必定會明白的

  • 第一步,代碼執行到第一個 setTimeout 打印 123 的函數推入宏任務隊列
  • 第二部,代碼執行到 Promse.resolve 裏面的 new Promise,啥也沒幹...繼續執行下面的代碼
  • 第三步,代碼執行到 new Promise 裏面的 setTimeout,打印 55 的函數推入宏任務隊列
  • 第四步,代碼執行到 new Promise 裏面的 new Promise,執行構造函數,再把 then 函數推入微任務隊列
  • 第五步,代碼執行到第一個 setTimeout 打印 456 的函數推入宏任務隊列
  • 第六步,代碼執行到最後一個 p.then,推入微任務隊列

函數名後面的數字或者變量,是這個函數打印的東西,藉此區分函數

掃描完這些代碼,各任務隊列的狀況以下圖(注意此時由瀏覽器提供的 setTimeout 會檢查各定時任務是否到時間,若是到了則推入任務隊列,因此此時定時 1000ms 的回調函數並未出如今 macroTask 中) 而後執行完同步代碼,開始按上面介紹的狀況開始執行 macro Task 和 micro Task

先執行 micro Task,拿出 p.then p1 發現可執行,打印 p1;而後拿出 p.then p 發現不可執行,即 status 爲「pending」, 這一輪 micro Task 執行完畢 開始執行 macro Task,拿出 setTimeout 123,發現可執行(此時同步代碼已執行完畢),打印 123,檢查執行 micro Task, p.then p 依舊不可執行 等到 macro Task 執行完一段時間,發現 micro Task 裏面的 p.then p 可執行了,打印,結束 event loop

因此這一段代碼的打印結果是

5
p1
123
456
55
p
複製代碼

你有作對嗎,這只是小 case,還沒加上 async 函數呢,接下來看看 async 函數

async/await

當一個 async 函數裏面執行 await 的時候,實際上是標誌這個 async 函數要讓出線程了(我我的以爲這就像執行 一個特殊的 函數同樣,該函數會推動第一輪微任務隊列末尾),當 async 函數裏面的 await 語句後面的函數或者表達式執行完,該函數立馬退出執行,調用棧也會撤銷, 當本輪事件循環完畢的時候又會回來執行剩下的代碼

再來看看 MDN 咋說的

An async function can contain an await expression that pauses the execution of the async function and waits for the passed Promise's resolution, and then resumes the async function's execution and returns the resolved value.

翻譯過來就是 async 函數能夠包含一個 await 表達式,該表達式暫停執行 async 函數並等待返回的 Promise resovle/reject 完成,而後恢復 async 函數的執行並返回已解析的值

看完你應該知道爲啥 await 表達式會讓 async 函數讓出線程了吧?(若是不讓出線程,還不如寫同步代碼了,阻塞後面全部代碼), 結合前面的 Event Loop,能夠肯定,await 表達式須要等待 Promise 解析完成,await 恢復 async 函數執行須要等待執行完第一輪微任務之後,畢竟不是每一個 async 函數都是直接返回一個非 Promise 的值或者當即解析的 Promise,因此等 mainline JS 執行完還須要等待一輪 event loop

await 阻塞什麼代碼的執行

await 阻塞的是當前屬於 async 函數做用域下後面的代碼

何時恢復被阻塞的代碼的執行?

答案是當每一輪 microTask 執行完畢後恢復,具體哪一輪,看返回的 Promise 何時解析完成

進入正題,看看 async/await

async function b() {
  console.log('1');
}

async function c() {
  console.log('7');
}

async function a() {
  console.log('2');
  await b();
  //console.log(3);
  await c();
  console.log(8);
}

a();
console.log(5);
Promise.resolve()
  .then(() => {
    console.log(4);
  })
  .then(() => {
    console.log(6);
  });

new Promise(resolve => {
  setTimeout(() => resolve(), 1000);
}).then(() => console.log(55555555));

setTimeout(() => {
  console.log(123);
});
複製代碼

有了上面的解釋,加上下面這個 GIF,上面這段代碼執行過程一目瞭然了 我就再也不贅述了,你們直接看我單步執行這些代碼順序應該就懂了(使用了定時器可能單步調試打印的信息可能會和正常執行不同)

總結

  • macroTask 和 microTask 哪一個先執行

macroTask 先執行(畢竟標準就是這麼定的),至於爲何,我我的認爲是由於 macroTask 都是和用戶交互有關的事件,因此須要及時響應

  • async 函數作了什麼

    async 函數裏面可使用 await 表達式,async 函數的返回值會被 Promise.resolve 包裹(返回值是一個 Promise 對象就直接返回該對象)
// 驗證
const p = new Promise(resolve => resolve());
console.log(p === Promise.resolve(p)); // true
複製代碼
  • await 語句作了什麼

await 語句會先執行其後面的表達式,(若是該表達式是函數且該函數裏面遇到 await,則會按一樣的套路執行),而後阻塞屬於當前 async 函數做用域下後面的代碼

  • 何時恢復 await 語句後面代碼的執行

當執行完 await 語句以後的 某一輪 eventloop 結束後恢復執行(它須要等待它右側的返回的 Promise 解析完成,而 Promise 解析多是同步的(new Promise),也多是異步的(.then),而 then 回調須要等到 eventloop 最後去執行)

參考資料

來源 連接
IMWeb 前端博客 imweb.io/
MDN developer.mozilla.org/en-US/
前端精讀週刊 github.com/dt-fe/weekl…
sessionstack blog.sessionstack.com/
v8 博客 fastasync(中文版) v8.js.cn/blog/fast-a…
Tasks, microtasks, queues and schedules jakearchibald.com/2015/tasks-…
Secrets of the JavaScript Ninja livebook.manning.com/#!/book/sec…
相關文章
相關標籤/搜索