如何寫一個讓面試官滿意的 Generator 執行器?

雖然如今使用 async 函數 就能夠替代 Generator 執行器了,不過了解下 Generator 執行器的原理仍是挺有必要的。es6

若是你不瞭解 Generator,那麼你須要看這裏json

例子均可以在 Console 中運行的(谷歌版本 76.0.3809.100),都是以最新瀏覽器支持的 JavaScript 特性來編寫的,不考慮兼容性。數組

術語區分

Generator 是 Generator function 運行後返回的對象。promise

執行器的原理

原理一

有 Generator next 函數的特性,next 函數運行後會返回以下結構:瀏覽器

{ value: 'world', done: false }

或者app

{ value: undefined, done: true }

那麼咱們可使用遞歸運行 next 函數,若是 done 爲 true 則中止 next 函數的運行。async

原理二

yield 表達式自己沒有返回值,或者說老是返回 undefined。函數

next 方法能夠帶一個參數,該參數就會被看成上一個 yield 表達式的返回值。prototype

因爲 next 方法的參數表示上一個 yield 表達式的返回值,因此在第一次使用 next 方法時,傳遞參數是無效的。code

function* test() {
  const a = yield "a"
  console.log(a)
  return true
}
const gen = test()
// 第一次 next() 會卡在 yield a 中,next 返回 {value: "a", done: false}
// 第一個 next() 函數的參數是無效的,無需傳遞
const nextOne = gen.next() 
// 傳遞參數 nextOne.value
// test 函數中的 console.log 輸出 'a'
// 第二次的 next() 返回值爲 {value: true, done: true}
gen.next(nextOne.value)

原理三

gen.throw(exception) 能夠拋出異常,並恢復生成器的執行(前提須要 try catch yield 語句),返回帶有 done 及 value 兩個屬性的對象。並且該異常能夠經過 try...catch 塊進行捕獲。那麼咱們能夠經過 gen.throw 拋出 Promise 錯誤,這樣就可使用 try catch 攔截 Promise 的錯誤了。

function* gen() {
  let a
  try {
    a = yield 42
  } catch (e) {
    console.log('Error caught!')
  }
  console.log(a)
  yield 33
}

var g = gen()
g.next() // { value: 42, done: false }
g.throw(new Error('Something went wrong')) // "Error caught!"

簡單的執行器

簡單的執行器代碼以下:

/**
 * generator 執行器
 * @param {Function} func generator 函數
 * @return {Promise} 返回一個 Promise 對象
 */
function generatorExecuter(func) {
  const gen = func()

  function recursion(prevValue) {
    const next = gen.next(prevValue)
    const value = next.value
    const done = next.done
    if (done) {
      return Promise.resolve(value)
    } else {
      return recursion(value)
    }
  }
  return recursion()
}

代碼並不複雜,最簡單的執行器就出來了。若是你是一步一步的看文章過來的,都理解了原理,那麼這些代碼也很好理解。

考慮 yield 的類型是 Promise

上面的代碼執行以下的 Generator 函數是不正確的:

function* test() {
  const a = yield Promise.resolve('a')
  console.log(a)
  return true
}
generatorExecuter(test)

運行上面的代碼後,test 函數 console.log 輸出的不是 a,而是 Promise {<resolved>: "a"}

那麼咱們代碼須要這樣處理:

/**
 * generator 執行器
 * @param {GeneratorFunction} generatorFunc generator 函數
 * @return {Promise} 返回一個 Promise 對象
 */
function generatorExecuter(generatorFunc) {
  return new Promise((resolve) => {
    const generator = generatorFunc()
    // 觸發第一次執行下面定義的 next 函數
    onFullfilled()
    
    /**
     * Promise 成功處理的回調函數
     * 回調函數會執行 generator.next()
     * @param {Any} value yield 表達式的返回值
     */
    function onFullfilled(value) {
      let result
      // next() 會一步一步的執行 generator 函數體的代碼,
      result = generator.next(value)
      next(result)
    }
    
    function next(result) {
      const value = result.value
      const done = result.done

      if (done) {
        return resolve(value)
      } else {
        return Promise.resolve(value).then(onFullfilled)
      }
    }
  })
}

