一、Generator基础认知
最基础的原则就是见到yield就暂停,next()就继续到下一个yield……以此知道函数执行完毕。
先不上理论,直接看一段代码,这里的step()是一个辅助函数,用来控制迭代器,替代手动next()
var a = 1;
var b = 2;
function* foo() {
a++;
yield;
b = b * a;
a = (yield b) + 3;
}
function* bar() {
b--;
yield;
a = (yield 8) + b;
b = a * (yield 2);
}
function step(gen) {
var it = gen();
var last;
return function() {
// 不管yield出来的是什么,下一次都把它原样传回去!
last = it.next(last).value;
};
}
a = 1;
b = 2;
var s1 = step(foo);
var s2 = step(bar);
yield和next()调用有一个数量上的不匹配,就是说,想要完整跑完一个生成器函数,next()调用总是比yield的数量多一次
为什么会有这个不匹配?
因为第一个 next(..) 总是启动一个生成器,并运行到第一个 yield 处。不过,是第二个 next(..) 调用完成第一个被暂停的 yield 表达式,第三个 next(..) 调用完成第二个 yield, 以此类推。
所以上面的代码中,foo有两个yield,bar有三个yield
所以接下来要跑三次s1(),四次s2()
我们在控制台看每一步的输出,一步一步来分析

分析到这里,对generator的基础工作原理应该就有了大概的认知了。
如果想加深一点理解(皮一下),可以随意调换一下s1和s2的执行顺序,总之就是三个s1和四个s2,对于理解多个生成器如何在共享的作用域上并发运行也有指导意义。
二、异步迭代生成器
这一段,我们来理解一下生成器与异步编程之间的问题,最直接的就是网络请求了
let data = ajax(url); // ajax是假设的封装过的网络请求方法,并不是原生那家伙
console.log(data)
这段代码,大家都知道不能正常工作吧,data是underfined
ajax是一个异步操作,它并没有停下来等到拿到数据之后再赋值给data
而是在发出请求之后,直接就执行了下一句console.log(data)
既然知道了问题核心在于“没有停下来”
那刚好生成器又有“yield”停下来这个操作,那么二者是不是刚好合拍了呢
看一段代码
function foo() {
ajax(url, (err, data) => {
if (err) {
// 向*main()抛出一个错误 it.throw( err );
} else {
// 用收到的data恢复*main()
it.next(data);
}
});
}
function* main() {
try {
let data = yield foo();
console.log(data);
} catch (err) {
console.error(err);
}
}
这段代码使用了生成器,其实跟上一段代码干的是一样的事情,虽然更长更复杂,但实际上更好用,具体原因继续往下看
首先,两段代码的核心区别在于生成器中使用了yield
在yield foo()的时候,调用了foo(),没有返回值(underfined),所以发出了一个ajax请求,虽然依然是yield underfined,但是没关系,因为这段代码不依赖yield的值来做什么事情,大不了就打印underfined嘛对不对
这里并不是在消息传递的意义上使用 yield,而只是将其用于流程控制实现暂停 / 阻塞。实 际上,它还是会有消息传递,但只是生成器恢复运行之后的单向消息传递。
所以,生成器在 yield 处暂停,本质上是在提出一个问题:“我应该返回什么值来赋给变量 data ?”谁来回答这个问题呢?
看foo,如果ajax请求成功,调用it.next( data )会用响应数据恢复生成器,意味着我们暂停的 yield 表达式直接接收到了这个值。然后 随着生成器代码继续运行,这个值被赋给局部变量 data
在生成器内部有了看似完全同步的代码
(除了 yield 关键字本身),但隐藏在背后的是,在 foo(..) 内的运行可以完全异步
这一部分对于理解生成器与异步编程之间的核心非常重要,万望深刻理解为什么
三、Generator+Promise处理并发流程与优化
接下来来点高级货吧,总不能一直停留在理论上
request是假设封装好的基于Promise的实现方法
run也是假设封装好的能实现重复迭代的驱动Promise链的方法
function *foo() {
let r1 = yield request(url1);
let r2 = yield request(url2);
let r3 = yield request(`${url3}/${r1}/${r2}`);
console.log(r3)
}
run(foo)
这段代码里,r3是依赖于r1和r2的,同时r1和r2是串行的,但这两个请求是相对独立的,那是不是应该考虑并发执行呢?
但yield 只是代码中一个单独 的暂停点,并不可能同时在两个点上暂停
这样试一下
function *foo() {
let p1 = request(url1);
let p2 = request(url2);
let r1 = yield p1;
let r2 = yield p2;
let r3 = yield request(`${url3}/${r1}/${r2}`);
console.log(r3)
}
run(foo)
看一下yield的位置,p1和p2是并发同时执行的用于 Ajax 请求的 promise,哪一个先完成都无所谓,因为 promise 会按照需要 在决议状态保持任意长时间
然后使用接下来的两个 yield 语句等待并取得 promise 的决议(分别写入 r1 和 r2)。
如果p1先决议,那么yield p1就会先恢复执行,然后等待yield p2恢复。
如果p2先决 议,它就会耐心保持其决议值等待请求,但是 yield p1 将会先等待,直到 p1 决议。
不管哪种情况,p1 和 p2 都会并发执行,无论完成顺序如何,两者都要全部完成,然后才 会发出 r3 = yield request..Ajax 请求。
这种流程控制模型和Promise.all([ .. ]) 工具实现的 gate 模式相同
function *foo() {
let rs = yield Promise.all([
request(url1),
request(url2)
]);
let r1 = rs[0];
let r2 = rs[1];
let r3 = yield request(`${url3}/${r1}/${r2}`);
console.log(r3)
}
run(foo)
四、抽象异步Promise流,简化生成器
到目前位置,Promise都是直接暴露在生成器内部的,但生成器实现异步的要点在于:创建简单、顺序、看似同步的代码,将异步的 细节尽可能隐藏起来。
能不能考虑一下把多余的信息都藏起来,特别是看起来比较复杂的Promise代码呢?
function bar(url1, url2) {
return Promise.all([request(url1), request(url2)]);
}
function* foo() {
// 隐藏bar(..)内部基于Promise的并发细节
let rs = yield bar(url1, url2);
let r1 = rs[0];
let r2 = rs[1];
let r3 = yield request(`${url3}/${r1}/${r2}`);
console.log(r3);
}
run(foo);
把Promise的实现细节都封装在bar里面,对bar的要求就是给我们一下rs结果而已,我们也不需要关系底层是用什么来实现的
异步,实际上是把Promise,作为一个实现细节看待。
具体到实际生产中,一系列的异步流程控制有可能就是下面的实现方式
function bar() {
Promise.all([
bax(...).then(...),
Promise.race([...])
])
.then(...)
}
这些代码可能非常复杂,如果把实现直接放到生成器内部的话,那几乎就失去了使用生成器的理由了
好好记一下这句话:创建简单、顺序、看似同步的代码,将异步的细节尽可能隐藏起来。
后话
感谢您耐心看到这里,希望有所收获!
如果不是很忙的话,麻烦点个star⭐【Github博客传送门】,举手之劳,却是对作者莫大的鼓励。
我在学习过程中喜欢做记录,分享的是自己在前端之路上的一些积累和思考,希望能跟大家一起交流与进步,更多文章请看【amandakelake的Github博客】