【转向Javascript系列】深入理解Generators
随着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的更多相关文章
- 【转向Javascript系列】深入理解Web Worker
本文首发在alloyteam团队博客,链接地址http://www.alloyteam.com/2015/11/deep-in-web-worker/ 上一篇文章<从setTimeout说事件循 ...
- 深入理解JavaScript系列+ 深入理解javascript之执行上下文
http://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html http://blog.csdn.net/hi_kevin/article/d ...
- 【转向Javascript系列】从setTimeout说事件循环模型
本文首发在alloyteam团队博客,链接地址http://www.alloyteam.com/2015/10/turning-to-javascript-series-from-settimeout ...
- 深入理解JavaScript系列(20):《你真懂JavaScript吗?》答案详解
介绍 昨天发的<大叔手记(19):你真懂JavaScript吗?>里面的5个题目,有很多回答,发现强人还是很多的,很多人都全部答对了. 今天我们来对这5个题目详细分析一下,希望对大家有所帮 ...
- 深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点
深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点 2011-12-28 23:00 by 汤姆大叔, 139489 阅读, 119 评论, 收藏, 编辑 才华横溢的 ...
- [JS]深入理解JavaScript系列(4):立即调用的函数表达式
转自:汤姆大叔的博客 前言 大家学JavaScript的时候,经常遇到自执行匿名函数的代码,今天我们主要就来想想说一下自执行.在详细了解这个之前,我们来谈了解一下"自执行"这个叫法 ...
- 深入理解JavaScript系列(转载)
深入理解JavaScript系列 深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点 深入理解JavaScript系列(2):揭秘命名函数表达式 深入理解JavaSc ...
- 深入理解JavaScript系列(50):Function模式(下篇)
介绍 本篇我们介绍的一些模式称为初始化模式和性能模式,主要是用在初始化以及提高性能方面,一些模式之前已经提到过,这里只是做一下总结. 立即执行的函数 在本系列第4篇的<立即调用的函数表达式> ...
- 深入理解JavaScript系列(结局篇)
介绍 最近几个月忙得实在是不可开交,终于把<深入理解JavaScript系列>的最后两篇“补全”了,所谓的全是不准确的,因为很多内容都没有写呢,比如高性能.Ajax安全.DOM详解.Jav ...
随机推荐
- android面试题目大全<完结部分>,android笔试题目集锦
1. 下列哪些语句关于内存回收的说明是正确的? (b ) A. 程序员必须创建一个线程来释放内存 B.内存回收程序负责释放无用内存 C.内存回收程序允许程序员直接释放内存 D.内存回收 ...
- hdu 1536 SG函数模板题
S-Nim Time Limit: 5000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Total Subm ...
- [置顶] 单键模式的C++描述
设计模式-单键(Signelton):其实单键的设计模式说来很简单,说的直白一点就是程序运行过程中保证只有一个实例在运行而已.在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例, ...
- 老罗android开发视频教程 下载地址
感觉老罗android开发视频教程讲得挺好挺全面的,适合新手学习.分享 老罗android开发视频教程 下载地址: 电驴:http://www.verycd.com/topics/2929580/ 老 ...
- KNN算法理解
一.算法概述 1.kNN算法又称为k近邻分类(k-nearest neighbor classification)算法. 最简单平庸的分类器或许是那种死记硬背式的分类器,记住全部的训练数据,对于新的数 ...
- Swift Swift语言Storyboard教程:第二部
本文由CocoaChina翻译小组@TurtleFromMars翻译自raywenderlich,原文:Storyboards Tutorial in Swift: Part 2 更新记录:该Stor ...
- 选择屏幕中的下拉框和dialog中下拉框设计
REPORT YTEST014. PARAMETERS: auart LIKE vapma-auart AS LISTBOX VISIBLE LENGTH 6. AT SELECTION-SC ...
- libevent简单分析
一看名字就知道是围绕eventloop转的. 那首先肯定是eventloop是个什么?一般都是IO事件,timer事件的管理器. 那首先看如何new出来一个eventloop: 1.因为libeven ...
- 做web项目时对代码改动后浏览器端不生效的应对方法(持续更新)
做web项目时,常常会遇到改动了代码,但浏览器端没有生效,原因是多种多样的,我会依据我遇到的情况逐步更新解决的方法 1.执行的时候採用debug模式,普通情况下使用项目部署button右边那个butt ...
- 14.3.2.2 autocommit, Commit, and Rollback 自动提交 提交和回滚
14.3.2.2 autocommit, Commit, and Rollback 自动提交 提交和回滚 如果自动提交模式被启用,在InnoDB里, 所有的用户活动发生在一个事务里, 每个SQL语句 ...