学习JavaScript中的异步Generator

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习JavaScript中的异步Generator相关的知识,希望对你有一定的参考价值。

本文和大家分享的主要是javascript异步Generator相关内容,一起来看看吧,希望对大家学习javascript 有所帮助。

异步的generators和异步iteration已经到来 ! 这是错误的, 它们现在还在 阶段 3 ,这表示他们很有可能在JavaScript未来的一个版本中发布。 在他们发布之前,你可以通过 Babel 来在你的项目中使用还在阶段3的建议内容。

网站基本上还是一些分散运行的应用,因为任何在语言上的修改都会造成永久的影响,所以所有的未来的版本都需要向后兼容。因此,被加入到ECMAScript标准的特性,它必须是十分的可靠,而且它的语法需要很优雅。

考虑到这一点,我们希望异步generator和迭代器可以显著地影响我们如何构建今后的代码,同时也解决现在的问题。让我们开始了解异步generator是如何工作的,它在我们的正式开发中又会遇到什么样的问题。

总结: 异步的Generators是如何工作的呢

简而言之,异步的generators和普通的generator函数很像,但是它可以yield Promises

总的来说,普通的generator函数基本上就是一个 迭代器  观察者 模式的集合。generator是一个可以中止的函数,你可以通过调用 .next() 来一步步执行。可以同通过 .next() 来多次从generator输出内容,也可以通过 .next(valueToPush) 来多次传入参数。这种双向的接口可以使你通过一种语法同时完成迭代器和观察者的功能!

当然generators也有它的缺点:它在调用 .next() 的时候必须立即(同步)返回数据。换句话来说,就是代码在调用 .next() 的时候就需要得到数据。在generator需要时能够生成新数据的情况下是可以的,但是没有办法处理迭代一个异步的(或者临时的)数据来源,它们需要自己控制在下一次数据准备好的时候执行下一次。

WebSocket消息机制就是一个很好的异步获取数据的例子。如果我们已经接收到了所有的数据,那么我们当然可以同步地遍历它们。但是,我们也可能会遇到我们并不知道什么时候会接收到数据,所以我们需要一个机制去等待数据接收完成后去遍历。异步generators和异步迭代器可以让我们做到这个。

简单的来说就是:generator函数适用于数据可以被使用者控制的情况,异步generators适用于允许数据源本身控制的情况。

一个简单的例子: 生成和使用AsyncGenerator

让我们用一个例子来练习我们的异步方案。我们需要编写一个异步的generator函数,它可以重复的等待一个随机的毫秒数后生成一个新的数字。在几秒钟中时间里,它可能会从0开始生成5个左右的数字。首先我们先通过创建一个Promise来创建一个定时器:

// 创建一个Promise,并在msresolvesvar timer = function(ms) {

return new Promise(resolve => {

setTimeout(resolve, ms);

});

};

运行 timer(5000) 会返回一个Promise,并且会在5秒后resolve。现在我们可以写一个异步generator

// Repeatedly generate a number starting// from 0 after a random amount of timevar source = async function\*() {

var i = 0;

while (true) {

await timer(Math.random() \* 1000);

yield i++;

}

};

如此复杂的功能却可以写的如此优雅!我们的异步generator函数等待一个随机的时间后 yield并减小i的值。如果我们没有异步generator,我们可以像下面一样使用普通的generator函数,通过yield Promises来实现:

var source = function\*() {

var i = 0;

while (true) {

yield timer(Math.random() \* 1000)

.then(() => i++);

}

};

当然,这里还有一些特殊情况和引用需要我们处理,所以最好有一个专门的函数类型!现在是时候编写使用代码了;因为我们需要 await 操作符,所以我们将会创建一个异步的 run() 函数。

// 把所有都集合到一起var run = async function() {

var stream = source();

for await (let n of stream) {

console.log(n);

}

};

run();// => 0// => 1// => 2// => 3// ...

这是多么神奇,只有20行不到的代码。首先,我们先运行了异步generator函数 source ,它返回了一个特殊的 AsyncGenerator 对象。然后,我们使用一个语法上叫异步迭代for await...of 循环遍历 source 生成的对象。

