随着Javascript语言的发展,ES6规范为我们带来了许多新的内容,其中生成器Generators是一项重要的特性。利用这一特性,我们可以简化迭代器的创建,更加令人兴奋的,是Generators允许我们在函数执行过程中暂停、并在将来某一时刻恢复执行。这一特性改变了以往函数必须执行完成才返回的特点,将这一特性应用到异步代码编写中,可以有效的简化异步方法的写法,同时避免陷入回调地狱。

本文将对Generators进行简单介绍,然后结合笔者在C#上的一点经验,重点探讨Generators运行机制及在ES5的实现原理。

1.Generators简单介绍

一个简单的Generator函数示例

function* example() {
yield 1;
yield 2;
yield 3;
}
var iter=example();
iter.next();//{value:1,done:false}
iter.next();//{value:2,done:false}
iter.next();//{value:3,done:false}
iter.next();//{value:undefined,done:true}

上述代码中定义了一个生成器函数,当调用生成器函数example()时,并非立即执行该函数,而是返回一个生成器对象。每当调用生成器对象的.next()方法时,函数将运行到下一个yield表达式,返回表达式结果并暂停自身。当抵达生成器函数的末尾时,返回结果中done的值为true,value的值为undefined。我们将上述example()函数称之为生成器函数,与普通函数相比二者有如下区别

  • 普通函数使用function声明,生成器函数用function*声明
  • 普通函数使用return返回值,生成器函数使用yield返回值
  • 普通函数是run to completion模式,即普通函数开始执行后,会一直执行到该函数所有语句完成,在此期间别的代码语句是不会被执行的;生成器函数是run-pause-run模式,即生成器函数可以在函数运行中被暂停一次或多次,并且在后面再恢复执行,在暂停期间允许其他代码语句被执行

对于Generators的使用,本文不再多做介绍,如需了解更多内容推荐阅读下面系列文章,《ES6 Generators: Complete Series》或者《深入掌握 ECMAScript 6 异步编程》系列文章

2.Generators in C#

生成器不是一个新的概念,我最初接触这一概念是在学习使用C#时。C#从2.0版本便引入了yield关键字,使得我们可以更简单的创建枚举数和可枚举类型。不同的是C#中未将其命名为生成器Generators,而将其称之为迭代器。

本文不会介绍C#中可枚举类IEnumerable和枚举数IEnumerator内容,如需了解推荐阅读《C#4.0图解教程》相关章节。

2.1 C#迭代器介绍

让我们先看一个示例,下面方法声明实现了一个产生和返回枚举数的迭代器

public IEnumerable <int> Example()
{
yield return ;
yield return ;
yield return ;
}

方法定义与ES6 Generators定义很接近,定义中声明返回了一个int类型的泛型可枚举类型,方法体内通过yield return语句返回值并将自身暂停执行。

使用迭代器来创建可枚举类型的类

class YieldClass
{
public IEnumerable<int> Example()//迭代器
{
yield return ;
yield return ;
yield return ;
}
}
class Program
{
static void Main()
{
YieldClass yc=new YieldClass ();
foreach(var a in yc.Example())
Console.WriteLine(a);
}
}

上述代码会产生如下输入


2.2 C#迭代器原理

在.Net中,yield并不是.Net runtime的特性,而是一个语法糖,代码编译时,这一语法糖会被C#编译器编译成简单的IL代码。

继续研究上述示例,通过Reflector反编译工具可以看到,编译器为我们生成了一个带有如下声明的内部类

[CompilerGenerated]
private sealed class YieldEnumerator :
IEnumerable<object>, IEnumerator<object>
{
// Fields字段
private int state;
private int current;
public YieldClass owner;
private int initialThreadId; // Methods方法
[DebuggerHidden]
public YieldEnumerator(int state);
private bool MoveNext();
[DebuggerHidden]
IEnumerator<int> IEnumerable<int>.GetEnumerator();
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator();
[DebuggerHidden]
void IEnumerator.Reset();
void IDisposable.Dispose(); // Properties属性
object IEnumerator<object>.Current
{ [DebuggerHidden] get; } object IEnumerator.Current
{ [DebuggerHidden] get; }
}

原始的Example()方法仅返回一个YieldEnumerator的实例,并将初始状态-2传递给它自身和其引用者,每一个迭代器保存一个状态指示

  • -2:初始化为可迭代类Enumerable
  • -1:迭代结束
  • 0:初始化为迭代器Enumerator
  • 1-n:原始Example()方法中的yield return索引值

Example()方法中代码被转换为YieldingEnumerator.MoveNext(),在我们的示例中转换后代码如下

bool MoveNext()
{
switch (state)
{
case :
state = -;
current = ;
state = ;
return true;
case :
state = -;
current = ;
state = ;
return true;
case :
state = -;
current = ;
state = ;
return true;
case :
state = -;
break;
}
return false;
}

利用上述的代码转换,编译器为我们生成了一个状态机,正是基于这一状态机模型,实现了yield关键字的特性。

