我對(duì)異步的好奇心起于學(xué)習(xí)Promise時(shí)老是一知半解岸浑,最近在看《你所不知道的js(中)》泼返,書(shū)中對(duì)異步這部分的講解還是很到位的们镜,所以結(jié)合自己的理解整理一下相關(guān)知識(shí)點(diǎn)币叹。
本文將從是什么、為什么模狭、怎么樣這三步式來(lái)講這個(gè)問(wèn)題颈抚。
一、什么是異步嚼鹉?
我們一般喜歡把異步和同步贩汉、并行拿出來(lái)比較,我以前的理解總是很模糊锚赤,總是生硬地記著“同步就是排隊(duì)執(zhí)行匹舞,異步就是一起執(zhí)行”,現(xiàn)在一看线脚,當(dāng)初簡(jiǎn)直就是傻赐稽,所以我們第一步先把這三個(gè)概念搞清楚,我不太喜歡看網(wǎng)上有些博客里很含糊地說(shuō)“xxxx是同步酒贬,xxxx是異步”,還有舉什么通俗的例子翠霍,其實(shí)對(duì)不懂的人來(lái)說(shuō)還是懵逼锭吨。
首先我們要知道這一切的根源都是“Javascript是單線程”,也就是一次只能做一件事寒匙,那么為什么是單線程呢零如?因?yàn)閖s渲染在瀏覽器上,包含了許多與用戶的交互锄弱,如果是多線程考蕾,那么試想一個(gè)場(chǎng)景:一個(gè)線程在某個(gè)DOM上添加內(nèi)容,而另一個(gè)線程刪除這個(gè)DOM会宪,那么瀏覽器要如何反應(yīng)呢肖卧?這就亂套了。
單線程下所有的任務(wù)都是需要排隊(duì)的掸鹅,而這些任務(wù)分為兩種:同步任務(wù)和異步任務(wù)塞帐,同步任務(wù)就是在主線程上排隊(duì)執(zhí)行的任務(wù)拦赠,只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù)葵姥;異步任務(wù)指的是荷鼠,不進(jìn)入主線程
、而進(jìn)入任務(wù)隊(duì)列
(task queue)的任務(wù)榔幸,只有任務(wù)隊(duì)列
通知主線程
允乐,某個(gè)異步任務(wù)可以執(zhí)行了,該任務(wù)才會(huì)進(jìn)入主線程
執(zhí)行削咆。所以說(shuō)同步執(zhí)行其實(shí)也是一種只有主線程的異步執(zhí)行牍疏。這里有一個(gè)視頻關(guān)于異步操作是如何被執(zhí)行的,講得非常好《what the hack is event loop》态辛,我給大家畫(huà)個(gè)圖再來(lái)理解一下麸澜。
這里補(bǔ)充說(shuō)明下不同的異步操作添加到任務(wù)隊(duì)列的時(shí)機(jī)不同,如 onclick, setTimeout, ajax 處理的方式都不同奏黑,這些異步操作是由瀏覽器內(nèi)核的 webcore 來(lái)執(zhí)行的炊邦,webcore 包含上面提到的3種 webAPI,分別是 DOM Binding熟史、timer馁害、network模塊。
onclick 由瀏覽器內(nèi)核的 DOM Binding 模塊來(lái)處理蹂匹,當(dāng)事件觸發(fā)的時(shí)候碘菜,回調(diào)函數(shù)會(huì)立即添加到任務(wù)隊(duì)列中。
setTimeout 會(huì)由瀏覽器內(nèi)核的 timer 模塊來(lái)進(jìn)行延時(shí)處理限寞,當(dāng)時(shí)間到達(dá)的時(shí)候忍啸,才會(huì)將回調(diào)函數(shù)添加到任務(wù)隊(duì)列中。
ajax 則會(huì)由瀏覽器內(nèi)核的 network 模塊來(lái)處理履植,在網(wǎng)絡(luò)請(qǐng)求完成返回之后计雌,才將回調(diào)添加到任務(wù)隊(duì)列中。
最后再來(lái)說(shuō)下并行玫霎,并行是關(guān)于能夠同時(shí)發(fā)生的事情凿滤,是一種多線程的運(yùn)行機(jī)制,而不管同步異步都是單線程的庶近。
二翁脆、為什么要用異步操作
這個(gè)很好理解,同步下前一個(gè)事件執(zhí)行完了才能執(zhí)行后一個(gè)事件鼻种,那么要是遇到Ajax請(qǐng)求這種耗時(shí)很長(zhǎng)的反番,那頁(yè)面在這段時(shí)間就沒(méi)法操作了,卡在那兒,更有甚者恬口,萬(wàn)一這個(gè)請(qǐng)求由于某種原因一直沒(méi)有完成校读,那頁(yè)面就block了,很不友好祖能。
三歉秫、如何實(shí)現(xiàn)異步
我們可以通過(guò)回調(diào)函數(shù)
、Promise
养铸、生成器
雁芙、Async/Await
等來(lái)實(shí)現(xiàn)異步。
今天我們先說(shuō)最基礎(chǔ)的回調(diào)函數(shù)處理方法來(lái)實(shí)現(xiàn)钞螟,列舉幾個(gè)大家熟悉的使用場(chǎng)景兔甘,比如:ajax請(qǐng)求、IO操作鳞滨、定時(shí)器洞焙。
ajax(url, function(){
//這就是回調(diào)函數(shù)
});
setTimeOut(function(){
//回調(diào)函數(shù)
}, 1000)
回調(diào)本身是比較好用的,但是隨著Javascript越來(lái)越成熟拯啦,對(duì)于異步編程領(lǐng)域的發(fā)展澡匪,回調(diào)已經(jīng)不夠用了,體現(xiàn)在以下幾點(diǎn):
1褒链、大腦處理程序是順序的唁情,對(duì)于復(fù)雜的回調(diào)函數(shù)會(huì)不易理解,我們需要一種更同步甫匹、更順序的方式來(lái)表達(dá)異步甸鸟。
舉例說(shuō)明:
//回調(diào)函數(shù)實(shí)現(xiàn)兩數(shù)相加
function add(getX, getY, cb){
var x, y;
getX(function(xVal){
x=xVal;
if(y!=undefined){
cb(x+y);
}
});
getY(function(){
y=yVal;
if(x!=undefined){
cb(x+y);
}
});
}
add(fetchX, fetchY, function(sum){
console.log(sum);
})
//Promise實(shí)現(xiàn)兩數(shù)相加
function add(xPromise, yPromise){
return Promise.all([xPromise, yPromise])
.then(function(values){
return value[0] + value[1];
});
}
//fetchX()、fetchY()返回相應(yīng)值的Promise
add(fetchX(), fetchY())
.then(function(sum){
console.log(sum);
})
只看結(jié)構(gòu)是不是Promise的寫(xiě)法更順序話一些兵迅。
2抢韭、回調(diào)一般會(huì)把控制權(quán)交給第三方,從而帶來(lái)信任問(wèn)題恍箭,比如:
- 調(diào)用回調(diào)過(guò)早
- 調(diào)用回調(diào)過(guò)晚(或未調(diào)用)
- 調(diào)用回調(diào)次數(shù)過(guò)多或過(guò)少
- 未能傳遞所需的環(huán)境和參數(shù)
- 吞掉可能出現(xiàn)的錯(cuò)誤和異常
而Promise的特性就有效地解決了這些問(wèn)題刻恭,它是如何解決的呢?
調(diào)用回調(diào)過(guò)早
這種顧慮主要是代碼是否會(huì)引入類(lèi)Zalgo效應(yīng)季惯,也就是一個(gè)任務(wù)有時(shí)會(huì)同步完地成吠各,而有時(shí)會(huì)異步地完成臀突,這將導(dǎo)致竟合狀態(tài)勉抓。
Promise被定義為不能受這種顧慮的影響,因?yàn)榧幢闶橇⒓赐瓿傻腜romise(比如 new Promise(function(resolve){ resolve(42); }))也不可能被同步地 監(jiān)聽(tīng)候学。也就是說(shuō)藕筋,但你在Promise上調(diào)用then(..)的時(shí)候,即便這個(gè)Promise已經(jīng)被解析了梳码,你給then(..)提供的回調(diào)也將總是被異步地調(diào)用隐圾。
調(diào)用回調(diào)過(guò)晚
當(dāng)一個(gè)Promise被調(diào)用時(shí)伍掀,這個(gè)Promise 上的then注冊(cè)的回調(diào)函數(shù)都會(huì)在下一個(gè)異步時(shí)機(jī)點(diǎn)上,按順序地暇藏,被立即調(diào)用蜜笤。這些回調(diào)中的任意一個(gè)都無(wú)法影響或延誤對(duì)其它回調(diào)的調(diào)用。
舉例說(shuō)明:
p.then( function(){
p.then( function(){
console.log( "C" );
} );
console.log( "A" );
} );
p.then( function(){
console.log( "B" );
} );
// A B C
為什么“C”沒(méi)有排到“B”的前面盐碱?因?yàn)橐驗(yàn)椤癈”所處的.then回調(diào)函數(shù)是在下一個(gè)事件循環(huán)tick把兔。
回調(diào)未調(diào)用
這是一個(gè)很常見(jiàn)的顧慮。Promise用幾種方式解決它瓮顽。
首先县好,當(dāng)Promise被解析后,在代碼不出錯(cuò)的情況下它一定會(huì)告知你解析結(jié)果暖混。如果代碼有錯(cuò)誤缕贡,歸類(lèi)于后面的“吞掉錯(cuò)誤或異常”中拣播。
那如果Promise本身不管怎樣永遠(yuǎn)沒(méi)有被解析呢晾咪?那么Promise會(huì)用Promise.race來(lái)解決。
看代碼示例:
// 一個(gè)使Promise超時(shí)的工具
function timeoutPromise(delay) {
return new Promise( function(resolve,reject){
setTimeout( function(){
reject( "Timeout!" );
}, delay );
} );
}
// 為`foo()`設(shè)置一個(gè)超時(shí)
Promise.race( [
foo(), // 嘗試調(diào)用`foo()`
timeoutPromise( 3000 ) // 給它3秒鐘
] )
.then(
function(){
// `foo(..)`及時(shí)地完成了诫尽!
},
function(err){
// `foo()`不是被拒絕了禀酱,就是它沒(méi)有及時(shí)完成
// 那么可以考察`err`來(lái)知道是哪種情況
}
);
調(diào)用次數(shù)過(guò)少或過(guò)多
正常是調(diào)用一次,“過(guò)少”就是未被調(diào)用牧嫉,參考上文剂跟;“過(guò)多”的情況也很容易理解。Promise的定義方式使得它只能被決議一次酣藻,如果出于某種情況決議了多次曹洽,Promise也只會(huì)接受第一次決議,并忽略后續(xù)調(diào)用辽剧。
未能傳遞所需的參數(shù)/環(huán)境值
Promise只會(huì)有一個(gè)解析結(jié)果(完成或拒絕)送淆。如果沒(méi)有用一個(gè)值明確地解析它,它的值就是undefined怕轿,就像JS中常見(jiàn)的那樣偷崩。
吞掉錯(cuò)誤或異常
Promise中異常會(huì)被捕獲,并且使這個(gè)Promise被拒絕撞羽。
舉個(gè)例子:
var p = new Promise( function(resolve,reject){
foo.bar(); // `foo`沒(méi)有定義阐斜,所以這是一個(gè)錯(cuò)誤!
resolve( 42 ); // 永遠(yuǎn)不會(huì)跑到這里 :(
} );
p.then(
function fulfilled(){
// 永遠(yuǎn)不會(huì)跑到這里 :(
},
function rejected(err){
// `err`將是一個(gè)來(lái)自`foo.bar()`那一行的`TypeError`異常對(duì)象
}
);
Promise就先說(shuō)到這里诀紊,關(guān)于PromiseAPI及其源碼還有生成器谒出、Async/Await 在后續(xù)文章中整理報(bào)道。
【寫(xiě)得不好的地方請(qǐng)大膽吐槽,非常感謝大家?guī)疫M(jìn)步笤喳∥樱】
參考資料:
阮一峰e(cuò)vent-loop
王福朋深入理解javascript異步系列一
你不知道的javascript
你不懂JS: 異步與性能 第三章: Promise(上)