【ES6】從 Generator 到 Async/Await

什么是 Generator 函數(shù)

Generator 函數(shù)是 ES6 提供的一種異步編程解決方案(可以按序執(zhí)行異步方法),但其語法行為與傳統(tǒng)函數(shù)完全不同。

先看看 Generator 函數(shù)在形式上的定義:

let log = console.log

function* gen() {
  yield 1
  yield 2
  yield 3
  return 4
}

let g = gen()

log(g.next()) // { value: 1, done: false }
log(g.next()) // { value: 2, done: false }
log(g.next()) // { value: 3, done: false }
log(g.next()) // { value: 4, done: true }
log(g.next()) // { value: undefined, done: true }
  1. generator 函數(shù)和普通函數(shù)不同的是,generator 由 function*定義(function后帶有一個星號 *)
  2. yield 表達式:你可以理解為 Generator 函數(shù)是一個狀態(tài)機,封裝了多個內(nèi)部狀態(tài),而函數(shù)體內(nèi)部使用 yield 表達式來定義不同的內(nèi)部狀態(tài)
  3. 執(zhí)行 Generator 函數(shù)后會返回一個遍歷器對象(不會直接得到 return 的結(jié)果)
  4. 依次調(diào)用遍歷器對象的 next 方法,可以遍歷 Generator 函數(shù)內(nèi)部的每一個狀態(tài)

繼續(xù)分析上面的代碼,如果一個 yield 表達式算一個 generator 的一個狀態(tài),上述的代碼一共有4個狀態(tài),即 3 個 yield 表達式和 1 個 return 語句(return 也算一個狀態(tài))。

每次調(diào)用遍歷器的 next 方法,都會返回一個對象 {value: xxx, done: xxx},表示當前遍歷器狀態(tài)的信息,該對象包含兩個屬性,一個是 value 屬性,表示當前 yield 表達式的值, 一個是 done 屬性,表示當前遍歷是否結(jié)束。當所有狀態(tài)遍歷結(jié)束后,done 的值變?yōu)?true。

當遍歷結(jié)束后如果繼續(xù)調(diào)用遍歷器的 next 方法,done 的值不再改變,而 value 的值變?yōu)?undefined。

yield 表達式

  1. yield 語句就是暫停標志,遇到 yield 語句就暫停執(zhí)行后面的操作,并將緊跟在 yield 后的表達式的值作為返回的對象的 value 屬性值。
  2. 下一次調(diào)用next 方法時再繼續(xù)往下執(zhí)行,知道遇到下一條 yield。
  3. 如果沒有再遇到新的 yield 語句,就一直運行到函數(shù)結(jié)束,直到 return 語句為止,并將 return 語句后面的表達式的值作為返回對象的 value 屬性值。
  4. 如果該函數(shù)沒有 return 語句,則返回對象的 value 屬性值為 undefined。

不能在其他普通函數(shù)體中使用 yield,會報語法錯誤

  (function (){
    yield 1;
  })()  // SyntaxError: Unexpected number

yield 表達式如果用在另一個表達式中,必須放在圓括號里面

  function* demo() {
    console.log('Hello' + yield); // SyntaxError
    console.log('Hello' + yield 123); // SyntaxError
  
    console.log('Hello' + (yield)); // OK
    console.log('Hello' + (yield 123)); // OK
  }

next 方法

注意,yield 語句本身沒有返回值,或者說總是返回 undefined。next 方法可以帶有一個參數(shù),該參數(shù)會被當作上一條 yield 語句的返回值。

因為上面這句話,也就有了下面這個經(jīng)典的例子:

function* foo(x) {
  let y = 2 * (yield(x + 1))
  let z = yield(y / 3)
  return (x + y + z)
}
let g = foo(5)
log(g.next())
log(g.next())
log(g.next())

猜猜打印的結(jié)果什么?答案如下:

{ value: 6, done: false }
{ value: NaN, done: false }
{ value: NaN, done: true }

第一個 next() 語句中的參數(shù)總是無效的,無論傳入什么值,都不會對接下來的表達式產(chǎn)生影響,因為在執(zhí)行第一個 next() 方法的時候,還沒遇到 yield 語句,參數(shù)也無法作為返回值,所以第一個next的參數(shù)是無意義的。

可能有點繞,按步驟說明下上述代碼的執(zhí)行過程:

