你不知道的JavaScript(中卷)|生成器(二)

异步迭代生成器
回想一下回调方法:

function foo(x, y, cb) {
    ajax("http://some.url.1/?x=" + x + "&y=" + y, cb);
}
foo(11, 31, function(err, text) {
    if (err) {
        console.error(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.error(err);
    }
}
var it = main();
// 这里启动!
it.next();

在yield foo(11,31) 中,首先调用foo(11,31),它没有返回值(即返回undefined),所以我们发出了一个调用来请求数据,但实际上之后做的是yield undefined。这没问题,因为这段代码当前并不依赖yield 出来的值来做任何事情。
这里并不是在消息传递的意义上使用yield,而只是将其用于流程控制实现暂停/ 阻塞。实际上,它还是会有消息传递,但只是生成器恢复运行之后的单向消息传递。
所以,生成器在yield 处暂停,本质上是在提出一个问题:“我应该返回什么值来赋给变量text ?”谁来回答这个问题呢?
看一下foo(..)。如果这个Ajax 请求成功,我们调用:

it.next( data );

这会用响应数据恢复生成器,意味着我们暂停的yield 表达式直接接收到了这个值。然后随着生成器代码继续运行,这个值被赋给局部变量text。
我们在生成器内部有了看似完全同步的代码(除了yield 关键字本身),但隐藏在背后的是,在foo(..) 内的运行可以完全异步。
这是巨大的改进!对于我们前面陈述的回调无法以顺序同步的、符合我们大脑思考模式的方式表达异步这个问题,这是一个近乎完美的解决方案。

同步错误处理

try {
    var text = yield foo(11, 31);
    console.log(text);
} catch (err) {
    console.error(err);
}

我们已经看到yield 是如何让赋值语句暂停来等待foo(..) 完成,使得响应完成后可以被赋给text。精彩的部分在于yield 暂停也使得生成器能够捕获错误。通过这段前面列出的代码把错误抛出到生成器中。
生成器yield 暂停的特性意味着我们不仅能够从异步函数调用得到看似同步的返回值,还可以同步捕获来自这些异步函数调用的错误!

生成器+Promise
ES6 中最完美的世界就是生成器(看似同步的异步代码)和Promise(可信任可组合)的结合。

function foo(x, y) {
    return request("http://some.url.1/?x=" + x + "&y=" + y);
}
foo(11, 31).then(function(text) {
    console.log(text);
}, function(err) {
    console.error(err);
});

而这里支持Promise 的foo(..) 在发出Ajax 调用之后返回了一个promise。这暗示我们可以通过foo(..) 构造一个promise,然后通过生成器把它yield 出来,然后迭代器控制代码就可以接收到这个promise 了。
获得Promise 和生成器最大效用的最自然的方法就是yield 出来一个Promise,然后通过这个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.error(err);
    }
}
var it = main();
var p = it.next().value;
// 等待promise p决议
p.then(function(text) {
    it.next(text);
}, function(err) {
    it.throw(err);
});

支持Promise 的Generator Runner
定义一个独立工具,叫作run(..):

// 在此感谢Benjamin Gruenbaum (@benjamingr on GitHub)的巨大改进!
function run(gen) {
    var args = [].slice.call(arguments, 1),
        it;
    // 在当前上下文中初始化生成器
    it = gen.apply(this, args);
    // 返回一个promise用于生成器完成
    return Promise.resolve().then(function handleNext(value) {
        // 对下一个yield出的值运行
        var next = it.next(value);
        return (function handleResult(next) {
            // 生成器运行完毕了吗?
            if (next.done) {
                return next.value;
            }
            // 否则继续运行
            else {
                return Promise.resolve(next.value).then(
                    // 成功就恢复异步循环,把决议的值发回生成器
                    handleNext,
                    // 如果value是被拒绝的 promise,
                    // 就把错误传回生成器进行出错处理
                    function handleErr(err) {
                        return Promise.resolve(it.throw(err)).then(handleResult);
                    });
            }
        })(next);
    });
}

诚如所见,你可能并不愿意编写这么复杂的工具,并且也会特别不希望为每个使用的生成器都重复这段代码。所以,一个工具或库中的辅助函数绝对是必要的。尽管如此,我还是建议你花费几分钟时间学习这段代码,以更好地理解生成器+Promise 协同运作模式。
如何在运行Ajax 的例子中使用run(..) 和*main() 呢?

function *main() {
    // ..
}
run( main );

