JavaScript 異步編程--async、await實現原理

JavaScript 異步編程–await實現原理

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 }

上述代碼中,調用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
})
})
})

##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);
      }
    }
  }
};

##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);
  }
  done();
}
rungenerator(generator, '123.txt', '1234.txt', 'he.txt')

上述代碼中,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';
});

看起來舒服多了。。。

##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();

##致謝
主要學習了nullcc的博客《深入解析Javascript異步編程》、無腦的博客《node的 thunkify模塊說明》、阮一峯老師的博客《Thunk 函數的含義和用法》,由衷地感謝以上作者!!!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章