摘要:三即生成器,它是生成器函數(shù)返回的一個(gè)對(duì)象,是中提供的一種異步編程解決方案而生成器函數(shù)有兩個(gè)特征,一是函數(shù)名前帶星號(hào),二是內(nèi)部執(zhí)行語(yǔ)句前有關(guān)鍵字調(diào)用一個(gè)生成器函數(shù)并不會(huì)馬上執(zhí)行它里面的語(yǔ)句,而是返回一個(gè)這個(gè)生成器的迭代器對(duì)象。
文章來(lái)自微信公眾號(hào):前端工坊(fe_workshop),不定期更新有趣、好玩的前端相關(guān)原創(chuàng)技術(shù)文章。 如果喜歡,請(qǐng)關(guān)注公眾號(hào):前端工坊
版權(quán)歸微信公眾號(hào)所有,轉(zhuǎn)載請(qǐng)注明出處。
作者:京東金融-移動(dòng)研發(fā)部-前端開(kāi)發(fā)工程師 張恒
作為Web工程師,相信大家在開(kāi)發(fā)項(xiàng)目的過(guò)程中,都存在與服務(wù)器端的通信,如登錄驗(yàn)證、獲取用戶信息、獲取應(yīng)用數(shù)據(jù)等都需要通過(guò)調(diào)用后端的API來(lái)進(jìn)行操作,而實(shí)現(xiàn)這一操作的正是異步調(diào)用;
這篇文章旨在通過(guò)一些異步調(diào)用的概念和相應(yīng)的代碼演示,盡量詳細(xì)地介紹異步調(diào)用的實(shí)現(xiàn)、各種異步編程的使用方式和區(qū)別,以及他們的發(fā)展演變;
在Web應(yīng)用的開(kāi)發(fā)過(guò)程中,為了實(shí)現(xiàn)良好的交互體驗(yàn),我們都會(huì)使用 ajax 的方式與后端通信,實(shí)現(xiàn)無(wú)刷新數(shù)據(jù)提取和快速展現(xiàn),極大地提升了用戶體驗(yàn);
ajax 的全稱是Asynchronous JavaScript and XML,Asynchronous 即異步,它有別于傳統(tǒng)web開(kāi)發(fā)中采用的同步的方式。
ajax 的原理簡(jiǎn)單來(lái)說(shuō)就是通過(guò) XmlHttpRequest 對(duì)象來(lái)向服務(wù)器發(fā)異步請(qǐng)求,從服務(wù)器獲得數(shù)據(jù),然后用JavaScript來(lái)操作DOM而更新頁(yè)面,這其中 XMLHttpRequest 是 ajax 的核心機(jī)制,
通過(guò)這種異步技術(shù),JavaScript可以及時(shí)向服務(wù)器提出請(qǐng)求和處理響應(yīng),而不阻塞用戶,從而達(dá)到無(wú)刷新頁(yè)面的效果。
相信廣大的Web工程師們對(duì)此已經(jīng)耳熟能詳,我就不在這里細(xì)講了,如果你是剛?cè)胄星岸瞬⑶也涣私獯烁拍?,可以移步ajax;
但是必須提到的是XmlHttpRequest 對(duì)象有一個(gè)屬性 onreadystatechange 用于當(dāng)異步請(qǐng)求狀態(tài)改變時(shí)觸發(fā)事件執(zhí)行后續(xù)動(dòng)作,
這也就是本文要講的異步調(diào)與回調(diào)處理;對(duì)于單個(gè)的異步請(qǐng)求及其回調(diào)結(jié)果處理實(shí)際上沒(méi)太大問(wèn)題,但當(dāng)碰到某些復(fù)雜場(chǎng)景,需要多次異步調(diào)用接口,并且后一個(gè)的調(diào)用需要前一個(gè)異步調(diào)用的返回結(jié)果作為參數(shù)時(shí),
由于是異步形式,不能像同步編程那樣編寫代碼,咱們就不得不嵌套編寫,而當(dāng)嵌套層過(guò)多就會(huì)出現(xiàn)難以閱讀和維護(hù)代碼。
舉個(gè)例子,在一個(gè)Web App中,需要獲取用戶的某篇博客的所有跟帖,這時(shí)我們就需要有如下的APIs;
1、獲取用戶會(huì)話的token(也可能是一開(kāi)始進(jìn)入博客通過(guò)登錄返回的)
{ status:"success", data:{ token: "******" } }
2、通過(guò)token獲取用戶詳細(xì)信息
{ status:"success", data:{ userInfo: { id: 10001, name:"test", email:"test@test.com" } } }
3、通過(guò)userId獲取用戶文章列表
{ status:"success", data:[ { id: 1, title:"my first article", content:"a long content will be here...", date:"2018-02-28" }, { id: 2, title:"my second article", content:"a long content will be here...", date:"2018-02-28" }, ] }
4、通過(guò)博客id獲取所有用戶評(píng)論
{ status:"success", data:[ { id: 1, userId:,10005, comment:"it"s an great article...", date:"2018-02-28" }, { id: 2, userId:,10008, comment:"it"s very useful article for me, thanks blogger...", date:"2018-03-01" }, ] }
接下來(lái)我們就通過(guò)code去實(shí)現(xiàn)這樣一個(gè)邏輯,首定義一個(gè)異步調(diào)用的公用方法:
function ajaxRequest(url,successHandler){ var xhr; if (window.XmlHttpRequest) { xhr = new XmlHttpRequest(); }else if (window.ActiveXObject) { try { xhr = new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) { try { xhr = new ActiveXObject("msxml2.XMLHTTP"); } catch (ex) { } } } xhr.onreadystatechange = function () { if (xhr.readyState == 4) { if (xhr.status == 200) { successHandler(xhr.responseText); } } } xhr.open("GET", url); xhr.send(); }
使用ajax實(shí)現(xiàn)獲取用戶評(píng)論的邏輯則如下所示:
/*獲取評(píng)論*/ ajaxRequest("your-host/api/get-token",function(res1){ var token = res1.data.token; ajaxRequest("your-host/api/get-user?token="+token,function(res2){ var userId = res2.data.userInfo.id; ajaxRequest("your-host/api/get-article?userId="+userId,function(res3){ var artcleId=res3.data[0].id; ajaxRequest("your-host/api/get-comments?artcleId="+artcleId,function(res4){ var comments = res4.data; console.log(comments); }); }) }) })
OK,上面的代碼是不是讓人頭暈,如果碰到某些更復(fù)雜的邏輯,就會(huì)出現(xiàn)更多的嵌套回調(diào),這即稱為"回調(diào)地獄(callback hell)";
我們可以稍加重構(gòu)以提高可閱讀性:
function getToken(callback){ ajaxRequest("your-host/api/get-token",function(res1){ callback(res1.data.token); }); } function getUserByToken(token,callback){ ajaxRequest("your-host/api/get-user?token="+token,function(res2){ callback(res2.data.userInfo.id); }); } function getArticlesByUserId(userId,callback){ ajaxRequest("your-host/api/get-article?userId="+userId,function(res3){ callback(res3.data[0].id); }); } function getCommentsByArtcleId(artcleId,callback){ ajaxRequest("your-host/api/get-comments?artcleId="+artcleId,function(res4){ callback(res4.data); }); } /*獲取評(píng)論*/ getToken(function(token){ getUserByToken(token,function(userId){ getArticlesByUserId(userId,function(artcleId){ getCommentsByArtcleId(artcleId,function(comments){ console.log(comments); }); }); }); });
上面的代碼看著是不是稍微清晰了一些,不過(guò)函數(shù)里面調(diào)函數(shù)的方式仍然丑陋,下面我們將介紹另一種異步調(diào)用方式Promise。
二、PromisePromise 對(duì)象是一個(gè)代理對(duì)象(代理一個(gè)值),被代理的值在Promise對(duì)象創(chuàng)建時(shí)可能是未知的。
它允許你為異步操作的成功和失敗分別綁定相應(yīng)的處理方法(handlers)。 這讓異步方法可以像同步方法那樣返回值,
但并不是立即返回最終執(zhí)行結(jié)果,而是一個(gè)能代表未來(lái)出現(xiàn)的結(jié)果的promise對(duì)象,如果你不了解Promise,
可以移步Promise查看詳細(xì)說(shuō)明。
一個(gè) Promise對(duì)象有且僅有三種狀態(tài):
* pending:初始狀態(tài),既不是成功,也不是失敗狀態(tài) * fulfilled:意味著操作成功完成 * rejected:意味著操作失敗
pending狀態(tài)的 Promise 對(duì)象可能觸發(fā)fulfilled 狀態(tài)并傳遞一個(gè)值給相應(yīng)的狀態(tài)處理方法,也可能觸發(fā)失敗狀態(tài)(rejected)并傳遞失敗信息。
當(dāng)其中任一種情況出現(xiàn)時(shí),Promise 對(duì)象的 then 方法綁定的處理方法(handlers )就會(huì)被調(diào)用(then方法包含兩個(gè)參數(shù):onfulfilled 和 onrejected,
它們都是Function類型。當(dāng)Promise狀態(tài)為fulfilled時(shí),調(diào)用 then 的 onfulfilled 方法,當(dāng)Promise狀態(tài)為rejected時(shí),調(diào)用 then 的 onrejected 方法,
所以在異步操作的完成和綁定處理方法之間不存在競(jìng)爭(zhēng)),限于樣例代碼限制,在上面的例子中我并沒(méi)有對(duì)請(qǐng)求異常做處理,在實(shí)際項(xiàng)目中讀者朋友可以自行加上處理。
因?yàn)?Promise.prototype.then和Promise.prototype.catch方法返回promise 對(duì)象, 所以它們可以被鏈?zhǔn)秸{(diào)用。
還是以上的場(chǎng)景為例子來(lái)看Promise實(shí)現(xiàn)的異步調(diào)用的代碼片段,如下所示:
function getToken(){ return new Promise(function(resolve,reject){ ajaxRequest("your-host/api/get-token",function(res1){ resolve(res1.data.token); }); }); } function getUserByToken(token){ return new Promise(function(resolve,reject){ ajaxRequest("your-host/api/get-user?token="+token,function(res2){ resolve(res2.data.userInfo.id); }); }); } function getArticlesByUserId(userId){ return new Promise(function(resolve,reject){ ajaxRequest("your-host/api/get-article?userId="+userId,function(res3){ resolve(res3.data[0].id); }); }); } function getCommentsByArtcleId(artcleId){ return new Promise(function(resolve,reject){ ajaxRequest("your-host/api/get-comments?artcleId="+artcleId,function(res4){ resolve(res4.data); }); }); } /*獲取評(píng)論*/ getToken().then(function(token){ return getUserByToken(token); }).then(function(userId){ return getArticlesByUserId(userId); }).then(function(artcleId){ return getCommentsByArtcleId(artcleId); }).then(function(comments){ console.log(comments); });
從上面獲取comments的代碼可以看出,后一個(gè)方法的調(diào)用總是在前一個(gè)異步調(diào)用完成后,通過(guò)前一個(gè)結(jié)果作為參數(shù)去執(zhí)行下一個(gè)請(qǐng)求,
一步一步往后執(zhí)行直到所有異步請(qǐng)求都執(zhí)行完成,這個(gè)過(guò)程不僅代碼結(jié)構(gòu)上清晰了許多,而且從編程風(fēng)格上看也能看出些類同步編碼的影子。
下面介紹一個(gè)更接近同步編程的風(fēng)格的異步編碼方式生成器函數(shù)Generator。
Generator即生成器,它是生成器函數(shù)(Function*)返回的一個(gè)對(duì)象,是ES2015中提供的一種異步編程解決方案;
而生成器函數(shù)有兩個(gè)特征,一是函數(shù)名前帶星號(hào),二是內(nèi)部執(zhí)行語(yǔ)句前有關(guān)鍵字 yield,調(diào)用一個(gè)生成器函數(shù)并不會(huì)馬上執(zhí)行它里面的語(yǔ)句,而是返回一個(gè)這個(gè)生成器的迭代器對(duì)象。當(dāng)這個(gè)迭代器的 next() 方法被首次調(diào)用時(shí),
其內(nèi)的語(yǔ)句會(huì)執(zhí)行到第一個(gè)出現(xiàn)yield的位置為止,yield 后緊跟迭代器要返回的值?;蛘呷绻玫氖?yield*(多了個(gè)星號(hào)),則表示將執(zhí)行權(quán)移交給另一個(gè)生成器函數(shù)(當(dāng)前生成器暫停執(zhí)行)。
next() 方法返回一個(gè)對(duì)象,這個(gè)對(duì)象包含兩個(gè)屬性:value 和 done,value 屬性表示本次 yield 表達(dá)式的返回值,done 屬性為布爾類型,表示生成器后續(xù)是否還有 yield 語(yǔ)句,即生成器函數(shù)是否已經(jīng)執(zhí)行完畢并返回。
調(diào)用 next() 方法時(shí),如果傳入了參數(shù),那么這個(gè)參數(shù)會(huì)作為上一條執(zhí)行的 yield 語(yǔ)句的返回值??匆粋€(gè)簡(jiǎn)單的例子:
function* genFun(){ yield "initial"; var anotherVal=yield "Hello"; yield anotherVal; } var gObj=genFun(); console.log(gObj.next());// 執(zhí)行 yield "initial";,返回 "initial",{value:"initial",done:false} console.log(gObj.next());// 執(zhí)行 yield "Hello",返回 "Hello",{value:"Hello",done:false console.log(gObj.next("World"));// 將"World"賦給上一條 yield "Hello"的左值anotherVal,即執(zhí)行 anotherVal="World",返回"World",{value:"World",done:false} console.log(gObj.next());// 執(zhí)行完畢,{value:undefined,done:true}
在上面的例子中,如果第三個(gè) next() 的調(diào)用是在給anotherVal賦值,這樣執(zhí)行之后返回的 value 即為傳入的參數(shù),如果不傳參數(shù),則返回的 value 為undefined,且此時(shí)的 done 還是 false,這里需要注意。
當(dāng)在生成器函數(shù)中顯式 return 時(shí),會(huì)導(dǎo)致生成器立即變?yōu)橥瓿蔂顟B(tài),即調(diào)用 next() 方法返回的對(duì)象的 done 為 true。如果 return 后面跟了一個(gè)值,那么這個(gè)值會(huì)作為當(dāng)前調(diào)用 next() 方法返回的 value 值。請(qǐng)看如下代碼:
function* yieldAndReturn() { yield "Y"; return "R";//顯式返回處,可以觀察到 done 也立即變?yōu)榱?true yield "unreachable";// 不會(huì)被執(zhí)行了 } var gen = yieldAndReturn() console.log(gen.next()); // { value: "Y", done: false } console.log(gen.next()); // { value: "R", done: true } console.log(gen.next()); // { value: undefined, done: true }
了解了Generator的簡(jiǎn)單概念之后,那它到底與本文核心內(nèi)容有什么關(guān)聯(lián)呢?OK,咱們還是以上面的場(chǎng)景來(lái)使用Generator方式實(shí)現(xiàn)(異步調(diào)用api的幾個(gè)方法公用上面的),代碼片段如下:
function* myGen(){ var token = yield getToken(); var userId = yield getUserByToken(token); var articleId = yield getArticlesByUserId(userId); var comments = yield getCommentsByArtcleId(articleId); console.log(comments); } var gen = myGen(); gen.next().value.then(function(res1){ gen.next(res1).value.then(function(res2){ gen.next(res2).value.then(function(res3){ gen.next(res3).value.then(function(res4){ gen.next(res4); console.log("executing done"); }); }); }); });
從上面的代碼中,咱們可以看到生成器函數(shù) myGen里面的語(yǔ)句就跟平時(shí)寫同步代碼一樣類似,只是多了關(guān)鍵字 yield,這即是Generator的關(guān)鍵之處,用同步的編碼方式,處理異步邏輯。
但同時(shí)我們也看到后半部分代碼的執(zhí)行跟之前的Promise幾乎一樣,一連串的 then 語(yǔ)句看起來(lái)還是不怎么美觀,咱們可以對(duì)它進(jìn)行再一次封裝:
function genRunner(){ var gen = myGen(); function run(result){ if(result.done) { return; } result.value.then(function(res){ run(gen.next(res)); }); } run(gen.next()); } genRunner();
通過(guò)封裝一個(gè)函數(shù)執(zhí)行器,通過(guò)在函數(shù)內(nèi)部循環(huán)調(diào)用自身來(lái)執(zhí)行Generator函數(shù)內(nèi)部的所有yield 語(yǔ)句,這樣的代碼閱讀起來(lái)就更加清晰且優(yōu)雅了!
四、async/awaitasync 是ES2017引入的一種函數(shù)形式,可以使用它加在 function 前來(lái)聲明定義異步函數(shù),使用它能給異步編程帶來(lái)極大的便利,從code形式上看就跟編寫同步代碼一樣。當(dāng)一個(gè)async 函數(shù)被調(diào)用時(shí),它返回一個(gè) Promise 對(duì)象。
當(dāng)async 函數(shù)返回一個(gè)值時(shí),Promise 將用返回的值 resolved。 當(dāng)async 函數(shù)拋出異?;蚰硞€(gè)值時(shí),Promise將被拋出的值 rejected。
async 函數(shù)可以包含 await 表達(dá)式,帶有 await 的語(yǔ)句會(huì)暫停async 函數(shù)的執(zhí)行并等待傳遞的Promise的解析,然后再恢復(fù)async 函數(shù)的執(zhí)行并返回解析后的值。
async/await 函數(shù)的目的是簡(jiǎn)化同步使用 Promise 的行為,并對(duì)一組 Promise 執(zhí)行某些行為,就像 Promises 類似于結(jié)構(gòu)化回調(diào)一樣,async/await 相當(dāng)于 Generator和 Promise 的集合體。
先來(lái)看一個(gè)簡(jiǎn)單的例子:
function fakeRequest() { return new Promise(resolve => { setTimeout(() => { resolve("second output"); }, 500); }); } async function asyncCall() { console.log("first output"); var result = await fakeRequest(); console.log(result); console.log("last output"); } asyncCall(); /*執(zhí)行后的輸出*/ //first output //second output //last output
從上面的代碼寫法以及async 函數(shù)內(nèi)部的執(zhí)行結(jié)果可以看出,這簡(jiǎn)直就是同步調(diào)用的同步編程風(fēng)格和執(zhí)行順序,有沒(méi)有?其實(shí)如上所述,await 的語(yǔ)句會(huì)暫停async 函數(shù)的執(zhí)行并等待傳遞的Promise的解析,
因此才會(huì)有console.log("last output");再最后輸出,如果在async 函數(shù)體外面在寫一個(gè)執(zhí)行代碼,則會(huì)先于await 結(jié)果輸出;
咱們還是以最初的場(chǎng)景為例,使用async/await 的方式來(lái)實(shí)現(xiàn)一遍,看看代碼風(fēng)格上的差異:
async function getComments(){ var token = await getToken(); var userId = await getUserByToken(token); var articleId = await getArticlesByUserId(userId); var comments = await getCommentsByArtcleId(articleId); console.log(comments); }
從代碼風(fēng)格上看是不是跟Generator函數(shù)基本一樣,只是把星號(hào)去掉,前面加了async ,函數(shù)體內(nèi)語(yǔ)句中把 yield 換成來(lái) await;但是調(diào)用執(zhí)行函數(shù)時(shí)則完全不一樣了,
Generator函數(shù)需要額外定義執(zhí)行函數(shù)器,通過(guò)不斷調(diào)用 next() 來(lái)完成調(diào)用獲取結(jié)果,而async 函數(shù)自帶來(lái)執(zhí)行函數(shù)器,只要調(diào)用函數(shù)即會(huì)執(zhí)行,因此使用上也方便來(lái)許多。
## 總結(jié)
咱們?cè)倩仡櫼幌挛恼聝?nèi)容,首先通過(guò)最傳統(tǒng)的 ajax 方式異步調(diào)用和回調(diào)函數(shù)處理;然后加入Promise對(duì)象,通過(guò)鏈?zhǔn)秸{(diào)用使代碼編寫更加有條理性;
之后又引入了新的異步編程解決方案 Generator ,其函數(shù)內(nèi)部的編碼方式與同步寫法及其類似,只是 Generator 的執(zhí)行權(quán)交由了另外一個(gè)函數(shù),其執(zhí)行方式仍然需要不斷的調(diào)用 next() 而略顯繁瑣;
最后引入了ES2017新標(biāo)準(zhǔn)中收錄的新函數(shù) async,通過(guò)與await 相結(jié)合,使其異步調(diào)用的編碼實(shí)現(xiàn)基本跟同步編碼相差無(wú)幾,且非常易于理解和提高了代碼的維護(hù)行。
好了,到這里也該是文章結(jié)束的時(shí)候了,雖然篇幅不長(zhǎng),并且描述文字也不多,但還是希望閱讀之后的朋友們能有所收獲;由于寫作倉(cāng)促,文中難免出現(xiàn)錯(cuò)誤或描述不清的地方,希望朋友們能諒解,并歡迎指正。
注:文中所有的代碼都沒(méi)對(duì)異常進(jìn)行處理,如果你在實(shí)際項(xiàng)目中使用,請(qǐng)記得加上異常和錯(cuò)誤處理邏輯!
參考資源AJAX
Promise
Generator
Function*
async/await
阮一峰老師的ECMAScript 6 入門
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://hztianpu.com/yun/93223.html
摘要:更詳細(xì)的內(nèi)容下一章開(kāi)篇深入聊聊前后分離講述關(guān)于我目前在寫從零構(gòu)建前后分離項(xiàng)目系列,修正和補(bǔ)充以此為準(zhǔn)不斷更新的項(xiàng)目實(shí)踐地址彩蛋提前預(yù)覽下一章傳送門 開(kāi)篇 : 縱觀WEB歷史演變 在校學(xué)習(xí)和幾年工作工作中不知不覺(jué)經(jīng)歷了一半的 WEB 歷史演變、對(duì)近幾年的發(fā)展比較了解,結(jié)合經(jīng)驗(yàn)聊聊 WEB 發(fā)展歷史。 演變不易,但也是必然,因?yàn)闉槿耸冀K要進(jìn)步。 WEB 的發(fā)展史 一、開(kāi)山鼻祖 - 石器時(shí)代...
摘要:更詳細(xì)的內(nèi)容下一章開(kāi)篇深入聊聊前后分離講述關(guān)于我目前在寫從零構(gòu)建前后分離項(xiàng)目系列,修正和補(bǔ)充以此為準(zhǔn)不斷更新的項(xiàng)目實(shí)踐地址彩蛋提前預(yù)覽下一章傳送門 開(kāi)篇 : 縱觀WEB歷史演變 在校學(xué)習(xí)和幾年工作工作中不知不覺(jué)經(jīng)歷了一半的 WEB 歷史演變、對(duì)近幾年的發(fā)展比較了解,結(jié)合經(jīng)驗(yàn)聊聊 WEB 發(fā)展歷史。 演變不易,但也是必然,因?yàn)闉槿耸冀K要進(jìn)步。 WEB 的發(fā)展史 一、開(kāi)山鼻祖 - 石器時(shí)代...
摘要:微服務(wù)如何演變而來(lái)網(wǎng)關(guān)在微服務(wù)中如何發(fā)揮作用本文將以此作為話題,聊聊網(wǎng)關(guān)如何影響企業(yè)技術(shù)架構(gòu)的演變。微服務(wù)之間相互獨(dú)立,使用者無(wú)需配置環(huán)境,直接調(diào)用即可完成開(kāi)發(fā)。 互聯(lián)網(wǎng)技術(shù)日新月異,項(xiàng)目架構(gòu)不斷升級(jí)優(yōu)化。隨著企業(yè)微服務(wù)的興起和第三方API的發(fā)展,API網(wǎng)關(guān)這一作為微服務(wù)核心組件的產(chǎn)品也逐漸被越來(lái)越多的人認(rèn)識(shí)。微服務(wù)如何演變而來(lái)?網(wǎng)關(guān)在微服務(wù)中如何發(fā)揮作用?本文將以此作為話題,聊聊AP...
閱讀 1126·2021-10-11 10:59
閱讀 3711·2021-09-26 09:55
閱讀 1019·2019-08-30 15:55
閱讀 2739·2019-08-30 15:44
閱讀 505·2019-08-30 14:06
閱讀 818·2019-08-30 11:26
閱讀 3424·2019-08-30 10:49
閱讀 2741·2019-08-29 12:53