• 异步三部曲之生成器


    概述

    这是我看你不知道的JavaScript(中卷)的读书笔记,供以后开发时参考,相信对其他人也有用。

    生成器和迭代器

    生成器是另一个封装了时间的框架,它用function*来标识,执行生成器函数会返回一个迭代器,执行迭代器能控制生成器中代码的执行。下面是一个简单的实例:

    var x = 1;
    //这是一个生成器函数foo
    function *foo() {
        x++;
        yield; // 暂停!
        console.log( "x:", x );
    }
    function bar() {
        x++;
    }
    // 构造一个迭代器it来控制这个生成器
    var it = foo();
    // 这里启动foo()!
    it.next();
    x; // 2
    bar();
    x; // 3
    it.next(); // x: 3
    

    it.next().value等于每次执行时yield后面表达式的值或者函数return后面的值,所以上面两次it.next().value都等于undefined。下面是一个加了表达式的示例:

    function *foo(x) {
        var y = x * (yield "Hello"); // <-- yield一个值!
        return y;
    }
    var it = foo( 6 );
    var res = it.next(); // 第一个next(),并不传入任何东西
    res.value; // "Hello"
    res = it.next( 7 ); // 向等待的yield传入7
    res.value; // 42
    

    需要注意的是,每次构建一个迭代器,实际上就隐式构建了生成器的一个实例,通过这个迭代器控制的是这个生成器实例。

    更多时候,如果我们不通过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和b
    a = 1;
    b = 2;
    var s1 = step( foo );
    var s2 = step( bar );
    // 首次运行*foo()
    s1();
    s1();
    s1();
    // 现在运行*bar()
    s2();
    s2();
    s2();
    s2();
    console.log( a, b ); // 11 22
    

    迭代器接口

    除了执行生成器来建立一个迭代器外,我们还可以自己编写一个迭代器接口:

    var something = (function(){
        var nextVal;
        return {
            // for..of循环需要
            [Symbol.iterator]: function(){ return this; },
            // 标准迭代器接口方法
            next: function(){
                if (nextVal === undefined) {
                    nextVal = 1;
                } else {
                    nextVal = (3 * nextVal) + 6;
                }
                return { done:false, value:nextVal };
            }
        };
    })();
    something.next().value; // 1
    something.next().value; // 9
    something.next().value; // 33
    something.next().value; // 105
    

    从上述代码可以看出,迭代器都有一个[Symbol.iterator]方法,它返回自身。并且可以看出next方法的执行逻辑。

    值得一提的是,es6为每个数组对象添加了[Symbol.iterator]方法,使用它可以得到一个迭代器对象。示例如下:

    var a = [1,3,5,7,9][Symbol.iterator]()
    a.next().value; //1
    

    每个iterable(可迭代)的对象都有这个[Symbol.iterator]方法,利用这个方法可以返回一个包含自身的迭代器对象。
    es6还新增了一个for..of循环,使用它可以自动迭代迭代器:

    for (var v of something) {
        console.log( v );
        // 不要死循环!
        if (v > 500) {
            break;
        }
    }
    // 1 9 33 105 321 969
    

    另外,利用Object.keys(obj)可以返回对象的所有实例属性然后进行迭代,利用for..in则可以迭代对象的所有实例属性和原型属性。

    迭代器的异步使用

    首先让我们来看下面这段代码:

    var data = ajax( "..url 1.." );
    console.log( data );
    

    很显然,这段代码是不能运行的,但是我们在执行异步操作的时候,脑中其实更偏向于理解上面这种形式的代码,那么怎么让上面这种形式的代码能够正常运行呢?可以用生成器迭代器改写成下面这个样子:

    function* foo(url) {
        ajax(url, function(data) {
            yield data;
        })
    }
    var data = foo(url);
    console.log(data.next().value);
    

    虽然我们期望这么运行,然而事实上上面这段代码根本不能运行,原因是yield只能在function*里面运行,不能在其它函数里面运行,否则会报错。所以只能退而求其次,改成如下代码:

    function foo(url) {
        ajax(url, function(data) {
            it.next(data);
        })
    }
    function* main(url) {
        var text = yield foo(url);
        console.log(text);
    }
    var it = main(url);
    it.next();
    

    上述代码还是很有技巧性的,看起来不是很容易理解,原因在于回调函数没有和ajax函数分离。所以用生成器+Promise的形式则容易理解得多:

    function foo() {
        return request(url);
    }
    function* main(url) {
        var text = yield foo(url);
    }
    var it = main(url);
    console.log(it.next().value);
    

    虽然这里打印的是一个Promise对象,而不是返回的数据。但是整个过程看起来是同步的,实际执行却是异步的。其中的机制是,我们把异步过程封装在一个函数中,然后通过yield这个函数,使得我们能够在需要的时刻执行这个异步过程。

    es5实现生成器

    其实生成器并不是es6引入的新的东西,而是es5的一个polyfill。比如说下面这段生成器代码:

    // request(..)是一个支持Promise的Ajax工具
    function *foo(url) {
        try {
            console.log( "requesting:", url );
            var val = yield request( url );
            console.log( val );
        }
        catch (err) {
        console.log( "Oops:", err );
        return false;
        }
    }
    var it = foo( "http://some.url.1" );
    

    可以利用es5来实现这个生成器过程,代码如下。通过看这段代码,可以加深我们对生成器的理解。

    function foo(url) {
        // 管理生成器状态
        var state;
        // 生成器变量范围声明
        var val;
        function process(v) {
            switch (state) {
            case 1:
                console.log( "requesting:", url );
                return request( url );
            case 2:
                val = v;
                console.log( val );
                return;
            case 3:
                var err = v;
                console.log( "Oops:", err );
                return false;
            }
        }
        // 构造并返回一个生成器
        return {
            next: function(v) {
                // 初始状态
                if (!state) {
                    state = 1;
                    return {
                        done: false,
                        value: process()
                    };
                }
                // yield成功恢复
                else if (state == 1) {
                    state = 2;
                    return {
                        done: true,
                        value: process( v )
                    };
                }
                // 生成器已经完成
                else {
                    return {
                        done: true,
                        value: undefined
                    };
                }
            },
            "throw": function(e) {
                // 唯一的显式错误处理在状态1
                if (state == 1) {
                    state = 3;
                    return {
                        done: true,
                        value: process( e )
                    };
                }
                // 否则错误就不会处理,所以只把它抛回
                else {
                    throw e;
                }
            }
        };
    }
    

    可以看到,代码中用闭包封装了一个状态属性state和一个process方法,这个process方法根据不同的状态把原来的代码分块,最后通过构造返回一个生成器,来判断state的状态,然后根据process方法调用不同的代码块。

  • 相关阅读:
    springMVC源码分析
    世界近代史二
    世界近代历史
    UVA
    UVA
    UVA
    Web 前端开发学习之路(入门篇)
    01 Linux入门介绍
    2. Python基本知识
    1. 初识Python
  • 原文地址:https://www.cnblogs.com/yangzhou33/p/8837293.html
Copyright © 2020-2023  润新知