[前端漫談_2] 從 Dva 的 Effect 到 Generator + Promise 實現非同步程式設計

dendoink發表於2019-01-17

你能學到什麼

  • 如何使用 Generator + Promise 實現非同步程式設計
  • 非同步程式設計的原理解析

前言

結合 上一篇文章 ,我們來聊聊 Generator

基礎原理

說到非同步程式設計,你想到的是asyncawait ,但那也只是 Generator 的語法糖而已。dva 中有一個 Effect 的概念,它就是使用 Generator 來解決非同步請求的問題,我們也來聊一聊 Generator + Promise 如何非同步程式設計:

開始之前,我們需要了解一些基本的概念:

  • Generator作為 ES6 中使用協程的解決方案來處理非同步程式設計的具體實現,它的特點是: Generator 中可以使用 yield 關鍵字配合例項 gen 呼叫 next() 方法,來將其內部的語句分割執行。 簡言之 : next() 被呼叫一次,則 yield 語句被執行一句,隨著 next() 呼叫, yield 語句被依次執行。

  • Promise表示一個非同步操作的最終狀態(完成或失敗),以及其返回的值。參考Promise-MDN

所以,非同步程式設計使用 GeneratorPromise 來實現的原理是什麼呢?

  1. 因為 Generator 本身 yield 語句是分離執行的,所以我們利用這一點,在 yield 語句中返回一個 Promise 物件
  2. 首次呼叫 Generator 中的 next() 後, 假設返回值叫 result ,那麼此時 result.value 就是我們定義在 yield 語句中的 Promise 物件

注意:在這一步,我們已經把原來的執行流程暫停,轉而執行 Promise 的內容,已經實現了控制非同步程式碼的執行,因為此時我們如果不繼續執行 next()generator 中位於當前被執行的 yield 後面的內容,將不會繼續執行,這已經達到了我們需要的效果

  1. 接下來我們就是在執行完當前 Promise 之後,讓程式碼繼續往下執行,直到遇到下一個 yield 語句:
    這一步是最關鍵的 所以我們怎麼做呢:

    步驟1: 在當前的 Promisethen() 方法中,繼續執行 gen.next()

    步驟2: 當 gen.next() 返回的結果 result.done === true 時,我們拿到 result.value【也就是一個新的 Promise 物件】再次執行並且在它的then() 方法中繼續上面的步驟1,直至 result.done === false 的時候。這時候呼叫 resolve() 使 promise 狀態改變,因為所有的 yield 語句已經被執行完。

  • 步驟1 保證了我們可以走到下一個 yield 語句
  • 步驟2 保證了下一個 yield 語句執行完不會中斷,直至 Generator 中的最後一個 yield 語句被執行完。 流程示意圖:

[前端漫談_2] 從 Dva 的 Effect 到 Generator + Promise 實現非同步程式設計

具體實現

co 是著名大神 TJ 實現的 Generator 的二次封裝庫,那麼我們就從co庫中的一個demo開始,瞭解我們的整個非同步請求封裝實現:

co(function*() {
    yield me.loginAction(me.form);
    ...
});
複製程式碼

在這裡我們引入了co庫,並且用co來包裹了一個generator(生成器)物件。
接下來我們看下co對於包裹起來的generator做了什麼處理

function co(gen) {
  // 1.獲取當前co函式的執行上下文環境,獲取到引數列表
  var ctx = this;
  var args = slice.call(arguments, 1);
  // 2.返回一個Promise物件
  return new Promise(function(resolve, reject) {
    //  判斷並且使用ctx:context(上下文環境)和arg:arguments(引數列表)初始化generator並且複製給gen
    // 注意:
    // gen = gen.apply(ctx, args)之後
    // 我們呼叫 gen.next() 時,返回的是一個指標,實際的值是一個物件
    // 物件的形式:{done:[false | true], value: ''}
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    // 當返回值不為gen時或者gen.next的型別不為function【實際是判斷是否為generator】時
    // 當前promise狀態被設定為resolve而結束
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
    // 否則執行onFulfilled()
    onFulfilled();
  });
}
複製程式碼

總結一下這裡發生了什麼

  1. 返回一個 promise
  2. promise 中將被包裹的 generator 例項化為一個指標,指向 generator 中第一個 yield 語句
  3. 判斷 generator 例項化出來的指標是否存在:如果沒有 yield 語句則指標不存在 判斷指標 gen.next() 方法是否為 function :如果不為 function 證明無法執行 gen.next() 條件有一項不滿足就將 promise 的狀態置為 resolve 否則執行 onFulfilled()

接下來我們看下 onFulfilled() 的實現

    function onFulfilled(res) {
      // 在執行onFulfilled時,定義了一個ret來儲存gen.next(res)執行後的指標物件
      var ret;
      try {
        ret = gen.next(res);
      // 在這裡,yield語句丟擲的值就是{value:me.loginAction(me.form), done:false}
      } catch (e) {
        return reject(e);
      }
    // 將ret物件傳入到我們定義在promise中的next方法中
      next(ret);
      return null;
    }
複製程式碼

總結一下,onFulfilled 最主要的工作就是

  1. 執行 gen.next() 使程式碼執行到 yield 語句
  2. 將執行後返回的結果傳入我們自定義的 next() 方法中

那麼我們再來看 next() 方法

    function next(ret) {
    // 進入next中首先判斷我們傳入的ret的done狀態:
    // 情況1:ret.done = true 代表我們這個generator中所有yield語句都已經執行完。
    // 那麼將ret.value傳入到resolve()中,promise的狀態變成解決,整個過程結束。
      if (ret.done) return resolve(ret.value);
    // 情況2:當前ret.done = false 代表generator還未將所有的yield語句執行完,那麼這時候
    // 我們把當前上下文和ret.value傳入toPromise中,將其轉換為對應的Promise物件`value`
      var value = toPromise.call(ctx, ret.value);
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
    // 當value確實是一個promise物件的時候,return value.then(onFulfilled,onRejected)
    // 我們重新進入到了generator中,執行下一條yield語句
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
複製程式碼

總結一下,next 主要工作

  1. 判斷上一次 yield 語句的執行結果
  2. yieldresultvalue 值【其實就是我們要非同步執行的 Promise
  3. 執行 valuethen 方法,重新進入到 onFulfilled 方法中,而在 onFulfilled 中,我們又將進入到當前方法,如此迴圈的呼叫,實現了 generatorPromise 的執行切換,從而實現了 Promise 的內容按照我們所定義的順序執行。

有同學可能對這裡的 toPromise 方法有一些疑惑,我先把程式碼貼出來

function toPromise(obj) {
  if (!obj) return obj;
  if (isPromise(obj)) return obj;
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}
複製程式碼

其實這個函式做的事情就是,根據不同的型別進行轉換,使得最後輸出的型別都是一個 Promise。那具體的轉換細節,大家可以參考co庫的原始碼

至此實現非同步操作的控制。

最後

小冊 你不知道的 Chrome 除錯技巧 已經開始預售啦。

歡迎關注公眾號 「前端惡霸」,掃碼關注,會有很多好東西等著你~

[前端漫談_2] 從 Dva 的 Effect 到 Generator + Promise 實現非同步程式設計

相關文章