Generator 是 ES6 提供的新语法,相对于原来的异步语法提供了一个新的实现方式。首先我们来回顾一下。

生成器的语法

话不多说,来一段代码:

  1. function* shopee() {
  2. console.log("shopee");
  3. let a = yield 1;
  4. let b = yield (function () {return 2})();
  5. return 3;
  6. }
  7. var g = shopee() // 暂停,不会执行任何语句
  8. console.log(typeof g) // object 是个object ,不是"function"
  9. console.log(g.next())
  10. console.log(g.next())
  11. console.log(g.next())
  12. console.log(g.next())
  13. // enter
  14. // { value: 1, done: false }
  15. // { value: 2, done: false }
  16. // { value: 3, done: true }
  17. // { value: undefined, done: true }

生成器是一个带星号的”函数”(注意:它并不是真正的函数),可以通过yield关键字暂停执行和恢复。
由上面的代码可以看出:
1、调用 shopee() 后,程序会阻塞住,不会执行任何语句。
2、调用 g.next() 后,程序继续执行,直到遇到 yield 程序暂停。
3、next 方法返回一个对象, 有两个属性: value 和 done。value 为当前 yield 后面的结果,done 表示是否执行完,遇到了return 后,done 会由false变为true。

yield*

生成器之间可以通过 yield* 互相调用

  1. function* shopee1() {
  2. yield 1;
  3. yield 4;
  4. }
  5. function* shopee2() {
  6. yield 2;
  7. yield 3;
  8. }

我们对 shopee1 做一些修改就可以让代码按 1234 执行

  1. function* shopee1() {
  2. yield 1;
  3. yield* shopee2();
  4. yield 4;
  5. }

那么生成器是如何实现的呢?

这和原来的异步回调不同,看起来直接停止了代码?它底层的实现机制是什么呢?这就是今天主要的内容——协程

什么是协程?

协程是一种比线程更加轻量级的存在,协程处在线程的环境中,一个线程可以存在多个协程,可以将协程理解为线程中的一个个任务。不像进程和线程,协程并不受操作系统的管理,而是被具体的应用程序代码所控制。

协程的运作过程

那你可能要问了,JS 不是单线程执行的吗,开这么多协程难道可以一起执行吗?
答案是:并不能。一个线程一次只能执行一个协程。比如当前执行 A 协程,另外还有一个 B 协程,如果想要执行 B 的任务,就必须在 A 协程中将 JS 线程的控制权转交给 B 协程,那么现在 B 执行,A 就相当于处于暂停的状态。

  1. function* A() {
  2. console.log("我是A");
  3. yield B(); // A停住,在这里转交线程执行权给B
  4. console.log("结束了");
  5. }
  6. function B() {
  7. console.log("我是B");
  8. return 100;// 返回,并且将线程执行权还给A
  9. }
  10. let gen = A();
  11. gen.next();
  12. gen.next();
  13. // 我是A
  14. // 我是B
  15. // 结束了

在这个过程中,A 将执行权交给 B,也就是 A 启动 B,我们也称 A 是 B 的父协程。因此 B 当中最后return 100其实是将 100 传给了父协程。
需要强调的是,对于协程来说,它并不受操作系统的控制,完全由用户自定义切换,因此并没有进程/线程上下文切换的开销,这是高性能的重要原因。

thunk 函数

那 Generator 和异步有什么关系呢?我们又如何让 Generator 按顺序执行?那么首先我们要搞清楚 Thunk 函数
举个判断类型的例子:

  1. let isString = (obj) => {
  2. return Object.prototype.toString.call(obj) === '[object String]';
  3. };
  4. let isFunction = (obj) => {
  5. return Object.prototype.toString.call(obj) === '[object Function]';
  6. };
  7. let isArray = (obj) => {
  8. return Object.prototype.toString.call(obj) === '[object Array]';
  9. };
  10. let isSet = (obj) => {
  11. return Object.prototype.toString.call(obj) === '[object Set]';
  12. };

