宏任務(wù)和微任務(wù)到底是什么宰闰?

先來一道常見的面試題:

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()
stack.jpg

當(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)行的呢庄呈?

image

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:asyncawait是如何處理異步任務(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í)行?

setImmediateprocess.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í)行順序如下:

  1. timers: 執(zhí)行setTimeout和setInterval的回調(diào)
  2. pending callbacks: 執(zhí)行延遲到下一個循環(huán)迭代的 I/O 回調(diào)
  3. idle, prepare: 僅系統(tǒng)內(nèi)部使用
  4. poll: 檢索新的 I/O 事件;執(zhí)行與 I/O 相關(guān)的回調(diào)。事實(shí)上除了其他幾個階段處理的事情奕删,其他幾乎所有的異步都在這個階段處理俺泣。
  5. check: setImmediate在這里執(zhí)行
  6. 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>

  1. 外層是一個setTimeout,所以執(zhí)行它的回調(diào)的時候已經(jīng)在timers階段了
  2. 處理里面的setTimeout谨设,因?yàn)楸敬窝h(huán)的timers正在執(zhí)行熟掂,所以其回調(diào)其實(shí)加到了下個timers階段
  3. 處理里面的setImmediate,將它的回調(diào)加入check階段的隊(duì)列
  4. 外層timers階段執(zhí)行完扎拣,進(jìn)入pending callbacks赴肚,idle, prepare,poll二蓝,這幾個隊(duì)列都是空的誉券,所以繼續(xù)往下
  5. 到了check階段,發(fā)現(xiàn)了setImmediate的回調(diào)刊愚,拿出來執(zhí)行
  6. 然后是close callbacks踊跟,隊(duì)列是空的,跳過
  7. 又是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>

  1. 遇到setTimeout,雖然設(shè)置的是0毫秒觸發(fā),但是被node.js強(qiáng)制改為1毫秒刻像,塞入times階段
  2. 遇到setImmediate塞入check階段
  3. 同步代碼執(zhí)行完畢畅买,進(jìn)入Event Loop
  4. 先進(jìn)入times階段,檢查當(dāng)前時間過去了1毫秒沒有细睡,如果過了1毫秒谷羞,滿足setTimeout條件,執(zhí)行回調(diào)溜徙,如果沒過1毫秒湃缎,跳過
  5. 跳過空的階段,進(jìn)入check階段蠢壹,執(zhí)行setImmediate回調(diào)

可見嗓违,1毫秒是個關(guān)鍵點(diǎn),所以在上面的例子中图贸,setImmediate不一定在setTimeout之前執(zhí)行了蹂季。

拓展 3:Promiseprocess.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何吝。

所以,nextTickPromise同時出現(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ù):

  1. 優(yōu)先檢測是否支持原生 setImmediate(這是一個高版本 IE 和 Edge 才支持的特性)
  2. 如果不支持,再去檢測是否支持原生的MessageChannel
  3. 如果也不支持的話就會降級為 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')
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末搀捷,一起剝皮案震驚了整個濱河市星掰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌指煎,老刑警劉巖蹋偏,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異至壤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)枢纠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門像街,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人晋渺,你說我怎么就攤上這事镰绎。” “怎么了木西?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵畴栖,是天一觀的道長。 經(jīng)常有香客問我八千,道長吗讶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任恋捆,我火速辦了婚禮照皆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘沸停。我一直安慰自己膜毁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布愤钾。 她就那樣靜靜地躺著瘟滨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪能颁。 梳的紋絲不亂的頭發(fā)上杂瘸,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機(jī)與錄音劲装,去河邊找鬼胧沫。 笑死昌简,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的绒怨。 我是一名探鬼主播纯赎,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼南蹂!你這毒婦竟也來了犬金?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤六剥,失蹤者是張志新(化名)和其女友劉穎晚顷,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疗疟,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡该默,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了策彤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片栓袖。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖店诗,靈堂內(nèi)的尸體忽然破棺而出裹刮,到底是詐尸還是另有隱情,我是刑警寧澤庞瘸,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布捧弃,位于F島的核電站,受9級特大地震影響擦囊,放射性物質(zhì)發(fā)生泄漏违霞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一霜第、第九天 我趴在偏房一處隱蔽的房頂上張望葛家。 院中可真熱鬧,春花似錦泌类、人聲如沸癞谒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽弹砚。三九已至,卻和暖如春枢希,著一層夾襖步出監(jiān)牢的瞬間桌吃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工苞轿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留茅诱,地道東北人逗物。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像瑟俭,于是被迫代替她去往敵國和親翎卓。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344