JS異步編程的幾種方式及區別

前言

衆所周知Javascript是「單線程」語言,在實際開發中咱們又不得不面臨異步邏輯的處理,這時候異步編程就變得十分必要。所謂異步,就是指在執行一件任務,這件任務分A、B兩個階段,執行完A階段後,須要去作另一個任務獲得結果後才能執行B階段。異步編程有如下幾種經常使用方式:callbackPromiseGeneratorasynchtml

callback函數

callback函數是指經過函數傳參傳遞到其餘執行代碼的,某一塊可執行代碼的引用,被主函數調用後又回到主函數,以下例:程序員

function add(a, b, callback){
    var num = a + b;
    callback(num)
}
add(1, 2, function(num){
    console.log(num); # 3
    # ...
})
複製代碼

若是是有個任務隊列,裏面包含多個任務的話,那就須要層層嵌套了es6

var readFile = require('fs-readfile-promise'); # 讀取文件函數
readFile(fileA, function(data) {
    readFile(fileB, function(data) {
        # ...
    })
})
複製代碼

如上若是我存在n個任務,那須要層層嵌套n層,這樣代碼顯得很是冗餘龐雜而且耦合度很高,修改其中某一個函數的話,會影響上下函數代碼塊的邏輯。這種狀況被稱爲「回調地獄」(callback hell)編程

Promise

Promise是咱們經常使用來解決異步回調問題的方法。容許將回調函數的嵌套,改成鏈式調用。以上多個任務的話,能夠改形成以下例子:json

function add(a, b){
    return new Promise((resolve, reject) => {
        var result = a+b;
        resolve(result);
    })
}
add(10, 20).then(res => {
    return add(res, 20) # res = 30
}).then(res => {
    return add(res, 20) # res = 50
}).then(res => {
    // ...
}).catch(err => {
    // 錯誤處理
})
複製代碼

add函數執行後會返回一個Promise,它的結果會進入then方法中,第一個參數是Promiseresolve結果,第二個參數(可選)是Promisereject結果。咱們能夠把回調後的邏輯在then方法中寫,這樣的鏈式寫法有效的將各個事件的回調處理分割開來,使得代碼結構更加清晰。另外咱們能夠在catch中處理報錯。數組

若是是咱們的異步請求不是按照順序A->B->C->D這種,而是[A,B,C]->D,先並行執行A、B、C完而後在執行D,咱們能夠用Promise.all();promise

# 生成一個Promise對象的數組
const promises = [2, 3, 5].map(function (id) {
  return getJSON('/post/' + id + ".json"); # getJSON 是返回被Promise包裝的數據請求函數
});

Promise.all(promises).then(function (posts) {
  # promises裏面裝了三個Promise
  # posts返回的是一個數組,對應三個Promise的返回數據
  # 在這能夠執行D任務
}).then(res => {
    //...
}).catch(function(reason){
    //...
});
複製代碼

可是Promise的代碼仍是有些多餘的代碼,好比被Promise包裝的函數有一堆new Promisethencatchbash

Generator函數

Generator函數是ES6提供的一種異步編程解決方案,由每執行一次函數返回的是一個遍歷器對象,返回的對象能夠依次遍歷Generator裏面的每一個狀態,咱們須要用遍歷器對象的next方法來執行函數。併發

先來個例子:異步

function* foo() {
    yield 'stepone';
    yield 'steptwo';
    return 'stepthree';
}
var _foo = foo();
_foo.next(); #{value: 'stepone', done: false}
_foo.next(); #{value: 'stepone', done: false}
_foo.next(); #{value: 'stepone', done: false}
複製代碼

Generator有三個特徵:函數命名時function後面須要加*;函數內部有yield;外部執行須要調用next方法。每一個yield會將跟在她後面的值包裹成一個對象的返回,返回的對象中包括返回值和函數運行狀態,直到return,返回donetrue

若是每次運行Generator函數咱們都須要用next的話,你那就太麻煩了,咱們須要一個能夠自動執行器。co 模塊是著名程序員 TJ Holowaychuk 於 2013 年 6 月發佈的一個小工具,用於 Generator 函數的自動執行。 運用co模塊時,yield後面只能是 Thunk函數 或者Promise對象,co函數執行完成以後返回的是Promise。以下:

var co = require('co');
var gen = function* () {
  var img1 = yield getImage('/image01');
  var img2 = yield getImage('/image02');
  ...
};
co(gen).then(function (res){
  console.log(res);
}).catch(err){
    # 錯誤處理
};
複製代碼

co模塊的任務的並行處理,等多個任務並行執行完成以後再進行下一步操做:

