摘要:三即生成器,它是生成器函數返回的一個對象,是中提供的一種異步編程解決方案而生成器函數有兩個特征,一是函數名前帶星號,二是內部執行語句前有關鍵字調用一個生成器函數并不會馬上執行它里面的語句,而是返回一個這個生成器的迭代器對象。
文章來自微信公眾號:前端工坊(fe_workshop),不定期更新有趣、好玩的前端相關原創技術文章。 如果喜歡,請關注公眾號:前端工坊
版權歸微信公眾號所有,轉載請注明出處。
作者:京東金融-移動研發部-前端開發工程師 張恒
作為Web工程師,相信大家在開發項目的過程中,都存在與服務器端的通信,如登錄驗證、獲取用戶信息、獲取應用數據等都需要通過調用后端的API來進行操作,而實現這一操作的正是異步調用;
這篇文章旨在通過一些異步調用的概念和相應的代碼演示,盡量詳細地介紹異步調用的實現、各種異步編程的使用方式和區別,以及他們的發展演變;
在Web應用的開發過程中,為了實現良好的交互體驗,我們都會使用 ajax 的方式與后端通信,實現無刷新數據提取和快速展現,極大地提升了用戶體驗;
ajax 的全稱是Asynchronous JavaScript and XML,Asynchronous 即異步,它有別于傳統web開發中采用的同步的方式。
ajax 的原理簡單來說就是通過 XmlHttpRequest 對象來向服務器發異步請求,從服務器獲得數據,然后用JavaScript來操作DOM而更新頁面,這其中 XMLHttpRequest 是 ajax 的核心機制,
通過這種異步技術,JavaScript可以及時向服務器提出請求和處理響應,而不阻塞用戶,從而達到無刷新頁面的效果。
相信廣大的Web工程師們對此已經耳熟能詳,我就不在這里細講了,如果你是剛入行前端并且不了解此概念,可以移步ajax;
但是必須提到的是XmlHttpRequest 對象有一個屬性 onreadystatechange 用于當異步請求狀態改變時觸發事件執行后續動作,
這也就是本文要講的異步調與回調處理;對于單個的異步請求及其回調結果處理實際上沒太大問題,但當碰到某些復雜場景,需要多次異步調用接口,并且后一個的調用需要前一個異步調用的返回結果作為參數時,
由于是異步形式,不能像同步編程那樣編寫代碼,咱們就不得不嵌套編寫,而當嵌套層過多就會出現難以閱讀和維護代碼。
舉個例子,在一個Web App中,需要獲取用戶的某篇博客的所有跟帖,這時我們就需要有如下的APIs;
1、獲取用戶會話的token(也可能是一開始進入博客通過登錄返回的)
{ status:"success", data:{ token: "******" } }
2、通過token獲取用戶詳細信息
{ status:"success", data:{ userInfo: { id: 10001, name:"test", email:"test@test.com" } } }
3、通過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、通過博客id獲取所有用戶評論
{ 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" }, ] }
接下來我們就通過code去實現這樣一個邏輯,首定義一個異步調用的公用方法:
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實現獲取用戶評論的邏輯則如下所示:
/*獲取評論*/ 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,上面的代碼是不是讓人頭暈,如果碰到某些更復雜的邏輯,就會出現更多的嵌套回調,這即稱為"回調地獄(callback hell)";
我們可以稍加重構以提高可閱讀性:
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); }); } /*獲取評論*/ getToken(function(token){ getUserByToken(token,function(userId){ getArticlesByUserId(userId,function(artcleId){ getCommentsByArtcleId(artcleId,function(comments){ console.log(comments); }); }); }); });
上面的代碼看著是不是稍微清晰了一些,不過函數里面調函數的方式仍然丑陋,下面我們將介紹另一種異步調用方式Promise。
二、PromisePromise 對象是一個代理對象(代理一個值),被代理的值在Promise對象創建時可能是未知的。
它允許你為異步操作的成功和失敗分別綁定相應的處理方法(handlers)。 這讓異步方法可以像同步方法那樣返回值,
但并不是立即返回最終執行結果,而是一個能代表未來出現的結果的promise對象,如果你不了解Promise,
可以移步Promise查看詳細說明。
一個 Promise對象有且僅有三種狀態:
* pending:初始狀態,既不是成功,也不是失敗狀態 * fulfilled:意味著操作成功完成 * rejected:意味著操作失敗
pending狀態的 Promise 對象可能觸發fulfilled 狀態并傳遞一個值給相應的狀態處理方法,也可能觸發失敗狀態(rejected)并傳遞失敗信息。
當其中任一種情況出現時,Promise 對象的 then 方法綁定的處理方法(handlers )就會被調用(then方法包含兩個參數:onfulfilled 和 onrejected,
它們都是Function類型。當Promise狀態為fulfilled時,調用 then 的 onfulfilled 方法,當Promise狀態為rejected時,調用 then 的 onrejected 方法,
所以在異步操作的完成和綁定處理方法之間不存在競爭),限于樣例代碼限制,在上面的例子中我并沒有對請求異常做處理,在實際項目中讀者朋友可以自行加上處理。
因為 Promise.prototype.then和Promise.prototype.catch方法返回promise 對象, 所以它們可以被鏈式調用。
還是以上的場景為例子來看Promise實現的異步調用的代碼片段,如下所示:
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); }); }); } /*獲取評論*/ 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的代碼可以看出,后一個方法的調用總是在前一個異步調用完成后,通過前一個結果作為參數去執行下一個請求,
一步一步往后執行直到所有異步請求都執行完成,這個過程不僅代碼結構上清晰了許多,而且從編程風格上看也能看出些類同步編碼的影子。
下面介紹一個更接近同步編程的風格的異步編碼方式生成器函數Generator。
Generator即生成器,它是生成器函數(Function*)返回的一個對象,是ES2015中提供的一種異步編程解決方案;
而生成器函數有兩個特征,一是函數名前帶星號,二是內部執行語句前有關鍵字 yield,調用一個生成器函數并不會馬上執行它里面的語句,而是返回一個這個生成器的迭代器對象。當這個迭代器的 next() 方法被首次調用時,
其內的語句會執行到第一個出現yield的位置為止,yield 后緊跟迭代器要返回的值。或者如果用的是 yield*(多了個星號),則表示將執行權移交給另一個生成器函數(當前生成器暫停執行)。
next() 方法返回一個對象,這個對象包含兩個屬性:value 和 done,value 屬性表示本次 yield 表達式的返回值,done 屬性為布爾類型,表示生成器后續是否還有 yield 語句,即生成器函數是否已經執行完畢并返回。
調用 next() 方法時,如果傳入了參數,那么這個參數會作為上一條執行的 yield 語句的返回值。看一個簡單的例子:
function* genFun(){ yield "initial"; var anotherVal=yield "Hello"; yield anotherVal; } var gObj=genFun(); console.log(gObj.next());// 執行 yield "initial";,返回 "initial",{value:"initial",done:false} console.log(gObj.next());// 執行 yield "Hello",返回 "Hello",{value:"Hello",done:false console.log(gObj.next("World"));// 將"World"賦給上一條 yield "Hello"的左值anotherVal,即執行 anotherVal="World",返回"World",{value:"World",done:false} console.log(gObj.next());// 執行完畢,{value:undefined,done:true}
在上面的例子中,如果第三個 next() 的調用是在給anotherVal賦值,這樣執行之后返回的 value 即為傳入的參數,如果不傳參數,則返回的 value 為undefined,且此時的 done 還是 false,這里需要注意。
當在生成器函數中顯式 return 時,會導致生成器立即變為完成狀態,即調用 next() 方法返回的對象的 done 為 true。如果 return 后面跟了一個值,那么這個值會作為當前調用 next() 方法返回的 value 值。請看如下代碼:
function* yieldAndReturn() { yield "Y"; return "R";//顯式返回處,可以觀察到 done 也立即變為了 true yield "unreachable";// 不會被執行了 } 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的簡單概念之后,那它到底與本文核心內容有什么關聯呢?OK,咱們還是以上面的場景來使用Generator方式實現(異步調用api的幾個方法公用上面的),代碼片段如下:
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"); }); }); }); });
從上面的代碼中,咱們可以看到生成器函數 myGen里面的語句就跟平時寫同步代碼一樣類似,只是多了關鍵字 yield,這即是Generator的關鍵之處,用同步的編碼方式,處理異步邏輯。
但同時我們也看到后半部分代碼的執行跟之前的Promise幾乎一樣,一連串的 then 語句看起來還是不怎么美觀,咱們可以對它進行再一次封裝:
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();
通過封裝一個函數執行器,通過在函數內部循環調用自身來執行Generator函數內部的所有yield 語句,這樣的代碼閱讀起來就更加清晰且優雅了!
四、async/awaitasync 是ES2017引入的一種函數形式,可以使用它加在 function 前來聲明定義異步函數,使用它能給異步編程帶來極大的便利,從code形式上看就跟編寫同步代碼一樣。當一個async 函數被調用時,它返回一個 Promise 對象。
當async 函數返回一個值時,Promise 將用返回的值 resolved。 當async 函數拋出異常或某個值時,Promise將被拋出的值 rejected。
async 函數可以包含 await 表達式,帶有 await 的語句會暫停async 函數的執行并等待傳遞的Promise的解析,然后再恢復async 函數的執行并返回解析后的值。
async/await 函數的目的是簡化同步使用 Promise 的行為,并對一組 Promise 執行某些行為,就像 Promises 類似于結構化回調一樣,async/await 相當于 Generator和 Promise 的集合體。
先來看一個簡單的例子:
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(); /*執行后的輸出*/ //first output //second output //last output
從上面的代碼寫法以及async 函數內部的執行結果可以看出,這簡直就是同步調用的同步編程風格和執行順序,有沒有?其實如上所述,await 的語句會暫停async 函數的執行并等待傳遞的Promise的解析,
因此才會有console.log("last output");再最后輸出,如果在async 函數體外面在寫一個執行代碼,則會先于await 結果輸出;
咱們還是以最初的場景為例,使用async/await 的方式來實現一遍,看看代碼風格上的差異:
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); }
從代碼風格上看是不是跟Generator函數基本一樣,只是把星號去掉,前面加了async ,函數體內語句中把 yield 換成來 await;但是調用執行函數時則完全不一樣了,
Generator函數需要額外定義執行函數器,通過不斷調用 next() 來完成調用獲取結果,而async 函數自帶來執行函數器,只要調用函數即會執行,因此使用上也方便來許多。
## 總結
咱們再回顧一下文章內容,首先通過最傳統的 ajax 方式異步調用和回調函數處理;然后加入Promise對象,通過鏈式調用使代碼編寫更加有條理性;
之后又引入了新的異步編程解決方案 Generator ,其函數內部的編碼方式與同步寫法及其類似,只是 Generator 的執行權交由了另外一個函數,其執行方式仍然需要不斷的調用 next() 而略顯繁瑣;
最后引入了ES2017新標準中收錄的新函數 async,通過與await 相結合,使其異步調用的編碼實現基本跟同步編碼相差無幾,且非常易于理解和提高了代碼的維護行。
好了,到這里也該是文章結束的時候了,雖然篇幅不長,并且描述文字也不多,但還是希望閱讀之后的朋友們能有所收獲;由于寫作倉促,文中難免出現錯誤或描述不清的地方,希望朋友們能諒解,并歡迎指正。
注:文中所有的代碼都沒對異常進行處理,如果你在實際項目中使用,請記得加上異常和錯誤處理邏輯!
參考資源AJAX
Promise
Generator
Function*
async/await
阮一峰老師的ECMAScript 6 入門
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93223.html
摘要:更詳細的內容下一章開篇深入聊聊前后分離講述關于我目前在寫從零構建前后分離項目系列,修正和補充以此為準不斷更新的項目實踐地址彩蛋提前預覽下一章傳送門 開篇 : 縱觀WEB歷史演變 在校學習和幾年工作工作中不知不覺經歷了一半的 WEB 歷史演變、對近幾年的發展比較了解,結合經驗聊聊 WEB 發展歷史。 演變不易,但也是必然,因為為人始終要進步。 WEB 的發展史 一、開山鼻祖 - 石器時代...
摘要:更詳細的內容下一章開篇深入聊聊前后分離講述關于我目前在寫從零構建前后分離項目系列,修正和補充以此為準不斷更新的項目實踐地址彩蛋提前預覽下一章傳送門 開篇 : 縱觀WEB歷史演變 在校學習和幾年工作工作中不知不覺經歷了一半的 WEB 歷史演變、對近幾年的發展比較了解,結合經驗聊聊 WEB 發展歷史。 演變不易,但也是必然,因為為人始終要進步。 WEB 的發展史 一、開山鼻祖 - 石器時代...
摘要:微服務如何演變而來網關在微服務中如何發揮作用本文將以此作為話題,聊聊網關如何影響企業技術架構的演變。微服務之間相互獨立,使用者無需配置環境,直接調用即可完成開發。 互聯網技術日新月異,項目架構不斷升級優化。隨著企業微服務的興起和第三方API的發展,API網關這一作為微服務核心組件的產品也逐漸被越來越多的人認識。微服務如何演變而來?網關在微服務中如何發揮作用?本文將以此作為話題,聊聊AP...
閱讀 1048·2021-10-11 10:59
閱讀 3601·2021-09-26 09:55
閱讀 891·2019-08-30 15:55
閱讀 2650·2019-08-30 15:44
閱讀 434·2019-08-30 14:06
閱讀 680·2019-08-30 11:26
閱讀 3336·2019-08-30 10:49
閱讀 2466·2019-08-29 12:53