【前端進階】深入淺出瀏覽器事件循環(huán)

引子:為什么會有事件循環(huán)

重點: javascript 從誕生之日起就是一門單線程的非阻塞的腳本語言

我們先來聊下 JavaScript 這兩個特點:

  • 單線程: JavaScript 是單線程的,單線程是指 JavaScript 引擎中解析和執(zhí)行 JavaScript 代碼的線程只有一個(主線程),每次只能做一件事情。單線程存在是必然的,在瀏覽器中, 如果 javascript 是多線程的,那么當兩個線程同時對 dom 進行一項操作,例如一個向其添加事件,而另一個刪除了這個 dom,這個時候其實是矛盾的

  • 非阻塞: 當我們的 Javascript 代碼運行一個異步任務(wù)的時候(像 Ajax 等),主線程會掛起這個任務(wù),然后異步任務(wù)返回結(jié)果的時候再根據(jù)特定的結(jié)果去執(zhí)行相應(yīng)的回調(diào)函數(shù)

如何做到非阻塞呢?這就需要我們的主角——事件循環(huán)(Event Loop

瀏覽器中的事件循環(huán)

我們看一個很經(jīng)典的圖,這張圖基本可以概括了事件循環(huán)(該圖來自演講—— 菲利普·羅伯茨:到底什么是Event Loop呢? | 歐洲 JSConf 2014[1])后面演示用的 Loupe[2] 也是該演講者寫的((Loupe是一種可視化工具,可以幫助您了解JavaScript的調(diào)用堆棧/事件循環(huán)/回調(diào)隊列如何相互影響))

[圖片上傳失敗...(image-e56a0a-1601973516487)]

javascript 代碼執(zhí)行的時候會將不同的變量存于內(nèi)存中的不同位置:堆(heap)和棧(stack)中來加以區(qū)分。其中,堆里存放著一些對象。而棧中則存放著一些基礎(chǔ)類型變量以及對象的指針

執(zhí)行棧(call stack: 當我們調(diào)用一個方法的時候,js會生成一個與這個方法對應(yīng)的執(zhí)行環(huán)境(context),又叫執(zhí)行上下文。這個執(zhí)行環(huán)境中存在著這個方法的私有作用域,上層作用域的指向,方法的參數(shù),這個作用域中定義的變量以及這個作用域的this對象。 而當一系列方法被依次調(diào)用的時候,因為js是單線程的,同一時間只能執(zhí)行一個方法,于是這些方法被排隊在一個單獨的地方。這個地方被稱為執(zhí)行棧

比如,如下是一段同步代碼的執(zhí)行

<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">function a() { b(); console.log('a'); } function b() { console.log('b') } a(); </pre>

我們通過 Loupe 演示下代碼的執(zhí)行過程:

[圖片上傳失敗...(image-3c4ea4-1601973516487)]

  • 執(zhí)行函數(shù) a()先入棧
  • a()中先執(zhí)行函數(shù) b() 函數(shù)b() 入棧
  • 執(zhí)行函數(shù)b(), console.log('b') 入棧
  • 輸出 b, console.log('b')出棧
  • 函數(shù)b() 執(zhí)行完成,出棧
  • console.log('a') 入棧,執(zhí)行,輸出 a, 出棧
  • 函數(shù)a 執(zhí)行完成,出棧

同步代碼的執(zhí)行過程是相對比較簡單的,但涉及到異步執(zhí)行的話,又是怎樣的呢?

事件隊列(callback queue): js 引擎遇到一個異步事件后并不會一直等待其返回結(jié)果,而是會將這個事件掛起,繼續(xù)執(zhí)行執(zhí)行棧中的其他任務(wù)。當一個異步事件返回結(jié)果后,js 會將這個事件加入與當前執(zhí)行棧不同的另一個隊列,我們稱之為事件隊列

被放入事件隊列不會立刻執(zhí)行起回調(diào),而是等待當前執(zhí)行棧中所有任務(wù)都執(zhí)行完畢,主線程空閑狀態(tài),主線程會去查找事件隊列中是否有任務(wù),如果有,則取出排在第一位的事件,并把這個事件對應(yīng)的回調(diào)放到執(zhí)行棧中,然后執(zhí)行其中的同步代碼

Loupe 官方的一個例子:

<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`$.on('button', 'click', function onClick() {
setTimeout(function timer() {
console.log('You clicked the button!');
}, 2000);
});

console.log("Hi!");

setTimeout(function timeout() {
console.log("Click the button!");
}, 5000);

console.log("Welcome to loupe.");` </pre>

[圖片上傳失敗...(image-d78414-1601973516487)]

我們分析一下這個執(zhí)行的過程:

  • 首先是,注冊了點擊事件,異步執(zhí)行,這個時候會將它放在 Web Api
  • console.log("Hi!") 入棧,直接執(zhí)行,輸出 Hi
  • 執(zhí)行 setTimeout,異步執(zhí)行,將其掛載起來
  • 執(zhí)行 console.log("Welcome to loupe."), 輸出 Welcome to loupe.
  • 5 秒鐘后,setTimeout 執(zhí)行回調(diào),將回調(diào)放入到事件隊列中,一旦主線程空閑,則取出運行
  • 我點擊了按鈕【這里我只操作了一次】,觸發(fā)了點擊事件,將點擊事件的回調(diào)放入到事件隊列中,一旦主線程空閑,則取出運行
  • 運行點擊事件回調(diào)中的 setTimeout
  • 2 秒鐘后,setTimeout 執(zhí)行回調(diào),將回調(diào)放入到事件隊列中,一旦主線程空閑,則取出運行

再回頭看看這張圖,應(yīng)該有種豁然開朗的感覺

[圖片上傳失敗...(image-adf411-1601973516487)]

以上的過程按照類似如下的方式實現(xiàn),queue.waitForMessage() 會同步地等待消息到達(如果當前沒有任何消息等待被處理),故我們稱之為事件循環(huán)(Event Loop

<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">while (queue.waitForMessage()) { queue.processNextMessage(); } </pre>

微任務(wù)和宏任務(wù)

微任務(wù)——Micro-Task

常見的 micro-task:new Promise().then(callback)、MutationObserve 等(asyncawait)實際上是 Promise 的語法糖

宏任務(wù)——Macro-Task

常見的 macro-tasksetTimeout、setInterval、script(整體代碼)、 I/O 操作、UI 交互事件、postMessage

事件循環(huán)的執(zhí)行順序

異步任務(wù)的返回結(jié)果會被放到一個事件隊列中,根據(jù)上面提到的異步事件的類型,這個事件實際上會被放到對應(yīng)的宏任務(wù)和微任務(wù)隊列中去

Eveent Loop 的循環(huán)過程如下:

  • 執(zhí)行一個宏任務(wù)(一般一開始是整體代碼(script)),如果沒有可選的宏任務(wù),則直接處理微任務(wù)
  • 執(zhí)行過程中如果遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊列中
  • 執(zhí)行過程中如果遇到宏任務(wù),就將它添加到宏任務(wù)的任務(wù)隊列中
  • 執(zhí)行一個宏任務(wù)完成之后,就需要檢測微任務(wù)隊列有沒有需要執(zhí)行的任務(wù),有的話,全部執(zhí)行,沒有的話,進入下一步
  • 檢查渲染,然后 GUI 線程接管渲染,進行瀏覽器渲染
  • 渲染完畢后,JS線程繼續(xù)接管,開始下一個宏任務(wù)...(循環(huán)上面的步驟)

如下圖所示:

[圖片上傳失敗...(image-3801f0-1601973516486)]

執(zhí)行順序總結(jié):執(zhí)行宏任務(wù),然后執(zhí)行該宏任務(wù)產(chǎn)生的微任務(wù),若微任務(wù)在執(zhí)行過程中產(chǎn)生了新的微任務(wù),則繼續(xù)執(zhí)行微任務(wù),微任務(wù)執(zhí)行完畢后,再回到宏任務(wù)中進行下一輪循環(huán)

[圖片上傳失敗...(image-29d6f4-1601973516486)]

為了更好的理解,我們來看一個例子

<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`console.log('start')

setTimeout(function() {
console.log('setTimeout')
}, 0)

Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})

console.log('end')` </pre>

[圖片上傳失敗...(image-533012-1601973516486)]

我們來分析一下:

  • 執(zhí)行全局 script,輸出 start
  • 執(zhí)行 setTimeout 壓入 macrotask 隊列,promise.then 回調(diào)放入 microtask 隊列,最后執(zhí)行 console.log('end'),輸出 end
  • 全局 script 屬于宏任務(wù),執(zhí)行完成那接下來就是執(zhí)行 microtask 隊列的任務(wù)了,執(zhí)行 promise 回調(diào)打印 promise1
  • promise 回調(diào)函數(shù)默認返回 undefinedpromise 狀態(tài)變?yōu)?fullfill 觸發(fā)接下來的 then 回調(diào),繼續(xù)壓入 microtask 隊列,event loop 會把當前的microtask 隊列一直執(zhí)行完,此時執(zhí)行第二個promise.then` 回調(diào)打印出promise2
  • 這時 microtask 隊列已經(jīng)為空,接下來主線程會去做一些 UI 渲染工作(不一定會做),然后開始下一輪 event loop,執(zhí)行 setTimeout 的回調(diào),打印出 setTimeout

故最后的結(jié)果如下:

<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">start end promise1 promise2 setTimeout </pre>

練習題

增加這個環(huán)境在于,現(xiàn)在面試筆試都會出事件循環(huán)的題目,實際上的可能比上面的例子難,原因在于微任務(wù)和宏任務(wù)涉及的知識點不少,這就需要我們進一步鞏固我們的基礎(chǔ)知識,我相信能夠認真對待以下題目的,都能夠更好的掌握事件循環(huán)

我就暫不做分析,大家不懂的有疑問的可以在評論區(qū)一起交流

題目一

<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`console.log('start');
setTimeout(() => {
console.log('children2');
Promise.resolve().then(() => {
console.log('children3');
})
}, 0);

new Promise(function(resolve, reject) {
console.log('children4');
setTimeout(function() {
console.log('children5');
resolve('children6')
}, 0)
}).then((res) => {
console.log('children7');
setTimeout(() => {
console.log(res);
}, 0)
})` </pre>

<details data-tool="mdnice編輯器"><summary>點擊查看答案</summary> start children4 children2 children3 children5 children7</details>

題目2

<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`const p = function() {
return new Promise((resolve, reject) => {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
resolve(2)
})
p1.then((res) => {
console.log(res);
})
console.log(3);
resolve(4);
})
}