這樣就再運行 generatorExecuter(test) 就沒問題了。

考慮 try catch 捕獲 yield 錯誤

這裏用到了上面提到的原理三,若是不清楚能夠回去看看。

以下代碼,使用上面的執行器是沒法 try catch 到 yield 錯誤的:

function* test() {
  try {
    const a = yield Promise.reject('error')
  }catch (err) {
    console.log('發生錯誤了:', err)
  }
  return true
}
generatorExecuter(test)

運行上面代碼後,會報錯 Uncaught (in promise) error,而不是攔截後輸出 發生錯誤了: error

要改爲這樣才行(使用 gen.throw ):

/**
 * generator 執行器
 * @param {GeneratorFunction} generatorFunc generator 函數
 * @return {Promise} 返回一個 Promise 對象
 */
function generatorExecuter(generatorFunc) {
  return new Promise((resolve, reject) => {
    const generator = generatorFunc()
    // 觸發第一次執行下面定義的 next 函數
    onFullfilled()
    /**
     * Promise 成功處理的回調函數
     * 回調函數會執行 generator.next()
     * @param {Any} value yield 表達式的返回值
     */
    function onFullfilled(value) {
      let result
      // next() 會一步一步的執行 generator 函數體的代碼,
      // 若是報錯,咱們須要攔截,而後 reject
      try {
        // yield 表達式自己沒有返回值,或者說老是返回 undefined。
        // generator.next 方法能夠帶一個參數,該參數就會被看成上一個 yield 表達式的返回值。
        // 因爲 generator.next 方法的參數表示上一個 yield 表達式的返回值,
        // 因此在第一次使用 generator.next 方法時,傳遞參數是無效的。
        result = generator.next(value)
      } catch (error) {
        return reject(error)
      }
      next(result)
    }
    /**
     * Promise 失敗處理的回調函數
     * 回調函數會執行 generator.throw() ,這樣能夠 try catch 攔截 yield xxx 的錯誤
     * @param {Any} reason 失敗緣由
     */
    function onRejected(reason) {
      let result
      try {
        // 這裏 try catch 是捕獲恢復生成器執行的時候,代碼發生錯誤的狀況
        // gen.throw() 方法用來向生成器拋出異常,並恢復生成器的執行,
        // 返回帶有 done 及 value 兩個屬性的對象。
        result = generator.throw(reason)
      } catch (error) {
        return reject(error)
      }
      // gen.throw() 的錯誤被捕獲後,能夠繼續執行下去,不然終止後續的運行
      // 這能夠達到同步效果
      //(不是上面的 try catch,是使用者 try catch yield 語句)
      next(result)
    }

    function next(result) {
      const value = result.value
      const done = result.done

      if (done) {
        return resolve(value)
      } else {
        return Promise.resolve(value).then(onFullfilled, onRejected)
      }
    }
  })
}

考慮 yield 的其餘類型

考慮 yield 的其餘類型,如 generator 函數,能夠把這些類型適配爲 Promise 就很好處理了

上面的代碼執行以下的 Generator 函數是不正確的:

function* aFun() {
  return 'a'
}

function* test() {
  const a = yield aFun
  console.log(a)
  return true
}
generatorExecuter(test)

運行上面的代碼後,test 函數 console.log 輸出的不是 a,而是輸出下面的字符串:

ƒ* aFun() {
  return 'a'
}

那麼咱們代碼須要這樣處理:

/**
 * generator 執行器
 * @param {GeneratorFunction | Generator} generatorFunc Generator 函數或者 Generator
 * @return {Promise} 返回一個 Promise 對象
 */
