Javascript異步編程:Callback、Promise、Generator

同步和異步(Synchronous and Asynchronous)

瞭解javascript的同學想必對同步和異步的概念應該都很熟悉了,如果還有不熟悉的同學,我這裏舉個形象的例子,比如我們早上起牀後要幹三件事:燒水、洗臉、吃早飯,同步相當於我們先燒水,水燒開了再洗臉,洗完臉再吃早飯,三件事順序執行,一件幹完了再幹下一件;而異步相當於我們在燒水的同時吃早飯(不洗臉就吃早飯不太衛生),吃完早飯再洗臉。顯然異步比同步更加高效,省去了很多等待的時間,同步過程的執行時間取決於所有行爲的總和,而異步過程的執行時間只取決於最長的那個行爲,如下圖所示:

image

由於Javascript是單線程的,同時只能處理一件事,在上面的例子中這個單線程就是“我”,比如我不能同時洗臉和吃早飯一樣。所以爲了讓執行效率提高,我們要儘量讓這個線程一直處於忙碌狀態而不是閒置狀態,就像我們不用幹等燒水,可以同時去做其他事情,而燒水由系統的其他線程去處理(該線程不屬於Javascript)。在計算機的世界中,很多I/O密集型的操作是需要等待的,比如網絡請求、文件讀寫等,所以異步方法在處理這些操作會更加得心應手。

異步過程控制

瞭解異步的意義之後,我們來對比目前主流幾種異步過程控制方法,探討一下異步編程的最佳實踐。

1. Callback

誤區

首先callback和異步沒有必然聯繫,callback本質就是類型爲function的函數參數,對於該callback是同步還是異步執行則取決於函數本身。雖然callback常用於異步方法的回調,但其實有不少同步方法也可以傳入callback,比如最常見的數組的forEach方法:

var arr = [1, 2, 3];
arr.forEach(function (val) {
  console.log(val);
});
console.log('finish');

// 打印結果:1,2,3,finish

類似的還有數組的map, filter, reduce等很多方法。

異步Callback

常見的異步callback如setTimeout中的回調:

setTimeout(function () {
  console.log("time's up");
}, 1000);
console.log('finish');

// 打印結果:finish, time's up

如果我們將延遲時間改爲0,打印結果仍將是finish, time's up,因爲異步callback會等函數中的同步方法都執行完成後再執行。

Callback Hell

在實際項目中我們經常會遇到這樣的問題:下一步操作依賴於上一步操作的結果,上一步操作又依賴於上上步操作,而每一步操作都是異步的。。這樣遞進的層級多了會形成很多層callback嵌套,導致代碼可讀性和可維護性變的很差,形成所謂的Callback Hell,類似這樣:

step1(param, function (result1) {
  step2(result1, function (result2) {
    step3(result2, function (result3) {
      step4(result3, function (result4) {
        done(result4);
      })
    })
  })
})

當然在不放棄使用callback的前提下,上面的代碼還是有優化空間的,我們可以將它重新組織一下:

step1(param, callbac1);

function callback1(result1){
  step2(result1, callback2);
}

function callback2(result2){
  step3(result2, callback3);
}

function callback3(result3){
  step4(result3, callback4);
}

function callback4(result4){
  done(result4);
}

相當於將Callback Hell的橫向深度轉化爲代碼的縱向高度,變得更接近於我們習慣的由上到下的同步調用, 複雜度沒有變,只是看起來更清晰了,缺點就是要定義額外的函數、變量。將這一思想進一步延伸就有了下面的Promise。

2. Promise

Promise中文譯爲“承諾”,在Javascript中是一個抽象的概念,代表當前沒有實現,但未來的某個時間點會(也可能不會)實現的一件事。舉個實例化的例子:早上燒水,我給你一個承諾(Promise),十分鐘後水能燒開,如果一切正常,10分鐘之後水確實能燒開,代表這個promise兌現了(fullfilled),但是如果中途停電了,10分鐘水沒燒開,那這個promise兌現失敗(rejected)。用代碼可以表示爲:

const boilWaterInTenMins = new Promise(function (resolve, reject) {
  boiler.work(function (timeSpent) {
    if (timeSpent <= 10) {
      resolve();
    } else {
      reject();
    }
  });
});

兼容性

image

