【ES6】从 Generator 到 Async/Await

什么是 Generator 函数

Generator 函数是 ES6 提供的一种异步编程解决方案(可以按序执行异步方法),但其语法行为与传统函数完全不同。

先看看 Generator 函数在形式上的定义:

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 函数和普通函数不同的是,generator 由 function*定义(function后带有一个星号 *)
  2. yield 表达式:你可以理解为 Generator 函数是一个状态机,封装了多个内部状态,而函数体内部使用 yield 表达式来定义不同的内部状态
  3. 执行 Generator 函数后会返回一个遍历器对象(不会直接得到 return 的结果)
  4. 依次调用遍历器对象的 next 方法,可以遍历 Generator 函数内部的每一个状态

继续分析上面的代码,如果一个 yield 表达式算一个 generator 的一个状态,上述的代码一共有4个状态,即 3 个 yield 表达式和 1 个 return 语句(return 也算一个状态)。

每次调用遍历器的 next 方法,都会返回一个对象 {value: xxx, done: xxx},表示当前遍历器状态的信息,该对象包含两个属性,一个是 value 属性,表示当前 yield 表达式的值, 一个是 done 属性,表示当前遍历是否结束。当所有状态遍历结束后,done 的值变为 true。

当遍历结束后如果继续调用遍历器的 next 方法,done 的值不再改变,而 value 的值变为 undefined。

yield 表达式

  1. yield 语句就是暂停标志,遇到 yield 语句就暂停执行后面的操作,并将紧跟在 yield 后的表达式的值作为返回的对象的 value 属性值。
  2. 下一次调用next 方法时再继续往下执行,知道遇到下一条 yield。
  3. 如果没有再遇到新的 yield 语句,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值作为返回对象的 value 属性值。
  4. 如果该函数没有 return 语句,则返回对象的 value 属性值为 undefined。

不能在其他普通函数体中使用 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 方法可以带有一个参数,该参数会被当作上一条 yield 语句的返回值。

因为上面这句话,也就有了下面这个经典的例子:

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())

猜猜打印的结果什么?答案如下:

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

第一个 next() 语句中的参数总是无效的,无论传入什么值,都不会对接下来的表达式产生影响,因为在执行第一个 next() 方法的时候,还没遇到 yield 语句,参数也无法作为返回值,所以第一个next的参数是无意义的。

可能有点绕,按步骤说明下上述代码的执行过程:

g = foo(), foo 生成了遍历器,返回给了 g 变量
执行第一个,g.next(),g 执行 next 方法,此时在代码中体现为执行了 x + 1 = 6
然后遇到了 yield 语句,暂停,返回结果 {value: 6, done: false}
执行第二个,g.next(), next 参数为空,即默认上一个yield语句的返回值为 undefined,执行 let y = 2 * undefind, y = NaN, 继续计算 y/3 , 即 NaN/3 = NaN
然后遇到了 yield 语句,暂停,返回结果 {value: NaN, done: false}
执行第三个,g.next(), next 参数为空,即默认上一个yield语句的返回值为 undefined, 执行 let z = undefined, return (x+y+z) 即 return (5+NaN+undefined) 最后的返回值 undefined。

为 next 方法传入一些参数:

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 }

可以按上述的过程走一遍,然后把参数代入就得到了注释中的结果。

for...of 循环

for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用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循环,依次显示 5 个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

Generator.prototype.throw()

Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

var g = function* () {
 try {
   yield;
 } catch (e) {
   console.log('内部捕获', e);
 }
};

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