function generatorExecuter(generatorFunc) {
  if (!isGernerator(generatorFunc) && !isGerneratorFunction(generatorFunc)) {
    throw new TypeError(
      'Expected the generatorFunc to be a GeneratorFunction or a Generator.'
    )
  }

  let generator = generatorFunc
  if (isGerneratorFunction(generatorFunc)) {
    generator = generatorFunc()
  }

  return new Promise((resolve, reject) => {
    // 觸發第一次執行下面定義的 next 函數
    onFullfilled()
    /**
     * Promise 成功處理的回調函數
     * 回調函數會執行 generator.next()
     * @param {Any} value yield 表達式的返回值
     */
    function onFullfilled(value) {
      let result
      // next() 會一步一步的執行 generator 函數體的代碼,
      // 若是報錯,咱們須要攔截,而後 reject
      try {
        // yield 表達式自己沒有返回值,或者說老是返回 undefined。
        // generator.next 方法能夠帶一個參數,該參數就會被看成上一個 yield 表達式的返回值。
        // 因爲 generator.next 方法的參數表示上一個 yield 表達式的返回值,
        // 因此在第一次使用 generator.next 方法時,傳遞參數是無效的。
        result = generator.next(value)
      } catch (error) {
        return reject(error)
      }
      next(result)
    }
    /**
     * Promise 失敗處理的回調函數
     * 回調函數會執行 generator.throw() ,這樣能夠 try catch 攔截 yield xxx 的錯誤
     * @param {Any} reason 失敗緣由
     */
    function onRejected(reason) {
      let result
      try {
        // 這裏 try catch 是捕獲恢復生成器執行的時候,代碼發生錯誤的狀況
        // gen.throw() 方法用來向生成器拋出異常,並恢復生成器的執行,
        // 返回帶有 done 及 value 兩個屬性的對象。
        result = generator.throw(reason)
      } catch (error) {
        return reject(error)
      }
      // gen.throw() 的錯誤被捕獲後,能夠繼續執行下去,不然終止後續的運行
      // 這能夠達到同步效果
      //(不是上面的 try catch,是使用者 try catch yield 語句)
      next(result)
    }

    function next(result) {
      const value = toPromise(result.value)
      const done = result.done

      if (done) {
        return resolve(value)
      } else {
        return value.then(onFullfilled, onRejected)
      }
    }
  })
}

/**
 * 考慮 yield 的其餘類型,如 generator 函數
 * 能夠把這些類型適配爲 Promise 就很好處理了
 */
function toPromise(value) {
  if (isGerneratorFunction(value) || isGernerator(value)) {
    // generatorExecuter 返回 Promise
    return generatorExecuter(value)
  } else {
    // 字符串、對象、數組、Promise 等都轉成 Promise
    return Promise.resolve(value)
  }
}

/**
 * 是不是 generator 函數
 */
function isGerneratorFunction(target) {
  if (
    Object.prototype.toString.apply(target) === '[object GeneratorFunction]'
  ) {
    return true
  } else {
    return false
  }
}

/**
 * 是不是 generator
 */
function isGernerator(target) {
  if (Object.prototype.toString.apply(target) === '[object Generator]') {
    return true
  } else {
    return false
  }
}

這樣就再運行 generatorExecuter(test) 就沒問題了。

最終版

Generator 執行器沒想的那麼難,花點時間就能夠吃透了。

/**
 * generator 執行器
 * @param {GeneratorFunction | Generator} generatorFunc Generator 函數或者 Generator
 * @return {Promise} 返回一個 Promise 對象
 */
