异步(二):Generator深刻理解

1、Generator基础认知

最基础的原则就是见到yield就暂停,next()就继续到下一个yield……以此知道函数执行完毕。前端

 先不上理论,直接看一段代码,这里的step()是一个辅助函数,用来控制迭代器,替代手动next()
git

var a = 1;
var b = 2;
function* foo() {
  a++;
  yield;
  b = b * a;
  a = (yield b) + 3;
}
function* bar() {
  b--;
  yield;
  a = (yield 8) + b;
  b = a * (yield 2);
}
function step(gen) {
  var it = gen();
  var last;
  return function() {
    // 无论yield出来的是什么,下一次都把它原样传回去!
    last = it.next(last).value;
  };
}

a = 1;
b = 2;

var s1 = step(foo);
var s2 = step(bar);
复制代码

yield和next()调用有一个数量上的不匹配,就是说,想要完整跑完一个生成器函数,next()调用老是比yield的数量多一次
github


为何会有这个不匹配? ajax

由于第一个 next(..) 老是启动一个生成器,并运行到第一个 yield 处。不过,是第二个 next(..) 调用完成第一个被暂停的 yield 表达式,第三个 next(..) 调用完成第二个 yield, 以此类推。
编程


因此上面的代码中,foo有两个yield,bar有三个yield 因此接下来要跑三次s1(),四次s2() 咱们在控制台看每一步的输出,一步一步来分析
promise


分析到这里,对generator的基础工做原理应该就有了大概的认知了。
bash

若是想加深一点理解(皮一下),能够随意调换一下s1和s2的执行顺序,总之就是三个s1和四个s2,对于理解多个生成器如何在共享的做用域上并发运行也有指导意义。
网络


2、异步迭代生成器

这一段,咱们来理解一下生成器与异步编程之间的问题,最直接的就是网络请求了
并发

let data = ajax(url); // ajax是假设的封装过的网络请求方法,并非原生那家伙
console.log(data)
复制代码

这段代码,你们都知道不能正常工做吧,data是underfined 异步

ajax是一个异步操做,它并无停下来等到拿到数据以后再赋值给data 而是在发出请求以后,直接就执行了下一句console.log(data)

既然知道了问题核心在于“没有停下来” 那恰好生成器又有“yield”停下来这个操做,那么两者是否是恰好合拍了呢

看一段代码

function foo() {
  ajax(url, (err, data) => {
    if (err) {
      // 向*main()抛出一个错误 it.throw( err );
    } else {
      // 用收到的data恢复*main()
      it.next(data);
    }
  });
}