出现了很多的判断,优化一下:

  1. let isType = (type) => {
  2. return (obj) => {
  3. return Object.prototype.toString.call(obj) === `[object ${type}]`;
  4. }
  5. }
  6. // 转成 Thunk 函数
  7. let isString = isType('String');
  8. let isFunction = isType('Function');

isType这样的函数我们称为thunk 函数。它的核心逻辑是接收一定的参数,生产出定制化的函数,然后使用定制化的函数去完成功能。thunk函数的实现会比单个的判断函数复杂一点点,但就是这一点点的复杂,大大方便了后续的操作,感觉有点类似珂里化。

Generator 和 异步

Thunk 的实现

以文件操作为例,我们来看看 异步操作 如何应用于Generator。

  1. const readFileThunk = (filename) => {
  2. return (callback) => {
  3. fs.readFile(filename, callback);
  4. }
  5. }

readFileThunk就是一个thunk函数。异步操作核心的一环就是绑定回调函数,而thunk函数可以帮我们做到。首先传入文件名,然后生成一个针对某个文件的定制化函数。这个函数中传入回调,这个回调就会成为异步操作的回调。这样就让 Generator 和异步关联起来了。
紧接者我们做如下的操作:

  1. const shopee = function* () {
  2. const data1 = yield readFileThunk('shopee1.txt')
  3. console.log(data1.toString())
  4. const data2 = yield readFileThunk('shopee2.txt')
  5. console.log(data2.toString)
  6. }
  7. let g = shopee();
  8. // 第一步: 我们调用next,让它开始执行。
  9. // next返回值中有一个value值,这个value是yield后面的结果,放在这里也就是是thunk函数生成的定制化函数,里面需要传一个回调函数作为参数
  10. g.next().value((err, data1) => {
  11. // 第二步: 拿到上一次得到的结果,调用next, 将结果作为参数传入,程序继续执行。
  12. // 同理,value传入回调
  13. g.next(data1).value((err, data2) => {
  14. g.next(data2);
  15. })
  16. })
  17. //shopee1.txt的内容
  18. //shopee2.txt的内容

优化一下

  1. function run(shopee){
  2. const next = (err, data) => {
  3. let res = shopee.next(data);
  4. if(res.done) return;
  5. res.value(next);
  6. }
  7. next();
  8. }
  9. run(g);

Ok,再次执行,依然打印正确的结果。代码虽然就这么几行,但包含了递归的过程,好好体会一下。
这是通过thunk完成异步操作的情况。

Promise 的实现

还是拿上面的例子,用Promise来实现就轻松一些:

  1. const readFilePromise = (filename) => {
  2. return new Promise((resolve, reject) => {
  3. fs.readFile(filename, (err, data) => {
  4. if(err) {
  5. reject(err);
  6. }else {
  7. resolve(data);
  8. }
  9. })
  10. }).then(res => res);
  11. }
  12. const shopee = function* () {
  13. const data1 = yield readFilePromise('shopee1.txt')
  14. console.log(data1.toString())
  15. const data2 = yield readFilePromise('shopee2.txt')
  16. console.log(data2.toString)
  17. }
  18. let g = gen();
  19. function getShopeePromise(shopee, data) {
  20. return shopee.next(data).value;
  21. }
  22. getShopeePromise(g).then(data1 => {
  23. return getShopeePromise(g, data1);
  24. }).then(data2 => {
  25. return getShopeePromise(g, data2);
  26. })

同样,我们可以对执行Generator的代码加以封装:

  1. function run(g) {
  2. const next = (data) => {
  3. let res = g.next();
  4. if(res.done) return;
  5. res.value.then(data => {
  6. next(data);
  7. })
  8. }
  9. next();
  10. }

这里再推荐一个已经封装好的工具包 co 库,其实核心原理就是我们已经手写过了(就是刚刚封装的Promise情况下的执行代码),只不过源码会各种边界情况做了处理。使用起来非常简单:

  1. const co = require('co');
  2. let g = shopee();
  3. co(g).then(res =>{
  4. console.log(res);
  5. })

简单几行代码就完成了Generator所有的操作。