引子:為什么會(huì)有事件循環(huán)
重點(diǎn): javascript
從誕生之日起就是一門單線程的非阻塞的腳本語(yǔ)言
我們先來(lái)聊下 JavaScript
這兩個(gè)特點(diǎn):
單線程:
JavaScript
是單線程的妻怎,單線程是指JavaScript
引擎中解析和執(zhí)行JavaScript
代碼的線程只有一個(gè)(主線程),每次只能做一件事情。單線程存在是必然的钠右,在瀏覽器中助析, 如果javascript
是多線程的误辑,那么當(dāng)兩個(gè)線程同時(shí)對(duì)dom
進(jìn)行一項(xiàng)操作散址,例如一個(gè)向其添加事件霎桅,而另一個(gè)刪除了這個(gè)dom
,這個(gè)時(shí)候其實(shí)是矛盾的非阻塞: 當(dāng)我們的
Javascript
代碼運(yùn)行一個(gè)異步任務(wù)的時(shí)候(像Ajax
等)产园,主線程會(huì)掛起這個(gè)任務(wù)汞斧,然后異步任務(wù)返回結(jié)果的時(shí)候再根據(jù)特定的結(jié)果去執(zhí)行相應(yīng)的回調(diào)函數(shù)
如何做到非阻塞呢夜郁?這就需要我們的主角——事件循環(huán)(Event Loop
)
瀏覽器中的事件循環(huán)
我們看一個(gè)很經(jīng)典的圖,這張圖基本可以概括了事件循環(huán)(該圖來(lái)自演講—— 菲利普·羅伯茨:到底什么是Event Loop呢断箫? | 歐洲 JSConf 2014[1])后面演示用的 Loupe[2] 也是該演講者寫的((Loupe是一種可視化工具拂酣,可以幫助您了解JavaScript的調(diào)用堆棧/事件循環(huán)/回調(diào)隊(duì)列如何相互影響))
[圖片上傳失敗...(image-e56a0a-1601973516487)]
當(dāng) javascript
代碼執(zhí)行的時(shí)候會(huì)將不同的變量存于內(nèi)存中的不同位置:堆(heap
)和棧(stack
)中來(lái)加以區(qū)分秋冰。其中仲义,堆里存放著一些對(duì)象。而棧中則存放著一些基礎(chǔ)類型變量以及對(duì)象的指針
執(zhí)行棧(call stack
): 當(dāng)我們調(diào)用一個(gè)方法的時(shí)候剑勾,js會(huì)生成一個(gè)與這個(gè)方法對(duì)應(yīng)的執(zhí)行環(huán)境(context)埃撵,又叫執(zhí)行上下文。這個(gè)執(zhí)行環(huán)境中存在著這個(gè)方法的私有作用域虽另,上層作用域的指向暂刘,方法的參數(shù),這個(gè)作用域中定義的變量以及這個(gè)作用域的this對(duì)象捂刺。 而當(dāng)一系列方法被依次調(diào)用的時(shí)候谣拣,因?yàn)閖s是單線程的,同一時(shí)間只能執(zhí)行一個(gè)方法族展,于是這些方法被排隊(duì)在一個(gè)單獨(dú)的地方森缠。這個(gè)地方被稱為執(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í)行過程是相對(duì)比較簡(jiǎn)單的,但涉及到異步執(zhí)行的話拴还,又是怎樣的呢跨晴?
事件隊(duì)列(callback queue): js
引擎遇到一個(gè)異步事件后并不會(huì)一直等待其返回結(jié)果,而是會(huì)將這個(gè)事件掛起片林,繼續(xù)執(zhí)行執(zhí)行棧中的其他任務(wù)端盆。當(dāng)一個(gè)異步事件返回結(jié)果后,js
會(huì)將這個(gè)事件加入與當(dāng)前執(zhí)行棧不同的另一個(gè)隊(duì)列拇厢,我們稱之為事件隊(duì)列
被放入事件隊(duì)列不會(huì)立刻執(zhí)行起回調(diào)爱谁,而是等待當(dāng)前執(zhí)行棧中所有任務(wù)都執(zhí)行完畢,主線程空閑狀態(tài)孝偎,主線程會(huì)去查找事件隊(duì)列中是否有任務(wù)访敌,如果有,則取出排在第一位的事件衣盾,并把這個(gè)事件對(duì)應(yīng)的回調(diào)放到執(zhí)行棧中寺旺,然后執(zhí)行其中的同步代碼
看 Loupe
官方的一個(gè)例子:
<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)]
我們分析一下這個(gè)執(zhí)行的過程:
- 首先是爷抓,注冊(cè)了點(diǎn)擊事件,異步執(zhí)行阻塑,這個(gè)時(shí)候會(huì)將它放在
Web Api
中 - console.log("Hi!") 入棧蓝撇,直接執(zhí)行,輸出 Hi
- 執(zhí)行
setTimeout
陈莽,異步執(zhí)行渤昌,將其掛載起來(lái) - 執(zhí)行 console.log("Welcome to loupe."), 輸出 Welcome to loupe.
- 5 秒鐘后走搁,
setTimeout
執(zhí)行回調(diào)独柑,將回調(diào)放入到事件隊(duì)列中,一旦主線程空閑私植,則取出運(yùn)行 - 我點(diǎn)擊了按鈕【這里我只操作了一次】忌栅,觸發(fā)了點(diǎn)擊事件,將點(diǎn)擊事件的回調(diào)放入到事件隊(duì)列中曲稼,一旦主線程空閑索绪,則取出運(yùn)行
- 運(yùn)行點(diǎn)擊事件回調(diào)中的
setTimeout
- 2 秒鐘后,
setTimeout
執(zhí)行回調(diào)贫悄,將回調(diào)放入到事件隊(duì)列中瑞驱,一旦主線程空閑,則取出運(yùn)行
再回頭看看這張圖清女,應(yīng)該有種豁然開朗的感覺
[圖片上傳失敗...(image-adf411-1601973516487)]
以上的過程按照類似如下的方式實(shí)現(xiàn)钱烟,queue.waitForMessage()
會(huì)同步地等待消息到達(dá)(如果當(dāng)前沒有任何消息等待被處理),故我們稱之為事件循環(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
)實(shí)際上是 Promise
的語(yǔ)法糖
宏任務(wù)——Macro-Task
常見的 macro-task
:setTimeout
拴袭、setInterval
、script
(整體代碼)曙博、 I/O
操作拥刻、UI 交互事件、postMessage
等
事件循環(huán)的執(zhí)行順序
異步任務(wù)的返回結(jié)果會(huì)被放到一個(gè)事件隊(duì)列中父泳,根據(jù)上面提到的異步事件的類型般哼,這個(gè)事件實(shí)際上會(huì)被放到對(duì)應(yīng)的宏任務(wù)和微任務(wù)隊(duì)列中去
Eveent Loop
的循環(huán)過程如下:
- 執(zhí)行一個(gè)宏任務(wù)(一般一開始是整體代碼(
script
)),如果沒有可選的宏任務(wù)惠窄,則直接處理微任務(wù) - 執(zhí)行過程中如果遇到微任務(wù)蒸眠,就將它添加到微任務(wù)的任務(wù)隊(duì)列中
- 執(zhí)行過程中如果遇到宏任務(wù),就將它添加到宏任務(wù)的任務(wù)隊(duì)列中
- 執(zhí)行一個(gè)宏任務(wù)完成之后杆融,就需要檢測(cè)微任務(wù)隊(duì)列有沒有需要執(zhí)行的任務(wù)楞卡,有的話,全部執(zhí)行,沒有的話蒋腮,進(jìn)入下一步
- 檢查渲染淘捡,然后
GUI
線程接管渲染,進(jìn)行瀏覽器渲染 - 渲染完畢后池摧,JS線程繼續(xù)接管焦除,開始下一個(gè)宏任務(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ù)中進(jìn)行下一輪循環(huán)
[圖片上傳失敗...(image-29d6f4-1601973516486)]
為了更好的理解瓣距,我們來(lái)看一個(gè)例子
<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)]
我們來(lái)分析一下:
- 執(zhí)行全局
script
黔帕,輸出 start - 執(zhí)行
setTimeout
壓入macrotask
隊(duì)列代咸,promise.then
回調(diào)放入microtask
隊(duì)列,最后執(zhí)行console.log('end')
成黄,輸出end
- 全局
script
屬于宏任務(wù)呐芥,執(zhí)行完成那接下來(lái)就是執(zhí)行microtask
隊(duì)列的任務(wù)了,執(zhí)行promise
回調(diào)打印promise1
-
promise
回調(diào)函數(shù)默認(rèn)返回undefined
奋岁,promise
狀態(tài)變?yōu)?fullfill
觸發(fā)接下來(lái)的then
回調(diào)思瘟,繼續(xù)壓入microtask
隊(duì)列,event loop
會(huì)把當(dāng)前的microtask 隊(duì)列一直執(zhí)行完闻伶,此時(shí)執(zhí)行第二個(gè)
promise.then` 回調(diào)打印出promise2 - 這時(shí)
microtask
隊(duì)列已經(jīng)為空滨攻,接下來(lái)主線程會(huì)去做一些UI
渲染工作(不一定會(huì)做),然后開始下一輪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>
練習(xí)題
增加這個(gè)環(huán)境在于,現(xiàn)在面試筆試都會(huì)出事件循環(huán)的題目畜份,實(shí)際上的可能比上面的例子難诞帐,原因在于微任務(wù)和宏任務(wù)涉及的知識(shí)點(diǎn)不少,這就需要我們進(jìn)一步鞏固我們的基礎(chǔ)知識(shí)爆雹,我相信能夠認(rèn)真對(duì)待以下題目的停蕉,都能夠更好的掌握事件循環(huán)
我就暫不做分析,大家不懂的有疑問的可以在評(píng)論區(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>點(diǎn)擊查看答案</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>點(diǎn)擊查看答案</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>點(diǎn)擊查看答案</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>點(diǎn)擊查看答案</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()方法返回一個(gè)帶有拒絕原因的Promise對(duì)象
// 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>點(diǎn)擊查看答案</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>點(diǎn)擊查看答案</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>點(diǎn)擊查看答案</summary> 1 7 12 8 2 4 9 11 3 5 10 12
</details>
總結(jié)
本文從 JS
的兩個(gè)特點(diǎn):?jiǎn)尉€程以及非阻塞介紹了事件循環(huán)的必要性钙态,因?yàn)槭录h(huán)在瀏覽器和 Node.js
的表現(xiàn)是很大不一樣的慧起,本人只談?wù)摰搅藶g覽器中的事件循環(huán),并介紹了微任務(wù)和宏任務(wù)册倒,以及它們的執(zhí)行流程蚓挤,最后通過 7 道題目幫助大家鞏固知識(shí)
大家喜歡的話,別忘了點(diǎn)贊關(guān)注~
往期優(yōu)秀文章推薦
- 一個(gè)合格的中級(jí)前端工程師應(yīng)該掌握的 20 個(gè) Vue 技巧[3]
- 【Vue進(jìn)階】——如何實(shí)現(xiàn)組件屬性透?jìng)鳎?sup>[4]
- 前端應(yīng)該知道的 HTTP 知識(shí)【金九銀十必備】[5]
- 最強(qiáng)大的 CSS 布局 —— Grid 布局[6]
- 如何用 Typescript 寫一個(gè)完整的 Vue 應(yīng)用程序[7]
- 前端應(yīng)該知道的web調(diào)試工具——whistle[8]
參考
詳解JavaScript中的Event Loop(事件循環(huán))機(jī)制[9]
深入理解NodeJS事件循環(huán)機(jī)制[10]
并發(fā)模型與事件循環(huán)[11]
【前端體系】從一道面試題談?wù)剬?duì)EventLoop的理解[12]
菲利普·羅伯茨:到底什么是Event Loop呢? | 歐洲 JSConf 2014[13]
JavaScript中的Event Loop(事件循環(huán))機(jī)制[14]
JS事件循環(huán)機(jī)制(event loop)之宏任務(wù)/微任務(wù)[15]
深入理解js事件循環(huán)機(jī)制(瀏覽器篇)[16]
從面試題看 JS 事件循環(huán)與 macro micro 任務(wù)隊(duì)列[17]
參考資料
[1]
菲利普·羅伯茨:到底什么是Event Loop呢屈尼? | 歐洲 JSConf 2014: https://www.youtube.com/watch?v=8aGhZQkoFbQ [2]
一個(gè)合格的中級(jí)前端工程師應(yīng)該掌握的 20 個(gè) Vue 技巧: https://juejin.im/post/6872128694639394830 [4]
【Vue進(jìn)階】——如何實(shí)現(xiàn)組件屬性透?jìng)鳎? https://juejin.im/post/6865451649817640968 [5]
前端應(yīng)該知道的 HTTP 知識(shí)【金九銀十必備】: https://juejin.im/post/6864119706500988935 [6]
最強(qiáng)大的 CSS 布局 —— Grid 布局: https://juejin.im/post/6854573220306255880 [7]
如何用 Typescript 寫一個(gè)完整的 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))機(jī)制: https://zhuanlan.zhihu.com/p/33058983 [10]
深入理解NodeJS事件循環(huán)機(jī)制: https://juejin.im/post/6844903999506923528 [11]
并發(fā)模型與事件循環(huán): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop [12]
【前端體系】從一道面試題談?wù)剬?duì)EventLoop的理解: https://juejin.im/post/6868849475008331783 [13]
菲利普·羅伯茨:到底什么是Event Loop呢册着? | 歐洲 JSConf 2014: https://www.youtube.com/watch?v=8aGhZQkoFbQ [14]
JavaScript中的Event Loop(事件循環(huán))機(jī)制: https://segmentfault.com/a/1190000022805523 [15]
JS事件循環(huán)機(jī)制(event loop)之宏任務(wù)/微任務(wù): https://juejin.im/post/6844903638238756878 [16]
深入理解js事件循環(huán)機(jī)制(瀏覽器篇): http://lynnelv.github.io/js-event-loop-browser [17]
從面試題看 JS 事件循環(huán)與 macro micro 任務(wù)隊(duì)列: https://juejin.im/post/6844903796754104334