在主范围内使用由自定义函数触发的异步迭代器

Use async iterator triggered by a custom function in main scope

我想做的是创建一个迭代器,它只在调用外部函数时触发,比如外部事件。

一个只等待自定义事件的迭代器。

function createIteratorWithFunction() {
  var thingThatResolves;
  var asyncIterable = {
    thingThatResolves,
    [Symbol.asyncIterator]() {
      return {
        next() {
          return (new Promise((resolve, reject) => asyncIterable.thingThatResolves = (resolve))).then(_ => ({
            value: _,
            done: false
          }));

        },
        return () {
          return {
            done: true
          }
        }
      };
    }
  };
  return asyncIterable;

}

iter = createIteratorWithFunction();
(async function() {
  for await (let val of iter) {
    console.log(val);
  }
})()
<button onclick="iter.thingThatResolves('execute');iter.thingThatResolves(3)">execute next!</button>

如你所见,它只解析了'execute',没有解析3,当然因为promises不能解析多次,而且只是异步更新,这个我理解,但是自从迭代器是异步的,我将如何创建一个队列,以便 next() 也可以检索任何可能被同步触发的值?

我觉得有一个涉及承诺链的更优雅的解决方案,但目前它正在逃避我。 :-) 查看内联评论:

function createIteratorWithFunction() {
    // Our pending promise, if any
    let promise = null;
    // The `resolve` function for our `pending` promise
    let resolve = null;
    // The values in the queue
    const values = [];
    // The async iterable
    const asyncIterable = {
        add(value) {
            // Add a value to the queue; if there's a pending promise, fulfill it
            values.push(value);
            const r = resolve;
            resolve = pending = null;
            r?.();
        },
        [Symbol.asyncIterator]() {
            return {
                async next() {
                    // If we don't have a value...
                    while (!values.length) {
                        // ...we need to wait for one; make sure we have something
                        // to wait for
                        if (!resolve) {
                            pending = new Promise(r => { resolve = r; });
                        }
                        await pending;
                    }
                    // Get the value we waited for and return it
                    const value = values.shift();
                    return {
                        value,
                        done: false,
                    };
                },
                return() {
                    return {
                        done: true,
                    };
                }
            };
        }
    };
    return asyncIterable;
}

const iter = createIteratorWithFunction();
(async function() {
    for await (let val of iter) {
        console.log(val);
    }
})();

document.getElementById("execute").addEventListener("click", () => {
    iter.add("execute");
    iter.add(3);
});
<button id="execute">execute next!</button>

这里的关键之一是异步可迭代对象可以有重叠的迭代,它不能因此而混淆。此实现通过创建它会在需要时同步等待的承诺来避免这种情况。

function createIteratorWithFunction() {
    // Our pending promise, if any
    let promise = null;
    // The `resolve` function for our `pending` promise
    let resolve = null;
    // The values in the queue
    const values = [];
    // The async iterable
    const asyncIterable = {
        add(value) {
            // Add a value to the queue; if there's a pending promise, fulfill it
            values.push(value);
            const r = resolve;
            resolve = pending = null;
            r?.();
        },
        [Symbol.asyncIterator]() {
            return {
                async next() {
                    // If we don't have a value...
                    while (!values.length) {
                        // ...we need to wait for one; make sure we have something
                        // to wait for
                        if (!resolve) {
                            pending = new Promise(r => { resolve = r; });
                        }
                        await pending;
                    }
                    // Get the value we waited for and return it
                    const value = values.shift();
                    return {
                        value,
                        done: false,
                    };
                },
                return() {
                    return {
                        done: true,
                    };
                }
            };
        }
    };
    return asyncIterable;
}

const iter = createIteratorWithFunction();
(async function() {
    for await (let val of iter) {
        console.log("first:", val);
    }
})();
(async function() {
    for await (let val of iter) {
        console.log("second:", val);
    }
})();

document.getElementById("execute").addEventListener("click", () => {
    iter.add("execute");
    iter.add(3);
});
<button id="execute">execute next!</button>

当我必须让 promise 的 resolve 函数可以在 promise 执行器函数(您传递的函数 new Promise)之外访问时,我永远不会开心,但正如我所说,优雅的解决方案承诺链正在逃避我。我强烈地感觉到它在那里……某处……:-)

另一种想法和方法,您可以使用自定义事件,一个优点是代码更容易推理。

下面我敲了一个简单的例子,它也可以让你取消迭代器和处理错误。 makeIter简单给你4个功能,

  1. add = 使用它向迭代器添加一个项目。
  2. iter = 这是您可以 for await 使用的迭代器。
  3. done = 如果已经完成,您可以调用它并让 GC 执行它。
  4. error = 允许您将错误放入迭代器,您可以通过 un-commenting 最后一行进行测试。

为了防止任何竞争条件,我只是使用了一个堆栈..

function makeIter() {
  const obj = new EventTarget();
  const evName = 'my-iter'; 
  
  const stack = [];
  
  obj.addEventListener(evName, e => {
    stack.push(e.detail);
    resolve();
  }); 

  async function *iter() {
    while (true) {
      await new Promise(r => resolve = r);
      while (stack.length) {
        const s = stack.shift();
        if (s.resolve) yield(s.resolve);
        if (s.reject) throw s.reject;
        if (s.cancel) return;
      }
    }
  }
  function ev(p) {
    obj.dispatchEvent(new CustomEvent(evName, {detail:p}));
  }
  
  return {
    error: (e) => ev({reject: e}), 
    done: () => ev({cancel: true}),
    add: item => ev({resolve: item}),
    iter: iter()
  }
}




///testing...
const test = makeIter();

(async function () {
  try {
    for await (const item of test.iter) {
      console.log(item);
    }
  } finally {
    console.log('iter done');
  }
}()); 

test.add('hello');
setTimeout(() => test.add('there'), 100);
setTimeout(() => {test.add('1'); test.add('2'); test.add('3'); }, 200);
setTimeout(() => test.add('4'), 400);

setTimeout(() => test.done(), 1000);
//setTimeout(() => test.error(new Error('oops')), 1000);