JS:如何在回调中使用生成器和 yield

问题描述 投票:0回答:5

我使用 JS 生成器在

setTimeout
的回调中产生一个值:

function* sleep() {
  // Using yield here is OK
  // yield 5; 
  setTimeout(function() {
    // Using yield here will throw error
    yield 5;
  }, 5000);
}

// sync
const sleepTime = sleep().next()

为什么我不能在生成器的回调中产生值?

javascript callback yield
5个回答
20
投票

function*
声明是同步的。您可以生成一个新的
Promise
对象,将
.then()
链接到
.next().value
以检索已解决的
Promise

function* sleep() {
  yield new Promise(resolve => {
    setTimeout(() => {
      resolve(5);
    }, 5000);
  })
}

// sync
const sleepTime = sleep().next().value
  .then(n => console.log(n))
  .catch(e => console.error(e));

4
投票

我遇到这个问题是为了寻找一种方法将经常调用的回调(例如节点流、事件侦听器或

setInterval
回调)转换为异步可迭代对象,经过一些研究后我发现了这个 NPM 包做所有这些:
EventIterator
.

EventIterator
是一个小模块,它大大简化了将事件发射器、事件目标和类似对象转换为 EcmaScript 异步迭代器的过程。它适用于浏览器和 Node.js 环境。

一个基本的

setInterval
可迭代:

import { EventIterator } from "event-iterator"
const counter = ms =>
  new EventIterator(({ push }) => {
    let count = 0
    const interval = setInterval(() => push(++count), ms)
    return () => clearInterval(interval)
  })

for await (const count of counter(1000)) console.log(count)

(把

push
想象成
yield
)。

虽然这在技术上没有回答问题,但公认的答案也没有真正回答它,而且这个解决方案似乎非常接近 OP 所寻找的。


0
投票

要直接回答问题,使用“*”/“yield”语法是不可能的。从这里: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield

...“yield”关键字只能在包含它的生成器函数中直接使用。 “它不能在嵌套函数中使用”,例如回调。

对 OP 的“为什么”问题没有帮助的答案是它被 ECMAScript 语言规范禁止,至少在严格模式下是这样: https://262.ecma-international.org/9.0/#sec-generator-abstract-operations

关于原因的更多直觉:生成器的实现是“yield”关键字暂停其生成器函数的执行。生成器函数的执行是普通的,当它返回时,它生成的可迭代对象结束。这向调用者发出信号,表明没有更多的值到来,任何等待它的循环也将结束。之后,即使嵌套回调再次运行,也没有机会向任何感兴趣的调用者产生任何东西。

虽然回调或其他嵌套函数可以绑定来自外部生成器的变量,但它可以逃脱生成器的生命周期并在任何其他时间/地点/上下文中运行。这意味着所需的 yield 关键字可能没有要暂停的函数,也没有调用程序或循环来产生值。我推测严格模式语法错误被放在这里是为了避免代码作者发生一些不希望发生的事情。

也就是说,生成器语法对于创建 OP 的预期效果不是必需的。当目标只是让“next()”工作,或参与异步迭代器协议(“for await (...)”)时,这些协议可以符合使用普通函数和对象,而不需要屈服。您要遵守的协议记录在此处:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols

...提到 EventIterator 的另一个答案是帮助程序代码的示例,它使这更容易做到。


0
投票

如果回调只被调用一次,那么只需将它包装成一个承诺就很简单了,就像其他答案显示的那样。
但这不适用于重复调用的回调(如

setInterval
),因为承诺只能解决一次。

这就是为什么您需要在“使用”后创建新的承诺/解决对。
在主生成器函数中,您只需等待循环中的承诺。

async function* seconds(max) {
  let count = 1;
  // create the first resolve/promise pair
  let resolve;
  let promise = new Promise(r => resolve = r);
  const interval_handle = setInterval(() => {
    const value = new Date();
    count++;
    const last = count > max;
    // use the resolve callback
    resolve({last, value});
    if (last) {
      clearInterval(interval_handle);
      return;
    }
    // create new resolve/promise
    promise = new Promise(r => resolve = r);
  }, 1000);
  
  // await the promises
  for (;;) {
    const {last, value} = await promise;
    yield value;
    if (last) return;
  }
}

(async () => {
  for await (const datetime of seconds(10)) {
    console.log(datetime.toLocaleString());
  }
})();


-1
投票

扩展上述@BananaAcid 和@guest271314 的答案

下面的代码循环产生。因此可以不止一次地产生

async function* foo(loopVal) {
  for(let i=0;i<loopVal;i++){
    yield new Promise(resolve => {
      setTimeout(() => {
        resolve(i);
      }, 5000);
    })
  }
}

(async function() {
  for await (const num of foo(5)) {
    console.log(num);
  }
})();
© www.soinside.com 2019 - 2024. All rights reserved.