引言
首先需要知道的是JavaScript是門的語言赌厅。之所以如此設(shè)計咆霜,是因為JavaScript主要應(yīng)用于瀏覽器的互動邓馒,即操作DOM。所以一次只能完成一件任務(wù)蛾坯。如果有多個任務(wù)光酣,就必須排隊,前面一個任務(wù)完成脉课,再執(zhí)行后面一個任務(wù)救军,以此類推。我們假設(shè)JavaScript是多線程的倘零,那么多個線程同時進(jìn)行唱遭,兩個線程同時操作一個DOM,那么以誰的操作為準(zhǔn)呢呈驶?
是當(dāng)代碼需要進(jìn)行一項異步任務(wù)(無法立刻返回結(jié)果拷泽,需要花一定時間才能返回的任務(wù),如I/O事件)的時候,主線程會掛起(pending)這個任務(wù)跌穗,然后在異步任務(wù)返回結(jié)果的時候再根據(jù)一定規(guī)則去執(zhí)行相應(yīng)的回調(diào)订晌。
單線程雖然實現(xiàn)起來比較簡單,執(zhí)行環(huán)境相對單純蚌吸;但是只要有一個任務(wù)耗時很長,后面的任務(wù)都必須排隊等著砌庄,會拖延整個程序的執(zhí)行羹唠。因此為了解決這個問題Javascript語言將任務(wù)的執(zhí)行模式分成兩種:同步(Synchronous)和異步(Asynchronous)。
同步模式
就是后一個任務(wù)等待前一個任務(wù)結(jié)束娄昆,然后再執(zhí)行佩微,程序的執(zhí)行順序與任務(wù)的排列順序是一致的、同步的萌焰。
異步模式
每一個任務(wù)有一個或多個回調(diào)函數(shù)(callback)哺眯,前一個任務(wù)結(jié)束后,不是執(zhí)行隊列上的后一個任務(wù)扒俯,而是執(zhí)行回調(diào)函數(shù)奶卓;后一個任務(wù)則是不等前一個任務(wù)的回調(diào)函數(shù)的執(zhí)行而執(zhí)行,所以程序的執(zhí)行順序與任務(wù)的排列順序是不一致的撼玄、異步的夺姑。
"異步模式"非常重要。在瀏覽器端掌猛,耗時很長的操作都應(yīng)該異步執(zhí)行盏浙,避免瀏覽器失去響應(yīng),最好的例子就是Ajax操作荔茬。在服務(wù)器端废膘,"異步模式"甚至是唯一的模式,因為執(zhí)行環(huán)境是單線程的慕蔚,如果允許同步執(zhí)行所有http請求丐黄,服務(wù)器性能會急劇下降,很快就會失去響應(yīng)坊萝。
JavaScript為何能執(zhí)行異步任務(wù)
Javascript是單線程的孵稽,但是卻能執(zhí)行異步任務(wù),這主要是因為 JS 中存在(Event Loop)和
(Task Queue)十偶。
示例
setTimeout(function(){
console.log(2);
},0);
new Promise(function(resolve){
console.log(3);
resolve();
console.log(4);
}).then(function(){
console.log(5);
});
console.log(6);
setTimeout(function(){
console.log(7);
},0);
事件循環(huán)(Event Loop)
JS 會創(chuàng)建一個類似于 while (true) 的循環(huán)菩鲜,每執(zhí)行一次循環(huán)體的過程稱之為Tick。每次Tick的過程就是查看是否有待處理事件惦积,如果有則取出相關(guān)事件及回調(diào)函數(shù)放入執(zhí)行棧中由主線程執(zhí)行接校。待處理的事件會存儲在一個任務(wù)隊列中,也就是每次Tick會查看任務(wù)隊列中是否有需要執(zhí)行的任務(wù)。
示例
while (queue.waitForMessage()) {
queue.processNextMessage();
}
事件循環(huán)會按照上圖所示的模式進(jìn)行操作蛛勉,queue.waitForMessage() 會同步地等待消息到達(dá)(如果當(dāng)前沒有任何消息等待被處理)鹿寻。
任務(wù)隊列
和事件循環(huán)聯(lián)系在一起的是任務(wù)隊列,
-所有同步任務(wù)都在主線程上執(zhí)行诽凌,形成一個執(zhí)行棧(execution context stack)毡熏。
-主線程之外,還存在一個”任務(wù)隊列”(task queue)侣诵。只要異步任務(wù)有了運行結(jié)果痢法,就在”任務(wù)隊列”之中放置一個事件。
-一旦”執(zhí)行椂潘常”中的所有同步任務(wù)執(zhí)行完畢财搁,系統(tǒng)就會讀取”任務(wù)隊列”,看看里面有哪些事件躬络。那些對應(yīng)的異步任務(wù)尖奔,于是結(jié)束等待狀態(tài),進(jìn)入執(zhí)行棧穷当,開始執(zhí)行提茁。
-主線程不斷重復(fù)上面的第三步。
異步任務(wù)
異步操作會將相關(guān)回調(diào)添加到任務(wù)隊列中膘滨。而不同的異步操作添加到任務(wù)隊列的時機也不同甘凭,如onclick, setTimeout,ajax 處理的方式都不同,這些異步操作是由瀏覽器內(nèi)核的webcore來執(zhí)行的火邓,webcore包含下圖中的3種 webAPI丹弱,分別是DOM Binding、network铲咨、timer模塊躲胳。
-DOM Binding 模塊處理一些DOM綁定事件,如onclick事件觸發(fā)時纤勒,回調(diào)函數(shù)會立即被-webcore添加到任務(wù)隊列中坯苹。
-network 模塊處理Ajax請求,在網(wǎng)絡(luò)請求返回時摇天,才會將對應(yīng)的回調(diào)函數(shù)添加到任務(wù)隊列中粹湃。
-timer 模塊會對setTimeout等計時器進(jìn)行延時處理,當(dāng)時間到達(dá)的時候泉坐,才會將回調(diào)函數(shù)添加到任務(wù)隊列中为鳄。
事件循環(huán)和任務(wù)隊列之間的關(guān)系
規(guī)范中中提到,一個瀏覽器環(huán)境腕让,只能有一個事件循環(huán)孤钦,而一個事件循環(huán)可以多個任務(wù)隊列,每個任務(wù)都有一個任務(wù)源(Task source)。相同任務(wù)源的任務(wù)偏形,只能放到一個任務(wù)隊列中静袖。
不同任務(wù)源的任務(wù),可以放到不同任務(wù)隊列中俊扭。
簡單來說:一個事件循環(huán)可以有多個任務(wù)隊列队橙,隊列之間可有不同的優(yōu)先級,同一隊列中的任務(wù)按先進(jìn)先出的順序執(zhí)行萨惑,但是不保證多個任務(wù)隊列中的任務(wù)優(yōu)先級喘帚,具體實現(xiàn)可能會交叉執(zhí)行。
不同任務(wù)隊列的優(yōu)先級
在異步代碼測試結(jié)果的圖中看到咒钟,代碼的執(zhí)行順序并不是按著,代碼書寫順序依次執(zhí)行的若未,這是因為不同的異步任務(wù)之間也有優(yōu)先級的區(qū)別朱嘴。異步任務(wù)分為兩類,macrotask(宏任務(wù))和 microtask(微任務(wù))
宏任務(wù)(macro task)
script(你的全部JS代碼粗合,“同步代碼”), setTimeout, setInterval, setImmediate, I/O,UI rendering
微任務(wù)(micro task)
process.nextTick,Promises(這里指瀏覽器原生實現(xiàn)的 Promise), Object.observe, MutationObserver
執(zhí)行順序
瀏覽器為了能夠使得JS內(nèi)部task與DOM任務(wù)能夠有序的執(zhí)行萍嬉,會在一個task執(zhí)行結(jié)束后,在下一個 task 執(zhí)行開始前隙疚,對頁面進(jìn)行重新渲染 (task->渲染->task->...)壤追,鼠標(biāo)點擊會觸發(fā)一個事件回調(diào),需要執(zhí)行一個宏任務(wù)供屉,然后解析HTMl行冰。微任務(wù)通常來說就是需要在當(dāng)前 task 執(zhí)行結(jié)束后立即執(zhí)行的任務(wù),比如對一系列動作做出反饋伶丐,或或者是需要異步的執(zhí)行任務(wù)而又不需要分配一個新的 task悼做,這樣便可以減小一點性能的開銷。所有微任務(wù)總會在下一個宏任務(wù)之前全部執(zhí)行完畢哗魂。
所以肛走,瀏覽器環(huán)境中,js執(zhí)行任務(wù)的流程是這樣的:
1.第一個事件循環(huán)录别,先執(zhí)行script中的所有同步代碼(即 macrotask 中的第一項任務(wù))
2.再取出 microtask 中的全部任務(wù)執(zhí)行(先清空process.nextTick隊列朽色,再清空promise.then隊列)
3.下一個事件循環(huán),再回到 macrotask 取其中的下一項任務(wù)
4.再重復(fù)2
5.反復(fù)執(zhí)行事件循環(huán)…
一些常見的異步任務(wù)
setTimeout()
將事件插入到了事件隊列组题,必須等到當(dāng)前代碼(執(zhí)行棧)執(zhí)行完葫男,主線程才會去執(zhí)行它指定的回調(diào)函數(shù)。
當(dāng)主線程時間執(zhí)行過長往踢,無法保證回調(diào)會在事件指定的時間執(zhí)行腾誉。
瀏覽器端每次setTimeout會有4ms的延遲,當(dāng)連續(xù)執(zhí)行多個setTimeout,有可能會阻塞進(jìn)程利职,造成性能問題趣效。
setImmediate()
事件插入到事件隊列尾部,主線程和事件隊列的函數(shù)執(zhí)行完成之后立即執(zhí)行猪贪。和setTimeout(fn,0)的效果差不多跷敬。
服務(wù)端node提供的方法。瀏覽器端最新的api也有類似實現(xiàn):window.setImmediate,但支持的瀏覽器很少热押。
process.nextTick()
插入到事件隊列尾部西傀,但在下次事件隊列之前會執(zhí)行。也就是說桶癣,它指定的任務(wù)總是發(fā)生在所有異步任務(wù)之前拥褂,當(dāng)前主線程的末尾。
大致流程:當(dāng)前”執(zhí)行椦滥”的尾部–>下一次Event Loop(主線程讀取”任務(wù)隊列”)之前–>觸發(fā)process指定的回調(diào)函數(shù)饺鹃。
服務(wù)器端node提供的辦法。用此方法可以用于處于異步延遲的問題间雀。
可以理解為:此次不行悔详,預(yù)約下次優(yōu)先執(zhí)行。
Promise
Promise本身是同步的立即執(zhí)行函數(shù)惹挟, 當(dāng)在 executor 中執(zhí)行 resolve 或者 reject 的時候, 此時是異步操作茄螃, 會先執(zhí)行 then/catch 等,當(dāng)主棧完成后连锯,才會去調(diào)用 resolve/reject 中存放的方法執(zhí)行归苍,打印 p 的時候,是打印的返回結(jié)果萎庭,一個 Promise 實例霜医。
async await
Async/Await就是一個自執(zhí)行的generate函數(shù)。利用generate函數(shù)的特性把異步的代碼寫成“同步”的形式驳规。
async 函數(shù)返回一個 Promise 對象肴敛,當(dāng)函數(shù)執(zhí)行的時候,一旦遇到 await 就會先返回吗购,等到觸發(fā)的異步操作完成医男,再執(zhí)行函數(shù)體內(nèi)后面的語句∧砻悖可以理解為镀梭,是讓出了線程,跳出了 async 函數(shù)體踱启。
參考資料
https://blog.csdn.net/happyqyt/article/details/90644667
https://blog.csdn.net/github_35549695/article/details/82390345
https://www.cnblogs.com/nayek/p/11729923.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop#%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF