先來一道常見的面試題:
console.log('start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
new Promise((resolve) => {
console.log('promise')
resolve()
})
.then(() => {
console.log('then1')
})
.then(() => {
console.log('then2')
})
console.log('end')
應(yīng)該不少同學(xué)都能答出來,結(jié)果為:
start
promise
end
then1
then2
setTimeout
這個就涉及到JavaScript事件輪詢中的宏任務(wù)和微任務(wù)簿透。那么移袍,你能說清楚到底宏任務(wù)和微任務(wù)是什么?是誰發(fā)起的老充?為什么微任務(wù)的執(zhí)行要先于宏任務(wù)呢葡盗?
首先,我們需要先知道JS運(yùn)行機(jī)制啡浊。
JS運(yùn)行機(jī)制
概念1: JS是單線程執(zhí)行
”JS是單線程的”指的是JS 引擎線程觅够。
在瀏覽器環(huán)境中,有JS 引擎線程和渲染線程巷嚣,且兩個線程互斥喘先。
Node環(huán)境中,只有JS 線程廷粒。
概念2:宿主
JS運(yùn)行的環(huán)境窘拯。一般為瀏覽器或者Node。
概念3:執(zhí)行棧
是一個存儲函數(shù)調(diào)用的棧結(jié)構(gòu)评雌,遵循先進(jìn)后出的原則树枫。
function foo() {
throw new Error('error')
}
function bar() {
foo()
}
bar()
當(dāng)開始執(zhí)行 JS 代碼時,首先會執(zhí)行一個 main
函數(shù)景东,然后執(zhí)行我們的代碼砂轻。根據(jù)先進(jìn)后出的原則,后執(zhí)行的函數(shù)會先彈出棧斤吐,在圖中我們也可以發(fā)現(xiàn)搔涝,foo
函數(shù)后執(zhí)行,當(dāng)執(zhí)行完畢后就從棧中彈出了和措。
概念4:Event Loop
JS到底是怎么運(yùn)行的呢庄呈?
JS引擎常駐于內(nèi)存中,等待宿主將JS代碼或函數(shù)傳遞給它派阱。
也就是等待宿主環(huán)境分配宏觀任務(wù)诬留,反復(fù)等待 - 執(zhí)行即為事件循環(huán)。
Event Loop中,每一次循環(huán)稱為tick文兑,每一次tick的任務(wù)如下:
- 執(zhí)行棧選擇最先進(jìn)入隊(duì)列的宏任務(wù)(一般都是script)盒刚,執(zhí)行其同步代碼直至結(jié)束;
- 檢查是否存在微任務(wù)绿贞,有則會執(zhí)行至微任務(wù)隊(duì)列為空因块;
- 如果宿主為瀏覽器,可能會渲染頁面籍铁;
- 開始下一輪tick涡上,執(zhí)行宏任務(wù)中的異步代碼(setTimeout等回調(diào))。
概念5:宏任務(wù)和微任務(wù)
ES6 規(guī)范中拒名,microtask 稱為
jobs
吩愧,macrotask 稱為task
宏任務(wù)是由宿主發(fā)起的,而微任務(wù)由JavaScript自身發(fā)起靡狞。
在ES3以及以前的版本中耻警,JavaScript本身沒有發(fā)起異步請求的能力,也就沒有微任務(wù)的存在甸怕。在ES5之后甘穿,JavaScript引入了Promise
,這樣梢杭,不需要瀏覽器温兼,JavaScript引擎自身也能夠發(fā)起異步任務(wù)了。
所以武契,總結(jié)一下募判,兩者區(qū)別為:
宏任務(wù)(macrotask) | 微任務(wù)(microtask) | |
---|---|---|
誰發(fā)起的 | 宿主(Node、瀏覽器) | JS引擎 |
具體事件 | 1. script (可以理解為外層同步代碼) 2. setTimeout/setInterval 3. UI rendering/UI事件 4. postMessage咒唆,MessageChannel 5. setImmediate届垫,I/O(Node.js) |
1. Promise 2. MutaionObserver 3. Object.observe(已廢棄; Proxy 對象替代)4. process.nextTick(Node.js) |
誰先運(yùn)行 | 后運(yùn)行 | 先運(yùn)行 |
會觸發(fā)新一輪Tick嗎 | 會 | 不會 |
拓展 1:async
和await
是如何處理異步任務(wù)的全释?
簡單說装处,async
是通過Promise
包裝異步任務(wù)。
比如有如下代碼:
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
改為ES5的寫法:
new Promise((resolve, reject) => {
// console.log('async2 end')
async2()
...
}).then(() => {
// 執(zhí)行async1()函數(shù)await之后的語句
console.log('async1 end')
})
當(dāng)調(diào)用 async1
函數(shù)時浸船,會馬上輸出 async2 end
妄迁,并且函數(shù)返回一個 Promise
,接下來在遇到 await
的時候會就讓出線程開始執(zhí)行 async1
外的代碼(可以把 await
看成是讓出線程的標(biāo)志)李命。
然后當(dāng)同步代碼全部執(zhí)行完畢以后登淘,就會去執(zhí)行所有的異步代碼,那么又會回到 await
的位置封字,去執(zhí)行 then
中的回調(diào)黔州。
拓展 2:setTimeout
耍鬓,setImmediate
誰先執(zhí)行?
setImmediate
和process.nextTick
為Node環(huán)境下常用的方法(IE11支持setImmediate
)辩撑,所以界斜,后續(xù)的分析都基于Node宿主。
Node.js是運(yùn)行在服務(wù)端的js合冀,雖然用到也是V8引擎,但由于服務(wù)目的和環(huán)境不同项贺,導(dǎo)致了它的API與原生JS有些區(qū)別君躺,其Event Loop還要處理一些I/O,比如新的網(wǎng)絡(luò)連接等开缎,所以與瀏覽器Event Loop不太一樣棕叫。
執(zhí)行順序如下:
- timers: 執(zhí)行setTimeout和setInterval的回調(diào)
- pending callbacks: 執(zhí)行延遲到下一個循環(huán)迭代的 I/O 回調(diào)
- idle, prepare: 僅系統(tǒng)內(nèi)部使用
- poll: 檢索新的 I/O 事件;執(zhí)行與 I/O 相關(guān)的回調(diào)。事實(shí)上除了其他幾個階段處理的事情奕删,其他幾乎所有的異步都在這個階段處理俺泣。
- check: setImmediate在這里執(zhí)行
- close callbacks: 一些關(guān)閉的回調(diào)函數(shù),如:socket.on('close', ...)
一般來說完残,setImmediate
會在setTimeout
之前執(zhí)行伏钠,如下:
console.log('outer');
setTimeout(() => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
}, 0);
其執(zhí)行順序?yàn)椋?/p>
- 外層是一個setTimeout,所以執(zhí)行它的回調(diào)的時候已經(jīng)在timers階段了
- 處理里面的setTimeout谨设,因?yàn)楸敬窝h(huán)的timers正在執(zhí)行熟掂,所以其回調(diào)其實(shí)加到了下個timers階段
- 處理里面的setImmediate,將它的回調(diào)加入check階段的隊(duì)列
- 外層timers階段執(zhí)行完扎拣,進(jìn)入pending callbacks赴肚,idle, prepare,poll二蓝,這幾個隊(duì)列都是空的誉券,所以繼續(xù)往下
- 到了check階段,發(fā)現(xiàn)了setImmediate的回調(diào)刊愚,拿出來執(zhí)行
- 然后是close callbacks踊跟,隊(duì)列是空的,跳過
- 又是timers階段百拓,執(zhí)行
console.log('setTimeout')
但是琴锭,如果當(dāng)前執(zhí)行環(huán)境不是timers階段,就不一定了衙传。决帖。。蓖捶。順便科普一下Node里面對setTimeout
的特殊處理:setTimeout(fn, 0)
會被強(qiáng)制改為setTimeout(fn, 1)
地回。
看看下面的例子:
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
其執(zhí)行順序?yàn)椋?/p>
- 遇到
setTimeout
,雖然設(shè)置的是0毫秒觸發(fā),但是被node.js強(qiáng)制改為1毫秒刻像,塞入times階段 - 遇到
setImmediate
塞入check階段 - 同步代碼執(zhí)行完畢畅买,進(jìn)入
Event Loop
- 先進(jìn)入
times
階段,檢查當(dāng)前時間過去了1毫秒沒有细睡,如果過了1毫秒谷羞,滿足setTimeout
條件,執(zhí)行回調(diào)溜徙,如果沒過1毫秒湃缎,跳過 - 跳過空的階段,進(jìn)入check階段蠢壹,執(zhí)行
setImmediate
回調(diào)
可見嗓违,1毫秒是個關(guān)鍵點(diǎn),所以在上面的例子中图贸,setImmediate
不一定在setTimeout
之前執(zhí)行了蹂季。
拓展 3:Promise
,process.nextTick
誰先執(zhí)行疏日?
process.nextTick
為Node環(huán)境下的方法偿洁。它是一個特殊的異步API,其不屬于任何的Event Loop階段制恍。事實(shí)上Node在遇到這個API時父能,Event Loop根本就不會繼續(xù)進(jìn)行,會馬上停下來執(zhí)行process.nextTick()净神,這個執(zhí)行完后才會繼續(xù)Event Loop何吝。
所以,nextTick
和Promise
同時出現(xiàn)時鹃唯,肯定是nextTick
先執(zhí)行爱榕,原因是nextTick
的隊(duì)列比Promise
隊(duì)列優(yōu)先級更高。
拓展 4:應(yīng)用場景 - Vue中的vm.$nextTick
vm.$nextTick
接受一個回調(diào)函數(shù)作為參數(shù)坡慌,用于將回調(diào)延遲到下次DOM更新周期之后執(zhí)行黔酥。
這個API就是基于事件循環(huán)實(shí)現(xiàn)的。
“下次DOM更新周期”的意思就是下次微任務(wù)執(zhí)行時更新DOM洪橘,而vm.$nextTick
就是將回調(diào)函數(shù)添加到微任務(wù)中(在特殊情況下會降級為宏任務(wù))跪者。
因?yàn)槲⑷蝿?wù)優(yōu)先級太高,Vue 2.4版本之后熄求,提供了強(qiáng)制使用宏任務(wù)的方法渣玲。
vm.$nextTick優(yōu)先使用Promise,創(chuàng)建微任務(wù)弟晚。
如果不支持Promise或者強(qiáng)制開啟宏任務(wù)忘衍,那么逾苫,會按照如下順序發(fā)起宏任務(wù):
- 優(yōu)先檢測是否支持原生 setImmediate(這是一個高版本 IE 和 Edge 才支持的特性)
- 如果不支持,再去檢測是否支持原生的MessageChannel
- 如果也不支持的話就會降級為 setTimeout枚钓。
小結(jié)
下面是道加強(qiáng)版的考題铅搓,大家可以試一試。
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')