但是我们还可以再改进一下:假设我们是想要输出 source 生成的数字。我们可以在 for await...of 循环里面直接输出它们,但是我们最好在循环的外面转换”stream 的值,像是使用 .map() 一样来转换数组里的值。它是如此的简单:

// Return a new async iterator that applies a// transform to the values from another async generatorvar map = async function\*(stream, transform) {

for await (let n of stream) {

yield transform(n);

}

};

接下来我们只需要再往 run() 函数中加一行代码就好了:

// Tie everything together

var run = async function() {

var stream = source();

+ // Square values generated by source() as they arrive

+ stream = map(stream, n => n \* n);

for await (let n of stream) {

console.log(n);

}

};

当我们运行 run() 就会输出:

// => 0// => 1// => 4// => 9// ...

多么感人啊!但是只是用于计算数字有一点大材小用了。

中级例子: WebSockets中使用AsyncIterator(异步迭代器)

我们一般是通过绑定事件来监听WebSocket的数据:

var ws = new WebSocket(’ws://localhost:3000/’);

ws.addEventListener(’message’, event => {

console.log(event.data);

});

但是如果可以把WebSocket的信息当做stream,这样就可以用我们上面的办法“iterate”这些信息。不幸的是,WebSockets还没有异步迭代器的功能,但是我们只需要写短短的几行就可以自己来实现这个功能。我们的 run() 函数大概的样子如下:

// Tie everything togethervar run = async () => {

var ws = new WebSocket(’ws://localhost:3000/’);

for await (let message of ws) {

console.log(message);

}

};

Now for that polyfill.你可能会回忆起 Chris Aquino’s blog series 中写到的内容,一个对象要使用 for...of 循环,必须要有 Symbol.iterator 属性。同样的,一个对象要想使用 for await...of 循环,它必须要有 Symbol.asyncIterator 属性。下面就是具体的实现:

// Add an async iterator to all WebSockets

WebSocket.prototype[Symbol.asyncIterator] = async function\*() {

while(this.readyState !== 3) {

yield (await oncePromise(this, ’message’)).data;

}

};

这个异步迭代器会等待接受信息,然后会对WebSocketMessageEvent 返回的数据的 data属性进行 yield  oncePromise() 函数有一点黑科技:它返回了一个Promise,当事件触发时它会被resolves,然后立即移除事件监听。

// Generate a Promise that listens only once for an eventvar oncePromise = (emitter, event) => {

return new Promise(resolve => {

var handler = (...args) => {

emitter.removeEventListener(event, handler);

resolve(...args);

};

emitter.addEventListener(event, handler);

});

};

这样看上去有一点低效,但是证明了websocket的信息接收确实可以用我们的异步迭代器实现。如果你在 http://localhost:3000 有一个运行的WebSocket服务,那么你可以通过调用 run()来监听信息流:

run();// => "hello"// => "sandwich"// => "otters"// ...

高级例子: 重写 RxJS

现在是时候面对最后的挑战了。 反应型函数编程 (FRP)UI编程和JavaScript中被大量使用, RxJS 是这种编程方式中最流行的框架。RxJS中模型事件来源例如Observable--它们很想一个一个事件流或者lazy array,它们可以被类似数组语法中的 map()  filter() 处理。

自从FRP补充了JavaScript中的非阻塞式理念, RxJSAPI 很有可能会加入到JavaScript未来的一个版本中。同时,我们可以使用异步generators编写我们自己的类似RxJS的功能,而这仅仅只需要80行代码。下面就是我们要实现的目标:

1.监听所有的点击事件

2.过滤点击事件只获取点击anchor标签的事件

3.只允许不同的点击Only allow distinct clicks

4.将点击事件映射到点击计数器和点击事件

5.500ms只可以触发一次点击

6.打印点击的次数和事件

7.这些问题都是RxJS解决了的问题,所以我们将要尝试重新实现。下面是我们的实现:

// Tie everything togethervar run = async () => {

var i = 0;

var clicks = streamify(’click’, document.querySelector(’body’));

clicks = filter(clicks, e => e.target.matches(’a’));

clicks = distinct(clicks, e => e.target);

clicks = map(clicks, e => [i++, e]);

clicks = throttle(clicks, 500);

subscribe(clicks, ([ id, click ]) => {

console.log(id);

console.log(click);

click.preventDefault();

});

};

