ECMAScript6(16):異步編程

異步編程

程序執行分爲同步和異步,若是程序每執行一步都須要等待上一步完成才能開始,此所謂同步。若是程序在執行一段代碼的同時能夠去執行另外一段代碼,等到這段代碼執行完畢再吧結果交給另外一段代碼,此所謂異步。
好比咱們須要請求一個網絡資源,因爲網速比較慢,同步編程就意味着用戶必須等待下載處理結束才能繼續操做,因此用戶體驗極爲很差;若是採用異步,下載進行中用戶繼續操做,當下載結束了,告訴用戶下載的數據,這樣體檢就提高了不少。所以異步編程十分重要。
從計算機的角度來說,js 只有一個線程,若是沒有異步編程那必定會卡死的!異步編程主要包括如下幾種:node

  • 回調函數
  • 事件監聽
  • 發佈/訂閱模型
  • Promise對象
  • ES6異步編程

回調函數 和 Promise

回調函數應該是 js 中十分基礎和簡單的部分,咱們在定義事件,在計時器等等使用過程當中都使用過:git

fs.readFile('/etc/passwd', function(err, data){
  if(err) throw err;
  console.log(data);
});

好比這裏的這個文件讀取,定義了一個回調函數,在讀取文件成功或失敗是調用,並不會馬上調用。github

如同以前在 Promise 中提到的,當我想不斷的讀入多個文件,就會遇到回調函數嵌套,書寫代碼及其的不方便,咱們稱之爲"回調地獄"。所以 ES6 中引入是了 Promise 解決這個問題。具體表現參看以前的 Promise 部分。可是 Promise 也帶來了新的問題,就是代碼冗餘很嚴重,一大堆的 then 使得回調的語義不明確。npm

協程

所謂協程就是幾個程序交替執行:A開始執行,執行一段時間後 B 執行,執行一段時間後再 A 繼續執行,如此反覆。編程

function* asyncJob(){
  //...
  var f = yield readFile(fileA);
  //...
}

經過一個 Generator 函數的 yield, 能夠將一個協程中斷,去執行另外一個協程。咱們能夠換一個角度理解 Generator 函數:它是協程在 ES6 中的具體體現。咱們能夠簡單寫一個異步任務的封裝:json

var fetch = require('node-fetch');
function* gen(){
  var url = 'http://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

var g = gen();
var result = g.next();    //返回的 value 是一個 Promise 對象
result.value.then(function(data){
  return data.json;
}).then(function(data){
  g.next(data);
});

Thunk 函數

在函數傳參數時咱們考慮這樣一個問題:api

function fun(x){
  return x + 5;
}
var a = 10;
fun(a + 10);

這個函數返回25確定沒錯,可是,咱們傳給函數 fun 的參數在編譯時到底保留 a + 10 仍是直接傳入 20?顯然前者沒有事先計算,若是函數內屢次使用這個參數,就會產生屢次計算,影響性能;然後者事先計算了,但若是函數裏不使用這個變量就白浪費了性能。採用把參數原封不動的放入一個函數(咱們將這個函數稱爲 Thunk 函數),用的使用調用該函數的方式。也就是上面的前一種方式傳值。因此上面代碼等價於:數組

function fun(x){
  return x() + 5;
}
var a = 10;
var thunk = function(){ return a + 10};
fun(thunk);

可是 js 不是這樣的!js 會把多參數函數給 Thunk 了,以減小參數:promise

var fs = require('fs');
fs.readFile(fileName, callback);
var readFileThunk = Thunk(fileName);
readFileThunk(callback);

var Thunk = function(fileName){
  return function(callback){
    return fs.readFile(fileName,callback);
  };
};

這裏任何具備回調函數的函數均可以寫成這樣的 Thunk 函數,方法以下:網絡

function Thunk(fn){
  return function(){
    var args = Array.prototype.slice.call(arguments);
    return function (callback){
      args.push(callback);
      return fn.apply(this, args);
    }
  }
}

//這樣fs.readFile(fileName, callback); 寫做以下形式

Thunk(fs.readFile)(fileName)(callback);

關於 Thunk 函數, 能夠直接使用 thunkify 模塊:

npm install thunkify

使用格式和上面的Thunk(fs.readFile)(fileName)(callback);一致,但使用過程當中須要注意,其內部加入了檢查機制,只容許 callback 被回調一次!

結合 Thunk 函數和協程,咱們能夠實現自動流程管理。以前咱們使用 Generator 時候使用 yield 關鍵字將 cpu 資源釋放,執行移出 Generator 函數。能夠怎麼移回來呢?以前咱們手動調用 Generator 返回的迭代器的 next() 方法,可這畢竟是手動的,如今咱們就利用 Thunk 函數實現一個自動的:

var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);

