【雜談】淺談Web編程中的異步調用的發展演變

作為Web工程師,相信大家在開發項目的過程中,都存在與服務器端的通信,如登錄驗證、獲取用戶信息、獲取應用數據等都需要通過調用後端的API來進行操作,而實現這一操作的正是異步調用。

這篇文章旨在通過一些異步調用的概念和相應的代碼演示,儘量詳細地介紹異步調用的實現、各種異步編程的使用方式和區別,以及他們的發展演變。

【雜談】淺談Web編程中的異步調用的發展演變

一、AJAX

在Web應用的開發過程中,為了實現良好的交互體驗,我們都會使用 ajax 的方式與後端通信,實現無刷新數據提取和快速展現,極大地提升了用戶體驗;

ajax 的全稱是Asynchronous JavaScript and XML,Asynchronous 即異步,它有別於傳統web開發中採用的同步的方式。

ajax 的原理簡單來說就是通過 XmlHttpRequest 對象來向服務器發異步請求,從服務器獲得數據,然後用JavaScript來操作DOM而更新頁面,這其中 XMLHttpRequest 是 ajax 的核心機制,通過這種異步技術,JavaScript可以及時向服務器提出請求和處理響應,而不阻塞用戶,從而達到無刷新頁面的效果。

相信廣大的Web工程師們對此已經耳熟能詳,我就不在這裡細講了,如果你是剛入行前端並且不瞭解此概念,可以移步 ajax https://developer.mozilla.org/en-US/docs/Web/Guide/AJAX

但是必須提到的是 XmlHttpRequest 對象有一個屬性 onreadystatechange 用於當異步請求狀態改變時觸發事件執行後續動作,這也就是本文要講的異步調與回調處理;對於單個的異步請求及其回調結果處理實際上沒太大問題,但當碰到某些複雜場景,需要多次異步調用接口,並且後一個的調用需要前一個異步調用的返回結果作為參數時,由於是異步形式,不能像同步編程那樣編寫代碼,咱們就不得不嵌套編寫,而當嵌套層過多就會出現難以閱讀和維護代碼。

舉個例子,在一個Web App中,需要獲取用戶的某篇博客的所有跟帖,這時我們就需要有如下的APIs;

1、獲取用戶會話的token(也可能是一開始進入博客通過登錄返回的)

【雜談】淺談Web編程中的異步調用的發展演變

2、通過token獲取用戶詳細信息

【雜談】淺談Web編程中的異步調用的發展演變

3、通過userId獲取用戶文章列表

【雜談】淺談Web編程中的異步調用的發展演變

4、通過博客id獲取所有用戶評論

【雜談】淺談Web編程中的異步調用的發展演變

接下來我們就通過code去實現這樣一個邏輯,首定義一個異步調用的公用方法:

【雜談】淺談Web編程中的異步調用的發展演變

使用ajax實現獲取用戶評論的邏輯則如下所示:

【雜談】淺談Web編程中的異步調用的發展演變

OK,上面的代碼是不是讓人頭暈,如果碰到某些更復雜的邏輯,就會出現更多的嵌套回調,這即稱為'回調地獄(callback hell)'。

我們可以稍加重構以提高可閱讀性:

【雜談】淺談Web編程中的異步調用的發展演變

上面的代碼看著是不是稍微清晰了一些,不過函數里面調函數的方式仍然醜陋,下面我們將介紹另一種異步調用方式Promise。

【雜談】淺談Web編程中的異步調用的發展演變

二、Promise

Promise 對象是一個代理對象(代理一個值),被代理的值在Promise對象創建時可能是未知的。

它允許你為異步操作的成功和失敗分別綁定相應的處理方法(handlers)。 這讓異步方法可以像同步方法那樣返回值,但並不是立即返回最終執行結果,而是一個能代表未來出現的結果的promise對象,如果你不瞭解Promise,可以移步 Promisehttps://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/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實現的異步調用的代碼片段,如下所示:

【雜談】淺談Web編程中的異步調用的發展演變

從上面獲取comments的代碼可以看出,後一個方法的調用總是在前一個異步調用完成後,通過前一個結果作為參數去執行下一個請求,一步一步往後執行直到所有異步請求都執行完成,這個過程不僅代碼結構上清晰了許多,而且從編程風格上看也能看出些類同步編碼的影子。

下面介紹一個更接近同步編程的風格的異步編碼方式生成器函數Generator。

三、Generator

Generator即生成器,它是生成器函數(Function*)返回的一個對象,是ES2015中提供的一種異步編程解決方案;而生成器函數有兩個特徵,一是函數名前帶星號,二是內部執行語句前有關鍵字 yield,調用一個生成器函數並不會馬上執行它裡面的語句,而是返回一個這個生成器的迭代器對象。當這個迭代器的 next() 方法被首次調用時,其內的語句會執行到第一個出現yield的位置為止,yield 後緊跟迭代器要返回的值。或者如果用的是 yield*(多了個星號),則表示將執行權移交給另一個生成器函數(當前生成器暫停執行)。