迭代器状态机模型可如下图所示

  • Before为迭代器初始状态
  • Running 为调用MoveNext后进入这个状态。在这个状态,枚举数检测并设置下一项的位置。遇到yield return、yield break或者迭代结束时,退出该状态
  • Suspended为状态机等待下次调用MoveNext的状态
  • After为迭代结束的状态

3.Generators in Javascript

通过阅读上文,我们了解了Generator在C#中的使用,并且通过查看编译器生成的IL代码,得知编译器会生成一个内部类来保存上下文信息,然后将yield return表达式转换成switch case,通过状态机模式实现yield关键字的特性。

3.1 Javascript Generators原理浅析

yield关键字在Javascript中如何实现呢?

首先,生成器不是线程。支持线程的语言中,多段不同的代码可以在同一时候运行,这经常会导致资源竞争,使用得当会有不错的性能提升。生成器则完全不同,Javascript执行引擎仍然是一个基于事件循环的单线程环境,当生成器运行的时候,它会在叫做 caller 的同一个线程中运行。执行的顺序是有序、确定的,并且永远不会产生并发。不同于系统的线程,生成器只会在其内部用到 yield 的时候才会被挂起。

既然生成器并非由引擎从底层提供额外的支持,我们可以沿用上文在C#中对yield特性的原理探究的经验,将生成器视为一个语法糖,用一个辅助工具将生成器函数转换为普通的Javascript代码,在经过转换的代码中,有两个关键点,一是要保存函数的上下文信息,二是实现一个完善的迭代方法,使得多个yield表达式按序执行,从而实现生成器的特性。

3.2 How Generators work in ES5

Regenerator工具已经实现了上述思路,借助Regenerator工具,我们已经可以在原生ES5中使用生成器函数,本节我们来分析Regenerator实现方式以深入理解Generators运行原理。

通过这个在线地址可以方便的查看经过转换后的代码,仍然以文章初始为例

function* example() {
yield 1;
yield 2;
yield 3;
}
var iter=example();
iter.next();

经过转换后为

var marked0$0 = [example].map(regeneratorRuntime.mark);
function example() {
return regeneratorRuntime.wrap(function example$(context$1$0) {
while (1) switch (context$1$0.prev = context$1$0.next) {
case 0:
context$1$0.next = 2;
return 1; case 2:
context$1$0.next = 4;
return 2; case 4:
context$1$0.next = 6;
return 3; case 6:
case "end":
return context$1$0.stop();
}
}, marked0$0[0], this);
}
var iter = example();
iter.next();

从转换后的代码中可以看到,与C#编译器对yield return表达式的转换相似,Regenerator将生成器函数中的yield表达式重写为switch case,同时,在每个case中使用context$1$0来保存函数当前的上下文状态。

switch case之外,迭代器函数example被regeneratorRuntime.mark包装,返回一个被regeneratorRuntime.wrap包装的迭代器对象。

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

通过mark包装,将example包装成如下对象

当调用生成器函数example()时,返回一个被wrap函数包装后的迭代器对象

runtime.wrap=function (innerFn, outerFn, self, tryLocsList) {
// If outerFn provided, then outerFn.prototype instanceof Generator.
var generator = Object.create((outerFn || Generator).prototype);
var context = new Context(tryLocsList || []); // The ._invoke method unifies the implementations of the .next,
// .throw, and .return methods.
generator._invoke = makeInvokeMethod(innerFn, self, context); return generator;
}

返回的迭代器对象如下所示

当调用迭代器对象iter.next()方法时,因为有如下代码,所以会执行_invoke方法,而根据前面wrap方法代码可知,最终是调用了迭代器对象的makeInvokeMethod (innerFn, self, context);方法

// 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);
};
});
}

makeInvokeMethod方法内容较多,这里选取部分分析。首先,我们发现生成器将自身状态初始化为“Suspended Start”

