手写generator核心原理及源码简析

背景

阮一峰在《es6标准入门》一书中,对async和await的讲解中有这样一句话:async和await其实是generator的语法糖,所以想真正理解async和await,深入学习一下generator是有必要的,本篇文章会对generator的核心流程手写重现,并分析一下相关源码,了解实现流程。

generator简介

我们日常开发中,其实对于generator的应用应该是比较少的,所以先简单介绍一下generator。

什么是generator

generator翻译过来就是发生器、生成器的意思,而实际上generator就是一个比较特殊的函数。

与普通函数写法上的区别

先看一下一个最简单的generator代码

function* gen(){
  yield 'result1'
  yield 'result2'
  yield 'result3'
  return 'ending'
}

从代码中可以看出,与普通函数的写法上的区别主要有两点,一点为function关键字后有一个星号,另一点为内部使用yield关键字来声明了一系列的状态

与普通函数使用上的区别

依然先看例子

function* gen(){
  yield 'result1'
  yield 'result2'
  yield 'result3'
  return 'ending'
}

var demo = gen() // 返回值为迭代器对象
console.log(demo.next()) // {value:'result1',done:false}
console.log(demo.next()) // {value:'result2',done:false}
console.log(demo.next()) // {value:'result3',done:false}
console.log(demo.next()) // {value:'ending',done:true}
console.log(demo.next()) // {value:undefind,done:true}

var demo2 = gen() // 一个全新的对象
console.log(demo.next()) // {value:'result1',done:false}

从代码中看出,调用定义的generator,获取到的实际上是一个对象,而且是互相独立的对象,到这可以理解为什么会起这样一个名字,因为每一个generator(生成器)调用的时候,都可以理解为生成了一个迭代器对象,这个对象中用next方法,当我们调用next方法时,代码会分段执行,每次执行到遇到yield为止,返回值为一个包含value和done的对象,value为我们yield声明的状态。当全部yield执行完后,done会变成true,如果我们遇到了return语句,会把返回值作为最终的value,如果没有就直接返回undefined。

以上就是generator的主逻辑,通俗的讲,其实就是一个思想,将函数的执行权交给了使用者,分段执行。

这一点其实和async和await是有一定的相似之处的,async和await是把await后面的代码暂时挂起,等待await的代码块执行完毕,再执行后面,所以说async和await是generator的语法糖是说得通的。

手写generator

接下来,我们从一个最简单的例子开始实现代码的手写。

function* gen(){
  yield 'result1'
  yield 'result2'
  yield 'result3'
  return 'ending'
}

首先我们整理一下思路,我们每次调用,都会执行到下一个yield,并返回我们设定的状态,如果用最基本的逻辑,我们可以选择用一个变量来确认我们执行到了哪一步,然后返回不同的返回值,如果不考虑其他,单纯识别执行步骤和返回结果,这里用switch case来实现是最简单的。

function gen$(nextStep){
  switch (nextStep){
    case 0 :
      return 'result1';
    case 1 :
      return 'result2';
    case 2 :
      return 'result3';
    case 3 :
      return 'ending'
  }
}

我们按最基本的思路,实现了一个简单的识别执行步骤,再进行返回的逻辑。这个函数可以是我们手写generator的一部分,nextStep在调用的时候传入,那么这个nextStep就有了一些说法,我们是让这个变量变成闭包变量,还是变成全局变量,就需要进行选择。

我们在这里可以简单思考一下,回想一下之前写的demo,在yield语句是否全部执行完,返回的done状态是不一样的,而done是根据执行的步骤产生的变化,所以大概率跟nextStep是同级的变量,如果较多变量变成闭包变量,这肯定是不太合适的,所以这里我们选择用全局变量的方式。

var context = {
  prev: 0,
  next: 0
};
function gen$(context) {
  switch (context.prev = context.next) {
    case 0:
     context.next = 1;
     return 'result1';
    case 1:
      context.next = 2;
      return 'result2';
    case 2:
      context.next = 3;
      return 'result3';
    case 3:
      return 'ending';
  }
}