function* main() {
  try {
    let data = yield foo();
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}
复制代码

这段代码使用了生成器,其实跟上一段代码干的是同样的事情,虽然更长更复杂,但实际上更好用,具体缘由继续往下看


首先,两段代码的核心区别在于生成器中使用了yield


在yield foo()的时候,调用了foo(),没有返回值(underfined),因此发出了一个ajax请求,虽然依然是yield underfined,可是不要紧,由于这段代码不依赖yield的值来作什么事情,大不了就打印underfined嘛对不对

这里并非在消息传递的意义上使用 yield,而只是将其用于流程控制实现暂停 / 阻塞。实 际上,它仍是会有消息传递,但只是生成器恢复运行以后的单向消息传递。

因此,生成器在 yield 处暂停,本质上是在提出一个问题:“我应该返回什么值来赋给变量 data ?”谁来回答这个问题呢?


看foo,若是ajax请求成功,调用it.next( data )会用响应数据恢复生成器,意味着咱们暂停的 yield 表达式直接接收到了这个值。而后 随着生成器代码继续运行,这个值被赋给局部变量 data


在生成器内部有了看似彻底同步的代码 (除了 yield 关键字自己),但隐藏在背后的是,在 foo(..) 内的运行能够彻底异步


这一部分对于理解生成器与异步编程之间的核心很是重要,万望深入理解为何


3、Generator+Promise处理并发流程与优化

接下来来点高级货吧,总不能一直停留在理论上 

request是假设封装好的基于Promise的实现方法 

run也是假设封装好的能实现重复迭代的驱动Promise链的方法

function *foo() {
  let r1 = yield request(url1);
  let r2 = yield request(url2);

  let r3 = yield request(`${url3}/${r1}/${r2}`);

  console.log(r3)
}
run(foo)
复制代码

这段代码里,r3是依赖于r1和r2的,同时r1和r2是串行的,但这两个请求是相对独立的,那是否是应该考虑并发执行呢? 

但yield 只是代码中一个单独 的暂停点,并不可能同时在两个点上暂停

这样试一下

function *foo() {
  let p1 = request(url1);
  let p2 = request(url2);

  let r1 = yield p1;
  let r2 = yield p2;

  let r3 = yield request(`${url3}/${r1}/${r2}`);

  console.log(r3)
}
run(foo)
复制代码

看一下yield的位置,p1和p2是并发同时执行的用于 Ajax 请求的 promise,哪个先完成都无所谓,由于 promise 会按照须要 在决议状态保持任意长时间 


而后使用接下来的两个 yield 语句等待并取得 promise 的决议(分别写入 r1 和 r2)。 

若是p1先决议,那么yield p1就会先恢复执行,而后等待yield p2恢复。 

若是p2先决 议,它就会耐心保持其决议值等待请求,可是 yield p1 将会先等待,直到 p1 决议。 

无论哪一种状况,p1 和 p2 都会并发执行,不管完成顺序如何,二者都要所有完成,而后才 会发出 r3 = yield request..Ajax 请求。


这种流程控制模型和Promise.all([ .. ]) 工具实现的 gate 模式相同

function *foo() {
  let rs = yield Promise.all([
    request(url1),
    request(url2)
  ]);

  let r1 = rs[0];
  let r2 = rs[1];

  let r3 = yield request(`${url3}/${r1}/${r2}`);
  console.log(r3)
}
run(foo)
复制代码


4、抽象异步Promise流,简化生成器

到目前位置,Promise都是直接暴露在生成器内部的,但生成器实现异步的要点在于:建立简单、顺序、看似同步的代码,将异步的 细节尽量隐藏起来。


能不能考虑一下把多余的信息都藏起来,特别是看起来比较复杂的Promise代码呢?

function bar(url1, url2) {
  return Promise.all([request(url1), request(url2)]);
}

function* foo() {
  // 隐藏bar(..)内部基于Promise的并发细节
  let rs = yield bar(url1, url2);
  let r1 = rs[0];
  let r2 = rs[1];

  let r3 = yield request(`${url3}/${r1}/${r2}`);
  console.log(r3);
}
run(foo);
复制代码

把Promise的实现细节都封装在bar里面,对bar的要求就是给咱们一下rs结果而已,咱们也不须要关系底层是用什么来实现的

异步,其实是把Promise,做为一个实现细节看待。

具体到实际生产中,一系列的异步流程控制有可能就是下面的实现方式

function bar() {
  Promise.all([
    bax(...).then(...),
    Promise.race([...])
  ])
  .then(...)
}
复制代码

这些代码可能很是复杂,若是把实现直接放到生成器内部的话,那几乎就失去了使用生成器的理由了


好好记一下这句话:建立简单、顺序、看似同步的代码,将异步的细节尽量隐藏起来。


后话

感谢您耐心看到这里,但愿有所收获!

若是不是很忙的话,麻烦点个star⭐【Github博客传送门】,举手之劳,倒是对做者莫大的鼓励。

我在学习过程当中喜欢作记录,分享的是本身在前端之路上的一些积累和思考,但愿能跟你们一块儿交流与进步,更多文章请看【amandakelake的Github博客】

相关文章
相关标签/搜索