p().then((res) => {
console.log(res);
})
console.log('end');` </pre>

<details data-tool="mdnice編輯器"><summary>點擊查看答案</summary> 3 end 2 4</details>

題目3

<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">async function async1(){ console.log('async1 start') await async2() console.log('async1 end') } async function async2(){ console.log('async2') } console.log('script start') setTimeout(function(){ console.log('setTimeout') },0) async1(); new Promise(function(resolve){ console.log('promise1') resolve(); }).then(function(){ console.log('promise2') }) console.log('script end') </pre>

<details data-tool="mdnice編輯器"><summary>點擊查看答案</summary> script start async1 start async2 promise1 script end async1 end promise2 setTimeout</details>

題目4

<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">let resolvePromise = new Promise(resolve => { let resolvedPromise = Promise.resolve() resolve(resolvedPromise); // 提示:resolve(resolvedPromise) 等同于: // Promise.resolve().then(() => resolvedPromise.then(resolve)); }) resolvePromise.then(() => { console.log('resolvePromise resolved') }) let resolvedPromiseThen = Promise.resolve().then(res => { console.log('promise1') }) resolvedPromiseThen .then(() => { console.log('promise2') }) .then(() => { console.log('promise3') }) </pre>

<details data-tool="mdnice編輯器"><summary>點擊查看答案</summary> promise1 -> promise2 -> resolvePromise resolved -> promise3</details>

題目5

<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`console.log('script start');