目前为止,我们只需要上一步、下一步两个变量,但是后续功能逐渐完善之后,我们还可能会继续添加其他的方法、属性等,所以在这里声明一个上下文对象context,作为后续全局变量、方法的容器。我们用上一步、下一步值更替替代了原本的逻辑。

我们回过头再看最开始的generator例子,我们还有很多不一致的,首先,第一次调用gen函数的时候,返回值应该是一个对象,这个对象包含一个next方法。调用这个next方法,返回值是一个对象,对象包含我们对每一步定义的结果value,以及当前运行状态done。我们按照这个思路来升级代码

var context = {
  prev: 0,
  next: 0,
  done: false,
  stop: function() {
    this.done = true
  }
}

function gen$(context) {
  switch (context.prev = context.next) {
    case 0:
     context.next = 1;
     return 'result1';
    case 1:
      context.next = 2;
      return 'result2';
    case 2:
      context.next = 3;
      return 'result3';
    case 3:
      context.stop();
      return 'ending';
  }
}

function foo() {
  return {
    next: function() {
      var value = gen$(context);
      var done = context.done
      return {
        value,
        done
      }
    }
  }
}

对代码进行如下升级后,我们基本上已经达成了generator最核心的逻辑,调用foo返回一个对象,对象包含next方法,调用next方法返回了我们设定的结果值和执行状态,但是还有另外一个较为核心的问题没有解决,就是现在只有一个context上下文对象,如果我们用foo函数生成了多个对象,这些对象其实是共用了这一个context,我们要做的最后一件事,就是要确保foo返回的不同对象里跟随独立的context,这里我们参考开发模式之一的单例模式来处理。

class Context {
  constructor() {
    this.prev = 0
    this.next = 0
    this.done = false
  }
  stop() {
    this.done = true
  }
}
function foo() {
  var context = new Context
    return {
    next: function() {
      var value = gen$(context);
      var done = context.done
      return {
        value,
        done
      }
    }
  }
}

我们把context变成一个类,每一次调用foo都实例化一个context来跟随对象,至此我们算是手写了generator最核心部分的原理。

generator源码简析

babel编译

我们虽然手写并实现了generator的核心原理,但是我们还需要确认我们的思路究竟是否正确,所以研究一下源码是有必要的,下面我们来看一下babel对一个最简单generator的编译结果

// 示例
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
 
console.log(hw.next()); // {value: "hello", done: false}
console.log(hw.next()); // {value: "world", done: false}
console.log(hw.next()); // {value: "ending", done: true}
console.log(hw.next()); // {value: undefined, done: true}
// 编译结果

var _marked = /*#__PURE__*/ regeneratorRuntime.mark(helloWorldGenerator)

function helloWorldGenerator() {
  return regeneratorRuntime.wrap(
    function helloWorldGenerator$(_context) {
      while (1) {
        switch ((_context.prev = _context.next)) {
          case 0:
            _context.next = 2;
            return "hello";
 
          case 2:
            _context.next = 4;
            return "world";
 
          case 4:
            return _context.abrupt("return", "ending");
 
          case 5:
          case "end":
            return _context.stop();
        }
      }
    },
    _marked);
}

以上就是generator的编译结果,乍一看代码并不多,看内部的逻辑也是用switch case实现的,大体和我们的思路相同,但是细看会发现,有几个东西不认识,regeneratorRuntime是个什么鬼,mark和wrap又是个啥?想要弄懂原理,我们有必要搞清楚这些东西都是什么。

先说一下regenerator,这个是facebook旗下的一个工具,用来编译es6的generator,如果想看到完整的generator代码,需要去这个工具里去看源码。

mark函数

我们先查看完整的mark函数源码

runtime.mark = function(genFun) {
  genFun.__proto__ = GeneratorFunctionPrototype;
  genFun.prototype = Object.create(Gp);
  return genFun;
};