function generatorExecuter(generatorFunc) {
  if (!isGernerator(generatorFunc) && !isGerneratorFunction(generatorFunc)) {
    throw new TypeError(
      'Expected the generatorFunc to be a GeneratorFunction or a Generator.'
    )
  }

  let generator = generatorFunc
  if (isGerneratorFunction(generatorFunc)) {
    generator = generatorFunc()
  }

  return new Promise((resolve, reject) => {
    // 觸發第一次執行下面定義的 next 函數
    onFullfilled()
    /**
     * Promise 成功處理的回調函數
     * 回調函數會執行 generator.next()
     * @param {Any} value yield 表達式的返回值
     */
    function onFullfilled(value) {
      let result
      // next() 會一步一步的執行 generator 函數體的代碼,
      // 若是報錯,咱們須要攔截,而後 reject
      try {
        // yield 表達式自己沒有返回值,或者說老是返回 undefined。
        // generator.next 方法能夠帶一個參數,該參數就會被看成上一個 yield 表達式的返回值。
        // 因爲 generator.next 方法的參數表示上一個 yield 表達式的返回值,
        // 因此在第一次使用 generator.next 方法時,傳遞參數是無效的。
        result = generator.next(value)
      } catch (error) {
        return reject(error)
      }
      next(result)
    }
    /**
     * Promise 失敗處理的回調函數
     * 回調函數會執行 generator.throw() ,這樣能夠 try catch 攔截 yield xxx 的錯誤
     * @param {Any} reason 失敗緣由
     */
    function onRejected(reason) {
      let result
      try {
        // 這裏 try catch 是捕獲恢復生成器執行的時候,代碼發生錯誤的狀況
        // gen.throw() 方法用來向生成器拋出異常,並恢復生成器的執行,
        // 返回帶有 done 及 value 兩個屬性的對象。
        result = generator.throw(reason)
      } catch (error) {
        return reject(error)
      }
      // gen.throw() 的錯誤被捕獲後,能夠繼續執行下去,不然終止後續的運行
      // 這能夠達到同步效果
      //(不是上面的 try catch,是使用者 try catch yield 語句)
      next(result)
    }

    function next(result) {
      const value = toPromise(result.value)
      const done = result.done

      if (done) {
        return resolve(value)
      } else {
        return value.then(onFullfilled, onRejected)
      }
    }
  })
}

/**
 * 考慮 yield 的其餘類型,如 generator 函數
 * 能夠把這些類型適配爲 Promise 就很好處理了
 */
function toPromise(value) {
  if (isGerneratorFunction(value) || isGernerator(value)) {
    // generatorExecuter 返回 Promise
    return generatorExecuter(value)
  } else {
    // 字符串、對象、數組、Promise 等都轉成 Promise
    return Promise.resolve(value)
  }
}

/**
 * 是不是 generator 函數
 */
function isGerneratorFunction(target) {
  if (
    Object.prototype.toString.apply(target) === '[object GeneratorFunction]'
  ) {
    return true
  } else {
    return false
  }
}

/**
 * 是不是 generator
 */
function isGernerator(target) {
  if (Object.prototype.toString.apply(target) === '[object Generator]') {
    return true
  } else {
    return false
  }
}

運行例子以下,直接在谷歌 console 運行便可:

// 例子
function* one() {
  return 'one'
}

function* two() {
  return yield 'two'
}

function* three() {
  return Promise.resolve('three')
}

function* four() {
  return yield Promise.resolve('four')
}

function* five() {
  const a = yield new Promise(resolve => {
    setTimeout(() => {
      resolve('waiting five over')
    }, 1000)
  })
  console.log(a)
  return 'five'
}

function* err() {
  const a = 2
  a = 3
  return a
}

function* all() {
  const a = yield one()
  console.log(a)
  const b = yield two()
  console.log(b)
  const c = yield three()
  console.log(c)
  const d = yield four()
  console.log(d)
  const e = yield five()
  console.log(e)
  try {
    yield err()
  } catch (err) {
    console.log('發生了錯誤', err)
  }
  return 'over'
}

generatorExecuter(all).then(value => {
  console.log(value)
})

// 或者
// generatorExecuter(all()).then(value => {
//   console.log(value)
// })
相關文章
相關標籤/搜索