try {
 i.throw('a');
 i.throw('b');
} catch (e) {
 console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

上面代码中,遍历器对象i连续抛出两个错误。第一个错误被 Generator 函数体内的catch语句捕获。i第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch语句捕获。

throw方法可以接受一个参数,该参数会被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命令抛出的。后者只能被函数体外的catch语句捕获。

利用 Genertor 函数返回的迭代器的 throw 方法可以很好地处理代码运行中的错误,这点在实现一个具有错误处理的 async 函数中也有一定体现。

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

async 和 await 的简单实现

async/await 被称为是 generator 的语法糖,实际上 async/await 就是 generator 函数加上自动执行器来实现的。

从上一小节知道,generator 函数是不会自动执行的,每一次调用它的 next 方法,会停留在下一个 yield 的位置。

利用这个特性,我们只要编写一个自动执行的函数,就可以让这个 generator 函数完全实现 async 函数的功能。

先看一个 async 函数的示例

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 函数的效果就是,每隔1秒,按序打印出 1,2,3

尝试利用 generator 函数替代 async 函数来实现这段代码。

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

为了保证每个状态获取到正确的值,并且按序执行,则 gen.next() 必须在 promise 对象的 then 方法的回调中执行,以保证在 generator 的 next 方法中传入正确的参数,当有多个异步方法要执行的时候,最终实现的效果就像一个回调地狱的调用。
不过好歹是用 generator 实现了 async 函数的效果,尽管代码不是很优美。

实现一个高阶函数来代替回调地狱

先不考虑包含错误处理的情况:

function asyncToGenerator(generatorFunc) {
  return function () {
   // 相当于 gen = generatorFunc() ,顺便引入上下文环境,返回一个遍历器
    const gen = generatorFunc.apply(this, arguments)

    return new Promise((resolve) => {
   // 定义一个步进函数,递归调用,直到遍历器的返回结果 done = true
      function step(arg) {
        let generatorResult = gen.next(arg)

        const {
          value,
          done
        } = generatorResult

        if (done) {
          return resolve(value)
        } else {
        // 不一定每一个返回值都是 promise 函数,这里需要用 Promise.resolve 方法来包装一下。
          return Promise.resolve(value).then(val => step(val)) 
        }
      }

      step() // 默认参数是 undefined,所以第一次执行这里不传参

    })
  }
}

asyncToGenerator(testG)()

考虑错误处理的情况,加逐行解释代码
参考:https://juejin.im/post/6844904102053281806

function asyncToGenerator(generatorFunc) {
  // 返回的是一个新的函数
  return function() {
  
    // 先调用generator函数 生成迭代器
    // 对应 var gen = testG()
    const gen = generatorFunc.apply(this, arguments)

    // 返回一个promise 因为外部是用.then的方式 或者await的方式去使用这个函数的返回值的
    // var test = asyncToGenerator(testG)
    // test().then(res => console.log(res))
    return new Promise((resolve, reject) => {
    
      // 内部定义一个step函数 用来一步一步的跨过yield的阻碍
      // key有next和throw两种取值,分别对应了gen的next和throw方法
      // arg参数则是用来把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() 得到的结果是一个 { value, done } 的结构
        const { value, done } = generatorResult

        if (done) {
          // 如果已经完成了 就直接resolve这个promise
          // 这个done是在最后一次调用next后才会为true
          // 以本文的例子来说 此时的结果是 { done: true, value: 'success' }
          // 这个value也就是generator函数最后的返回值
          return resolve(value)
        } else {
          // 除了最后结束的时候外,每次调用gen.next()
          // 其实是返回 { value: Promise, done: false } 的结构,
          // 这里要注意的是Promise.resolve可以接受一个promise为参数
          // 并且这个promise参数被resolve的时候,这个then才会被调用
          return Promise.resolve(
            // 这个value对应的是yield后面的promise
            value
          ).then(
            // value这个promise被resove的时候,就会执行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就开始执行了
            //    })
            // })
            function onResolve(val) {
              step("next", val)
            },
            // 如果promise被reject了 就再次进入step函数
            // 不同的是,这次的try catch中调用的是gen.throw(err)
            // 那么自然就被catch到 然后把promise给reject掉啦
            function onReject(err) {
              step("throw", err)
            },
          )
        }
      }
      step("next")
    })
  }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352