setTimeout(() => {
console.log('Gopal');
}, 1 * 2000);

Promise.resolve()
.then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});

async function foo() {
await bar()
console.log('async1 end')
}
foo()

async function errorFunc () {
try {
// Tips:參考:https://zh.javascript.info/promise-error-handling:隱式 try…catch
// Promise.reject()方法返回一個帶有拒絕原因的Promise對象
// Promise.reject('error!!!') === new Error('error!!!')
await Promise.reject('error!!!')
} catch(e) {
console.log(e)
}
console.log('async1');
return Promise.resolve('async1 success')
}
errorFunc().then(res => console.log(res))

function bar() {
console.log('async2 end')
}

console.log('script end');` </pre>

<details data-tool="mdnice編輯器"><summary>點擊查看答案</summary> script start async2 end script end promise1 async1 end error!!! async1 promise2 async1 success Gopal</details>

題目6

<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">new Promise((resolve, reject) => { console.log(1) resolve() }) .then(() => { console.log(2) new Promise((resolve, reject) => { console.log(3) setTimeout(() => { reject(); }, 3 * 1000); resolve() }) .then(() => { console.log(4) new Promise((resolve, reject) => { console.log(5) resolve(); }) .then(() => { console.log(7) }) .then(() => { console.log(9) }) }) .then(() => { console.log(8) }) }) .then(() => { console.log(6) }) </pre>

<details data-tool="mdnice編輯器"><summary>點擊查看答案</summary> 1 2 3 4 5 6 7 8 9</details>

題目7

<pre class="custom" data-tool="mdnice編輯器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`console.log('1');

setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
})
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5')
})
})

Promise.reject().then(() => {
console.log('13');
}, () => {
console.log('12');
})

new Promise((resolve) => {
console.log('7');
resolve();
}).then(() => {
console.log('8')
})