这部分代码比较少,虽然又牵扯到了我们两个不知道的东西,GeneratorFunctionPrototype和Gp,但是其实也无关紧要,从这部分代码中我们可以看出,mark函数其实就是对我们传入的genFun绑定了一系列的原型,继承了一些属性方法(想查看具体继承了什么可以查阅上面提到的regenerator)。

wrap函数

接下来我们再看看wrap函数究竟做了什么

function wrap(innerFn, outerFn, self) {
  var generator = Object.create(outerFn.prototype);
  var context = new Context([]);
  generator._invoke = makeInvokeMethod(innerFn, self, context);

  return generator;
}

从这段代码中可以看出,wrap做的东西比较简单,创建了一个generator,new了一个context对象,再给generator绑定了一个invoke方法,该方法是makeInvokeMethod,接收了三个参数,innerFn,self以及context,最后再把generator返回。

到这里我们先联系一下最开始我们用babel编译的结果,helloWorldGenerator被分成了两部分,一部分是外层的helloWorldGenerator函数,另一部分是用wrap包裹的helloWorldGenerator$函数,而wrap函数接受的是内层函数,所以在wrap定义中,第一个参数是innerFn,也就是内层函数的意思。

但是这部分代码里,我们还有一些东西不知道,Context类和makeInvokeMethod函数还需要继续阅读源码,我们先看context。

var ContinueSentinel = {};

var context = {
  done: false,
  method: "next",
  next: 0,
  prev: 0,
  abrupt: function(type, arg) {
    var record = {};
    record.type = type;
    record.arg = arg;

    return this.complete(record);
  },
  complete: function(record, afterLoc) {
    if (record.type === "return") {
      this.rval = this.arg = record.arg;
      this.method = "return";
      this.next = "end";
    }

    return ContinueSentinel;
  },
  stop: function() {
    this.done = true;
    return this.rval;
  }
};

以上就是generator中,对于context的定义,这部分可以联系我们之前手写那部分中的context,功能大体相同,存储了上下步next和prev,是否完成的done,还有一些方法。只是单纯看这部分代码,还是不是很好理解,我们接下来联系makeInvokeMethod方法的源码来一起理解。


var ContinueSentinel = {};
 
function makeInvokeMethod(innerFn, self, context) {
  // 状态设置为start
  var state = 'start';
  return function invoke(method, arg) {
  // 已完成
    if (state === 'completed') {
      return { value: undefined, done: true };
    }
    context.method = method;
    context.arg = arg;
    // 执行中
    while (true) {
      state = 'executing';
      var record = {
        type: 'normal',
        arg: innerFn.call(self, context) // 执行下一步,并获取状态(其实就是switch里return的值)
      };
      if (record.type === "normal") {
      // 判断是否已经执行完成
        state = context.done
          ? 'completed'
          : 'yield';
 		  // 	ContinueSentinel其实是一个空对象,record.arg === {}则跳过return进入下一个循环,那什么什么record.arg会为空对象呢,答案是没有后续yield语句或已经return 的时候,也就是switch反悔了空值的情况
        if (record.arg === ContinueSentinel) {
          continue;
        }
        return {
          value: record.arg,
          done: context.done
        };

      }
    }
  };
}

我们把这两部分代码连着解读,当函数一开始执行的时候,我们把状态设置为start,该状态被内部返回的invoke方法占用,所以不会被销毁,invoke内部先判定state是否是completed状态,如果是直接返回最终状态,如果不是我们把invoke方法传入method和arg赋值给上下文对象context。状态不为结束时,会进入下面的循环,在这里状态被改成了executing,在循环中最终的结果就是返回了value和done,只是在循环中,增加了运行状态的一些判定。

现在整片源码,我们剩下的只有循环中的数据处理,以及联系context上下文方法解读这两个部分没有分析完毕,我们再继续看这两个部分。

先看循环内部,循环的一开始,我们声明了record对象,定义了type为normal,arg为innerFn的返回值,也就是switch case那部分函数的返回值,而innerFn的返回值就是我们设定的每一步的结果。

再联系helloWorldGenerator$,也就是innerFn的内部逻辑,只有在倒数第二步的时候才通过调用context中的abrupt修改了record的type,把type修改为了return,abrupt又调用了complete方法,complete方法把record里面的arg,也就是我们设定的状态赋值给了context内部的rval和自己的arg,然后返回了ContinueSentinel这个空对象。这里我们连起来看,就是在没有执行到倒数第二步的时候,循环内声明的record的type一直是normal,arg一直是我们已经写好了的结果,到了倒数第二步的时候会有一些不同,这里我们的type变成了return,arg变成了ContinueSentinel这个空对象。然后在循环内部,record.arg === ContinueSentinel这个判定生效,没有执行到return,直接continue进入下一轮的循环。

而最后一轮的循环就很清晰了,调用了stop方法,stop把done变成了true,把我们在上一轮存起来的arg返回,形成了最终的结果,在最后一轮循环中,state因为context.done的值发生了变化, context.done ? ‘completed’ : 'yield的三元运算符也将取到completed的值,这样保证了在最后一次执行结束后,再进行调用的时候再函数的上层就直接return了{ value: undefined, done: true }这个结果,而且在complete调用时,也修改了next为end,同时保证了在拿到最终结果多次调用的时候也会走invoke函数。

至此为止,对源码的分析已经结束了,但是我们回过头看,这个invoke的功能是不是觉得有几分熟悉?这不就是调用generator后,返回的对象的next方法吗?可是为什么变了个名字?我们再查阅源码可以看出,其实invoke就是next方法。

// Helper for defining the .next, .throw, and .return methods of the
// Iterator interface in terms of a single ._invoke method.
function defineIteratorMethods(prototype) {
    ["next", "throw", "return"].forEach(function(method) {
      prototype[method] = function(arg) {
        return this._invoke(method, arg);
      };
    });
}

defineIteratorMethods(Gp);

这是在facebook的runtime中的一段代码,为generator生成迭代器对象绑定了next,throw,return三个方法,而这三个方法的原型上都绑定了_invoke,所以实际上我们在使用的时候,调用的next就是invoke

总结

我们现在回到开始,重新考虑,其实generator的核心部分和我们手写的代码模式差不多,都分成了三部分,上下文对象,函数主体,还有逻辑处理这三个部分。

其实generator的核心就是在于上下文的保存,函数并没有真的被挂起,每一次yield,其实都执行了一遍传入的生成器函数,只是在这个过程中间用了一个context对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行,看起来就像函数被挂起了一样

那么再看,async和await其实是generator的语法糖这一句话也就可以理解了,我们把两者互相比对不难发现,async和await给人的感觉也是类似挂起的感觉,把后面代码挂起,等await的代码有了结果再向后执行,从挂起的思路来看,这两者是一致的。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
generator是一种特殊的函数,可以被暂停和恢复执行。在generator函数中,使用关键字function*来定义一个生成器函数。生成器函数内部使用yield关键字来暂停执行,并通过yield返回一个值。当调用生成器函数时,它返回一个迭代器对象,可以通过调用next()方法来恢复执行,并返回yield语句后的值。每次调用next()方法时,生成器函数会从上次暂停的地方继续执行,直到遇到下一个yield语句或函数结束为止。通过这种方式,生成器函数可以实现在执行过程中主动交出控制权,从而实现一种协程的效果。 Express-generator是一个应用生成器,用于快速创建Express应用的骨架。它可以通过生成器工具来创建一个基础的Express应用结构,包括路由、模板引擎、静态文件等等。它是基于Express框架的一个工具,通过使用Express-generator可以大大简化Express应用的创建过程,提高开发效率。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [应用生成器express-generator/路由,数据库连接池](https://blog.csdn.net/z18237613052/article/details/115092982)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [前端面试系列-JavaScript-理解generator实现原理](https://blog.csdn.net/qq_39903567/article/details/115188020)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值