next() 方法返回一個對象,這個對象包含兩個屬性:value 和 done,value 屬性表示本次 yield 表達式的返回值,done 屬性為布爾類型,表示生成器後續是否還有 yield 語句,即生成器函數是否已經執行完畢並返回。

調用 next() 方法時,如果傳入了參數,那麼這個參數會作為上一條執行的 yield 語句的返回值。看一個簡單的例子:

【雜談】淺談Web編程中的異步調用的發展演變

在上面的例子中,如果第三個 next() 的調用是在給anotherVal賦值,這樣執行之後返回的 value 即為傳入的參數,如果不傳參數,則返回的 value 為undefined,且此時的 done 還是 false,這裡需要注意。

當在生成器函數中顯式 return 時,會導致生成器立即變為完成狀態,即調用 next() 方法返回的對象的done 為 true。如果 return 後面跟了一個值,那麼這個值會作為當前調用 next() 方法返回的 value 值。請看如下代碼:

【雜談】淺談Web編程中的異步調用的發展演變

瞭解了Generator的簡單概念之後,那它到底與本文核心內容有什麼關聯呢?OK,咱們還是以上面的場景來使用Generator方式實現(異步調用api的幾個方法共用上面的),代碼片段如下:

【雜談】淺談Web編程中的異步調用的發展演變

從上面的代碼中,咱們可以看到生成器函數 myGen裡面的語句就跟平時寫同步代碼一樣類似,只是多了關鍵字 yield,這即是Generator的關鍵之處,用同步的編碼方式,處理異步邏輯。

但同時我們也看到後半部分代碼的執行跟之前的Promise幾乎一樣,一連串的 then 語句看起來還是不怎麼美觀,咱們可以對它進行再一次封裝:

【雜談】淺談Web編程中的異步調用的發展演變

通過封裝一個函數執行器,通過在函數內部循環調用自身來執行Generator函數內部的所有yield 語句,這樣的代碼閱讀起來就更加清晰且優雅了!

四、async/await

當async 函數返回一個值時,Promise 將用返回的值 resolved。 當async 函數拋出異常或某個值時,Promise將被拋出的值 rejected。

async 函數可以包含 await 表達式,帶有 await 的語句會暫停async 函數的執行並等待傳遞的Promise的解析,然後再恢復async 函數的執行並返回解析後的值。

async/await 函數的目的是簡化同步使用 Promise 的行為,並對一組 Promise 執行某些行為,就像Promises 類似於結構化回調一樣,async/await 相當於 Generator和 Promise 的集合體。

先來看一個簡單的例子:

【雜談】淺談Web編程中的異步調用的發展演變

從上面的代碼寫法以及async 函數內部的執行結果可以看出,這簡直就是同步調用的同步編程風格和執行順序,有沒有?其實如上所述,await 的語句會暫停async 函數的執行並等待傳遞的Promise的解析,因此才會有console.log('last output');再最後輸出,如果在async 函數體外面在寫一個執行代碼,則會先於await 結果輸出;

咱們還是以最初的場景為例,使用async/await 的方式來實現一遍,看看代碼風格上的差異:

【雜談】淺談Web編程中的異步調用的發展演變

從代碼風格上看是不是跟Generator函數基本一樣,只是把星號去掉,前面加了async ,函數體內語句中把yield 換成來 await;但是調用執行函數時則完全不一樣了,

Generator函數需要額外定義執行函數器,通過不斷調用 next() 來完成調用獲取結果,而async 函數自帶來執行函數器,只要調用函數即會執行,因此使用上也方便來許多。

總結

咱們再回顧一下文章內容,首先通過最傳統的 ajax 方式異步調用和回調函數處理;然後加入Promise對象,通過鏈式調用使代碼編寫更加有條理性;

之後又引入了新的異步編程解決方案 Generator ,其函數內部的編碼方式與同步寫法及其類似,只是Generator 的執行權交由了另外一個函數,其執行方式仍然需要不斷的調用 next() 而略顯繁瑣;

最後引入了ES2017新標準中收錄的新函數 async,通過與await 相結合,使其異步調用的編碼實現基本跟同步編碼相差無幾,且非常易於理解和提高了代碼的維護性。

好了,到這裡也該是文章結束的時候了,雖然篇幅不長,並且描述文字也不多,但還是希望閱讀之後的朋友們能有所收穫;由於寫作倉促,文中難免出現錯誤或描述不清的地方,希望朋友們能諒解,並歡迎指正。

注:文中所有的代碼都沒對異常進行處理,如果你在實際項目中使用,請記得加上異常和錯誤處理邏輯!

參考資源

  1. AJAX https://developer.mozilla.org/zh-CN/docs/Web/Guide/AJAX

  2. Promise https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise

  3. Generator https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Generator

  4. Function* https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/function*

  5. async/await https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function

  6. 阮一峰老師的ECMAScript 6 入門 http://es6.ruanyifeng.com/


關注“編碼之道”,及時查看最新內容……


分享到:


相關文章: