ES6提供了一种新型的异步编程解决方案:Generator
函数(如下简称G函数)。它不是使用JS现有能力按照必定标准制定出来的东西(Promise
是如此出生的),而是具备新型底层操做能力,与传统编程彻底不一样,表明一种新编程逻辑的高大存在。简洁方便、受人喜好的async
函数就是以它为基础实现的。编程
JS引擎是单线程的,只有一个函数执行栈。
当当前函数执行完后,执行栈将其弹出,销毁包含其局部变量的栈空间,并开始执行前一个函数。执行权由此单向稳定的在不一样函数中切换。虽然Web Worker
的出现使咱们可以自行建立多个线程,但这离灵活的控制:暂停执行、切换执行权和中间的数据交换等等,仍是颇有距离的。segmentfault
G函数的意义在于,它能够在单线程的背景下,使执行权与数据自由的游走于多个执行栈之间,实现协程式编程。
调用G函数后,引擎会为其开辟一个独立的函数执行栈(如下简称G栈)。在执行它的过程当中,能够控制暂停执行,并将执行权转出给主执行栈或另外一个G栈(栈在这里可理解为函数)。而此G栈不会被销毁而是被冻结,当执行权再次回来时,会在与上次退出时彻底相同的条件下继续执行。api
下面是一个简单的交出和再次得到执行权的例子。数组
// 依次打印出: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); }
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 () => {} };
调用普通函数会直接执行函数体中的代码,以后返回函数的返回值。但G函数不一样,执行它会返回一个遍历器对象(此对象与数组中的遍历器对象相同),不会执行函数体内的代码。只有当调用它的next
方法(也多是其它实例方法)时,才开始了真正执行。异步
在G函数的执行过程当中,碰到yield
或return
命令时会中止执行并将执行权返回。固然,执行到此函数末尾时天然会返回执行权。每次返回执行权以后再次调用它的next
方法(也多是其它实例方法),会从新得到执行权,并从上次中止的地方继续执行,直到下一个中止点或结束。async
// 示例一 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); }
数据若是不能在执行权的更替中取得交互,其存在的意义就会大打折扣。异步编程
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; } }
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; }
历经了如此多的铺垫,是到将其应用到异步的时候了,来来来,喝了这坛酒咱就到马路上碰个瓷试试运气。
使用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); }); }); }
实例方法好比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'
实例方法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; } // 使用了 throw(2) 等价于使用 next() 并将代码改写成以下所示。 function* G() { try { yield 1; throw 2; // 替换原来的 yield 表达式,至关在后面添加。 } catch(e) { console.log('Error:', e); } yield 3; }
实例方法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; } // 使用了 return(4) 等价于使用 next() 并将代码改写成以下所示。 function* GG() { try { yield 1; return 4; // 替换原来的 yield 表达式,至关在后面添加。 } finally { yield 2; yield 3; } yield 5; }