JavaScript 异步编程指南 — 了解下 Generator 更好的掌握异步编程
Generator 是 ES6 对协程的实现,提供了一种异步编程的解决方案,和 Promise 一样都是线性的模式,相比 Promise 在复杂的业务场景下避免了 .then().then() 这样的代码冗余。
曾经一直认为 Generator 是一种过渡的解决方案,并没有过多的去了解它,后来在一些项目中还会看到它的身影,基于它还可以做很多有意思的事情,在不了解的情况下,你无法准确预知它的一些行为能够导致什么问题。
例如,Node.js 的可读流对象在 v10.0.0 版本已试验性的支持了异步迭代器,当监听来自可读流的数据时无需在基于事件和回调的方式 on('data', callback)
,可以方便的使用 for...await...of 异步迭代,看过源码会发现在它的内部实现中是用的异步生成器函数来生成的异步迭代器。还有目前的 Async/Await 是一种更好的异步解决方案,在下一节我们会讲,本质上还是基于 Generator 的语法糖。
如果想更好的理解 JavaScript 的异步编程,学习下 Generator 是没错的~
基本使用
Generator 函数声明
形式上 Generator 函数与普通函数没太大区别,两个特点:一是 function 关键字与函数名之间使用 * 号表达,二是内部使用使用 yield 表达式。
function *test() {
yield 'A';
yield 'B';
yield 'C';
}
next()
如果是普通函数,当 test() 后函数会立即执行,而生成器函数调用后函数不会立即执行,会给我们返回一个迭代器对象。
调用 next() 从函数头部或上一次暂停的地方执行,直到遇到下一个 yield 表达式暂停或 return 终止,当遇到 yield 表达式暂停后,想要继续执行下去,需接着调用 next() 恢复执行。
next() 返回 yield 表达式值,当 done 为 true 时迭代完成。
const gen = test();
gen.next() // { value: 'A', done: false }
gen.next() // { value: 'B', done: false }
gen.next() // { value: 'C', done: false }
gen.next() // { value: undefined, done: false }
return()
使用 return() 方法返回给定的值,可以强行终止,即使生成器还没有运行完毕。
const gen = test();
gen.next() // { value: 'A', done: false }
gen.next() // { value: 'B', done: false }
gen.return('termination'); // { value: 'termination', done: true }
gen.next() // { value: undefined, done: true }
gen.return() 相当于将 yield 语句替换为了 return 表达式 **yield 'B' 相当于 return 'termination'**
。
throw()
生成器函数返回的迭代器对象还有一个 throw() 方法,在函数体外抛出错误,在函数体内捕获。需要注意 throw() 方法抛出的错误要被内部捕获,必须至少执行过一次 next() 方法。
function *test() {
yield 'A';
try {
yield 'B';
} catch (err) {
console.error('内部错误', err); // 内部错误 unknown mistake
}
yield 'C';
}
const gen = test();
gen.next()
gen.next() // { value: 'B', done: false }
gen.throw('unknown mistake')
console.log(gen.next()); // { value: undefined, done: true }
gen.throw() 相当于将 yield 语句替换为了 throw 表达式 **yield 'B' 相当于 throw 'unknown mistake'**
。
再看 yield 表达式与 next 方法
yield 表达式本身自己没有值,返回 undefined,可以通过 next() 方法将上一个 yield 表达式的值做为参数传入。
下面我们将上面示例改下每一个 yiled 依赖前一个 yield 表达式的返回值。
function *test() {
const res1 = yield 'A';
const res2 = yield res1 + 'B';
const res3 = yield res2 + 'C';
return res3;
}
如果按照上面 gen.next() 不传入参数,结果只会拿到 undefined。
以下第一次调用 gen2.next() 拿到返回值为 A,第二次调用 next() 时传入第一次的返回值,test() 函数内部 res1 就可取到第一次 yield 表达式的值,后面执行一样。
因为 next() 传入的是第一次 yield 表达式的返回值,所以第一次在调用 next() 方法时无需传入参数。
const gen2 = test();
const res1 = gen2.next(); // { value: 'A', done: false }
const res2 = gen2.next(res1.value) // { value: 'AB', done: false }
const res3 = gen2.next(res2.value) // { value: 'ABC', done: false }
const res = gen2.next(res3.value); // { value: 'ABC', done: true }
console.log(res);
gen.next 相当于将 yield 语句替换为了一个表达式值,例如 gen.next('A') 可以这样理解 const res2 = yield res1 + 'B'** 相当于 **const res2 = 'A' + 'B'
。
Generator 与迭代器
迭代器是通过 next() 方法实现可迭代协议的任何一个对象,该方法返回 value 和 done 两个属性,其中 value 属性是当前成员的值,done 属性表示遍历是否结束。
生成器函数在最初调用时会返回一种称为 Generator 的迭代器,这样可以通过 for...of 遍历。
function *test() {
yield 'A';
yield 'B';
yield 'C';
return 'D';
}
const gen = test();
for (const item of gen) {
console.log(item); // A B C
}
有个点需要注意下,for...of 只遍历到最后一个 yield 关键字,最后一个 return 'D' 忽略掉了,如果使用 next() 是会处理 return 语句的。
实例:Generator + 状态机
Generator 用于实现状态机还是比较简单的,也是 JavaScript 里面高级的用法。例如,我们使用 A、B、C 三种状态去描述一个事物,状态之间是一种有序循环的,总是 A-B-C-A-B... 永远跑不出第 4 种状态。
const state = function* (){
while(1){
yield 'A';
yield 'B';
yield 'C';
}
}
const status = state();
setInterval(() => {
console.log(status.next().value) // A B C A B C A B...
}, 1000)
实例:Generator + Promise
在 Promise 小节中我们基于 Promise 做了一次改造,你可以回头去看下,下面我们使用 Generator 改造后看下差别是什么?
下例,去掉 yield 关键字和我们使用正常的普通函数没什么区别,为了使 Generator 迭代器对象能够自动执行,还要借助外部模块 co 实现。
co(function *() {
const files = yield fs.readdir(rootDir);
for (const filname of files) {
const file = path.resolve(rootDir, filname);
const stats = yield fs.lstat(file);
if (stats.isFile()) {
const chunk = yield fs.readFile(file);
console.log(chunk.toString());
}
}
});
总结
生成器是一个强大的通用控制结构,不像普通函数那样调用之后就直接运行到结束,在程序运行过程中当遇到 yield 关键字它可以使其保持暂停状态,直到将来某个时间点继续恢复执行。
在 ES6 中它的最大价值就是管理我们的异步代码,但是还不是很完美,我们不得不借助类似与 co 这样的工具来使我们的生成器函数自动调用 next() 方法运行。不过,在 ES7 到来之后,这一切都过去了,通过 Async/Await 可以更好的管理我们的异步任务。
往期回顾
·END·
汇聚精彩的免费实战教程
喜欢本文,点个“在看”告诉我