Generator与async/await与Generator的模拟
Generator
函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
next 方法的参数
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.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);
}
基于 Promise 对象的自动执行
首先,把fs模块的readFile方法包装成一个 Promise 对象
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) return reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
然后,手动执行上面的 Generator 函数。
var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
});
手动执行其实就是用then方法,层层添加回调函数。理解了这一点,就可以写出一个自动执行器。一般使用模块。
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
run(gen);
上面代码中,只要 Generator 函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。
async/await
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
async函数对 Generator 函数的改进,体现在以下四点。
(1)内置执行器。
Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。
(2)更好的语义。
async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。
co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
(4)返回值是 Promise。
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。
async 函数的实现原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。
下面给出spawn函数的实现,基本就是前文自动执行器的翻版。
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
模拟
生成器是通过暂停自己的作用域 / 状态实现它的“魔法”的。可以 通过函数闭包
- 1 是起始状态
- 2 是 request(..) 成功后的状态
- 3 是 request(..) 失败的状态
function foo(url) {
var state;//管理状态
var val;//生成器变量范围声明
function process(v) {
switch (state) {
case 1:
return request(url);
case 2:
val = v;
return;
case 3:
var err = v;
console.log( "Oops:", err );
return false;
}
// 构造并返回一个生成器
return {
next: function(v) { // 初始状态
if (!state) {
state = 1;
return {
done: false,
value: process()
};
}
// yield成功恢复
else if (state == 1) {
state = 2;
return {
done: true,
value: process( v )
};
}
// 生成器已经完成
else {
return {
done: true,
value: undefined
};
} },
"throw": function(e) {
// 唯一的显式错误处理在状态1
if (state == 1) {
state = 3;
return {
done: true,
value: process( e )
};
}
// 否则错误就不会处理,所以只把它抛回 else {
throw e; }
} };
}
这段代码是如何工作的呢?
- (1) 对迭代器的 next() 的第一个调用会把生成器从未初始化状态转移到状态 1,然后调用 process() 来处理这个状态。request(..) 的返回值是对应 Ajax 响应的 promise,作为 value 属性从 next() 调用返回。
- (2) 如果 Ajax 请求成功,第二个 next(..) 调用应该发送 Ajax 响应值进来,这会把状态转 移到状态 2。再次调用 process(..)(这次包括传入的 Ajax 响应值),从 next(..) 返回 的 value 属性将是 undefined。
- (3) 然而,如果 Ajax 请求失败的话,就会使用错误调用 throw(..),这会把状态从 1 转移到 3(而非 2)。再次调用 process(..),这一次包含错误值。这个 case 返回 false,被作 为 throw(..) 调用返回的 value 属性。
从外部来看(也就是说,只与迭代器交互),这个普通函数 foo(..) 与生成器 *foo(..) 的 工作几乎完全一样。所以我们已经成功地把 ES6 生成器转为了前 ES6 兼容代码!
然后就可以手工实例化生成器并控制它的迭代器了,调用var it = foo("..")和 it.next(..) 等。甚至更好的是,我们可以把它传给前面定义的工具 run(..),就像 run(foo,"..")。