前言
ES6 提供了一种新型的异步编程解决方案: Generator 函数(以下简称 G 函数). 它不是使用 JS 现有能力按照一定标准制定出来的东西(Promise 是如此出生的), 而是具有新型底层操作能力, 与传统编程完全不同, 代表一种新编程逻辑的高大存在. 简洁方便, 受人喜爱的 async 函数就是以它为基础实现的.
1 意义
JS 引擎是单线程的, 只有一个函数执行栈.
当当前函数执行完后, 执行栈将其弹出, 销毁包含其局部变量的栈空间, 并开始执行前一个函数. 执行权由此单向稳定的在不同函数中切换. 虽然 Web Worker 的出现使我们能够自行创建多个线程, 但这离灵活的控制: 暂停执行, 切换执行权和中间的数据交换等等, 还是很有距离的.
G 函数的意义在于, 它可以在单线程的背景下, 使执行权与数据自由的游走于多个执行栈之间, 实现协程式编程.
调用 G 函数后, 引擎会为其开辟一个独立的函数执行栈(以下简称 G 栈). 在执行它的过程中, 可以控制暂停执行, 并将执行权转出给主执行栈或另一个 G 栈(栈在这里可理解为函数). 而此 G 栈不会被销毁而是被冻结, 当执行权再次回来时, 会在与上次退出时完全相同的条件下继续执行.
下面是一个简单的交出和再次获得执行权的例子.
- // 依次打印出: 1 2 3 4 5.
- let g = G();
- console.log('1'); // 执行权在外部.
- g.next(); // 开始执行 G 函数, 遇到 yield 命令后停止执行返回执行权.
- console.log('3'); // 执行权再次回到外部.
- g.next(); // 再次进入到 G 函数中, 从上次停止的地方开始执行, 到最后自动返回执行权.
- console.log('5');
- function* G() {
- let n = 4;
- console.log('2');
- yield; // 遇到此命令, 会暂停执行并返回执行权.
- console.log(n);
- }
2 登堂
2.1 形式
G 函数也是函数, 所以具有普通函数该有的性质, 不过形式上有两点不同. 一是在 function 关键字和函数名之间有一个 * 号, 表示此为 G 函数. 二是只有在 G 函数里才能使用 yield 命令(以及 yield * 命令), 处于其内部的非 G 函数也不行. 由于箭头函数不能使用 yield 命令, 因此不能用作于 Generator 函数(可以用作于 async 函数).
以下是它的几种定义方式.
- // 声明式
- function* G() {}
- // 表达式
- let G = function* () {};
- // 作为对象属性
- let o = {
- G: function* () {}
- };
- // 作为对象属性的简写式
- let o = {
- * G() {}
- };
- // 箭头函数不能用作 G 函数, 报错!
- let o = {
- G: *() => {}
- };
- // 箭头函数可以用作 async 函数.
- let o = {
- G: async () => {}
- };
2.2 执行
调用普通函数会直接执行函数体中的代码, 之后返回函数的返回值. 但 G 函数不同, 执行它会返回一个遍历器对象 (此对象与数组中的遍历器对象相同), 不会执行函数体内的代码. 只有当调用它的 next 方法(也可能是其它实例方法) 时, 才开始了真正执行.
在 G 函数的执行过程中, 碰到 yield 或 return 命令时会停止执行并将执行权返回. 当然, 执行到此函数末尾时自然会返回执行权. 每次返回执行权之后再次调用它的 next 方法(也可能是其它实例方法), 会重新获得执行权, 并从上次停止的地方继续执行, 直到下一个停止点或结束.
- // 示例一
- let g = G();
- g.next(); // 打印出 1
- g.next(); // 打印出 2
- g.next(); // 打印出 3
- function* G() {
- console.log(1);
- yield;
- console.log(2);
- yield;
- console.log(3);
- }
- // 示例二
- let gg = GG();
- gg.next(); // 打印出 1
- gg.next(); // 打印出 2
- gg.next(); // 没有打印
- function* GG() {
- console.log(1);
- yield;
- console.log(2);
- return;
- yield;
- console.log(3);
- }
3 入室
3.1 数据交互
数据如果不能在执行权的更替中取得交互, 其存在的意义就会大打折扣.
G 函数的数据输出和输入是通过 yield 命令和 next 方法实现的.
yield 和 return 一样, 后面可以跟上任意数据, 程序执行到此会交出控制权并返回其后的跟随值(没有则为 undefined), 作为数据的输出. 每次调用 next 方法将控制权移交给 G 函数时, 可以传入任意数据, 该数据会等同替换 G 函数内部相应的 yield xxx 表达式, 作为数据的输入.
执行 G 函数, 返回的是一个遍历器对象. 每次调用它的 next 方法, 会得到一个具有 value 和 done 字段的对象. value 存储了移出控制权时输出的数据(即 yield 或 return 后的跟随值),done 为布尔值代表该 G 函数是否已经完成执行. 作为遍历器对象的它具有和数组遍历器相同的其它性质.
- // n1 的 value 为 10,a 和 n2 的 value 为 100.
- let g = G(10);
- let n1 = g.next(); // 得到 n 值.
- let n2 = g.next(100); // 相当将 yield n 替换成 100.
- function* G(n) {
- let a = yield n; // let a = 100;
- console.log(a); // 100
- return a;
- }
实际上, G 函数是实现遍历器接口最简单的途径, 不过有两点需要注意. 一是 G 函数中的 return 语句, 虽然通过遍历器对象可以获得 return 后面的返回值, 但此时 done 属性已为 true, 通过 for of 循环是遍历不到的. 二是 G 函数可以写成为永动机的形式, 类似服务器监听并执行请求, 这时通过 for of 遍历是没有尽头的.
--- 示例一: return 返回值.
- let g1 = G();
- console.log( g1.next() ); // value: 1, done: false
- console.log( g1.next() ); // value: 2, done: true
- console.log( g1.next() ); // value: undefined, done: true
- let g2 = G();
- for (let v of g2) {
- console.log(v); // 只打印出 1.
- }
- function* G() {
- yield 1;
- return 2;
- }
--- 示例二: 作为遍历器接口.
- let o = {
- id: 1,
- name: 2,
- ago: 3,
- *[Symbol.iterator]() {
- let arr = Object.keys(this);
- for (let v of arr) {
- yield this[v]; // 使用 yield 输出.
- }
- }
- }
- for (let v of o) {
- console.log(v); // 依次打印出: 1 2 3.
- }
--- 示例三: 永动机.
- let g = G();
- g.next(); // 打印出: Do ... .
- g.next(); // 打印出: Do ... .
- // ... 可以无穷次调用.
- // 可以尝试此例子, 虽然页面会崩溃.
- // 崩溃之后可以点击关闭页面, 或终止浏览器进程, 或辱骂作者.
- for (let v of G()) {
- console.log(v);
- }
- function* G() {
- while (true) {
- console.log('Do ...');
- yield;
- }
- }
- 3.2 yield*
yield * 命令的基本原理是自动遍历并用 yield 命令输出拥有遍历器接口的对象, 怪绕口的, 直接看示例吧.
- // G2 与 G22 函数等价.
- for (let v of G1()) {
- console.log(v); // 打印出: 1 [2, 3] 4.
- }
- for (let v of G2()) {
- console.log(v); // 打印出: 1 2 3 4.
- }
- for (let v of G22()) {
- console.log(v); // 打印出: 1 2 3 4.
- }
- function* G1() {
- yield 1;
- yield [2, 3];
- yield 4;
- }
- function* G2() {
- yield 1;
- yield* [2, 3]; // 使用 yield* 自动遍历.
- yield 4;
- }
- function* G22() {
- yield 1;
- for (let v of [2, 3]) { // 等价于 yield* 命令.
- yield v;
- }
- yield 4;
- }
在 G 函数中直接调用另一个 G 函数, 与在外部调用没什么区别, 即便前面加上 yield 命令. 但如果使用 yield * 命令就能直接整合子 G 函数到父函数中, 十分方便. 因为 G 函数返回的就是一个遍历器对象, 而 yield * 可以自动展开持有遍历器接口的对象, 并用 yield 输出. 如此就等价于将子 G 函数的函数体原原本本的复制到父 G 函数中.
- // G1 与 G2 等价.
- for (let v of G1()) {
- console.log(v); // 依次打印出: 1 2 '-' 3 4
- }
- for (let v of G2()) {
- console.log(v); // 依次打印出: 1 2 '-' 3 4
- }
- function* G1() {
- yield 1;
- yield* GG();
- yield 4;
- }
- function* G2() {
- yield 1;
- yield 2;
- console.log('-');
- yield 3;
- yield 4;
- }
- function* GG() {
- yield 2;
- console.log('-');
- yield 3;
- }
唯一需要注意的是子 G 函数中的 return 语句. yield * 虽然与 for of 一样不会遍历到该值, 但其能直接返回该值.
- let g = G();
- console.log( g.next().value ); // 1
- console.log( g.next().value ); // undefined, 打印出 return 2.
- function* G() {
- let n = yield* GG(); // 第二次执行 next 方法时, 这里等价于 let n = 2; .
- console.log('return', n);
- }
- function* GG() {
- yield 1;
- return 2;
- }
3.3 异步应用
历经了如此多的铺垫, 是到将其应用到异步的时候了, 来来来, 喝了这坛酒咱就到马路上碰个瓷试试运气.
使用 G 函数处理异步的优势, 相对于在这以前最优秀的 Promise 来说, 在于形式上使主逻辑代码更为的精简和清晰, 使其看起来与同步代码基本相同. 虽然在日常生活中, 我们说谁谁做事爱搞形式多少包含有贬低意味. 但在这程序的世界, 对于我们编写和他人阅读来说, 这些改进的效益可是相当可观哦.
- // 模拟请求数据.
- // 依次打印出 get api1, Do ..., get api2, Do ..., 最终值: 3000 .
- // 请求数据的主逻辑块
- function* G() {
- let api1 = yield createPromise(1000); // 发送第一个数据请求, 返回的是该 Promise .
- console.log('get api1', api1); // 得到数据.
- console.log('Do somethings with api1'); // 做些操作.
- let api2 = yield createPromise(2000); // 发送第二个数据请求, 返回的是该 Promise .
- console.log('get api2', api2); // 得到数据.
- console.log('Do somethings with api2'); // 做些操作.
- return api1 + api2;
- }
- // 开始执行 G 函数.
- let g = G();
- // 得到第一个 Promise 并等待其返回数据
- g.next().value.then(res => {
- // 获取到第一个请求的数据.
- return g.next(res).value; // 将第一个数据传回, 并获取到第二个 Promise .
- }).then(res => {
- // 获取到第二个请求的数据.
- return g.next(res).value; // 将第二个数据传回.
- }).then(res => {
- console.log('最终值:', res);
- });
- // 模拟请求数据
- function createPromise(time) {
- return new Promise(resolve => {
- setTimeout(() => {
- resolve(time);
- }, time);
- });
- }
上面的方式有很大的优化空间. 我们执行函数时的逻辑是: 先获取到异步请求并等待其返回结果, 再将结果传递回 G 函数, 之后重复操作. 而按照此方式, 意味着 G 函数中有多少异步请求, 我们就应该重复多少次该操作. 如果观众老爷们足够敏感, 此时就能想到这些步奏是能抽象成一个函数的. 而抽象出来的这个函数就是 G 函数的自执行器.
以下是一个简易的自执行器, 它会返回一个 Promise. 再往内是通过递归一步步的执行 G 函数, 对其返回的结果都统一使用 resolve 方法包装成 Promise 对象.
- // 与上一个示例等价.
- RunG(G).then(res => {
- console.log('G 函数执行结束:', res); // 3000
- });
- function* G() {
- let api1 = yield createPromise(1000);
- console.log('get api1', api1);
- console.log('Do somethings with api1');
- let api2 = yield createPromise(2000);
- console.log('get api2', api2);
- console.log('Do somethings with api2');
- return api1 + api2;
- }
- function RunG(G) {
- // 返回 Promise 对象.
- return new Promise((resolve, reject) => {
- let g = G();
- next();
- function next(data) {
- let r = g.next(data);
- // 成功执行完 G 函数, 则改变 Promise 的状态为成功.
- if (r.done) return resolve(r.value);
- // 将每次的返回值统一包装成 Promise 对象.
- // 成功则继续执行 G 函数, 否则改变 Promise 的状态为失败.
- Promise.resolve(r.value).then(next).catch(reject);
- }
- });
- }
- function createPromise(time) {
- return new Promise(resolve => {
- setTimeout(() => {
- resolve(time);
- }, time);
- });
- }
自执行器可以自动执行任意的 G 函数, 是应用于异步时必要的咖啡伴侣. 上面是接地气的写法, 我们来看看较为官方的版本. 可以直观的感受到, 两者主要的区别在对可能错误的捕获和处理上, 这也是平常写的代码和构建底层库主要的区别之一.
- 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); });
- });
- }
4 实例方法
实例方法比如 next 以及接下来的 throw 和 return, 实际是存在 G 函数的原型对象中. 执行 G 函数返回的遍历器对象会继承 G 函数的原型对象. 在此添加自定义方法也可以被继承. 这使得 G 函数看起来类似构造函数, 但实际两者不相同. 因为 G 函数本就不是构造函数, 不能被 new, 内部的 this 也不能被继承.
- function* G() {
- this.id = 123;
- }
- G.prototype.sayName = () => {
- console.log('Wmaker');
- };
- let g = G();
- g.id; // undefined
- g.sayName(); // 'Wmaker'
- 4.1 throw
实例方法 throw 和 next 方法的性质基本相同, 区别在于其是向 G 函数体内传递错误而不是值. 通俗的表达是将 yield xxx 表达式替换成
throw 传入的参数
. 其它比如会接着执行到下一个断点, 返回一个对象等等, 和 next 方法一致. 该方法使得异常处理更为简单, 而且多个 yield 表达式可以只用一个 try catch 代码块捕获.
当通过 throw 方法或 G 函数在执行中自己抛出错误时. 如果此代码正好被 try catch 块包裹, 便会像公园里行完方便的宠物一样, 没事的继续往下执行. 遇到下一个断点, 交出执行权传出返回值. 如果没有错误捕获, JS 会终止执行并认为函数已经结束运行, 此后再调用 next 方法会一直返回 value 为 undefined,done 为 true 的对象.
- // 依次打印出: 1, Error: 2, 3.
- let g = G();
- console.log( g.next().value ); // 1
- console.log( g.throw(2).value ); // 3, 打印出 Error: 2.
- function* G() {
- try {
- yield 1;
- } catch(e) {
- console.log('Error:', e);
- }
- yield 3;
- }
- // 等价于
- function* G() {
- try {
- yield 1;
- throw 2; // 替换原来的 yield 表达式, 相当在后面添加.
- } catch(e) {
- console.log('Error:', e);
- }
- yield 3;
- }
- 4.2 return
实例方法 return 和 throw 的情况相同, 与 next 具有相似的性质. 区别在于其会直接终止 G 函数的执行并返回传入的参数. 通俗的表达是将 yield xxx 表达式替换成
return 传入的参数
. 值得注意的是, 如果此时正好处于 try 代码块中, 且其带有 finally 模块, 那么 return 方法会推迟到 finally 代码块执行完后再执行.
- let g = G();
- console.log( g.next().value ); // 1
- console.log( g.return(4).value ); // 2
- console.log( g.next().value ); // 3
- console.log( g.next().value ); // 4,G 函数结束.
- console.log( g.next().value ); // undefined
- function* G() {
- try {
- yield 1;
- } finally {
- yield 2;
- yield 3;
- }
- yield 5;
- }
- // 等价于
- function* GG() {
- try {
- yield 1;
- return 4; // 替换原来的 yield 表达式, 相当在后面添加.
- } finally {
- yield 2;
- yield 3;
- }
- yield 5;
- }
延伸
ES6 精华: Symbol
ES6 精华: Promise
Iterator: 访问数据集合的统一接口
来源: https://segmentfault.com/a/1190000016047312