var gen = function*(...args){    //args 是文件路徑數組
  for(var i = 0, len = args.length; i < len; i++){
    var r = yield readFile(args[i]);
    console.log(r.toString());
  }
};

(function run(fn){
  var gen = fn();
  function next(err, data){
    if(err) throw err;
    var result =  gen.next(data);
    if(result.done) return;    //遞歸直到因此文件讀取完成
    result.value(next);    //遞歸執行
  }
  next();
})(gen);

//以後可使用 run 函數繼續讀取其餘文件操做

若是說 Thunk 能夠有現成的庫使用,那麼這個自動執行的 Generator 函數也有現成的庫可使用——co模塊(https://github.com/tj/co)。用法與上面相似,不過 co 模塊返回一個 Promise 對象。使用方式以下:

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

var gen = function*(...args){    //args 是文件路徑數組
  for(var i = 0, len = args.length; i < len; i++){
    var r = yield readFile(args[i]);
    console.log(r.toString());
  }
};
co(gen).then(function(){
  console.log("files loaded");
}).catch(function(err){
  console.log("load fail");
});

這裏須要注意的是:yield 後面只能跟一個 thunk 函數或 promise 對象。上例中第8行 yield 後面的 readFile 是一個 thunk 函數,因此可使用。
上面已經講解了 thunk 函數實現自動流程管理,下面使用 Promise 實現一下:

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

var gen = function*(){
  for(var i = 0, len = args.length; i < len; i++){
    var r = yield readFile(args[i]);
    console.log(r.toString());
  }
};

(function run(gen){
  var g = gen();

  var resolve = function(data){
    var result = g.next(data);
    if(result.done) return result.value;
    result.value.then(resolve);
  }
  g.next().value.then(function(data){
    resolve(data);
  });
  resolve();
})(gen);
//以後可使用 run 函數繼續讀取其餘文件操做

async 函數

ES7 中提出了 async 函數,可是如今已經能夠用了!可這個又是什麼呢?其實就是 Generator 函數的改進,咱們上文寫過一個這樣的 Generator 函數:

var gen = function*(){
  for(var i = 0, len = args.length; i < len; i++){
    var r = yield readFile(args[i]);
    console.log(r.toString());
  }
};

咱們把它改寫成 async 函數:

var asyncReadFiles = async function(){    //* 替換爲 async
  for(var i = 0, len = args.length; i < len; i++){
    var r = await readFile(args[i]);   //yield 替換爲 await
    console.log(r.toString());
  }
};

async 函數對 Generator 函數作了一下改進:

  • Generator 函數須要手動經過返回值的 next 方法執行,而 async 函數自帶執行器,執行方式和普通函數徹底同樣。
var result = asyncReadFiles(fileA, fileB, fileC);
  • 語義明確,async 表示異步,await 表示後續表達式須要等待觸發的異步操做結束
  • co 模塊中 yield 後面只能跟一個 thunk 函數或 promise 對象,而 await 後面能夠是任何類型(不是 Promise 對象就同步執行)
  • 返回值是一個 Promise 對象,不是 Iterator ,比 Generator 方便

咱們能夠實現這樣的一個 async 函數:

async function asyncFun(){
  //code here
}
//equal to...
function asyncFun(args){
  return fun(function*(){
    //code here...
  });
  function fun(genF){
    return new Promise(function(resolve, reject){
      var gen = genF();
      function step(nextF){
        try{
          var next = nextF();
        } catch(e) {
          return reject(e);
        }
        if(next.done){
          return resolve(next.value);
        }
        Promise.resolve(next.value).then(function(data){
          step(function(){ return gen.next(data); });
        }, function(e){
          step(function(){ return gen.throw(e); });
        });
      }
      step(function() { return gen.next(undefined); });
    });
  }
}

咱們使用 async 函數作點簡單的事情:

function timeout(ms){
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function delay(nap, ...values){
  while(1){
    try{
      await timeout(nap);
    } catch(e) {
      console.log(e);
    }
    var val = values.shift();
    if(val)
      console.log(val)
    else
      break;
  }
}
delay(600,1,2,3,4);   //每隔 600ms 輸出一個數

這裏須要注意:應該把後面跟 promise對象的 await 放在一個 try 中,防止其被 rejected。固然上面的 try 語句也能夠這樣寫:

var ms = await timeout(nap).catch((e) => console.log(e));

對於函數參數中的回調函數不建議使用,避免出現不該該的錯誤

//反例: 會獲得錯誤結果
async function fun(db){
  let docs = [{},{},{}];

  docs.forEach(async function(doc){   //ReferenceError: Invalid left-hand side in assignment
    await db.post(doc);
  });
}

//改寫, 但依然順序執行
async function fun(db){
  let docs = [{},{},{}];

  for(let doc of docs){
    await db.post(doc);
  }
}

//改寫, 併發執行
async function fun(db){
  let docs = [{},{},{}];
  let promises = docs.map((doc) => db.post(doc));
  let result = await Promise.all(promises)
  console.log(result);
}

//改寫, 併發執行
async function fun(db){
  let docs = [{},{},{}];
  let promises = docs.map((doc) => db.post(doc));
  let result = [];
  for(let promise of promises){
    result.push(await promise);
  }
  console.log(result);
}

Promise,Generator 和 async 函數比較

這裏咱們實現一個簡單的功能,能夠直觀的比較一下。實現以下功能:

在一個 DOM 元素上綁定一系列動畫,每個動畫完成纔開始下一個,若是某個動畫執行失敗,返回最後一個執行成功的動畫的返回值
  • Promise 方法
function chainAnimationPromise(ele, animations){
  var ret = null;  //存放上一個動畫的返回值
  var p = Promise.resolve();
  for(let anim of animations){
    p = p.then(function(val){
      ret = val;
      return anim(ele);
    });
  }
  return p.catch(function(e){
    /*忽略錯誤*/
  }).then(function(){
    return ret;  //返回最後一個執行成功的動畫的返回值
  });
}
  • Generator 方法
function chainAnimationGenerator(ele, animations){
  return fun(function*(){
    var ret = null;
    try{
      for(let anim of animations){
        ret = yield anim(ele);
      }
    } catch(e) {
      /*忽略錯誤*/
    }
    return ret;
  });

  function fun(genF){
    return new Promise(function(resolve, reject){
      var gen = genF();
      function step(nextF){
        try{
          var next = nextF();
        } catch(e) {
          return reject(e);
        }
        if(next.done){
          return resolve(next.value);
        }
        Promise.resolve(next.value).then(function(data){
          step(function(){ return gen.next(data); });
        }, function(e){
          step(function(){ return gen.throw(e); });
        });
      }
      step(function() { return gen.next(undefined); });
    });
  }
}
  • async 函數方法
async function chainAnimationAsync(ele, animations){
  var ret = null;
  try{
    for(let anim of animations){
      ret = await anim(elem);
    }
  } catch(e){
    /*忽略錯誤*/
  }
  return ret;
}

一個經典題

console.log(0);

setTimeout(function(){
  console.log(1)
},0);
setTimeout(function(){
  console.log(2);
},1000);

var pro = new Promise(function(resolve, reject){
  console.log(3);
  resolve();
}).then(resolve => console.log(4));

console.log(5);

setTimeout(function(){
  console.log(6)
},0);

pro.then(resolve => console.log(7));

var pro2 = new Promise(function(resolve, reject){
  console.log(8);
  resolve(10);
}).then(resolve => console.log(11))
  .then(resolve => console.log(12))
  .then(resolve => console.log(13));

console.log(14);

// 0 3 5 8 14 4 11 7 12 13 1 6 2
相關文章
相關標籤/搜索