js異步解決方案 --- 回調函數 vs promise vs generater/yield vs async/await

javascript -- 深度解析異步解決方案

高級語言層出不窮, 然而惟 js 鶴立雞羣, 這要說道js的設計理念, js天生爲異步而生, 正如佈道者樸靈在 node深刻淺出--(有興趣的能夠讀一下, 頗有意思^_^) , 異步很早就存在於操做系統的底層, 意外的是,在絕大多數高級編程語言中,異步並很少見,疑似被屏蔽了一搬. 形成這個現象的緣由或許使人驚訝, 程序員不太適合經過異步來實現進行程序設計 ^_^.

異步的理念是很好的, 然而在程序員編程過程當中確實會出現一些問題, 並非這種理念不容以讓人接受, 而是當有大量的異步操做時會讓你的代碼可讀性下降, 其中回調函數異步編程容易產生毀掉陷阱, 即 callback hell--(不要急, 後面會詳細講解)javascript

然而 js 社區從爲中止其腳步, 最新的 ES7 所推出的 async/await 終極異步解決方案, 說終很可能有所不嚴禁, 然而它確實已經徹底將原來經過模塊侵入式的異步編程解脫出來, 可讓程序員以接近傳統意義上的函數調用實現異步編程, 這是 js 里程碑式變革中極其重要的一部分.java

Javascript異步編程解決方案歷史與方法

ES 6之前:node

  • 回調函數
    回調函數是最原始的異步編程方案, 上篇文章已經講述, 這裏再也不累贅, 這裏給出傳送門 回調函數之美 然而若是業務邏輯過多時, 回調函數會產生深層嵌套, 對程序員極不友好,
    以下代碼所示有一個業務邏輯, 須要對a, b, c三個文件一次讀取程序員

    var fs = require('fs');
        
        fs.readFile('./a.txt', function(err1, data1) {
             fs.readFile('./b.txt', function(err2, data2) {
                  fs.writeFile('./ab.txt', data1 + data2, function(err) {
                       console.log('read and write done!');
                  });
             });
        });

    三個異步函數嵌套看起來挺簡單的, 這裏知識簡單假設, 拋磚引玉, 若是有5個,10個甚至更多的異步函數要順序執行,那要嵌套(你們都不喜歡身材橫着長吧哈哈)說實話至關恐怖,代碼會變得異常難讀,難調試,難維護。這就是所謂的回調地獄或者callback hell。正是爲了解決這個問題,纔有了後面兩節要講的內容,用promise或generator進行異步流程管理。異步流程管理說白了就是爲了解決回調地獄的問題。因此說任何事情都有兩面性,異步編程有它獨特的優點,卻也同時遇到了同步編程根本不會有的代碼組織難題。shell

  • 事件監聽(事件發佈/訂閱)
    事件監聽模式是一種普遍應用於異步編程的模式, 是回調函數的事件化,即發佈/訂閱模式,express

    var util = require('util');
        var events = require('events');
        
        function Stream() {
          events.EventEmitter.call(this);
        }
        util.inherits(Stream, events.EventEmitter)
        let got = new Stream();
        got.on("done", function (params) {
          console.log(params);
        });
        got.on("done", function (params) {
          console.log('QWER');
        });
        got.emit("done", 'diyige');
        console.log('-----------------');
        
        var emitter = new events.EventEmitter();
        
        emitter.on("done", function (params) {
          console.log(params);
        });
        emitter.on("done", function (params) {
          console.log('ZXCV');
        });
        emitter.emit("done", 'dierge');
        
        // diyige
        // QWER
        // dierge
        // ZXCV
  • Promise對象
    Promise 是異步編程的一種解決方案,它是比傳統的解決方案——回調函數和事件——更合理和更強大, 它的目的是替換之前回調函數的比不編程方案, 也是後續介紹的異步解決方案的基礎, 它由社區最先提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象, 如今的 js庫幾乎都支持這種異步方案編程

    promise對象有如下特色segmentfault

    • 對象的狀態不受外界影響。Promise對象表明一個異步操做,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是「承諾」,表示其餘手段沒法改變
    • 一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。Promise對象的狀態改變,只有兩種可能:從pending變爲fulfilled和從pending變爲rejected。只要這兩種狀況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱爲 resolved(已定型)。若是改變已經發生了,你再對Promise對象添加回調函數,也會當即獲得這個結果。這與事件(Event)徹底不一樣,事件的特色是,若是你錯過了它,再去監聽,是得不到結果的。

圖片描述

下面爲單個promise對象應用方法
var promise = new Promise(function(resolve,reject){
      // ... some code
      if(/* 異步操做成功 */){
        resolve(value);
      }else{
        reject(error);
      }
    });

一般用promise 的時候咱們通常把它相應的業務包裝起來下圖所示模擬了一個讀取文件的異步
promise 函數,後端

