回想起第一次使用 setTimeout
函數(shù)的時候,設(shè)置的回調(diào)函數(shù)并不是立即執(zhí)行衡蚂,而是過了一段定義好的時間之后才執(zhí)行喇勋,這讓當(dāng)時剛知道 Javascript 是單線程的我無法理解酗钞,誤以為“單線程”的定義是錯的岸裙。過了段時間的學(xué)習(xí)之后才知道這玩意叫“異步”。
代碼是從上往下一行一行地執(zhí)行旷偿,為什么類似這樣的異步的東西烹俗,卻“違反常規(guī)”,執(zhí)行到的時候會“忽略”萍程,跳到最后的時刻才執(zhí)行幢妄。換句話來說,同步代碼執(zhí)行完畢之后茫负,異步的東西才開始執(zhí)行蕉鸳。
結(jié)合堆棧來看在 Javascript 中代碼是如何執(zhí)行的
同步代碼
function foo() {
console.log( 'foo' )
}
function bar() {
foo()
console.log( 'bar )
}
bar()
/*
* 結(jié)果:
* 'foo'
* 'bar'
*/
代碼執(zhí)行到 bar()
的時候,對應(yīng)的堆棧情況為為下圖:
此時輸出 'foo'
朽褪,console.log( 'foo' ) 執(zhí)行完畢之后置吓,從棧中彈出,此時 foo 也執(zhí)行完畢缔赠,foo 也從棧中彈出,往下執(zhí)行到 console.log( 'bar ):
在這里執(zhí)行 console.log( 'bar ) 之后輸出 'bar'友题,之后彈出嗤堰,bar 執(zhí)行完畢也彈出,棧空踢匣。
setTimeout 代碼
function bar() {
setTimeout( () => {
console.log( 'foo' )
}, 2000 )
console.log( 'bar' )
}
bar()
/*
* 結(jié)果:
* 'bar'
* 'foo'
*/
代碼執(zhí)行到 bar()
的時候告匠,對應(yīng)的堆棧情況為為下圖:
setTimeout() 執(zhí)行之前,先說明 setTimeout 有關(guān)的東西离唬。
理解 setTimeout
setTimeout 是屬于 window 上的一個方法后专,在 ECMA-262規(guī)范 中找不到關(guān)于 setTimeout 的定義,但在 HTML規(guī)范 中找到了有關(guān)定義输莺。所以 setTimeout 是一個 DOM API 戚哎,是瀏覽器對其內(nèi)部進(jìn)行實現(xiàn)的(可能是 webkit),而非 js引擎(例如 V8)實現(xiàn)嫂用,然后掛在 window 全局屬性(window.setTimeout)上供 Javascript 訪問調(diào)用型凳。
具體來說是瀏覽器實現(xiàn)了一個 timer_handler,叫做“定時器觀察者”這么一個東西嘱函。其實瀏覽器實現(xiàn)了幾個 handler甘畅,比如發(fā)起 ajax 請求的 xhr_handler 等......瀏覽器對這個觀察者向 Javascript 提供了一個 API(setTimeout) 用來傳遞被觀察的對象進(jìn)去。也就是說 Javascript 在調(diào)用 setTimeout 的時候往弓,是往 timer_handler 傳遞了一個 timer 對象進(jìn)去疏唾,timer_handler 在收到這個對象之后,Javascript 對 setTimeout 的同步調(diào)用已經(jīng)結(jié)束函似,立即返回槐脏。可以說是一次 V8 到 webkit 的過程缴淋,即 Javascript 到 DOM API 的過程准给。
被傳遞的觀察者類似于 timer = { time: 2000ms, callback: function() {...} }
,Javascript 通過 setTimeout 將 timer 傳遞給 timer_handler重抖。
觀察者在收到 timer 之后露氮,就會一直監(jiān)控時間,如果到達(dá)觸發(fā)的條件钟沛,就會將 timer 推入一個隊列中畔规,等待 Javascript 主線程空閑之后執(zhí)行
回到上面 setTimeout () 的調(diào)用那里繼續(xù),此時增加一個 handlers 和一個 queue隊列恨统。
setTimeout 調(diào)用過程
setTimeout () 調(diào)用之后叁扫,會有一個 timer 對象被傳遞到 handlers 中的 timer_handler。
在 timer 傳遞完畢之后畜埋,對于 setTimeout 的調(diào)用結(jié)束莫绣,立即返回,setTimeout 出棧
與此同時悠鞍,timer_handler 會對該 aTimer 不停地進(jìn)行監(jiān)控对室,看是否達(dá)到時間觸發(fā)。由于之前設(shè)置了 2000ms,因此沒那么快觸發(fā)掩宜。handlers 與 stack 互不影響蔫骂,互不阻塞。
接著執(zhí)行到 console.log( 'bar' )牺汤,入棧辽旋。輸出 'bar'
之后 console.log( 'bar' ) 出棧,bar 調(diào)用結(jié)束檐迟,bar 出棧补胚。與此同時,timer_handler 會對該 aTimer 不停地進(jìn)行監(jiān)控锅减,看是否達(dá)到時間觸發(fā)糖儡。由于之前設(shè)置了 2000ms,因此沒那么快觸發(fā)怔匣。handlers 與 stack 互不影響握联,互不阻塞。
假設(shè)自 setTimeout 的調(diào)用開始到 bar 出棧每瞒,經(jīng)歷了 1000ms金闽,那么對于 aTimer 的 2000ms 來說,還有 1000ms剿骨,因此 timer_handler 還會繼續(xù)監(jiān)控代芜。
(1000ms過后)
timer_handler 監(jiān)控到 aTimer 的時間到了,于是會將 aTimer 的 callback 推入 queue 中浓利,然后將 aTimer 從 handler 中移除挤庇。
此時 aCallback 處于 queue 中贷掖,而 Javascript 主線程又是處于空閑狀態(tài)嫡秕,因此 aCallback 會被立即出隊,進(jìn)入主線程執(zhí)行苹威。
console.log( 'foo' )執(zhí)行,輸出 'foo'
執(zhí)行完畢牙甫,console.log( 'foo' ) 出棧掷酗,aCallback() 出棧。椏卟福空泻轰。
以上就是事件循環(huán)的一個過程且轨。事件循環(huán)不是某個函數(shù)或者部分 而是一套機(jī)制 這套機(jī)制的總稱叫事件循環(huán)糕殉。
在此過程中亩鬼,handlers會一直監(jiān)控名下所有的handler殖告,只要達(dá)到觸發(fā)條件阿蝶,就會形成一個任務(wù)推入 queue 中。而在 queue 中黄绩,會有一個類似 while循環(huán) 一直讀取 queue 中的任務(wù)羡洁,也一直監(jiān)控主線程是否空閑,如果空閑爽丹,而且 queue 中存在任務(wù)筑煮,就會取出隊列頭的任務(wù)出來,推入主線程執(zhí)行粤蝎。若主線程忙碌真仲,就會等待主線程空閑。
通俗解釋
小明要做一件事初澎,就是要走完一條10米長的路秸应,這條路上他需要做一些事。他從0米開始起步碑宴。走到2米處的時候软啼,看到一個綠色呼啦圈,他停下來了延柠,拿起綠色的呼啦圈開始轉(zhuǎn)了起來祸挪,轉(zhuǎn)了一段時間后,他繼續(xù)走贞间。走到了5米處的時候贿条,他看到了一個“待辦事項”的牌子,上面寫有一些任務(wù)增热,于是他停下來整以,拿起筆和紙,在紙上寫下了一段字:“時間:5秒后钓葫,任務(wù):大喊一聲‘旺旺’悄蕾,次數(shù):1〈「。”他把紙放到了路邊的一個人的手里帆调,這個人穿著黑色的衣服,衣服上寫著“定時管理”豆同,不在小明走的這條路中番刊,而在路的旁邊。交到這個人手里之后影锈,小明開始邁開腳步芹务,繼續(xù)往前走蝉绷。走到9米處的時候,看到一個紅色呼啦圈枣抱,他停下來轉(zhuǎn)了一段時間后又繼續(xù)走熔吗。走著就就走到了第10米,完成了走路佳晶。
而剛才那個黑衣人自打收到紙條之后桅狠,就開始計算時間。從收到紙條那一刻開始轿秧,0秒中跌、1秒、2秒......5秒菇篡。好了漩符,5秒的時間到了,這個黑衣人該準(zhǔn)備做點事情了驱还。只見這個黑衣人站起身嗜暴,走到一個盒子旁邊,將紙條放到盒子里面铝侵。放進(jìn)去之后灼伤,黑衣人手里就什么都沒有了。
然而咪鲜,還有一個人狐赡,他負(fù)責(zé)管理這個盒子,他也不在那條路上疟丙,也是在路旁邊颖侄,他只做一件事,就是不停地看著那個盒子享郊。如果盒子里面有若干張紙條览祖,他就會拿出最早放進(jìn)去的那張;如果只有一張紙條炊琉,當(dāng)然他就會拿出僅有的那一張≌沟伲現(xiàn)在,那個黑衣人往盒子里面放了一張紙條苔咪,那他就會拿出那張紙條锰悼,然后觀察小明是否走完了那段10米長的路。當(dāng)他看到小明走完那段路团赏,在終點空閑下來了之后箕般,他就會馬上把紙條遞給小明,小明就會按照紙條上的“任務(wù)”項去做事舔清,大聲喊出了“旺旺”丝里。
在這件事中曲初,黑衣人和管理盒子的人跟小明是互不干涉,只有交流杯聚。也就是說臼婆,小明走小明的路,那兩個人做他們自己的事械媒,井水不犯河水目锭,歲月靜好。當(dāng)然纷捞,那兩個人也不屬于“道路管理協(xié)會”的管理中,而小明是受到監(jiān)管的被去。
延伸1
function fn() {
setTimeout( () => {
console.log( 'hahaha' )
}, 1000)
console.log( 'fn' )
while( true ) {}
}
在這段代碼中主儡,setTimeout 調(diào)用返回之后,到 console.log( 'fn' ) 輸出 'fn'惨缆,往下執(zhí)行到一個 while( true ) 的循環(huán)糜值。此時主線程陷入死循環(huán),沒有空閑的時間坯墨,即使 timer_handler 監(jiān)控到 timer 觸發(fā)寂汇,推入 queue ,也無法執(zhí)行捣染,因為主線程一直繁忙骄瓣。所以 'hahaha' 一直都不會輸出。
延伸2
在 MDN 中學(xué)習(xí) async/await 的時候耍攘,看到了一個例子:
function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function add1(x) {
var a = resolveAfter2Seconds(20);
var b = resolveAfter2Seconds(30);
return x + await a + await b;
}
add1(10).then(v => {
console.log(v); // prints 60 after 2 seconds.
});
async function add2(x) {
var a = await resolveAfter2Seconds(20);
var b = await resolveAfter2Seconds(30);
return x + a + b;
}
add2(10).then(v => {
console.log(v); // prints 60 after 4 seconds.
});
突然對 add1函數(shù) 2秒輸出和 add2函數(shù) 4秒輸出產(chǎn)生了疑問榕栏,于是自己仔細(xì)琢磨了一下,認(rèn)為:
首先蕾各,async/await函數(shù) 可以看做是 generator函數(shù) 的一個進(jìn)化版扒磁,只不過返回的是一個 promise,await 有暫停函數(shù)的功能式曲,也相當(dāng)于一個執(zhí)行器妨托,看做自動執(zhí)行 then 或者 catch,自動拿出 promise 中 resolve 或者 reject 的值吝羞。
(未完待續(xù)...)