g = foo(), foo 生成了遍歷器,返回給了 g 變量
執(zhí)行第一個,g.next(),g 執(zhí)行 next 方法,此時在代碼中體現(xiàn)為執(zhí)行了 x + 1 = 6
然后遇到了 yield 語句,暫停,返回結(jié)果 {value: 6, done: false}
執(zhí)行第二個,g.next(), next 參數(shù)為空,即默認上一個yield語句的返回值為 undefined,執(zhí)行 let y = 2 * undefind, y = NaN, 繼續(xù)計算 y/3 , 即 NaN/3 = NaN
然后遇到了 yield 語句,暫停,返回結(jié)果 {value: NaN, done: false}
執(zhí)行第三個,g.next(), next 參數(shù)為空,即默認上一個yield語句的返回值為 undefined, 執(zhí)行 let z = undefined, return (x+y+z) 即 return (5+NaN+undefined) 最后的返回值 undefined。

為 next 方法傳入一些參數(shù):

function* foo(x) {
  let y = 2 * (yield(x + 1))
  let z = yield(y / 3)
  return (x + y + z)
}
let g = foo(5)
log(g.next()) // { value:6, done:false }
log(g.next(12)) // { value:8, done:false }
log(g.next(13)) // { value:42, done:true }

可以按上述的過程走一遍,然后把參數(shù)代入就得到了注釋中的結(jié)果。

for...of 循環(huán)

for...of循環(huán)可以自動遍歷 Generator 函數(shù)運行時生成的Iterator對象,且此時不再需要調(diào)用next方法。

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

上面代碼使用for...of循環(huán),依次顯示 5 個yield表達式的值。這里需要注意,一旦next方法的返回對象的done屬性為true,for...of循環(huán)就會中止,且不包含該返回對象,所以上面代碼的return語句返回的6,不包括在for...of循環(huán)之中。

Generator.prototype.throw()

Generator 函數(shù)返回的遍歷器對象,都有一個throw方法,可以在函數(shù)體外拋出錯誤,然后在 Generator 函數(shù)體內(nèi)捕獲。

var g = function* () {
 try {
   yield;
 } catch (e) {
   console.log('內(nèi)部捕獲', e);
 }
};

var i = g();
i.next();

try {
 i.throw('a');
 i.throw('b');
} catch (e) {
 console.log('外部捕獲', e);
}
// 內(nèi)部捕獲 a
// 外部捕獲 b

上面代碼中,遍歷器對象i連續(xù)拋出兩個錯誤。第一個錯誤被 Generator 函數(shù)體內(nèi)的catch語句捕獲。i第二次拋出錯誤,由于 Generator 函數(shù)內(nèi)部的catch語句已經(jīng)執(zhí)行過了,不會再捕捉到這個錯誤了,所以這個錯誤就被拋出了 Generator 函數(shù)體,被函數(shù)體外的catch語句捕獲。

throw方法可以接受一個參數(shù),該參數(shù)會被catch語句接收,建議拋出Error對象的實例。

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log(e);
  }
};

var i = g();
i.next();
i.throw(new Error('出錯了!'));
// Error: 出錯了!(…)

注意,不要混淆遍歷器對象的throw方法和全局的throw命令。上面代碼的錯誤,是用遍歷器對象的throw方法拋出的,而不是用throw命令拋出的。后者只能被函數(shù)體外的catch語句捕獲。

利用 Genertor 函數(shù)返回的迭代器的 throw 方法可以很好地處理代碼運行中的錯誤,這點在實現(xiàn)一個具有錯誤處理的 async 函數(shù)中也有一定體現(xiàn)。

參考文章:
[1] https://www.cnblogs.com/rogerwu/p/10764046.html
[2] https://es6.ruanyifeng.com/#docs/generator

async 和 await 的簡單實現(xiàn)

async/await 被稱為是 generator 的語法糖,實際上 async/await 就是 generator 函數(shù)加上自動執(zhí)行器來實現(xiàn)的。

從上一小節(jié)知道,generator 函數(shù)是不會自動執(zhí)行的,每一次調(diào)用它的 next 方法,會停留在下一個 yield 的位置。

利用這個特性,我們只要編寫一個自動執(zhí)行的函數(shù),就可以讓這個 generator 函數(shù)完全實現(xiàn) async 函數(shù)的功能。

先看一個 async 函數(shù)的示例

let p = function (val) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(val)
    }, 1000);

  })
}

async function testAsync() {
  const data1 = await p(1)
  console.log(data1)
  const data2 = await p(2)
  console.log(data2)
  const data3 = await p(3)
  console.log(data3)
}

testAsync()

這個 async 函數(shù)的效果就是,每隔1秒,按序打印出 1,2,3

嘗試利用 generator 函數(shù)替代 async 函數(shù)來實現(xiàn)這段代碼。

function* testG() {
  const data1 = yield p(1)
  console.log(data1)
  const data2 = yield p(2)
  console.log(data2)
  const data3 = yield p(3)
  console.log(data3)
}

let gen = testG()

let dataPromise = gen.next().value // 返回對象中的 value 值才是一個 promise


