詳解JS四種異步解決方案:回調(diào)函數(shù)、Promise、Generator、async/await
同步&異步的概念
在講這四種異步方案之前,我們先來明確一下同步和異步的概念:
所謂同步(synchronization),簡單來說,就是順序執(zhí)行,指的是同一時間只能做一件事情,只有目前正在執(zhí)行的事情做完之后,才能做下一件事情。比如咱們?nèi)セ疖囌举I票,假設(shè)窗口只有1個,那么同一時間只能處理1個人的購票業(yè)務(wù),其余的需要進(jìn)行排隊。這種one by one的動作就是同步。同步操作的優(yōu)點在于做任何事情都是依次執(zhí)行,井然有序,不會存在大家同時搶一個資源的問題。同步操作的缺點在于會阻塞后續(xù)代碼的執(zhí)行。如果當(dāng)前執(zhí)行的任務(wù)需要花費很長的時間,那么后面的程序就只能一直等待。從而影響效率,對應(yīng)到前端頁面的展示來說,有可能會造成頁面渲染的阻塞,大大影響用戶體驗。
所謂異步(Asynchronization),指的是當(dāng)前代碼的執(zhí)行不影響后面代碼的執(zhí)行。當(dāng)程序運(yùn)行到異步的代碼時,會將該異步的代碼作為任務(wù)放進(jìn)任務(wù)隊列,而不是推入主線程的調(diào)用棧。等主線程執(zhí)行完之后,再去任務(wù)隊列里執(zhí)行對應(yīng)的任務(wù)即可。因此,異步操作的優(yōu)點就是:不會阻塞后續(xù)代碼的執(zhí)行。
js中異步的應(yīng)用場景
開篇講了同步和異步的概念,那么在JS中異步的應(yīng)用場景有哪些呢?
定時任務(wù):setTimeout、setInterval 網(wǎng)絡(luò)請求:ajax請求、動態(tài)創(chuàng)建img標(biāo)簽的加載 事件監(jiān)聽器:addEventListener
實現(xiàn)異步的四種方法
對于setTimeout、setInterval、addEventListener這種異步場景,不需要我們手動實現(xiàn)異步,直接調(diào)用即可。但是對于ajax請求、node.js中操作數(shù)據(jù)庫這種異步,就需要我們自己來實現(xiàn)了~
1、 回調(diào)函數(shù)
在微任務(wù)隊列出現(xiàn)之前,JS實現(xiàn)異步的主要方式就是通過回調(diào)函數(shù)。以一個簡易版的Ajax請求為例,代碼結(jié)構(gòu)如下所示:
function?ajax(obj){
?let?default?=?{
???url:?'...',
???type:'GET',
???async:true,
???contentType:?'application/json',
???success:function(){}
????};
?for?(let?key?in?obj)?{
????????defaultParam[key]?=?obj[key];
????}
????let?xhr;
????if?(window.XMLHttpRequest)?{
????????xhr?=?new?XMLHttpRequest();
????}?else?{
????????xhr?=?new?ActiveXObject('Microsoft.XMLHTTP');
????}
????
????xhr.open(defaultParam.type,?defaultParam.url+'?'+dataStr,?defaultParam.async);
????xhr.send();
????xhr.onreadystatechange?=?function?(){
????????if?(xhr.readyState?===?4){
????????????if(xhr.status?===?200){
????????????????let?result?=?JSON.parse(xhr.responseText);
????????????????//?在此處調(diào)用回調(diào)函數(shù)
????????????????defaultParam.success(result);
????????????}
????????}
????}
}
復(fù)制代碼
我們在業(yè)務(wù)代碼里可以這樣調(diào)用ajax請求:
ajax({
???url:'#',
???type:GET,
???success:function(e){
????//?回調(diào)函數(shù)里就是對請求結(jié)果的處理
???}
});
復(fù)制代碼
ajax的success方法就是一個回調(diào)函數(shù),回調(diào)函數(shù)中執(zhí)行的是我們請求成功之后要做的進(jìn)一步操作。這樣就初步實現(xiàn)了異步,但是回調(diào)函數(shù)有一個非常嚴(yán)重的缺點,那就是回調(diào)地獄的問題。大家可以試想一下,如果我們在回調(diào)函數(shù)里再發(fā)起一個ajax請求呢?那豈不是要在success函數(shù)里繼續(xù)寫一個ajax請求?那如果需要多級嵌套發(fā)起ajax請求呢?豈不是需要多級嵌套?如果嵌套的層級很深的話,我們的代碼結(jié)構(gòu)可能就會變成這樣:
因此,為了解決回調(diào)地獄的問題,提出了Promise、async/await、generator的概念。
2、Promise
Promise作為典型的微任務(wù)之一,它的出現(xiàn)可以使JS達(dá)到異步執(zhí)行的效果。一個Promise函數(shù)的結(jié)構(gòu)如下列代碼如下:
const?promise?=?new?Promise((resolve,?reject)?=>?{
?resolve('a');
});
promise
????.then((arg)?=>?{?console.log(`執(zhí)行resolve,參數(shù)是${arg}`)?})
????.catch((arg)?=>?{?console.log(`執(zhí)行reject,參數(shù)是${arg}`)?})
????.finally(()?=>?{?console.log('結(jié)束promise')?});
復(fù)制代碼
如果,我們需要嵌套執(zhí)行異步代碼,相比于回調(diào)函數(shù)來說,Promise的執(zhí)行方式如下列代碼所示:
const?promise?=?new?Promise((resolve,?reject)?=>?{
?resolve(1);
});
promise.then((value)?=>?{
?????console.log(value);
?????return?value?*?2;
????}).then((value)?=>?{
?????console.log(value);
?????return?value?*?2;
????}).then((value)?=>?{
????console.log(value);
????}).catch((err)?=>?{
??console.log(err);
????});
復(fù)制代碼
即,通過then來實現(xiàn)多級嵌套(鏈?zhǔn)秸{(diào)用),這看起來是不是就比回調(diào)函數(shù)舒服多了~
每個Promise都會經(jīng)歷的生命周期是:
進(jìn)行中(pending) - 此時代碼執(zhí)行尚未結(jié)束,所以也叫未處理的(unsettled) 已處理(settled) - 異步代碼已執(zhí)行結(jié)束 已處理的代碼會進(jìn)入兩種狀態(tài)中的一種: 已完成(fulfilled) - 表明異步代碼執(zhí)行成功,由resolve()觸發(fā) 已拒絕(rejected)- 遇到錯誤,異步代碼執(zhí)行失敗 ,由reject()觸發(fā)
因此,pending,fulfilled, rejected就是Promise中的三種狀態(tài)啦~ 大家一定要牢記,在Promise中,要么包含resolve()來表示Promise的狀態(tài)為fulfilled,要么包含reject()來表示Promise的狀態(tài)為rejected。不然我們的Promise就會一直處于pending的狀態(tài),直至程序崩潰...
除此之外,Promise不僅很好的解決了鏈?zhǔn)秸{(diào)用的問題,它還有很多神奇的操作呢:
**Promise.all(promises)**:接收一個包含多個Promise對象的數(shù)組,等待所有都完成時,返回存放它們結(jié)果的數(shù)組。如果任一被拒絕,則立即拋出錯誤,其他已完成的結(jié)果會被忽略 Promise.allSettled(promises): 接收一個包含多個Promise對象的數(shù)組,等待所有都已完成或者已拒絕時,返回存放它們結(jié)果對象的數(shù)組。每個結(jié)果對象的結(jié)構(gòu)為{status:'fulfilled' // 或 'rejected', value // 或reason} Promise.race(promises): 接收一個包含多個Promise對象的數(shù)組,等待第一個有結(jié)果(完成/拒絕)的Promise,并把其result/error作為結(jié)果返回
function?getPromises(){
????return?[
????????new?Promise(((resolve,?reject)?=>?setTimeout(()?=>?resolve(1),?1000))),
????????new?Promise(((resolve,?reject)?=>?setTimeout(()?=>?reject(new?Error('2')),?2000))),
????????new?Promise(((resolve,?reject)?=>?setTimeout(()?=>?resolve(3),?3000))),
????];
}
Promise.all(getPromises()).then(console.log);
Promise.allSettled(getPromises()).then(console.log);
Promise.race(getPromises()).then(console.log);
復(fù)制代碼
打印結(jié)果如下:


3、Generator
Generator是ES6提出的一種異步編程的方案。因為手動創(chuàng)建一個iterator十分麻煩,因此ES6推出了generator,用于更方便的創(chuàng)建iterator。也就是說,Generator就是一個返回值為iterator對象的函數(shù)。
在講Generator之前,我們先來看看iterator是什么:
iterator是什么?
iterator中文名叫迭代器。它為js中各種不同的數(shù)據(jù)結(jié)構(gòu)(Object、Array、Set、Map)提供統(tǒng)一的訪問機(jī)制。任何數(shù)據(jù)結(jié)構(gòu)只要部署了Iterator接口,就可以完成遍歷操作。 因此iterator也是一種對象,不過相比于普通對象來說,它有著專為迭代而設(shè)計的接口。
iterator 的作用:
為各種數(shù)據(jù)結(jié)構(gòu),提供一個統(tǒng)一的、簡便的訪問接口; 使得數(shù)據(jù)結(jié)構(gòu)的成員能夠按某種次序排列; ES6 創(chuàng)造了一種新的遍歷命令for…of循環(huán),Iterator 接口主要供for…of消費
iterator的結(jié)構(gòu): 它有next方法,該方法返回一個包含value和done兩個屬性的對象(我們假設(shè)叫result)。value是迭代的值,后者是表明迭代是否完成的標(biāo)志。true表示迭代完成,false表示沒有。iterator內(nèi)部有指向迭代位置的指針,每次調(diào)用next,自動移動指針并返回相應(yīng)的result。
原生具備iterator接口的數(shù)據(jù)結(jié)構(gòu)如下:
Array Map Set String TypedArray 函數(shù)里的arguments對象 NodeList對象
這些數(shù)據(jù)結(jié)構(gòu)都有一個Symbol.iterator屬性,可以直接通過這個屬性來直接創(chuàng)建一個迭代器。也就是說,Symbol.iterator屬性只是一個用來創(chuàng)建迭代器的接口,而不是一個迭代器,因為它不含遍歷的部分。
使用Symbol.iterator接口生成iterator迭代器來遍歷數(shù)組的過程為:
let?arr?=?['a','b','c'];
let?iter?=?arr[Symbol.iterator]();
iter.next()?//?{?value:?'a',?done:?false?}
iter.next()?//?{?value:?'b',?done:?false?}
iter.next()?//?{?value:?'c',?done:?false?}
iter.next()?//?{?value:?undefined,?done:?true?}
復(fù)制代碼
for ... of的循環(huán)內(nèi)部實現(xiàn)機(jī)制其實就是iterator,它首先調(diào)用被遍歷集合對象的 Symbol.iterator 方法,該方法返回一個迭代器對象,迭代器對象是可以擁有.next()方法的任何對象,然后,在 for ... of 的每次循環(huán)中,都將調(diào)用該迭代器對象上的 .next 方法。然后使用for i of打印出來的i也就是調(diào)用.next方法后得到的對象上的value屬性。
對于原生不具備iterator接口的數(shù)據(jù)結(jié)構(gòu),比如Object,我們可以采用自定義的方式來創(chuàng)建一個遍歷器。
比如,我們可以自定義一個iterator來遍歷對象:
let?obj?=?{a:?"hello",?b:?"world"};
//?自定義迭代器
function?createIterator(items)?{
????let?keyArr?=?Object.keys(items);
????let?i?=?0;
????return?{
????????next:?function?()?{
????????????let?done?=?(i?>=?keyArr.length);
????????????let?value?=?!done???items[keyArr[i++]]?:?undefined;
????????????return?{
????????????????value:?value,
????????????????done:?done,
????????????};
????????}
????};
}
let?iterator?=?createIterator(obj);
console.log(iterator.next());?//?"{?value:?'hello',?done:?false?}"
console.log(iterator.next());??//?"{?value:?'world',?done:?false?}"
console.log(iterator.next());??//?"{?value:?undefined,?done:?true?}"
復(fù)制代碼
接下來,我們來聊聊Generator:
我們通過一個例子來看看Gnerator的特征:
function*?createIterator()?{
??yield?1;
??yield?2;
??yield?3;
}
//?generators可以像正常函數(shù)一樣被調(diào)用,不同的是會返回一個?iterator
let?iterator?=?createIterator();
console.log(iterator.next().value);?//?1
console.log(iterator.next().value);?//?2
console.log(iterator.next().value);?//?3
復(fù)制代碼
Generator 函數(shù)是 ES6 提供的一種異步編程解決方案。形式上,Generator 函數(shù)是一個普通函數(shù),但是有兩個特征:
function關(guān)鍵字與函數(shù)名之間有一個星號 函數(shù)體內(nèi)部使用yield語句,定義不同的內(nèi)部狀態(tài)
Generator函數(shù)的調(diào)用方法與普通函數(shù)一樣,也是在函數(shù)名后面加上一對圓括號。不同的是,調(diào)用Generator函數(shù)后,該函數(shù)并不執(zhí)行,返回的也不是函數(shù)運(yùn)行結(jié)果,而是一個指向內(nèi)部狀態(tài)的指針對象,也就是遍歷器對象(Iterator Object)
打印看看Generator函數(shù)返回值的內(nèi)容:
發(fā)現(xiàn)generator函數(shù)的返回值的原型鏈上確實有iterator對象該有的next,這充分說明了generator的返回值是一個iterator。除此之外還有函數(shù)該有的return方法和throw方法。
在普通函數(shù)中,我們想要一個函數(shù)最終的執(zhí)行結(jié)果,一般都是return出來,或者以return作為結(jié)束函數(shù)的標(biāo)準(zhǔn)。運(yùn)行函數(shù)時也不能被打斷,期間也不能從外部再傳入值到函數(shù)體內(nèi)。但在generator中,就打破了這幾點,所以generator和普通的函數(shù)完全不同。當(dāng)以function*的方式聲明了一個Generator生成器時,內(nèi)部是可以有許多狀態(tài)的,以yield進(jìn)行斷點間隔。期間我們執(zhí)行調(diào)用這個生成的Generator,他會返回一個遍歷器對象,用這個對象上的方法,實現(xiàn)獲得一個yield后面輸出的結(jié)果。
function*?generator()?{
????yield?1
????yield?2
};
let?iterator?=?generator();
iterator.next()??//?{value:?1,?done:?false}
iterator.next()??//?{value:?2,?done:?false}
iterator.next()??//?{value:?undefined,?done:?true}
復(fù)制代碼
yield和return的區(qū)別:
都能返回緊跟在語句后面的那個表達(dá)式的值 yield相比于return來說,更像是一個斷點。遇到y(tǒng)ield,函數(shù)暫停執(zhí)行,下一次再從該位置繼續(xù)向后執(zhí)行,而return語句不具備位置記憶的功能。 一個函數(shù)里面,只能執(zhí)行一個return語句,但是可以執(zhí)行多次yield表達(dá)式。 正常函數(shù)只能返回一個值,因為只能執(zhí)行一次return;Generator 函數(shù)可以返回一系列的值,因為可以有任意多個yield
語法注意點:
yield表達(dá)式只能用在 Generator 函數(shù)里面
yield表達(dá)式如果用在另一個表達(dá)式之中,必須放在圓括號里面
yield表達(dá)式用作函數(shù)參數(shù)或放在賦值表達(dá)式的右邊,可以不加括號。
如果 return 語句后面還有 yield 表達(dá)式,那么后面的 yield 完全不生效
使用Generator的其余注意事項:
需要注意的是,yield 不能跨函數(shù)。并且yield需要和*配套使用,別處使用無效
function*?createIterator(items)?{
??items.forEach(function?(item)?{
????//?語法錯誤
????yield?item?+?1;
??});
}
復(fù)制代碼
箭頭函數(shù)不能用做 generator
講了這么多,那么Generator到底有什么用呢?
因為Generator可以在執(zhí)行過程中多次返回,所以它看上去就像一個可以記住執(zhí)行狀態(tài)的函數(shù),利用這一點,寫一個generator就可以實現(xiàn)需要用面向?qū)ο蟛拍軐崿F(xiàn)的功能。 Generator還有另一個巨大的好處,就是把異步回調(diào)代碼變成“同步”代碼。這個在ajax請求中很有用,避免了回調(diào)地獄.
4、 async/await
最后我們來講講async/await,終于講到這兒了!??!async/await是ES7提出的關(guān)于異步的終極解決方案。我看網(wǎng)上關(guān)于async/await是誰的語法糖這塊有兩個版本:
第一個版本說async/await是Generator的語法糖 第二個版本說async/await是Promise的語法糖
其實,這兩種說法都沒有錯。關(guān)于async/await是Generator的語法糖: 所謂Generator語法糖,表明的就是aysnc/await實現(xiàn)的就是generator實現(xiàn)的功能。但是async/await比generator要好用。因為generator執(zhí)行yield設(shè)下的斷點采用的方式就是不斷的調(diào)用iterator方法,這是個手動調(diào)用的過程。針對generator的這個缺點,后面提出了co這個庫函數(shù)來自動執(zhí)行next,相比于之前的方案,這種方式確實有了進(jìn)步,但是仍然麻煩。而async配合await得到的就是斷點執(zhí)行后的結(jié)果。因此async/await比generator使用更普遍。
總結(jié)下來,async函數(shù)對 Generator函數(shù)的改進(jìn),主要體現(xiàn)在以下三點:
內(nèi)置執(zhí)行器:Generator函數(shù)的執(zhí)行必須靠執(zhí)行器,因為不能一次性執(zhí)行完成,所以之后才有了開源的 co函數(shù)庫。但是,async函數(shù)和正常的函數(shù)一樣執(zhí)行,也不用 co函數(shù)庫,也不用使用 next方法,而 async函數(shù)自帶執(zhí)行器,會自動執(zhí)行。 適用性更好:co函數(shù)庫有條件約束,yield命令后面只能是 Thunk函數(shù)或 Promise對象,但是 async函數(shù)的 await關(guān)鍵詞后面,可以不受約束。 可讀性更好:async和 await,比起使用 *號和 yield,語義更清晰明了。
關(guān)于async/await是Promise的語法糖: 如果不使用async/await的話,Promise就需要通過鏈?zhǔn)秸{(diào)用來依次執(zhí)行then之后的代碼:
function?counter(n){
?return?new?Promise((resolve,?reject)?=>?{?
????resolve(n?+?1);
????});
}
function?adder(a,?b){
????return?new?Promise((resolve,?reject)?=>?{?
????resolve(a?+?b);
????});
}
function?delay(a){
????return?new?Promise((resolve,?reject)?=>?{?
????setTimeout(()?=>?resolve(a),?1000);
????});
}
//?鏈?zhǔn)秸{(diào)用寫法
function?callAll(){
????counter(1)
???????.then((val)?=>?adder(val,?3))
???????.then((val)?=>?delay(val))
???????.then(console.log);
}
callAll();//5
復(fù)制代碼
雖然相比于回調(diào)地獄來說,鏈?zhǔn)秸{(diào)用確實順眼多了。但是其呈現(xiàn)仍然略繁瑣了一些。而async/await的出現(xiàn),就使得我們可以通過同步代碼來達(dá)到異步的效果:
async?function?callAll(){
???const?count?=?await?counter(1);
???const?sum?=?await?adder(count?+?3);
???console.log(await?delay(sum));
}
callAll();//?5
復(fù)制代碼
由此可見,Promise搭配async/await的使用才是正解!
總結(jié)
promise讓異步執(zhí)行看起來更清晰明了,通過then讓異步執(zhí)行結(jié)果分離出來。 async/await其實是基于Promise的。async函數(shù)其實是把promise包裝了一下。使用async函數(shù)可以讓代碼簡潔很多,不需要promise一樣需要些then,不需要寫匿名函數(shù)處理promise的resolve值,也不需要定義多余的data變量,還避免了嵌套代碼。 async函數(shù)是Generator函數(shù)的語法糖。async函數(shù)的返回值是 promise 對象,這比 Generator 函數(shù)的返回值是 Iterator 對象方便多了。同時,我們還可以用await來替代then方法指定下一步的操作。 感覺Promise+async的操作最為常見。因為Generator被async替代了呀~
關(guān)于本文
作者:DoubleSweet
https://juejin.cn/post/7082753409060716574
The End
