文章首發(fā)于 szhshp的第三邊境研究所 ,轉(zhuǎn)載請(qǐng)注明
一道Javascript面試題引發(fā)的血案
先來(lái)看幾道面試題竖瘾,公司的開(kāi)發(fā)們都嘗試做了一下,然而基本沒(méi)有人能夠全部答對(duì)。
覆蓋的考點(diǎn)很多卿嘲,也有一些難度,題目挺有意思建議手動(dòng)執(zhí)行一邊玩玩夫壁。
Question 1
for (var i = 0; i <5 ; i++) {
setTimeout(function(){
console.log(i)
),1000}
}
console.log(i)
- Q:這道題目會(huì)輸出什么拾枣?
- A:這道題目還比較簡(jiǎn)單,如果對(duì)Javascript稍微有一點(diǎn)深入的同學(xué)都會(huì)發(fā)現(xiàn)這道題目循環(huán)里面出現(xiàn)了閉包,因此輸出的數(shù)字是完全相同的梅肤,最后的輸出也是完全相同的司蔬。
- 考點(diǎn):閉包,(偽)異步
Question 2
for (let i = 0; i <5 ; i++) { //注意var變成了let
setTimeout(function(){
console.log(i)
},1000)
}
console.log(i)
Q:這道題目會(huì)輸出什么姨蝴?
-
A:這道題目其實(shí)是個(gè)坑俊啼。首先題目與Q1的區(qū)別就是變量i的定義改為了關(guān)鍵字let,使用let的時(shí)候會(huì)將變量限制在循環(huán)之中左医,因此第二個(gè)輸出其實(shí)會(huì)報(bào)錯(cuò)授帕。另外
setTimeout
實(shí)現(xiàn)了(偽)異步,同時(shí)因?yàn)閘et將變量作用域進(jìn)行了控制浮梢,破壞了閉包結(jié)構(gòu)跛十,因此會(huì)按照正常順序輸出。關(guān)于let關(guān)鍵字[1]
Use the let statement to declare a variable, the scope of which is restricted to the block in which it is declared. You can assign values to the variables when you declare them or later in your script.
A variable declared usinglet
cannot be used before its declaration or an error will result.. 考點(diǎn):閉包秕硝,(偽)異步芥映,作用域
Question 3
同樣是Q1的代碼
for (var i = 0; i <5 ; i++) { //DO NOT MODIFY
setTimeout(function(){ //DO NOT MODIFY
console.log(i)
},1000)
}
console.log(i) //DO NOT MODIFY
Q:修改上述代碼(部分行不允許修改,可以在代碼間插入)远豺,以實(shí)現(xiàn)“每隔一秒輸出一個(gè)數(shù)字并且順序?yàn)?-5”
-
A
- 首先考到了破壞閉包結(jié)構(gòu)奈偏,破壞閉包的方法很多,最簡(jiǎn)單的是將跨域變量轉(zhuǎn)換成范圍內(nèi)的變量
- 其次考到了
setTimeout
事件隊(duì)列的處理
for (var i = 0; i <5 ; i++) { (function(i){ setTimeout(function(){ console.log(i) },1000*i) })(i) //將i作為參數(shù)傳入匿名函數(shù)躯护,如此破壞了閉包內(nèi)跨域訪問(wèn) } setTimeout(function (){ console.log(i); }, 5000); //強(qiáng)行將5放到5sec后輸出
考點(diǎn):閉包惊来,(偽)異步,作用域榛做,事件隊(duì)列
Question 4
window.setTimeout(function (){
console.log(2)
},1);
//Ouput for a long time
for (var i = 0; i < 1000; i++) {
console.log('');
};
console.log(1)
window.setTimeout(function (){
console.log(3)
},0);
- Q:這道題目會(huì)輸出什么唁盏?
- A:可能有些同學(xué)會(huì)記得,setTimeout是一個(gè)回調(diào)函數(shù)检眯,因此無(wú)論延時(shí)多少結(jié)果都是最后輸出厘擂。
- 考點(diǎn):(偽)異步,事件隊(duì)列
Question 5
這道題目其實(shí)是其他地方抄襲來(lái)的[2]锰瘸,正好和之前考點(diǎn)有一定重疊因此一起放了過(guò)來(lái):
setTimeout(function(){console.log(4)},0);
new Promise(function(resolve){
console.log(1)
//time consuming ops
for( var i=0 ; i<10000 ; i++ ){
i==9999 && resolve();
}
console.log(2)
}).then(function(){
console.log(5)
});
console.log(3);
Q:這道題目會(huì)輸出什么刽严?
-
A:輸出是12354
關(guān)于這個(gè)輸出,有如下幾個(gè)邏輯:
- 4是
setTimeOut.callback
的輸出避凝,加入MacroTask
末端舞萄, - 輸出1
- 執(zhí)行
Promise.resolve()
將輸出5的callback放到MicroTask
中(注意這里不是MacroTask
) - 輸出2
- 輸出3
-
MacroTask
首個(gè)任務(wù)執(zhí)行完畢 - 查找
MicroTask
里面有沒(méi)有任務(wù),發(fā)現(xiàn)有管削,執(zhí)行倒脓,輸出5 - 查找
MacroTask
里面有沒(méi)有任務(wù),發(fā)現(xiàn)有含思,執(zhí)行崎弃,輸出4 - 查找
MicroTask
里面有沒(méi)有任務(wù)甘晤,發(fā)現(xiàn)沒(méi)有,可以休息了 - 查找
MacroTask
里面有沒(méi)有任務(wù)饲做,發(fā)現(xiàn)沒(méi)有线婚,可以睡覺(jué)了 - 執(zhí)行完畢
- 4是
關(guān)于事件循環(huán)/關(guān)于macrotask
和microtask
[3]
簡(jiǎn)介
一個(gè)事件循環(huán)(EventLoop)中會(huì)有一個(gè)正在執(zhí)行的任務(wù)(Task),而這個(gè)任務(wù)就是從 macrotask 隊(duì)列中來(lái)的盆均。在whatwg規(guī)范中有 queue 就是任務(wù)隊(duì)列塞弊。當(dāng)這個(gè) macrotask 執(zhí)行結(jié)束后所有可用的 microtask 將會(huì)在同一個(gè)事件循環(huán)中執(zhí)行,當(dāng)這些 microtask 執(zhí)行結(jié)束后還能繼續(xù)添加 microtask 一直到真?zhèn)€ microtask 隊(duì)列執(zhí)行結(jié)束泪姨。
怎么用
基本來(lái)說(shuō)游沿,當(dāng)我們想以同步的方式來(lái)處理異步任務(wù)時(shí)候就用 microtask(比如我們需要直接在某段代碼后就去執(zhí)行某個(gè)任務(wù),就像Promise一樣)驴娃。
其他情況就直接用 macrotask奏候。
兩者的具體實(shí)現(xiàn)
- macrotasks:
setTimeout
setInterval
setImmediate
I/O
UI渲染
- microtasks:
Promise
process.nextTick
Object.observe
MutationObserver
從規(guī)范中理解
規(guī)范:https://html.spec.whatwg.org/multipage/webappapis.html#task-queue
- 一個(gè)事件循環(huán)(event loop)會(huì)有一個(gè)或多個(gè)任務(wù)隊(duì)列(task queue) task queue 就是 macrotask queue
- 每一個(gè) event loop 都有一個(gè) microtask queue
- task queue == macrotask queue != microtask queue
- 一個(gè)任務(wù) task 可以放入 macrotask queue 也可以放入 microtask queue 中
- 當(dāng)一個(gè) task 被放入隊(duì)列 queue(macro或micro) 那這個(gè) task 就可以被立即執(zhí)行了
再來(lái)回顧下事件循環(huán)如何執(zhí)行一個(gè)任務(wù)的流程
當(dāng)執(zhí)行棧(call stack)為空的時(shí)候循集,開(kāi)始依次執(zhí)行:
- 把最早的任務(wù)(task A)放入任務(wù)隊(duì)列
- 如果 task A 為null (那任務(wù)隊(duì)列就是空)唇敞,直接跳到第6步
- 將 currently running task 設(shè)置為 task A
- 執(zhí)行 task A (也就是執(zhí)行回調(diào)函數(shù))
- 將 currently running task 設(shè)置為 null 并移出 task A
- 執(zhí)行 microtask 隊(duì)列
- 在 microtask 中選出最早的任務(wù) task X
- 如果 task X 為null (那 microtask 隊(duì)列就是空),直接跳到 g
- 將 currently running task 設(shè)置為 task X
- 執(zhí)行 task X
- 將 currently running task 設(shè)置為 null 并移出 task X
- 在 microtask 中選出最早的任務(wù) , 跳到 b
- 結(jié)束 microtask 隊(duì)列
- 跳到第一步
上面就算是一個(gè)簡(jiǎn)單的 event-loop 執(zhí)行模型
再簡(jiǎn)單點(diǎn)可以總結(jié)為:
- 在 macrotask 隊(duì)列中執(zhí)行最早的那個(gè) task 咒彤,然后移出
- 執(zhí)行 microtask 隊(duì)列中所有可用的任務(wù)疆柔,然后移出
- 下一個(gè)循環(huán),執(zhí)行下一個(gè) macrotask 中的任務(wù) (再跳到第2步)
其他
- 當(dāng)一個(gè)task(在 macrotask 隊(duì)列中)正處于執(zhí)行狀態(tài)镶柱,也可能會(huì)有新的事件被注冊(cè)旷档,那就會(huì)有新的 task 被創(chuàng)建。比如下面兩個(gè)
1. promiseA.then() 的回調(diào)就是一個(gè) task
1. promiseA 是 resolved或rejected: 那這個(gè) task 就會(huì)放入當(dāng)前事件循環(huán)回合的 microtask queue
1. promiseA 是 pending: 這個(gè) task 就會(huì)放入 事件循環(huán)的未來(lái)的某個(gè)(可能下一個(gè))回合的 microtask queue 中
1. setTimeout 的回調(diào)也是個(gè) task 歇拆,它會(huì)被放入 macrotask queue 即使是 0ms 的情況 - microtask queue 中的 task 會(huì)在事件循環(huán)的當(dāng)前回合中執(zhí)行鞋屈,因此 macrotask queue 中的 task 就只能等到事件循環(huán)的下一個(gè)回合中執(zhí)行了
- click ajax setTimeout 的回調(diào)是都是 task, 同時(shí),包裹在一個(gè) script 標(biāo)簽中的js代碼也是一個(gè) task 確切說(shuō)是 macrotask故觅。