[前端工坊]淺談Web編程中的異步調用的發展演變

文章來自微信公衆號:前端工坊(fe_workshop),不按期更新有趣、好玩的前端相關原創技術文章。 若是喜歡,請關注公衆號:前端工坊
版權歸微信公衆號全部,轉載請註明出處。
做者:京東金融-移動研發部-前端開發工程師 張恆

做爲Web工程師,相信你們在開發項目的過程當中,都存在與服務器端的通訊,如登陸驗證、獲取用戶信息、獲取應用數據等都須要經過調用後端的API來進行操做,而實現這一操做的正是異步調用;
這篇文章旨在經過一些異步調用的概念和相應的代碼演示,儘可能詳細地介紹異步調用的實現、各類異步編程的使用方式和區別,以及他們的發展演變;前端

1、AJAX

在Web應用的開發過程當中,爲了實現良好的交互體驗,咱們都會使用 ajax 的方式與後端通訊,實現無刷新數據提取和快速展示,極大地提高了用戶體驗;
ajax 的全稱是Asynchronous JavaScript and XML,Asynchronous 即異步,它有別於傳統web開發中採用的同步的方式。
ajax 的原理簡單來講就是經過 XmlHttpRequest 對象來向服務器發異步請求,從服務器得到數據,而後用JavaScript來操做DOM而更新頁面,這其中 XMLHttpRequestajax 的核心機制,
經過這種異步技術,JavaScript能夠及時向服務器提出請求和處理響應,而不阻塞用戶,從而達到無刷新頁面的效果。es6

相信廣大的Web工程師們對此已經耳熟能詳,我就不在這裏細講了,若是你是剛入行前端而且不瞭解此概念,能夠移步ajax
可是必須提到的是XmlHttpRequest 對象有一個屬性 onreadystatechange 用於當異步請求狀態改變時觸發事件執行後續動做,
這也就是本文要講的異步調與回調處理;對於單個的異步請求及其回調結果處理實際上沒太大問題,但當碰到某些複雜場景,須要屢次異步調用接口,而且後一個的調用須要前一個異步調用的返回結果做爲參數時,
因爲是異步形式,不能像同步編程那樣編寫代碼,我們就不得不嵌套編寫,而當嵌套層過多就會出現難以閱讀和維護代碼。web

舉個例子,在一個Web App中,須要獲取用戶的某篇博客的全部跟帖,這時咱們就須要有以下的APIs;ajax

一、獲取用戶會話的token(也多是一開始進入博客經過登陸返回的)編程

{ 
    status:'success',  
    data:{
        token: '******'
    }
}

二、經過token獲取用戶詳細信息後端

{
    status:'success',  
    data:{
        userInfo: {
            id: 10001,
            name:'test',
            email:'test@test.com'
        }
    }
}

三、經過userId獲取用戶文章列表api

{
  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'
    },
  ]
}

四、經過博客id獲取全部用戶評論promise

{
 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。

2、Promise

Promise 對象是一個代理對象(代理一個值),被代理的值在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。

3、Generator

Generator即生成器,它是生成器函數(Function*)返回的一個對象,是ES2015中提供的一種異步編程解決方案;
而生成器函數有兩個特徵,一是函數名前帶星號,二是內部執行語句前有關鍵字 yield,調用一個生成器函數並不會立刻執行它裏面的語句,而是返回一個這個生成器的迭代器對象。當這個迭代器的 next() 方法被首次調用時,
其內的語句會執行到第一個出現yield的位置爲止,yield 後緊跟迭代器要返回的值。或者若是用的是 yield*(多了個星號),則表示將執行權移交給另外一個生成器函數(當前生成器暫停執行)。
next() 方法返回一個對象,這個對象包含兩個屬性:valuedonevalue 屬性表示本次 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() 方法返回的對象的 donetrue。若是 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 語句,這樣的代碼閱讀起來就更加清晰且優雅了!

4、async/await

async 是ES2017引入的一種函數形式,可使用它加在 function 前來聲明定義異步函數,使用它能給異步編程帶來極大的便利,從code形式上看就跟編寫同步代碼同樣。當一個async 函數被調用時,它返回一個 Promise 對象。
async 函數返回一個值時,Promise 將用返回的值 resolved。 當async 函數拋出異常或某個值時,Promise將被拋出的值 rejected
async 函數能夠包含 await 表達式,帶有 await 的語句會暫停async 函數的執行並等待傳遞的Promise的解析,而後再恢復async 函數的執行並返回解析後的值。
async/await 函數的目的是簡化同步使用 Promise 的行爲,並對一組 Promise 執行某些行爲,就像 Promises 相似於結構化回調同樣,async/await 至關於 GeneratorPromise 的集合體。

先來看一個簡單的例子:

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 相結合,使其異步調用的編碼實現基本跟同步編碼相差無幾,且很是易於理解和提升了代碼的維護行。
好了,到這裏也該是文章結束的時候了,雖然篇幅不長,而且描述文字也很少,但仍是但願閱讀以後的朋友們能有所收穫;因爲寫做倉促,文中不免出現錯誤或描述不清的地方,但願朋友們能諒解,並歡迎指正。

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

參考資源

圖片描述

相關文章
相關標籤/搜索