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

27

我使用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()

为什么我无法在生成器的回调函数中返回值?

5个回答

18

function*声明是同步的。你可以yield一个新的Promise对象,链式调用.then()来检索解决的Promise值的.next().value

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));

27
这只会解决一次,因此不会产生太多 :) 例如,代码如何从setInterval回调中重复yield - Dan Dascalescu
.then(n => console(n)) should be .then(n => console.log(n)) - ahgood
6
一个只能产生一次的生成器有什么意义? - Lloyd
这对我很有用。我需要一个生成器来为执行ADB exec-out的生成进程产生一次yield。我在进程完成执行后立即捕获管道的stdout。然后,我使用字符串解析承诺来产生yield。对于这种类型的用法非常有效 :) - Fuczi Fuczi
一个只产生一次值的生成器有什么意义呢?-> 浏览器兼容性 - Benjamin

7
我来到这个问题是为了寻找将定期调用的回调函数(例如Node流、事件监听器或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所寻求的。


2
这应该是被接受的答案。另一个完全没用,因为它只能产生一次。另一方面,我可以成功地使用EventIterator轻松创建一个迭代器。 - Boiethios

1
直接回答这个问题,使用“*” / “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 (...)”)时,可以使用普通函数和对象符合这些协议,而无需使用yield。您想符合的协议在此处记录:

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

...提到EventIterator的另一个答案是一个辅助代码的例子,可以使这个过程更容易实现。


0

延续上面@BananaAcid和@guest271314的答案

下面的代码会产生循环。 因此可以yield多次

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);
  }
})();

1
这个答案不正确,因为它立即产生了五个超时。 - Jacob
BananaAcid没有提供答案,他们只是修正了一个错别字。 - gre_gor
问题在于如何使用 setInterval 来实现它。 - gre_gor

-1
如果回调函数只被调用一次,那么可以简单地将其包装成一个 Promise,就像其他答案所示。
但是对于重复调用的回调函数(例如 setInterval),这种方法不起作用,因为 Promise 只能被解决一次。
这就是为什么在“使用”后需要创建新的 Promise/resolve 对的原因。
在主生成器函数中,您只需在循环中等待 Promise 即可。

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());
  }
})();


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接