function makeInvokeMethod(innerFn, self, context) {
var state = GenStateSuspendedStart; return function invoke(method, arg) {

makeInvokeMethod返回invoke函数,当我们执行.next方法时,实际调用的是invoke方法中的下面语句

var record = tryCatch(innerFn, self, context);

这里tryCatch方法中fn为经过转换后的example$方法,arg为上下文对象context,因为invoke函数内部对context的引用形成闭包引用,所以context上下文得以在迭代期间一直保持。

function tryCatch(fn, obj, arg) {
try {
return { type: "normal", arg: fn.call(obj, arg) };
} catch (err) {
return { type: "throw", arg: err };
}
}

tryCatch方法会实际调用example$方法,进入转换后的switch case,执行代码逻辑。如果得到的结果是一个普通类型的值,我们将它包装成一个可迭代对象格式,并且更新生成器状态至GenStateCompleted或者GenStateSuspendedYield

var record = tryCatch(innerFn, self, context);
if (record.type === "normal") {
// If an exception is thrown from innerFn, we leave state ===
// GenStateExecuting and loop back for another invocation.
state = context.done
? GenStateCompleted
: GenStateSuspendedYield; var info = {
value: record.arg,
done: context.done
};

4.总结

通过对Regenerator转换后的生成器代码及工具源码分析,我们探究了生成器的运行原理。Regenerator通过工具函数将生成器函数包装,为其添加如next/return等方法。同时也对返回的生成器对象进行包装,使得对next等方法的调用,最终进入由switch case组成的状态机模型中。除此之外,利用闭包技巧,保存生成器函数上下文信息。

上述过程与C#中yield关键字的实现原理基本一致,都采用了编译转换思路,运用状态机模型,同时保存函数上下文信息,最终实现了新的yield关键字带来的新的语言特性。

参考文章

1.ES6 Generators:Complete Series系列文章

2.深入浅出ES6 Generators

3.《深入掌握 ECMAScript 6 异步编程》系列文章

4.ES6 Generators:How do they work?

5.Behind the scenes of the C# yield keyword

【转向Javascript系列】深入理解Generators的更多相关文章

  1. 【转向Javascript系列】深入理解Web Worker

    本文首发在alloyteam团队博客,链接地址http://www.alloyteam.com/2015/11/deep-in-web-worker/ 上一篇文章<从setTimeout说事件循 ...

  2. 深入理解JavaScript系列+ 深入理解javascript之执行上下文

    http://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html http://blog.csdn.net/hi_kevin/article/d ...

  3. 【转向Javascript系列】从setTimeout说事件循环模型

    本文首发在alloyteam团队博客,链接地址http://www.alloyteam.com/2015/10/turning-to-javascript-series-from-settimeout ...

  4. 深入理解JavaScript系列(20):《你真懂JavaScript吗?》答案详解

    介绍 昨天发的<大叔手记(19):你真懂JavaScript吗?>里面的5个题目,有很多回答,发现强人还是很多的,很多人都全部答对了. 今天我们来对这5个题目详细分析一下,希望对大家有所帮 ...

  5. 深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点

    深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点 2011-12-28 23:00 by 汤姆大叔, 139489 阅读, 119 评论, 收藏, 编辑 才华横溢的 ...

  6. [JS]深入理解JavaScript系列(4):立即调用的函数表达式

    转自:汤姆大叔的博客 前言 大家学JavaScript的时候,经常遇到自执行匿名函数的代码,今天我们主要就来想想说一下自执行.在详细了解这个之前,我们来谈了解一下"自执行"这个叫法 ...

  7. 深入理解JavaScript系列(转载)

    深入理解JavaScript系列 深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点 深入理解JavaScript系列(2):揭秘命名函数表达式 深入理解JavaSc ...

  8. 深入理解JavaScript系列(50):Function模式(下篇)

    介绍 本篇我们介绍的一些模式称为初始化模式和性能模式,主要是用在初始化以及提高性能方面,一些模式之前已经提到过,这里只是做一下总结. 立即执行的函数 在本系列第4篇的<立即调用的函数表达式> ...

  9. 深入理解JavaScript系列(结局篇)

    介绍 最近几个月忙得实在是不可开交,终于把<深入理解JavaScript系列>的最后两篇“补全”了,所谓的全是不准确的,因为很多内容都没有写呢,比如高性能.Ajax安全.DOM详解.Jav ...

随机推荐

  1. C# 它 抽象类和接口

    抽象类 C#同意把类和方法声明为abstract,即抽象类和抽象方法.抽象类通常代表一个抽象概念,它提供一个继承的出发点,当设计一个新的对象类时,一定是用来继承的,所以,在一个以继承关系形成的等级结构 ...

  2. [WP8] Binding时,依照DataType来选择DataTemplate

    原文 [WP8] Binding时,依照DataType来选择DataTemplate 范例下载 范例程序代码:点此下载 问题情景 在开发WPF.WP8...这类应用程序的时候,透过Binding机制 ...

  3. zabbix 监控jvm

    tomcat 机器配置: [root@wx03 lib]# pwd /usr/local/apache-tomcat-7.0.55/lib [root@wx03 lib]# ls -ltr *jmx* ...

  4. XML wsdl soap xslt xsl ide

    XML wsdl soap  xslt xsl ide 作者Attilax ,  EMAIL:1466519819@qq.com  来源:attilax的专栏 地址:http://blog.csdn. ...

  5. 判断指定进程是否为x64的方法(在ntdll判断某个x64函数是否存在)

    BOOL IsWow64ProcessEx(HANDLE hProcess) { // 如果系统是x86的,那么进程就不可能有x64 bool isX86 = false; #ifndef _WIN6 ...

  6. c++ inheritance -- 继承

    c++ inheritance -- 继承 终于要决心弄明白继承了,以前仅限于大学时学习,以后工作也没有用,现在就依照(百度百科)文章写些测试的代码. 文章说 ==================== ...

  7. Test oracle db iops

    Today, i need to test one database's iops and do something for oracle db's io test. How to test the ...

  8. 1038. Recover the Smallest Number (30) - 字符串排序

    题目例如以下: Given a collection of number segments, you are supposed to recover the smallest number from ...

  9. Microsoft JET Database Engine 错误 '80004005'不是一个有效的路径。 确定路径名称拼写是否正确,以及是否连接到文件存放的服务器。

  10. Delphi的VMT的结构图,很清楚

    Every Delphi class is defined internally by its vmt—​its virtual-method table. The vmt contains a li ...