Generator
generator能扮演的角色
1. generator作为迭代器(数据生产者)
2. generator作为观察者(数据消费者)
3. 协作程序(协同多任务)
考虑到generator是可以暂停的并且可以同时作为数据生产者和消费者,不会做太多的工作就可以把generator转变成协作程序(合作进行的多任务)
先看一个简单的例子:
|
|
|
|
那么我们如何用generator函数把任务进行协作呢?
|
|
我们可以看到generator函数通过yield
暂停,可以和其他任务进行协作
|
|
异步迭代生成器
生成器与异步编码模式及解决回调问题等有什么关系呢?
|
|
|
|
接下来我们用生成器实现
|
|
从本质上而言,我们把异步最为实现细节抽象了出去,使得我们可以以同步顺序的形式追踪流程控制:“发出一个ajax请求,等它完成后打印出响应结果。”并且,当然,我们只在这个流程控制中表达了这两个步骤,而这种表达能力是可以无限扩展的,以便我们无论需要多少步骤都可以表达。
同步错误处理
前面生成器代码还给我们带来了其他的好处,让我们把注意力转移到生成器内部的try...catch
|
|
我们知道try...catch
是无法捕捉到异步错误的。
精彩的部分在于yield
暂停也使得生成器能够捕获错误。是怎么做到的呢,我们往下看
|
|
生成器yield
暂停的特性意味着我们不仅能够从异步函数调用得到看似同步的返回值,还可以同步捕获来自这些异步函数调用的错误。
所以我们已经知道,我们可以把错误抛入生成器中,不过如果是从生成器向外抛出错误呢?
|
|
我们也可以捕获到。
Generator + Promise
首先复习一下Promise,这里用Promise封装一个原生ajax
|
|
在前面的ajax例子的生成器代码中,getDataGen(..)
没有返回值(undefined),并且我们的迭代器控制代码并不关心yield
出来的值。
而这里支持Promise的request(..)
返回了一个promise。这暗示我们可以通过request(..)
构造一个promise,然后通过生成器把它yield
出来,然后迭代器控制代码就可以接收到这个promise了。
问题:迭代器应该对这个promise做些什么呢?
它应该监听这个promise的决议(完成或拒绝),然后要么使用完成消息(next)恢复生成器运行,要么向生成器抛出(throw)一个带有拒绝原因的错误。
重要:获得Promise和生成器最大效用的最自然的方法就是yield
出来一个promise,然后通过这个promise来控制生成器的迭代器。
|
|
但现在如何运行foo2
呢?还有一些实现细节需要补充,来实现接收和连接yield
出来的promise,使它能够在决议之后恢复生成器。先从手工实现开始:
|
|
那么多个呢?
|
|
封住一个简单的runner
|
|
多个请求呢?
|
|
|
|
如果喜欢,还可以把step
函数拿出来
|
|
整个过程多么巧妙,生成器 yield
出 Promise ,然后其控制生成器的迭代器来执行它,直到结束—是非常强大有用的一种方法。如果我们能够无需辅助工具函数库(比如co)就好了,别急,我们接着往下看。
ES7:async/await
|
|
可以用箭头函数
|
|
生成器中的 Promise 并发
借用上面的 co
|
|
这段代码的意思其实是请求 url1
得到 res1
之后再请求 url2
得到 res2
后再请求 url3
然后得到 res3
,是有依赖的。
那么怎么并发请求呢?
最简单的方法:
|
|
仔细比较上面两段代码的区别。
观察一下 yield
的位置,p1 和 p2 是并发执行的用于 ajax 请求的 Promise。哪一个先完成都无所谓,因为 promise 会按照需要在决议状态保持任意长时间。
然后我们使用接下来的两个 yield
语句等待并取得 promise 的决议(分别写入 res1 和 res2)。如果 p1 先决议,那么 yield p1
就会先恢复执行,然后等待 yield p2
恢复。如果 p2 先决议,它就会耐心保持其决议值等待请求,但是 yield p1
将会先等待,直到 p1 决议。
不管哪种情况,p1 和 p2 都会并发执行,无论完成顺序如何,两者都要全部完成,然后才会发出 const res3 = yield request(url)
的 ajax 请求。
接下来我们用定时器来模拟一下耗时的 ajax 请求
|
|
|
|
|
|
Promise.all 并发
还记得 Promise.all(..)
吗?
|
|
继续用我们的定时器做验证
|
|
隐藏 Promise
作为一个风格方面的提醒:要注意你的生成器内部包含了多少 Promise 逻辑。我们介绍的使用生成器实现异步的方法的全部要点在于创建简单、顺序、看似同步的代码,将异步的细节尽可能隐藏起来。
比如,这可能是一个简洁的方案。
|
|
小结
生成器是 ES6 的一个新的函数类型,它并不像普通函数那样总是运行到结束。取而代之的是,生成器可以在运行当中(完全保持其状态)暂停,并且将来再从暂停的地方恢复运行。
这种交替的暂停和恢复时合作性的而不是抢占式的,这意味着生成器具有独一无二的能力来暂停自身,这是通过关键字 yield
实现的。不过,只有控制生成器的迭代器具有恢复生成器的能力(通过 next(..)
)。
yield/next()..
这一对不自是一种控制机制,实际上也是一种双向消息传递机制。yield ..
表达式本质上是暂停下来等待某个值,接下来的 next(..)
调用会向被暂停的 yield
表达式传回一个值(或者是隐式的 undefined)。
在异步流程控制方面,生成器的关键优点是:生成器内部的代码是以自然的同步 / 顺序方式表达任务的一系列步骤。其技巧在于,我们把可能的异步隐藏在了关键字 yield
的后面,把异步移动到了控制生成器的迭代器的代码部分。
换句话说,生成器为异步代码保持了顺序、同步、阻塞的代码模式,这是的大脑可以更自然地追踪代码,解决了基于回调的异步的两个关键缺陷之一。
参考资料
本文结束,感谢阅读。
本文作者:melody0z
本文链接:https://melodyvoid.github.io/JavaScript/generator-and-asynchronous.html
欢迎转载,转载请注明文本链接