var readFile =  function (params) {
      return new Promise(function(resolve, reject){
    
        setTimeout(function(){
            resolve(params);
        }, 2000);
      });
    }
    
    readFile('file1').then(function (data) {
      console.log(data);
      return readFile('file2')
    }).then(function (data) {
      console.log(data);
      return readFile('file3')
    }).then(function (data) {
      console.log(data);
      return readFile('file4')
    }).then(function (data) {
      console.log(data);
      return readFile('file5')
    }).then(function (data) {
      console.log(data);
    })
    //file1
    //file2
    //file3
    //file4
    //file5
  • 流程控制庫
    還有一種須要手工調用採可以處理後續任務的, 在這裏只簡單介紹一種, 咱們稱之爲尾觸發, 經常使用的關鍵字爲 next , 爲何要講到它是由於它是 node 神級框架 express中採用的模式, 這裏可能要涉及一些後端node的內容
    在 node 搭建服務器時須要面向 切面編程 ,這就須要各類各樣的中間件promise

    var app = connect();
        // Middleware
        app.use(connect.staticCache());
        app.use(connect.static(__dirname + '/public'));
        app.use(connect.cookieParser());
        app.use(connect.session());
        app.use(connect.query());
        app.use(connect.bodyParser());
        app.use(connect.csrf());
        app.listen(3001);

    在經過 use() 方法監聽好一系列中間件後, 監聽端口上的請求, 中間件採用的是尾觸發的機制, 下面是個一個簡單的中間件

    function (req, res, next) {
        // express中間件
        }

    每一箇中間件傳遞請求對象, 響應對象, 和尾觸發函數, 經過隊列造成一個處理流, 以下圖
    圖片描述
    中間件機制使得在處理網絡請求時, 能夠像面向切面編程同樣進行過濾, 驗證, 日誌等功能.

ES 6:

  • Generator函數(協程coroutine)
    Generator 函數有多種理解角度。語法上,Generator 函數是一個狀態機,封裝了多個內部狀態。
    執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,仍是一個遍歷器對象生成函數。執行函數後返回的是一個遍歷器對象,能夠依次遍歷 Generator 函數內部的每個狀態。

    function* helloWorldGenerator() {
              yield 'hello';
              yield 'world';
              return 'ending';
        }
        var hw = helloWorldGenerator();
        hw.next()
        // { value: 'hello', done: false }
        
        hw.next()
        // { value: 'world', done: false }
        
        hw.next()
        // { value: 'ending', done: true }
        
        hw.next()
        // { value: undefined, done: true }

    下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式(或return語句)爲止。換言之,Generator 函數是分段執行的,yield表達式是暫停執行的標記,而next方法能夠恢復執行。

  • 基於 Promise 對象的自動執行
    generater/yield函數還沒法真正解決異步方案的問題, 須要配合額外的執行模塊 如 TJ Holowaychuk 的 co 模塊, 在這裏用promise模塊進行generater函數的自動執行;

    var fs = require('fs');
        
        var readFile = function (fileName){
          return new Promise(function (resolve, reject){
            fs.readFile(fileName, function(error, data){
              if (error) return reject(error);
              resolve(data);
            });
          });
        };
        
        var gen = function* (){
          var f1 = yield readFile('/etc/fstab');
          var f2 = yield readFile('/etc/shells');
          console.log(f1.toString());
          console.log(f2.toString());
        };
    /*****************************************
           
            var g = gen();
            g.next().value.then(function(data){
              g.next(data).value.then(function(data){
                g.next(data);
              });
            });
    *****************************************/
        // 自動執行函數        
        function run(gen){
          var g = gen();
        
          function next(data){
            var result = g.next(data);
            if (result.done) return result.value;
            result.value.then(function(data){
              next(data);
            });
          }
        
          next();
        }
        run(gen);

ES 7:

  • async/await
    終於來到了咱們求之不得的的"終極"異步解決方案, 或許你有些失望, 固然這種失望是async/await 僅僅是語法糖, async/await 就是 generater/yield/promise + 自動執行模塊的封裝.相對於前輩 async 函數能夠自動執行 而且 await 關鍵字後面則只能帶promise隊形--這裏注意 await 後面支持其餘數據類型, 可是底層也會將其轉化爲promise對象

    async函數對 Generator 函數的改進,體如今如下四點。

    • 內置執行器。
      Generator 函數的執行必須靠執行器,因此纔有了co模塊,而async函數自帶執行器,這徹底不像 Generator 函數,須要調用next方法,或者用co模塊,才能真正執行,獲得最後結果。
    • 更好的語義。
      async和await,比起星號和yield,語義更清楚了。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。
    • 更廣的適用性。
      co模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,而async函數的await命令後面,能夠是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操做)
    • 返回值是 Promise。
      async函數的返回值是 Promise 對象,這比 Generator 函數的返回值是 Iterator 對象方便多了。你能夠用then方法指定下一步的操做。進一步說,async函數徹底能夠看做多個異步操做,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。

      function name(params) {
            return new Promise(function (resolve, reject) {
              setTimeout(() => {
                resolve(params)
              }, 3000);
            });
          }
          async function myf () {
            let gf = await name('xiaohua');
            let gf2 = await name('xiaohong');
            return gf + gf2 
          }
          async function myf3 (params) {
            let aaa = await myf();
            return aaa;
          }
          myf3().then(function (params) {
            console.log(params);
          });
          
          // xiaohuaxiaohong
async/await 對前者的generater/yield 進行了高度的封裝配合那些支持 promise 實現的庫能夠完美的像普通函數同樣調用, 而且async函數與其餘async函數也能夠完美無縫鏈接, 堪稱終極方案

koa2已經支持 async/await 可是最新的 express框架依然沒有支持這種寫法, async/await 是大勢所趨, 或許不久的未來 express也會支持它, 咱們拭目以待

相關文章
相關標籤/搜索