欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【JS】JavaScript异步系列(4)——生成器

程序员文章站 2024-01-28 09:26:04
...

本篇博客来源于王福明博客的异步系列和《你不知道的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

这段代码的运行过程是什么样呢?请往下看:

  1. it = foo()运算并没有执行生成器foo(),,而只是够早了一个迭代器(iterator),这个迭代器会控制它的执行。【后面会接受迭代器】
  2. 第一个it.next()启动了生成器foo(),并运行了foo()第一行的x++foo()yield语句处暂停,到这里第一个it.next()调用结束。此时的foo()仍在运行并且是活跃的,但处于暂停状态。
  3. 此时查看x的值为2.然后我们再调用bar(),再次递增x。
  4. 再次查看x的值,此时x的值为3.
  5. 最后的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.

迭代消息传递

处理能够接受参数并提供返回值之外,生成器还提供了更强大的内建消息输入输出能力,通过yieldnext(...)实现。

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,该如何使用呢?有两种方式:nextfor...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 }
  1. 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的隐藏

相关标签: 异步 生成器