# 數組的寫法
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res);
}).then(console.log).catch(onerror);

# 對象的寫法
co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2),
  };
  console.log(res);
}).then(console.log).catch(onerror);
複製代碼

Generator函數雖然相比Promise在寫法上更加精簡且邏輯清晰,可是須要額外有個運行co函數去執行,爲了解決優化這個問題,async函數出現了。

async函數

async函數Generator函數的語法糖。

var co = require('co');
var gen = function* () {
  var img1 = yield getImage('/image01');
  var img2 = yield getImage('/image02');
  ...
};
co(gen).then(function (res){
  console.log(res);
}).catch(err){
    # 錯誤處理
};
****
#以上Generator函數能夠改成
var gen = async function () {
  var img1 = await getImage('/image01');
  var img2 = await getImage('/image02');
  return [img1, img2];
  ...
};
gen().then(res => {
    console.log(res) # [img1, img2]
});
複製代碼

相比Generator函數,async函數在寫法上的區別就是async替代了*await替代了yield,而且async自帶執行器,只需gen()便可執行函數;擁有比較好的適應性,await後面能夠是Promise也能夠是原始類型的值;此外async函數返回的是Promise,便於咱們更好的處理返回值。

async function gen() {
  return '111';
  # 等同於 return await '111';
};
gen().then(res => {
    console.log(res) # 111
});
複製代碼

若是是直接return值,這個值會自動成爲then方法回調函數中的值。

async function gen() {
  var a = await getA();
  var b = await getB();
  return a + b;
};
gen().then(res => {
    console.log(res)
});
複製代碼

async函數返回的Promise,必須等到函數體內全部await後面的Promise對象都執行完畢後,或者return或者拋錯以後才能改變狀態;也就是隻有async裏面的異步操做所有操做完,才能回到主任務來,而且在then方法裏面繼續執行主任務。

# 錯誤處理1
async function gen() {
    await new Promise((resolve, reject) => {
        throw new Error('出錯了');
    })
};
gen().then(res => {
    console.log(res)
}).catch(err => {
    console.log(err) # 出錯了
});
# 錯誤處理2:以下處理,一個await任務的錯誤不會影響到後面await任務的執行
async function gen() {
    try{
        await new Promise((resolve, reject) => {
            throw new Error('出錯了');
        })
    }catch(e){
        console.log(e); # 出錯了
    }
    return Promise.resolve(1);
};
gen().then(res => {
    console.log(res) # 1
});
複製代碼

錯誤處理如上。

async function gen() {
    # 寫法一
    let result = await Promise.all([getName(), getAddress()]);
    return result;
    # 寫法二
    let namePromise = getName();
    let addressPromise = getAddress();
    let name = await namePromise;
    let address = await addressPromise;
    return [name, address];
};
gen().then(res => {
    console.log(res); # 一個數組,分別是getName和getAddress返回值
})
複製代碼

多個異步任務互相沒有依賴關係,須要併發時,可按照如上兩種方法書寫。

async與Promise、Generator函數之間的對比

function chainAnimationsPromise(elem, animations) {
  # 變量ret用來保存上一個動畫的返回值
  let ret = null;
  # 新建一個空的Promise
  let p = Promise.resolve();
  # 使用then方法,添加全部動畫
  for(let anim of animations) {
    p = p.then(function(val) {
      ret = val;
      return anim(elem);
    });
  }
  # 返回一個部署了錯誤捕捉機制的Promise
  return p.catch(function(e) {
    # 錯誤處理
  }).then(function() {
    return ret;
  });

}
複製代碼

Promise雖然很好的解決了地獄回調的問題,可是代碼中有不少與語義無關的thencatch等;

function chainAnimationsGenerator(elem, animations) {
  return co(function*() {
    let ret = null;
    try {
      for(let anim of animations) {
        ret = yield anim(elem);
      }
    } catch(e) {
      # 錯誤處理
    }
    return ret;
  });
}
複製代碼

Generator函數須要自動執行器來執行函數,且yield後面只能是Promise對象或者Thunk函數。

async function chainAnimationsAsync(elem, animations) {
  let ret = null;
  try {
    for(let anim of animations) {
      ret = await anim(elem);
    }
  } catch(e) {
    # 錯誤處理
  }
  return ret;
}
複製代碼

async 函數的實現最簡潔,最符合語義,幾乎沒有語義不相關的代碼。與Generator相比不須要程序員再提供一個執行器,async自己自動執行,使用起來方便簡潔。

參考:ECMAScript 6 阮一峯

相關文章
相關標籤/搜索