随着JavaScript语言的发展,ES6规范带来了许多内容, 其中生成器Generators是一项重要的特性。 利用这一特性,可以简化迭代器的创建, 更加令人兴奋的是Generators允许在函数执行过程中暂停、并在将来某一时刻恢复执行。 这一特性改变了以往函数必须执行完成才返回的特点, 将这一特性应用到异步编码中, 可以有效的简化异步方法的写法,同时避免陷入回调地狱。
Generators 介绍
一个简单的Generator函数示例:
function* fn() {
yield 1;
yield 2;
yield 3;
}
var fun = fn();
fun.next(); // {value: 1, done: false}
fun.next(); // {value: 2, done: false}
fun.next(); // {value: 3. done: false}
fun.next();// {value: undefiend, done: true}
上述代码中定义了一个生成器函数, 当调用生成器函数 fn()时, 并不是立即执行该函数, 而是返回一个生成器对象。 每当调用生成器对象的.next()方法时, 函数将运行到下一个yield表达式, 返回表达式的结果并暂停自身。 当抵达生成器函数的末尾时, 返回结果中done的值变为true, value的值为undefiend。我们将上述fn()函数称之为生成器函数, 与普通函数相比二者有如下区别:
- 普通函数使用function声明, 生成器函数用function*声明
- 普通函数使用 return 返回值, 生成器函数使用 yield返回值
- 普通函数是 run to completion模式, 即普通函数开始执行后, 会一直执行函数所有语句执行完成,在此期间别的代码语句是不会被执行的; 而生成器函数是run-pause-run模式, 即生成器函数可以在函数运行中被暂停一次或多次, 并且在后面再恢复执行,在暂停期间允许其他代码语句被执行。
yield在JavaScript中如何实现的呢?
首先,生成器不是线程,支持线程的语言中,多端代码可以同一时间运行, 这经常会导致资源竞争, 使用得当会有不错的性能提升。 生成器则完全不同, JavaScript执行引擎仍然是一个基于事件循环机制的单线程环境。 当生成器运行的时候, 它会在叫做caller的同一线程中运行。 执行的顺序是有序、确定的, 并且永远不会产生并发。 不同于系统的线程, 生成器只会在其内部用到 yield的时候才会被挂起。
通过babel
将生成器函数转换为ES5:
function* fn() {
yield 1;
yield 2;
yield 3;
}
var fun=fn();
fun.next();
"use strict";
var _marked = /*#__PURE__*/regeneratorRuntime.mark(fn);
function fn() {
return regeneratorRuntime.wrap(function fn$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return 1;
case 2:
_context.next = 4;
return 2;
case 4:
_context.next = 6;
return 3;
case 6:
case "end":
return _context.stop();
}
}
}, _marked);
}
var fun = fn();
fun.next();
从转换后的代码中可以看到, yield表达值转换时, Regenerator将生成器函数中的yield表达式重写为 switch case, 同时在每个case中使用context$1$0来保存函数当前的上下文状态。
switch case之外, 迭代器函数fn被regeneratorRuntime.mark包装, 返回一个被regeneratorRuntime.wrap包装的迭代器对象。
生成器函数的运行时Regenerator通过工具函数将生成器函数包装, 为其添加如next/return 等方法。同是也对返回的生成器对象进行包装, 使得对next等方法的调用, 最终进入由switch case 组成的状态机模型中。而保存生成器函数上下文信息则利用了闭包的技巧。
- yield关键字采用了编译转换思路, 运用状态机模型, 同时保存函数上下文信息,最终实现了新的yield关键字带来的新的语言特征。