Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

异步(二):Generator深入理解 #31

Open
amandakelake opened this issue Mar 14, 2018 · 0 comments
Open

异步(二):Generator深入理解 #31

amandakelake opened this issue Mar 14, 2018 · 0 comments

Comments

@amandakelake
Copy link
Owner

amandakelake commented Mar 14, 2018

一、Generator基础认知

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

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

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的数量多一次

为什么会有这个不匹配?
因为第一个 next(..) 总是启动一个生成器,并运行到第一个 yield 处。不过,是第二个 next(..) 调用完成第一个被暂停的 yield 表达式,第三个 next(..) 调用完成第二个 yield, 以此类推。

所以上面的代码中,foo有两个yield,bar有三个yield
所以接下来要跑三次s1(),四次s2()
我们在控制台看每一步的输出,一步一步来分析
faf6cb4b-c80c-4fc4-8b2a-8ce7b20664b9

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

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

二、异步迭代生成器

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

let data = ajax(url);
console.log(data)

这段代码,大家都知道不能正常工作吧,data是underfined
ajax是一个异步操作,它并没有停下来等到拿到数据之后再赋值给data
而是在发出请求之后,直接就执行了下一句console

既然知道了问题核心在于“没有停下来”
那刚好生成器又有“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(..) 内的运行可以完全异步

这一部分对于理解生成器与异步编程之间扎下了最核心的内容,万望深刻理解为什么

三、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)

四、抽象异步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(...)
}

这些代码可能非常复杂,如果把实现直接放到生成器内部的话,那几乎就失去了使用生成器的理由了
好好记一下这句话:创建简单、顺序、看似同步的代码,将异步的 细节尽可能隐藏起来。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant