引子:為什么會有事件循環(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 等(async 和 await)實際上是 Promise 的語法糖
宏任務(wù)——Macro-Task
常見的 macro-task:setTimeout、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ù)默認返回undefined,promise狀態(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]
一個合格的中級前端工程師應(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