dataPromise.then((val) => {
  let data2Promise = gen.next(val).value

  data2Promise.then((val2) => {
    let data3Promise = gen.next(val2).value

    data3Promise.then((val3) => {
      gen.next(val3)
    })
  })

})
// 按序每隔一秒打印 1、2、3

為了保證每個狀態(tài)獲取到正確的值,并且按序執(zhí)行,則 gen.next() 必須在 promise 對象的 then 方法的回調(diào)中執(zhí)行,以保證在 generator 的 next 方法中傳入正確的參數(shù),當有多個異步方法要執(zhí)行的時候,最終實現(xiàn)的效果就像一個回調(diào)地獄的調(diào)用。
不過好歹是用 generator 實現(xiàn)了 async 函數(shù)的效果,盡管代碼不是很優(yōu)美。

實現(xiàn)一個高階函數(shù)來代替回調(diào)地獄

先不考慮包含錯誤處理的情況:

function asyncToGenerator(generatorFunc) {
  return function () {
   // 相當于 gen = generatorFunc() ,順便引入上下文環(huán)境,返回一個遍歷器
    const gen = generatorFunc.apply(this, arguments)

    return new Promise((resolve) => {
   // 定義一個步進函數(shù),遞歸調(diào)用,直到遍歷器的返回結(jié)果 done = true
      function step(arg) {
        let generatorResult = gen.next(arg)

        const {
          value,
          done
        } = generatorResult

        if (done) {
          return resolve(value)
        } else {
        // 不一定每一個返回值都是 promise 函數(shù),這里需要用 Promise.resolve 方法來包裝一下。
          return Promise.resolve(value).then(val => step(val)) 
        }
      }

      step() // 默認參數(shù)是 undefined,所以第一次執(zhí)行這里不傳參

    })
  }
}

asyncToGenerator(testG)()

考慮錯誤處理的情況,加逐行解釋代碼
參考:https://juejin.im/post/6844904102053281806

function asyncToGenerator(generatorFunc) {
  // 返回的是一個新的函數(shù)
  return function() {
  
    // 先調(diào)用generator函數(shù) 生成迭代器
    // 對應 var gen = testG()
    const gen = generatorFunc.apply(this, arguments)

    // 返回一個promise 因為外部是用.then的方式 或者await的方式去使用這個函數(shù)的返回值的
    // var test = asyncToGenerator(testG)
    // test().then(res => console.log(res))
    return new Promise((resolve, reject) => {
    
      // 內(nèi)部定義一個step函數(shù) 用來一步一步的跨過yield的阻礙
      // key有next和throw兩種取值,分別對應了gen的next和throw方法
      // arg參數(shù)則是用來把promise resolve出來的值交給下一個yield
      function step(key, arg) {
        let generatorResult
        
        // 這個方法需要包裹在try catch中
        // 如果報錯了 就把promise給reject掉 外部通過.catch可以獲取到錯誤
        try {
          generatorResult = gen[key](arg)
        } catch (error) {
          return reject(error)
        }

        // gen.next() 得到的結(jié)果是一個 { value, done } 的結(jié)構(gòu)
        const { value, done } = generatorResult

        if (done) {
          // 如果已經(jīng)完成了 就直接resolve這個promise
          // 這個done是在最后一次調(diào)用next后才會為true
          // 以本文的例子來說 此時的結(jié)果是 { done: true, value: 'success' }
          // 這個value也就是generator函數(shù)最后的返回值
          return resolve(value)
        } else {
          // 除了最后結(jié)束的時候外,每次調(diào)用gen.next()
          // 其實是返回 { value: Promise, done: false } 的結(jié)構(gòu),
          // 這里要注意的是Promise.resolve可以接受一個promise為參數(shù)
          // 并且這個promise參數(shù)被resolve的時候,這個then才會被調(diào)用
          return Promise.resolve(
            // 這個value對應的是yield后面的promise
            value
          ).then(
            // value這個promise被resove的時候,就會執(zhí)行next
            // 并且只要done不是true的時候 就會遞歸的往下解開promise
            // 對應gen.next().value.then(value => {
            //    gen.next(value).value.then(value2 => {
            //       gen.next() 
            //
            //      // 此時done為true了 整個promise被resolve了 
            //      // 最外部的test().then(res => console.log(res))的then就開始執(zhí)行了
            //    })
            // })
            function onResolve(val) {
              step("next", val)
            },
            // 如果promise被reject了 就再次進入step函數(shù)
            // 不同的是,這次的try catch中調(diào)用的是gen.throw(err)
            // 那么自然就被catch到 然后把promise給reject掉啦
            function onReject(err) {
              step("throw", err)
            },
          )
        }
      }
      step("next")
    })
  }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容