JavaScript事件循環(huán)剖析宏任務(wù)與微任務(wù)
前言
相信對(duì)于剛學(xué)習(xí)JavaScript的新手來(lái)說(shuō),去理解JS中的事件循環(huán)原理以及異步執(zhí)行過程比較困難,但是這是JS必須要會(huì)的基礎(chǔ)知識(shí),逃避不能解決問題,筆者曾經(jīng)也被這個(gè)知識(shí)點(diǎn)困擾過,現(xiàn)根據(jù)以往的經(jīng)驗(yàn)編寫此文章,旨在幫助大家徹底搞懂它們以及自我鞏固,話不多說(shuō),進(jìn)入正題。
注意:本篇文章主要是基于瀏覽器環(huán)境,Node環(huán)境沒有研究過暫不討論
引言
我們先來(lái)小試牛刀,看看下面這段代碼是怎么執(zhí)行的,例1:
setTimeout(() => { console.log('time') }); new Promise((resolve, reject) => { console.log('p1'); resolve(); }).then(() => { console.log('res') }); console.log(1); // 輸出: p1 1 res time
怎么樣?你想的輸出結(jié)果和實(shí)際的輸出結(jié)果是一樣的嗎?如果是一樣的說(shuō)明你對(duì)事件循環(huán)有一定的了解,但是你真的已經(jīng)清楚的知道了事件循環(huán)的原理嗎?讓我們繼續(xù)往下看。
為什么會(huì)有事件循環(huán)?
JS是單線程的
眾所周知:JavaScript 是一門單線程語(yǔ)言,也就是說(shuō),同一個(gè)時(shí)間只能做一件事。這是因?yàn)?Javascript 這門腳 本語(yǔ)言誕生的使命所致——JavaScript 是為處理頁(yè)面中用戶的交互,以及操作 DOM 而誕生的。比如我們對(duì) 某個(gè) DOM 元素進(jìn)行添加和刪除操作,不能同時(shí)進(jìn)行。 應(yīng)該先進(jìn)行添加,之后再刪除。
單線程就意味著,所有任務(wù)需要排隊(duì),前一個(gè)任務(wù)結(jié)束,才會(huì)執(zhí)行后一個(gè)任務(wù)。這樣所導(dǎo)致的問題是: 如果 JS 執(zhí)行的時(shí)間過長(zhǎng),這樣就會(huì)造成頁(yè)面的渲染不連貫,導(dǎo)致頁(yè)面渲染加載阻塞的感覺
為了解決這個(gè)問題,利用多核 CPU 的計(jì)算能力,HTML5 提出 Web Worker 標(biāo)準(zhǔn),允許JavaScript 腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制。于是,JS 中出現(xiàn)了同步任務(wù)和異步任務(wù)。
同步任務(wù)和異步任務(wù)
- 同步任務(wù):
同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧。在主線程上排隊(duì)執(zhí)行的任務(wù),只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù);
- 異步任務(wù):
不進(jìn)入主線程、而進(jìn)入”任務(wù)隊(duì)列”的任務(wù),當(dāng)主線程中的任務(wù)運(yùn)行完了,才會(huì)從”任務(wù)隊(duì)列”取出異步任務(wù)放入主線程執(zhí)行。JS 的異步是通過回調(diào)函數(shù)實(shí)現(xiàn)的。異步任務(wù)相關(guān)回調(diào)函數(shù)添加到任務(wù)隊(duì)列中(任務(wù)隊(duì)列也稱為消息隊(duì)列)
注意:異步任務(wù)執(zhí)行機(jī)制在這里描述的比較籠統(tǒng),主要方便大家理解,具體細(xì)節(jié)在后面的“宏任務(wù)與微任務(wù)”中會(huì)詳細(xì)介紹
JS的事件循環(huán)就是基于同步任務(wù)與異步任務(wù)來(lái)展開的,讓我們繼續(xù)往下看:
JS事件循環(huán)
事件循環(huán)是JavaScript實(shí)現(xiàn)異步的一種方法,也是JavaScript的執(zhí)行機(jī)制
如圖:
當(dāng)一個(gè)腳本第一次執(zhí)行的時(shí)候,js引擎會(huì)解析這段代碼,并將其中的同步代碼按照?qǐng)?zhí)行順序加入執(zhí)行棧中,然后從頭開始執(zhí)行。
當(dāng)遇到異步任務(wù)時(shí)不會(huì)一直等待事件的返回結(jié)果,而是將事件掛起(即交給其他線程處理,上圖是指Web Worker),繼續(xù)執(zhí)行執(zhí)行棧中的其他任務(wù)。
當(dāng)異步事件返回結(jié)果時(shí),js將異步事件callback函數(shù)放入隊(duì)列中,被放入隊(duì)列中的異步事件不會(huì)立即回調(diào),等到當(dāng)前執(zhí)行棧中的任務(wù)都執(zhí)行完成,處于閑置狀態(tài)的主線程按照隊(duì)列順序?qū)⑻幱谑孜皇录腸allback函數(shù)放入執(zhí)行棧中,執(zhí)行該函數(shù)的同步代碼,如果遇到了異步事件,同樣也會(huì)將其回調(diào)函數(shù)放入事件隊(duì)列中…
如此反復(fù),就形成了一個(gè)循環(huán),這也是被稱為“事件循環(huán)(EventLoop)”的原因。
js事件循環(huán)的基本原理已經(jīng)描述清楚,但是異步任務(wù)之間也有所不同:
任務(wù)隊(duì)列實(shí)際上分為兩個(gè):宏任務(wù)隊(duì)列和微任務(wù)隊(duì)列。上圖只表示了一個(gè)是為了便于大家理解事件循環(huán),下面就是事件循環(huán)更細(xì)節(jié)的東西了
宏任務(wù)與微任務(wù)
上面講到,js在執(zhí)行異步任務(wù)時(shí),回調(diào)函數(shù)會(huì)被放在js的任務(wù)隊(duì)列中,實(shí)際上,回調(diào)函數(shù)的類別不同,執(zhí)行的優(yōu)先級(jí)也不同。
不同的優(yōu)先級(jí)被分為兩類,一類是宏任務(wù)(Micro task),一類是微任務(wù)(Macro task)。
回調(diào)函數(shù)是微任務(wù)時(shí),會(huì)被放在微任務(wù)隊(duì)列,回調(diào)函數(shù)是宏任務(wù)時(shí),會(huì)被放在宏任務(wù)隊(duì)列。
微任務(wù)的優(yōu)先級(jí)高于宏任務(wù),當(dāng)主線程的任務(wù)執(zhí)行完成時(shí),會(huì)首先去執(zhí)行微任務(wù)隊(duì)列中首位的回調(diào)函數(shù),當(dāng)微任務(wù)隊(duì)列中為空時(shí),才回去執(zhí)行宏任務(wù)隊(duì)列中的回調(diào)函數(shù)。
常見的宏任務(wù)有哪些?
- 包括整體代碼 script
- setTimeout()
- setInterval()
- setImmediate()(Node獨(dú)有)
- I/O
- UI 交互事件(瀏覽器獨(dú)有)
- requestAnimationFrame() (瀏覽器獨(dú)有)
常見的微任務(wù)有哪些?
- Promise.then(); Promise.cath()
- async/await
- process.nextTick() (Node獨(dú)有)
- MutationObserver() (H5新增,監(jiān)聽DOM樹變化)
- Object.observe() (異步監(jiān)視對(duì)象修改,已廢棄)
注意:new Promise()屬于同步任務(wù),但是Promise.then(); Promise.cath()屬于異步任務(wù)的微任務(wù)
執(zhí)行過程總結(jié)(重點(diǎn))
現(xiàn)在我們對(duì)事件循環(huán)有了深入了解了,但是它們的執(zhí)行過程還不是很清晰,我們?cè)侔褕?zhí)行過程弄清楚了以后就能游刃有余了。
同步任務(wù) —> 微任務(wù) —> 宏任務(wù)...
- 先執(zhí)行所有同步任務(wù),碰到異步任務(wù)放到任務(wù)隊(duì)列中
- 同步任務(wù)執(zhí)行完畢,開始執(zhí)行當(dāng)前所有的異步任務(wù)
- 先執(zhí)行任務(wù)隊(duì)列里面所有的微任務(wù),如果執(zhí)行過程中又產(chǎn)生了微任務(wù)也會(huì)在本次執(zhí)行過程中執(zhí)行(即在下一個(gè)宏任務(wù)執(zhí)行之前執(zhí)行,可以看看案例1)
- 然后執(zhí)行一個(gè)宏任務(wù)(從宏任務(wù)隊(duì)列頭部pop出一個(gè)宏任務(wù)進(jìn)執(zhí)行棧,該任務(wù)中的具體代碼也如步驟1執(zhí)行)
- 然后再執(zhí)行所有的微任務(wù)(此時(shí)的微任務(wù)一般為步驟4中產(chǎn)生出的微任務(wù))
- 再執(zhí)行一個(gè)宏任務(wù),再執(zhí)行所有的微任務(wù)·······依次類推到執(zhí)行結(jié)束。
3-6的這個(gè)循環(huán)稱為事件循環(huán)Event Loop
案例挑戰(zhàn)
學(xué)會(huì)了嗎?讓我們來(lái)做幾個(gè)案例鞏固一下吧
案例1:
const promise = new Promise((resolve, reject) => { resolve("10") }).then(res => { console.log("res1:", res) //res1: hahaha return 9 }).then(res => { console.log("res2:", res) //res2: 9 return 8 }).then(res => { console.log("res3:", res) //res3: 8 let promise2=new Promise((resolve,reject)=>{ resolve("p2") }).then(res=>{ console.log(res) setTimeout(function(){ console.log("setTimeout2") },0) }) }) console.log('aaa') setTimeout(function(){ console.log("setTimeout1") },0) const promise1 = new Promise((resolve, reject) => { console.log("p1") resolve(989) }).then(res => { console.log(res) return 990 }).then(res=>{ console.log(res) return 991 }).then(res=>{ console.log(res) return 0 }) /*輸出結(jié)果: aaa p1 res1: 10 989 res2: 9 990 res3: 8 991 p2 setTimeout1 setTimeout2 */
案例2:
console.log('1'); // 定義注解 setTimeout_1 用于下文使用方便 setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) // setTimeout_2 setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) }) // 輸出結(jié)果: 1 7 6 8 2 4 3 5 9 11 10 12
案例3:
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) }) // 輸出結(jié)果:1 7 6 8 2 4 3 5 9 11 10 12
以上就是JavaScript事件循環(huán)剖析宏任務(wù)與微任務(wù)的詳細(xì)內(nèi)容,更多關(guān)于JavaScript 事件循環(huán)宏任務(wù)微任務(wù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript中動(dòng)態(tài)加載js文件多種解決辦法總結(jié)
這篇文章主要介紹了javascript中動(dòng)態(tài)加載js文件多種解決辦法,有需要的朋友可以參考一下2013-11-11js動(dòng)態(tài)獲取子復(fù)選項(xiàng)并設(shè)計(jì)全選及提交的實(shí)現(xiàn)方法
下面小編就為大家?guī)?lái)一篇js動(dòng)態(tài)獲取子復(fù)選項(xiàng)并設(shè)計(jì)全選及提交的實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的, 現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2016-06-06mpvue微信小程序多列選擇器用法之省份城市選擇的實(shí)現(xiàn)
這篇文章主要給大家介紹了關(guān)于mpvue微信小程序多列選擇器用法之省份城市選擇實(shí)現(xiàn)的相關(guān)資料,文中通過示例代碼以及圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03javascript 用函數(shù)語(yǔ)句和表達(dá)式定義函數(shù)的區(qū)別詳解
本篇文章主要介紹了javascript 用函數(shù)語(yǔ)句和表達(dá)式定義函數(shù)的區(qū)別。需要的朋友可以過來(lái)參考下,希望對(duì)大家有所幫助2014-01-01JS自動(dòng)生成動(dòng)態(tài)HTML驗(yàn)證碼頁(yè)面
這篇文章主要介紹了JS自動(dòng)生成動(dòng)態(tài)HTML驗(yàn)證碼頁(yè)面,輸入錯(cuò)誤自動(dòng)清空輸入框的功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06純javascript實(shí)現(xiàn)四方向文本無(wú)縫滾動(dòng)效果
本文主要給大家分享了使用純javascript實(shí)現(xiàn)的可控制的四方向文本無(wú)縫滾動(dòng)的代碼,效果非常不錯(cuò),有需要的小伙伴可以參考下。2015-06-06