JavaScript非同步程式設計–Generator函式、async、await

雲棲大講堂發表於2018-06-19

JavaScript 非同步程式設計–Generator函式

Generator(生成器)是ES6標準引入的新的資料型別,其最大的特點就是可以交出函式的執行的控制權,即:通過yield關鍵字標明需要暫停的語句,執行時遇到yield語句則返回該語句執行結果,等到呼叫next函式時(也就是說可以通過控制呼叫next函式的時機達到控制generator執行的目的)重新回到暫停的地方往下執行,直至generator執行結束。

基本結構

以下是一個典型的generator函式的示例,以”*”標明為generator。

function* gen(x){
  var y = yield x + 2;
  console.log(y);         // undefine
  var yy = yield x + 3;
  console.log(yy);        // 6
  return y;               // 沒啥用
}

var g = gen(1);
var r1 = g.next();   
console.log(r1);     // { value: 3, done: false }
var r2 = g.next();
console.log(r2);     // { value: 4, done: false }
var r3 = g.next(6);  
console.log(r3);     // { value: undefined, done: true }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

上述程式碼中,呼叫gen函式,會返回一個內部指標(即遍歷器)g,這是Generator函式和一般函式不同的地方,呼叫它不會返回結果,而是一個指標物件。呼叫指標g的next方法,會移動內部指標,指向第一個遇到的yield語句,上例就是執行到x+2為止。換言之,next方法的作用是分階段執行Generator函式。每次呼叫next方法,會返回一個物件{value: any, done: boolean},表示當前階段的資訊,其中value屬性是yield語句後面表示式的值;done屬性是一個布林值,表示Generator函式是否執行完畢,即是否還有下一個階段。next方法輸入引數即為yield語句的值,因此生成器gen中y為第二次呼叫next的輸入引數”undefine”,yy為第三次呼叫next的輸入引數6。 
總結:

  • generator返回遍歷器,可遍歷所有yield
  • yield將生成器內部程式碼分割成n段,通過呼叫next方法一段一段執行
  • next方法返回的value屬性向外輸出資料,next方法通過實參向生成器內部輸入資料

思考

如果yield標記的語句是個非同步執行的函式func,然後在func回撥中呼叫next,則實現了等待func非同步執行的效果—–“func要做的事做完了,才會往下走”,這樣就避免了多重回撥巢狀(callback hell,回撥地獄, 如下所示)

func1(function (res) {
  // do something
  func2(function (res2) {
    // do something
    func3(function (res3) {
      // do something
    })
  })
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Thunk函式

什麼是thunk函式?詳見Thunk 函式的含義和用法 
簡單理解:thunk函式利用閉包可以快取狀態的特性,先傳引數,再執行函式,這樣就將函式呼叫過程分成了兩步。以下thunkify函式可將普通非同步函式轉化為thunk函式。

function thunkify(fn) {
  assert(`function` == typeof fn, `function required`);
  return function () {
   // arguments為非同步函式的引數(不包含回撥函式引數)
    var args = new Array(arguments.length);
    var ctx = this;
    for (var i = 0; i < args.length; ++i) {
      args[i] = arguments[i];
    }
    // done為非同步函式的回撥函式(callback)
    return function (done) {
      var called;

      args.push(function () {
        if (called) return;
        called = true;
        done.apply(null, arguments);
      });

      try {
        // 到這裡,非同步函式才真正被呼叫
        fn.apply(ctx, args);
      } catch (err) {
        done(err);
      }
    }
  }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

Generator執行控制

thunk函式有什麼用呢?其一個典型應用就是用於控制generator的執行,見如下示例是為了實現多個檔案的順序讀取,實現了同步寫法,避免回撥巢狀。

const fs = require(`fs`);
const readFileThunk = thunkify(fs.readFile);

var generator = function* () {
  for (var i = 0; i < arguments.length; i++) {
    console.log(`file: %s`, arguments[i]);
    // yield 返回thunkify最內部的 function (done){}  函式,此處傳入了readFile函式引數,但並沒有執行
    var r1 = yield readFileThunk(arguments[i], `utf8`);
    console.log(`r1: %s`, r1);
  }
}

function rungenerator(generator) {
  //檔名稱
  var args = [];
  for (var i = 1; i < arguments.length; i++) {
    args.push(arguments[i]);
  }
  //生成generator例項
  var gen = generator.apply(null, args);
  function done(err, data) {
    //執行跳到 generator中去
    var result = gen.next(data);
    if (result.done) { return; }
    // 此處才是真正的呼叫readFile函式開始讀取檔案內容,done作為回撥, 檔案讀取完成後,執行gen.next(),
    // 告訴generator繼續執行,並通過yield返回下一個thunk函式,開始讀取下一個檔案,從而達到順序執行的效果
    result.value(done);
  }
  next();
}
rungenerator(generator, `123.txt`, `1234.txt`, `he.txt`)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

上述程式碼中,rungenerator是一個執行generator的函式,具有通用性,封裝下就成了co庫—-generator函式自動執行的解決方案。

var fs = require(`fs`);
var co = require(`co`);
var thunkify = require(`thunkify`);
var readFile = thunkify(fs.readFile);

co(function*(){
    var files=[`./text1.txt`, `./text2.txt`, `./text3.txt`];

    var p1 = yield readFile(files[0]);
    console.log(files[0] + ` ->` + p1);

    var p2 = yield readFile(files[1]);
    console.log(files[1] + ` ->` + p2);

    var p3 = yield readFile(files[2]);
    console.log(files[2] + ` ->` + p3);

    return `done`;
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

看起來舒服多了。。。

async和await

async和await是ES7中的新語法,實際上是generator函式的語法糖 
Nodejs最新版已經支援了,瀏覽器不支援的,可以用Babel轉下。

var fs = require(`fs`);

var readFile = function (fileName){
    return new Promise(function (resolve, reject){
        fs.readFile(fileName, function(error, data){
            if (error){
                reject(error);
            }
            else {
                resolve(data);
            }
        });
    });
};

var asyncReadFile = async function (){
    var f1 = await readFile(`./text1.txt`);
    var f2 = await readFile(`./text2.txt`);
    console.log(f1.toString());
    console.log(f2.toString());
};

asyncReadFile();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

致謝

主要學習了nullcc的部落格《深入解析Javascript非同步程式設計》、無腦的部落格《node的 thunkify模組說明》、阮一峰老師的部落格《Thunk 函式的含義和用法》,由衷地感謝以上作者!!!!!

原文釋出時間:2018-6-19

原文作者:陽光七十米

本文來源csdn部落格如需轉載請緊急聯絡作者


相關文章