【JS】JavaScript异步系列(4)——生成器
本篇博客来源于王福明博客的异步系列和《你不知道的JavaScript》
原博客地址为:http://www.cnblogs.com/wangfupeng1988/p/6532713.html
1. 打破完整运行
在前面已经学习了promise的解决方法来处理异步操作的代码逻辑,接下来我们来看一种顺序、看似同步的异步流程控制表达风格——ES6的生成器(generator)。
先看一段代码:
var x = 1;
function* foo(){
x++;
yield;//暂停!
console.log("x:",x);
}
function bar(){
x++;
}
//构造一个迭代器it来控制这个生成器
var it = foo();
//这里启动foo()!
it.next();
console.log(x); //2
bar();
console.log(x); //3
it.next(); //x: 3
这段代码的运行过程是什么样呢?请往下看:
-
it = foo()
运算并没有执行生成器foo()
,,而只是够早了一个迭代器(iterator),这个迭代器会控制它的执行。【后面会接受迭代器】 - 第一个
it.next()
启动了生成器foo()
,并运行了foo()
第一行的x++
。foo()
在yield
语句处暂停,到这里第一个it.next()
调用结束。此时的foo()
仍在运行并且是活跃的,但处于暂停状态。 - 此时查看x的值为2.然后我们再调用
bar()
,再次递增x。 - 再次查看x的值,此时x的值为3.
- 最后的
it.next()
调用从暂停处恢复了生成器foo()
的执行,并运行console.log(...)
语句,这条语句使用当前x的值3.
显然,foo()
启动了,但还没有完整运行,它在yield
处暂停了。后面恢复了foo()
并让它运行到结束。
1.1 输入和输出
生成器函数实际是特殊的函数,它也可以接受参数(输入),也能够返回值(输出)。
function* foo(x,y){
return x * y;
}
var it = foo(6,7);
var res = it.next();
res.value; //42
我们向foo()
传入实参6和7分别作为参数x和y。foo()
向调用代码返回42.
迭代消息传递
处理能够接受参数并提供返回值之外,生成器还提供了更强大的内建消息输入输出能力,通过yield
和next(...)
实现。
function* foo(x){
var y = x*(yield);
return y;
}
var it = foo(6);
//启动foo(...)
it.next();
var res = it.next(7);
res.value; //42
首先,传入6作为参数x。然后调用it.next()
,这会启动foo()
.
在foo(...)
内部,开始执行语句var y = x …,但随后就遇到一个yield表达式。他就会在这点上暂停foo(...)
(在赋值语句中间暂停!)并在本质上要求调用代码为yield
表达式提供一个结果值。接下来,调用it.next(7)
,这句把值7传回作为被暂停的yield
表达式的结果。
所以,这时赋值语句实际上就是var y = 6 * 7
。现在,return y
返回值42作为调用it.next(7)
的结果。
多个迭代器
关于多个迭代器的,和前面的理解一样。看如下代码:
function* foo(){
var x = yield 2;
z++;
var y = yield (x*z);
console.log(x,y,z);
}
var z = 1;
var it1 = foo();
var it2 = foo();
var val1 = it1.next().value; //2 <--yield 2
var val2 = it2.next().value; //2 <--yield 2
val1 = it1.next(val2*10).value; //40 <-- x:20,z:2
val2 = it2.next(val1*5).value; //600 <-- x:200,z:3
it1.next(val2/2); //y:300
// 20 300 3
it2.next(val1/4); //y:10
// 200 10 3
能看出正确结果么?
2、Iterator 迭代器
前面介绍了生成器的一种有趣用法是作为一种产生值的方式。接下来,介绍一点迭代器。
2.1 Symbol数据类型简介
Symbol是一个特殊的数据类型,和number string等并列,详细可以看阮一峰老师的ES6入门中的介绍。
现在先记住,Symbol数据类型也可以作为对象属性的key。看如下代码:
var obj = {}
obj.a = 100
obj[Symbol.iterator] = 200
console.log(obj) // {a: 100, Symbol(Symbol.iterator): 200}
[Symbol.iterator]是一个特殊的数据类型——Symbol类型,但是也可以像number string类型一样,作为对象的属性key来使用。
2.2 iterable
iterable(可迭代):指一个包含可以在其值上迭代的迭代器对象。
从ES6开始,从一个iterable中提取迭代器的方法是:iterable必须支持一个函数,其名称是专门的ES6符号值Symbol.iterator
。调用这个函数时,它会返回一个迭代器。
在ES6中,元素具有[Symbol.iterator]
属性数据类型的有:数组、某些类似数字的对象(arguments等)、Set和Map。
原生具有[Symbol.iterator]
属性的数据类型有一个特点:可以使用for...of
来取值。
var item
for (item of [100, 200, 300]) {
console.log(item)
}
// 打印出:100 200 300
// 注意,这里每次获取的 item 是数组的 value,而不是 index ,这一点和 传统 for 循环以及 for...in 完全不一样
// for..of循环自动调用它的Symbol.iterator函数来构建一个迭代器
2.3 生成Iterator对象
首先定义一个数组,并生成该数组的Iterator对象
const arr = [100, 200, 300]
const iterator = arr[Symbol.iterator]() // 通过执行 [Symbol.iterator] 的属性值(函数)来返回一个 iterator 对象
生成了iterator,该如何使用呢?有两种方式:next
和for...of
1. next
console.log(iterator.next()) // { value: 100, done: false }
console.log(iterator.next()) // { value: 200, done: false }
console.log(iterator.next()) // { value: 300, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
- for…of
let i
for (i of iterator) {
console.log(i)
}
// 打印:100 200 300
2.4 Generator返回的也是Iterator对象
现在,你应该明白了,我们在第一部分说的生成器,就是生成一个Iterator对象。因此会有next()
,也可以通过for...of
来遍历。
function* Hello() {
yield 100
yield (function () {return 200})()
return 300
}
const h = Hello()
console.log(h[Symbol.iterator]) // function [Symbol.iterator](){[native code]}
执行const h = Hello()得到的就是一个iterator对象,因为h[Symbol.iterator]是有值的。既然是iterator对象,那么就可以使用next()和for…of进行操作
console.log(h.next()) // { value: 100, done: false }
console.log(h.next()) // { value: 200, done: false }
console.log(h.next()) // { value: 300, done: false }
console.log(h.next()) // { value: undefined, done: true }
let i
for (i of h) {
console.log(i);//100 200
}
3、生成器的应用
3.1 yield* 语句
如果有两个Generator,想要在第一个中包含第二个,如下需求:
function* G1() {
yield 'a'
yield 'b'
}
function* G2() {
yield 'x'
yield 'y'
}
针对以上两个Generator,我的需求是:一次输出a x y b,该如何做?
用for..of
解决:
var g1 = G1();
var g2 = G2();
for(var i of g1){
console.log(i);
for(var j of g2){
console.log(j);
}
}
但是,更简洁的方式yield*表达式
function* G1() {
yield 'a'
yield* G2() // 使用 yield* 执行 G2()
yield 'b'
}
function* G2() {
yield 'x'
yield 'y'
}
for (let item of G1()) {
console.log(item)
}
yield*
后面会接一个Generator,而且会把它其中的yield按照规则来一步一步执行。
4、异步迭代生成器
生成器与异步编码模式及解决回调问题等有什么关系呢?接下来我们就来看这个问题。
先看一段代码:
function foo(x, y, cb) {
ajax(
"http://some.url.1/?x="+x+"&y="+y,
cb
);
}
foo(11,31,function (err, text) {
if(err){
console.log(err);
}
else{
console.log(text);
}
});
若想要通过生成器来表达烔炀的任务流程控制,可以这样实现:
function foo(x, y) {
ajax(
"http://some.url.1/?x="+x+"&y="+y,
function (err, data) {
if(err){
//向*main()抛出一个错误
it.throw(err) ;
}
else{
//用收到的data恢复*main()
it.next(data);
}
}
);
}
function* main() {
try{
var text = yield foo(11,31);
console.log(text);
}
catch(err){
console.log(err);
}
}
var it =main();
//启动生成器
it.next();
第一眼看上去,与之前的回调代码对比起来,代码更长了,但是,别想得这么简单!
在yield foo(11,31)
中,首先调用foo(11,31)
,它没有返回值(返回undefined
),所以我们发出了一个调用来请求数据,但实际上之后做的是yield undefined
。因为这段代码当前不依赖yield
出来的值来做任何事。
这里yield
只是将其用于流程控制实现暂停/阻塞。它的消息传递,只是生成器恢复运行之后的单向消息传递。
总结一下:我们在生成器内部有了看似完全同步的代码(出来yield),但隐藏在背后的是,在foo(..)
内的运行可以完全异步。
这样,对于回调无法以顺序同步的、符合我们大脑思考模式的方式表达异步这个问题,是一个近乎完美的解决方案。
同步错误处理
try{
var text = yield foo(11,31);
console.log(text);
}
catch(err){
console.log(err);
}
在前面我们已经看到yield
是如何让赋值语句暂停来等待foo(...)
完成,使得响应完成后可以被赋给text
。精彩部分在于yield
暂停也使得生成器能够捕获错误。
通过这段代码,把错误抛出到生成器中:
if(err){
//向*main()抛出一个错误
it.throw(err) ;
}
还可以捕获通过throw(..)
抛入生成器的同一个错误,基本上也就是给生成器一个处理它的机会;如果生成器没有处理的话,迭代器代码就必须处理:
function* main() {
var x = yield "Hello World";
//永远不会到达这里
console.log(x);
}
var it = main();
it.next();
try{
// *main()会处理这个错误吗?
it.throw("Oops");
}
catch (err) {
//不行,没有处理!
console.log(err); //Oops
}
5、生成器+promise
这个看个简单的例子吧,感觉更偏向于结构设计了,一时半会儿没有实际应用感觉说不清楚,以后再细说吧!
简单的例子:
function foo(x, y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
function* main() {
try{
var text = yield foo(11,31);
console.log(text);
}
catch (err) {
console.log(err);
}
}
我们把promise
的实现细节抽象出来了,在生成器中只进行函数调用就行了。实现了promise
的隐藏
上一篇: js转换后端返回的时间
推荐阅读
-
【JS】JavaScript异步系列(4)——生成器
-
JavaScript 异步加载JS
-
【JS】JavaScript异步操作系列(3)——Promise【1】
-
JavaScript基础-----js时间线&异步加载
-
Javascript学习总结 - JS基础系列一
-
Node.js中的异步生成器与异步迭代详解
-
JavaScript编程开发之js异步编程教程
-
Javascript异步编程的4种方法让你写出更出色的程序_javascript技巧
-
js 异步操作回调函数如何控制执行顺序_javascript技巧
-
js下获得客户端操作系统的函数代码(1:vista,2:windows7,3:2000,4:xp,5:2003,6:2008)_javascript技巧