提示
: 本文是 github 上《Understanding ECMAScript 6》 的笔记整理,代码示例也来源于此。大家有时间可以直接读这本书。虽是英文,但通俗易懂,非常推荐。
前情:
在上一篇文章 你知道为什么会有 Generator 吗 里,我抛砖引玉,介绍了 generator
产生的原因。当时就有伙伴指出 “Generator是用来模拟多线程的休眠机制的”、 “Generator运行是惰性的”。那时我就说高级篇里会有介绍,这里就好好说一下。
摘要:
这里的重点,首先是如何与generator
里通信,一是用 next()
传参,二是还可以用 throw()
,不同的是它是往里抛错; 其次是有 yield
赋值语句时, generator
内部的执行顺序; 最后会是怎么用同步的方式写异步(有可能像 co
哦)。
如果对 generator
不太熟的,可以先看看 这里
1. 传参
简单说就是可以往next
传参数,而generator
里 yield
处可以接收到这个参数, 如下例子:
function *createIterator() {
let first = yield 1;
let second = yield first + 2; // 4 + 2
yield second + 3; // 5 + 3
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.next(5)); // "{ value: 8, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
复制代码
执行流程
要很明白地解释上面的执行过程,可借助这张图:
颜色相同的是同一次迭代里执行的,由浅到深,表示迭代的先后顺序。如:
- 第一次调用
next()
, 执行yield 1
到停止,返回{ value: 1, done: false }
。注意,这时赋值语句let fisrt = ...
没有执行; - 第二次调用
next(4)
, 先将参数4
传入上一次yield
处,可理解为:
let first = yield 1;
=>
let first = 4;
复制代码
再从上次停顿的地方开始执行,就是说先执行赋值语句
let first = 4
复制代码
然后执行到下个yield
为止,即
yield first + 2 // 4 + 2
复制代码
最后返回 { value: 6, done: false }
之后的 next
依上面的原理而执行,直到迭代完毕。
也就是说,通过next
的参数,generator
产生的 iterator
,与外部环境搭建起了沟通的桥梁,结合 iterator
可以停顿的特点,可以做一些有意思的事,如用同步方式写回调等,详见下文。
2. 往 iterator
里抛错
function *createIterator() {
let first = yield 1;
let second = yield first + 2; // yield 4 + 2, 然后抛出错误
yield second + 3; // 不会被执行
}
let iterator = createIterator();
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next(4)); // {value: 6, done: false}
console.log(iterator.throw(new Error("Boom"))); // generator 里抛出的错误
复制代码
根据上面说的执行机制,这里例子的执行流程可以用这张图表示:
第三次执行迭代时,我们调用 iterator.throw(new Error("Boom"))
, 向 iterator
里抛出错误,传入的参数为错误信息。
我们可以改造 createIterator
如下:
function* createIterator() {
let first = yield 1;
let second;
try {
second = yield first + 2;
} catch (ex) {
second = 6;
}
yield second + 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // "{ value: 9, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
复制代码
其执行流程解释如下:
-
前两次调用
next
情况和上面执行机制里的分析是一样的,就不赘述了。 -
第三次调用
iterator.throw(new Error("Boom")
往generator
往抛入错误,函数内部在上次停止处即yield first + 2
接收信息,抛出错误。但是被catch
了,所以继续执行到下一个停顿点:yield second + 3; // 6 + 3 复制代码
最后返回本次迭代结果
{ value: 9, done: false }
-
继续执行其他迭代,和上没无甚不同,不赘述。
小结: 这里有可以看到,
next()
和throw()
都可以让iterator
继续执行下去,不同的是后者会是以抛出错误的方式让iterator
继续执行的。但在这之后,generator
里会发生什么,取决于代码怎么写的了。
3. Generator
里的 return
语句
这里的 return
语句, 功能上与一般函数的 return
没太大区别,都会阻止 return
之后的语句执行。
function* createIterator() {
yield 1;
return;
yield 2;
yield 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
复制代码
上面的 return
, 使得之后的 yield
都被忽略了,所以,迭代二次而卒。
但是,如果 return
后有值,会被计入本次迭代的结果中:
function* createIterator() {
yield 1;
return 42;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 42, done: true }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
复制代码
这个iterator
执行两次就可收摊了,和上一个例子不同的是,最后一次返回结果里有 return
后的值 { value: 42, done: true }
。
又但是,这个返回值只能用一次,所以第三次执行next
, 返回结果变成了 { value: undefined, done: true }
。
特别注意: 展开操作符...
和 for-of
看到迭代结果里 done
是 true
就马上停止执行,连 return
后面的值也不管了,停止得很决绝。如上面的例子,用for-of
和 ...
执行:
function* createIterator() {
yield 1;
return 42;
}
let iterator = createIterator();
for(let item of iterator) {
console.log(item);
}
// 1
let anotherIterator = createIterator();
console.log([...anotherIterator])
// [1]
// 猜猜 [...iterator] 的结果是什么
复制代码
4. Generator 委托
generator
委托是什么,简单说就是把 generator
A 委托给 generator
B, 让 B 代为执行:
function* createNumberIterator() {
yield 1;
yield 2;
}
function* createColorIterator() {
yield "red";
yield "green";
}
function* createCombinedIterator() {
yield* createNumberIterator();
yield* createColorIterator();
yield true;
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "red", done: false }"
console.log(iterator.next()); // "{ value: "green", done: false }"
console.log(iterator.next()); // "{ value: true, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
复制代码
以上可见,委托的语法,就是在一个 generator
里, 用 yield*
操作另一个 generator
的执行结果。
通过委托把不同的 generator
放一起,再利用return
的返回值,可以在 generator
里通信,给出了更多的想象空间:
function* createNumberIterator() {
yield 1;
yield 2;
return 3;
}
function* createRepeatingIterator(count) {
for (let i = 0; i < count; i++) {
yield "repeat";
}
}
function* createCombinedIterator() {
let result = yield* createNumberIterator();
yield* createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
复制代码
如上, createNumberIterator
的返回值 3
传入了createRepeatingIterator
里, 如果拆开写,是这样:
function* createNumberIterator() {
yield 1;
yield 2;
return 3;
}
function* createRepeatingIterator(count) {
for (let i = 0; i < count; i++) {
yield "repeat";
}
}
function* createCombinedIterator() {
let result = yield* createNumberIterator();
yield result;
yield* createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
复制代码
注意:既然
yield *
后面接的是generator
的执行结果,而generator
是iterable
。就是说,yield *
后可以直接跟iterable
, 如字符串。如:
let g = function *() {
yield *['a', 'b', 'c']
}
for(let item of g()) {
console.log(item);
}
// a
// b
// c
复制代码
5. Genarator
与异步
关于 js
里异步的特点,这里展开说了。简单来讲,它让 js
这们单线程语言更强大; 但是,异步情况一复杂比如有异步之间有依赖,那就很容易写出如下的callback hell
, 极难维护:
合理利用 genarator
就可以用同步的写法,写异步。
从之前的介绍里已经知道,genarator
返回 iterator
, 需要手动调用 next
, 很麻烦。那如果封装一些,可以让 iterator
自己执行完毕,不就很好了:
-
前期准备,实现自动执行
generator
的函数run(function* () { let value = yield 1; console.log(value); value = yield value + 3; console.log(value); }); 复制代码
要让它自己执行,那么
run
需要:- 执行
generator
, 拿到iterator
; - 调用
iterator.next()
; - 把上一步的返回结果作为下一次
iterator.next(lastResult)
参数,继续迭代; - 重复 3 ,直到迭代完毕。
实现如下:
function run(taskDef) { // 创建并保存 iterator,留到后面使用 let task = taskDef(); let result = task.next(); // 递归地执行 `next` function step() { // 如果没完的话 if (!result.done) { result = task.next(result.value); step(); } } // 开始处理 step(); } 复制代码
- 执行
-
实现目标,用同步方式写异步
加入我们要让下面这段代码可行:
const asyncWork = new Promise((resolve, reject) => { setTimeout(() => resolve(5), 500) }) run(function* () { let value = yield asyncWork; console.log(value) value = yield value + 3; console.log(value) }); 复制代码
这里和上一个例子不同的地方在于,
yield
返回结果可能是个promise
, 那我们加个判断就可以了:if (result.value && typeof result.value.then === 'function') { result.value.then(d => { result = task.next(d) ... }) } 复制代码
就是判断如果是
promise
, 执行then
函数,把返回结果传入下一次迭代next(d)
即可。完整示例代码如下:function run(taskDef) { // 创建并保存 iterator,留到后面使用 let task = taskDef(); let result = task.next(); // 递归地执行 `next` function step() { // 如果没完的话 if (!result.done) { if (result.value && typeof result.value.then === 'function') { result.value.then(d => { result = task.next(d) step(); }) } else { result = task.next(result.value); step(); } } } // 开始处理 step(); } 复制代码
回头看看这个写法:
run(function* () { let value = yield asyncWork; console.log(value) value = yield value + 3; console.log(value) }); 复制代码
虽然第二个
yield
对上一个yield
结果有依赖,但不用写成回调,看着跟同步一样,很直白!
结语
generator
产生的 iterator
, 可以用next
,在函数外部往 generator
里传数据, 又可以通过 throw
往里抛错。它们相当于在 generator
里对外打开了多个通信窗口,这让清晰的异步成为可能。强大的 redux-saga
也是基于 generator
实现的。是不是有更多的玩法?一切都是抛砖引玉,不知道大家还有其他玩法没?
如果对 generator
由来不太清楚的,也可以先看看 这里
另外,这篇文章最先发布在 github,是个关于 ES6
的系列文章。如果觉得可以,帮忙 star
下呗,方便找工作啊。哎,找工作,真-是-累-啊!!!