如果想提高瀏覽器對Promise的兼容性可以使用babel或者第三方的實現(參考 github awesome promise

Promise Chaining

我們再來看Promise對於異步過程控制有怎樣的提升,還基於上面Callback Hell的例子,如果用Promise實現會如何呢?

首先我們需要將step1 ~ done 的函數用Promise實現(即返回一個Promise),然後進行一連串的鏈式調用就可以了:

stepOne(param)
  .then((result1) => { return step2(result1) })
  .then((result2) => { return step3(result2) })
  .then((result3) => { return step4(result3) })
  .then((result4) => { return done(result4) })
  .catch(err => handleError(err));

是不是簡單很多!

Async/Await

如果你不太習慣Promise的調用方式,那我們可以用async/await將其轉化成更接近同步調用的方式:

async function main() {
  try {
    var result1 = await step1(param);
    var result2 = await step2(result1);
    var result3 = await step3(result2);
    var result4 = await step4(result3);
    done(result4);
  } catch (err) {
    handleError(err);
  }
}

main();

3. Generator

Generator是一個更加抽象的概念,要弄懂什麼是Generator首先要理解另外幾個概念Iterable Protocol(可迭代協議),Iterator Protocol(迭代器協議)和 Iterator(迭代器)。

Iterable Protocol

Iterable Protocol 的特點可以概括爲:

  1. 用於定義javascript對象的迭代行爲
  2. 對象本身或者原型鏈上需要有一個名爲Symbol.iterator的方法
  3. 該方法不接收任何參數,且返回一個Iterator
  4. Iterable的對象可以使用for...of遍歷

Javascript Array就實現了Iterable Protocol,除了常規的取值方式,我們也可以利用array的Symbol.iterator

var arr = [1, 2, 3];
var iterator = arr[Symbol.iterator]();
iterator.next(); // {value: 1, done: false}

我們也可以修改Array默認的迭代方式,比如返回兩倍的值:

Array.prototype[Symbol.iterator] = function () {
  var nextIndex = 0;
  var self = this;
  return {
    next: function () {
      return nextIndex < self.length ?
        { value: self[nextIndex++] * 2, done: false } :
        { done: true }
    }
  };
}

for(let el of [1, 2, 3]){
  console.log(el);
}
// 輸出:2,4,6

Iterator Protocol

Iterator Protocol 的特點可以概括爲:

  1. 一種產生一個序列值(有限或無限)的標準方式
  2. 實現一個next方法
  3. next方法返回的對象爲 {value: any, done: boolean}
  4. value爲返回值,donetruevalue可以省略
  5. donetrue表示迭代結束,此時value表示最終返回值
  6. donefalse,則可以繼續迭代,產生下一個值

Iterator

顯然Iterator就是實現了Iterator Protocol的對象。

Generator

理解上面幾個概念後,理解Generator就簡單多了,generator的特點可概括爲:

  1. 同時實現Iterable Protocol和Iterator Protocol,所以Genrator即是一個iterable的對象又是一個iterator
  2. Generator由 generator function 生成

最簡單的generator function比如:

function* gen() {
  var x = yield 5 + 6;
}

var myGen = gen(); // myGen 就是一個generator

我們可以調用next方法來獲得yield表達式的值:

myGen.next(); // { value: 11, done: false }

但此時x並沒有被賦值,可以想象成javascript執行完 yield 5 + 6 就停住了,爲了繼續執行賦值操作我們需要再次調用next,並將得到的值回傳:

function* gen() {
  var x = yield 5 + 6;
  console.log(x); // 11
}

var myGen = gen();
console.log(myGen.next()); // { value: 11, done: false }
console.log(myGen.next(11)); // { value: undefined, done: true }

說了這麼多,generator和異步到底有什麼關係呢?我們來看Promise + Generator 實現的異步控制(step1 ~ done 返回Promise):

genWrap(function* () {
  var result1 = yield step1(param);
  var result2 = yield step2(result1);
  var result3 = yield step3(result2);
  var result4 = yield step4(result3);
  var result5 = yield done(result4);
});

function genWrap(genFunc) {
  var generator = genFunc();

  function handle(yielded) {
    if (!yielded.done) {
      yielded.value.then(function (result) {
        return handle(generator.next(result));
      });
    }
  }

  return handle(generator.next());
}

和async/await類似,這種實現也將異步方法轉化成了同步的寫法,實際上這就是 ES7中async/await的實現原理(將genWrap替換爲async,將yield替換成await)。

結語

希望本文對大家有點幫助,能更深刻的理解javascript異步編程,能寫出更優雅更高效的代碼。有錯誤歡迎指正。新年快樂!

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