setTimeout(() => {
console.log('9');
Promise.resolve().then(() => {
console.log('10');
})
new Promise((resolve) => {
console.log('11');
resolve();
}).then(() => {
console.log('12')
})
})` </pre>

<details data-tool="mdnice編輯器"><summary>點擊查看答案</summary> 1 7 12 8 2 4 9 11 3 5 10 12</details>

總結(jié)

本文從 JS 的兩個特點:單線程以及非阻塞介紹了事件循環(huán)的必要性,因為事件循環(huán)在瀏覽器和 Node.js 的表現(xiàn)是很大不一樣的,本人只談?wù)摰搅藶g覽器中的事件循環(huán),并介紹了微任務(wù)和宏任務(wù),以及它們的執(zhí)行流程,最后通過 7 道題目幫助大家鞏固知識

大家喜歡的話,別忘了點贊關(guān)注~

往期優(yōu)秀文章推薦

  • 一個合格的中級前端工程師應(yīng)該掌握的 20 個 Vue 技巧[3]
  • 【Vue進階】——如何實現(xiàn)組件屬性透傳?[4]
  • 前端應(yīng)該知道的 HTTP 知識【金九銀十必備】[5]
  • 最強大的 CSS 布局 —— Grid 布局[6]
  • 如何用 Typescript 寫一個完整的 Vue 應(yīng)用程序[7]
  • 前端應(yīng)該知道的web調(diào)試工具——whistle[8]

參考

詳解JavaScript中的Event Loop(事件循環(huán))機制[9]

深入理解NodeJS事件循環(huán)機制[10]

并發(fā)模型與事件循環(huán)[11]

【前端體系】從一道面試題談?wù)剬ventLoop的理解[12]

菲利普·羅伯茨:到底什么是Event Loop呢? | 歐洲 JSConf 2014[13]

JavaScript中的Event Loop(事件循環(huán))機制[14]

JS事件循環(huán)機制(event loop)之宏任務(wù)/微任務(wù)[15]

深入理解js事件循環(huán)機制(瀏覽器篇)[16]

從面試題看 JS 事件循環(huán)與 macro micro 任務(wù)隊列[17]

參考資料

[1]

菲利普·羅伯茨:到底什么是Event Loop呢? | 歐洲 JSConf 2014: https://www.youtube.com/watch?v=8aGhZQkoFbQ [2]

Loupe: http://latentflip.com/loupe/?code=JC5vbignYnV0dG9uJywgJ2NsaWNrJywgZnVuY3Rpb24gb25DbGljaygpIHsKICAgIHNldFRpbWVvdXQoZnVuY3Rpb24gdGltZXIoKSB7CiAgICAgICAgY29uc29sZS5sb2coJ1lvdSBjbGlja2VkIHRoZSBidXR0b24hJyk7ICAgIAogICAgfSwgMjAwMCk7Cn0pOwoKY29uc29sZS5sb2coIkhpISIpOwoKc2V0VGltZW91dChmdW5jdGlvbiB0aW1lb3V0KCkgewogICAgY29uc29sZS5sb2coIkNsaWNrIHRoZSBidXR0b24hIik7Cn0sIDUwMDApOwoKY29uc29sZS5sb2coIldlbGNvbWUgdG8gbG91cGUuIik7!!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4%3D [3]

一個合格的中級前端工程師應(yīng)該掌握的 20 個 Vue 技巧: https://juejin.im/post/6872128694639394830 [4]

【Vue進階】——如何實現(xiàn)組件屬性透傳?: https://juejin.im/post/6865451649817640968 [5]

前端應(yīng)該知道的 HTTP 知識【金九銀十必備】: https://juejin.im/post/6864119706500988935 [6]

最強大的 CSS 布局 —— Grid 布局: https://juejin.im/post/6854573220306255880 [7]

如何用 Typescript 寫一個完整的 Vue 應(yīng)用程序: https://juejin.im/post/6860703641037340686 [8]

前端應(yīng)該知道的web調(diào)試工具——whistle: https://juejin.im/post/6861882596927504392 [9]

詳解JavaScript中的Event Loop(事件循環(huán))機制: https://zhuanlan.zhihu.com/p/33058983 [10]

深入理解NodeJS事件循環(huán)機制: https://juejin.im/post/6844903999506923528 [11]

并發(fā)模型與事件循環(huán): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop [12]

【前端體系】從一道面試題談?wù)剬ventLoop的理解: https://juejin.im/post/6868849475008331783 [13]

菲利普·羅伯茨:到底什么是Event Loop呢? | 歐洲 JSConf 2014: https://www.youtube.com/watch?v=8aGhZQkoFbQ [14]

JavaScript中的Event Loop(事件循環(huán))機制: https://segmentfault.com/a/1190000022805523 [15]

JS事件循環(huán)機制(event loop)之宏任務(wù)/微任務(wù): https://juejin.im/post/6844903638238756878 [16]

深入理解js事件循環(huán)機制(瀏覽器篇): http://lynnelv.github.io/js-event-loop-browser [17]

從面試題看 JS 事件循環(huán)與 macro micro 任務(wù)隊列: https://juejin.im/post/6844903796754104334

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容