Fork me on GitHub

异步三部曲之生成器

概述

这是我看你不知道的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方法调用不同的代码块。

posted @ 2018-04-15 00:11  馒头加梨子  阅读(303)  评论(0编辑  收藏  举报