run();

为了使上面的函数正常运行,我们还需要6个函数: streamify() , filter() , distinct() ,map() , throttle()  subscribe() 

// 把所有的event emitter放入一个streamvar streamify = async function\*(event, element) {

while (true) {

yield await oncePromise(element, event);

}

};

streamify() 像是一个WebSocket异步迭代器: oncePromise() 使用 .addEventListener()去监听事件一次, 然后resolves Promise. 通过 while (true) 循环 , 我们可以一直监听事件。

// Only pass along events that meet a conditionvar filter = async function\*(stream, test) {

for await (var event of stream) {

if (test(event)) {

yield event;

}

}

};

filter() 会只允许通过test的事件被 yield . map() 几乎是相同的:

// Transform every event of the streamvar map = async function\*(stream, transform) {

for await (var event of stream) {

yield transform(event);

}

};

map() 可以简单地在yield之前变换事件。 distinct() 展示了异步generator的其中一个强大的功能:它可以保存局部变量!

var identity = e => e;

// 只允许与最后一个不相同的事件通过var distinct = async function\*(stream, extract = identity) {

var lastVal;

var thisVal;

for await (var event of stream) {

thisVal = extract(event);

if (thisVal !== lastVal) {

lastVal = thisVal;

yield event;

}

}

};

最后,强大的 throttle() 函数和 distinct() 很像:它记录最后一个事件的时间,且只允许超过最后一次 yield 事件一个确定的时间的事件通过。

// 只允许超过最后一次事件确定时间的事件通过。var throttle = async function\*(stream, delay) {

var lastTime;

var thisTime;

for await (var event of stream) {

thisTime = (new Date()).getTime();

if (!lastTime || thisTime - lastTime > delay) {

lastTime = thisTime;

yield event;

}

}

};

我们做了这么多,最后,我们还需要打印出每次的点击事件和当前的次数。 subscribe() 做了一些零碎的事情:它在每一次事件循环的时候运行,并执行callback,所以没有必要使用 yield

// 每次事件到达都调用一次回调函数var subscribe = async (stream, callback) => {

for await (var event of stream) {

callback(event);

}

};

到这里,我们已经写了一个我们自己的反应型函数式管道!

你可以在 这里 获取到所有的例子的代码和要点。

挑战

异步generators是如此的优雅。而generator函数允许我们从迭代器中回去数据,异步generators可以让我们迭代推送过来的数据。这是多么好的异步数据结构的抽象。当然,也有一些注意事项。

首先,对一个objects增加支持 for await...of 的功能有一些粗糙,除非你可以避免使用 yield  await 。尤其是,使用 .addEventListener() 转换任何东西都很棘手,因为你不可以在一个回调中使用 yield 操作:

var streamify = async function\*(event, element) {

element.addEventListener(event, e => {

// 这里将无法运行,因为yield

// 不可以在一个普通函数中被使用

yield e;

});

};

同样的,你也不可以在 .forEach() 和其他函数型的方法中使用 yield 。这是一个固有的限制因为我们不能保证在generator已经完成后不使用 yield 

为了绕过这个问题,我们写了一个 oncePromise() 函数来帮组我们。撇开一些潜在的性能问题,需要注意的是Promise的回调总是在当前的调用堆栈结束之后执行。在浏览器端,类似 microtasks 一样运行Promise的回调是不会出现问题的,但是一些Promisepolyfill在下一次事件循环运行之前是不会运行callback。因此,调用 .preventDefault() 函数有时候会没有有效果,因为可能DOM时间已经冒泡到浏览器了。

JavaScript现在已经有了多个异步流数据类型: Stream , AsyncGenerator 和最后的 Observable 。虽然三个都是属于推送数据源,但是在处理回调和控制底层资源上还是有一些微妙的语义上的不同。

 

来源:众成翻译

以上是关于学习JavaScript中的异步Generator的主要内容,如果未能解决你的问题,请参考以下文章

js-ES6学习笔记-Generator函数的异步应用

JavaScript的ES6语法11generator实例-KOA

JavaScript中的Generator函数

16.Generator 函数的异步应用

Generator 函数的异步应用

JavaScript异步编程:Generator与Async