就是这样!这种运行run(..) 的方式,它会自动异步运行你传给它的生成器,直到结束!

生成器中的Promise 并发
想象这样一个场景:你需要从两个不同的来源获取数据,然后把响应组合在一起以形成第三个请求,最终把最后一条响应打印出来。
你的第一直觉可能类似如下:

function* foo() {
    var r1 = yield request("http://some.url.1");
    var r2 = yield request("http://some.url.2");
    var r3 = yield request("http://some.url.3/?v=" + r1 + "," + r2);
    console.log(r3);
}
// 使用前面定义的工具run(..)
run(foo);

这段代码可以工作,但是针对我们特定的场景而言,它并不是最优的。
因为请求r1 和r2 能够——出于性能考虑也应该——并发执行,但是在这段代码中,它们是依次执行的; 直到请求URL"http://some.url.1" 完成后才会通过Ajax获取URL"http://some.url.2"。这两个请求是相互独立的,所以性能更高的方案应该是让它们同时运行。

function* foo() {
    // 让两个请求"并行"
    var p1 = request("http://some.url.1");
    var p2 = request("http://some.url.2");
    // 等待两个promise都决议
    var r1 = yield p1;
    var r2 = yield p2;
    var r3 = yield request("http://some.url.3/?v=" + r1 + "," + r2);
    console.log(r3);
}
// 使用前面定义的工具run(..)
run(foo);

这种流程控制模型如果听起来有点熟悉的话,是因为这基本上和我们在第3 章中通过Promise.all([ .. ]) 工具实现的gate 模式相同。因此,也可以这样表达这种流程控制:

function* foo() {
    // 让两个请求"并行",并等待两个promise都决议
    var results = yield Promise.all([
        request("http://some.url.1"),
        request("http://some.url.2")
    ]);
    var r1 = results[0];
    var r2 = results[1];
    var r3 = yield request("http://some.url.3/?v=" + r1 + "," + r2);
    console.log(r3);
}
// 使用前面定义的工具run(..)
run(foo);

生成器委托
你可能会从一个生成器调用另一个生成器,使用辅助函数run(..),就像这样:

function* foo() {
    var r2 = yield request("http://some.url.2");
    var r3 = yield request("http://some.url.3/?v=" + r2);
    return r3;
}

function* bar() {
    var r1 = yield request("http://some.url.1");
    // 通过 run(..) "委托"给*foo()
    var r3 = yield run(foo);
    console.log(r3);
}
run(bar);

我们再次通过run(..) 工具从bar() 内部运行foo()。这里我们利用了如下事实:我们前面定义的run(..) 返回一个promise,这个promise 在生成器运行结束时(或出错退出时)决议。因此,如果从一个run(..) 调用中yield 出来一个promise 到另一个run(..) 实例中,它会自动暂停bar(),直到foo() 结束。
但其实还有一个更好的方法可以实现从bar() 调用foo(),称为yield 委托。yield 委托的具体语法是:yield * __(注意多出来的*)。在我们弄清它在前面的例子中的使用之前,
先来看一个简单点的场景:

function* foo() {
    console.log("*foo() starting");
    yield 3;
    yield 4;
    console.log("*foo() finished");
}

function* bar() {
    yield 1;
    yield 2;
    yield* foo(); // yield委托!
    yield 5;
}
var it = bar();
it.next().value; // 1
it.next().value; // 2
it.next().value; // *foo()启动
// 3
it.next().value; // 4
it.next().value; // *foo()完成
// 5

首先,和我们以前看到的完全一样,调用foo() 创建一个迭代器。然后yield * 把迭代器实例控制(当前bar() 生成器的)委托给/ 转移到了这另一个foo() 迭代器。
所以,前面两个it.next() 调用控制的是bar()。但当我们发出第三个it.next() 调用时,foo() 现在启动了,我们现在控制的是foo() 而不是bar()。这也是为什么这被称为委托:bar() 把自己的迭代控制委托给了foo()。
一旦it 迭代器控制消耗了整个foo() 迭代器,it 就会自动转回控制bar()。
现在回到前面使用三个顺序Ajax 请求的例子:

function* foo() {
    var r2 = yield request("http://some.url.2");
    var r3 = yield request("http://some.url.3/?v=" + r2);
    return r3;
}

function* bar() {
    var r1 = yield request("http://some.url.1");
    // 通过 yeild* "委托"给*foo()
    var r3 = yield* foo();
    console.log(r3);
}
run(bar);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容