如何在基于回调的循环中使用yield?

6

虽然“yield”关键字的主要目的是为一些数据提供迭代器, 但它也非常方便用于创建异步循环:

function* bigLoop() {
    // Some nested loops
    for( ... ) {
        for( ... ) {
            // Yields current progress, eg. when parsing file
            // or processing an image                        
            yield percentCompleted;
        }
    }
}

这可以被异步调用:
function big_loop_async(delay) {
    var iterator = big_loop();
    function doNext() {
        var next = iterator.next();
        var percent_done = next.done?100:next.value;
        console.log(percent_done, " % done.");
        // start next iteration after delay, allowing other events to be processed
        if(!next.done)
            setTimeout(doNext, delay);
    }
    setTimeout(doNext, delay);
}

然而,在现代JavaScript中,基于回调的循环已经变得非常流行。我们有Array.prototype.forEachArray.prototype.findArray.prototype.sort。所有这些都是基于每次迭代传递的回调函数。我甚至听说如果可以使用它们,建议我们使用它们,因为它们可以比标准for循环更好地优化。
我经常使用基于回调的循环来抽象出一些复杂的循环模式。
问题在于,是否可能将这些转换为基于yield的迭代器?作为一个简单的例子,考虑我想异步地对数组进行排序。

使用生成器和单个JavaScript线程异步地对数组进行排序,是否有任何好处? - Davin Tryon
@DavinTryon 这只是一个例子,例子并不是为了实际应用而设计的,而是为了易于理解。无论如何,如果数组太长,排序可能需要太长时间,例如,会使人从您的服务器断开连接或导致用户浏览器出现延迟。 - Tomáš Zato
@TomášZato 这是一篇好文,你可能会觉得很有趣(不幸的标题)。因此,简短地回答你的帖子,是的,它是可以实现的。 - Davin Tryon
1
@DavinTryon,我已经阅读了你建议的那篇文章。对于生成器来说,那是一篇非常好的文章,但我不认为它包含任何解决这个问题的方法。 - Huan
“我甚至听说过建议我们尽可能使用它们,因为它们可以比标准的for循环更好地进行优化。” - 这已经不再是真实情况了。 - Bergi
@Bergi 是的,在发布这篇文章后不久,我实际上就调查过了,并发现它们在几乎所有情况下都更慢。 - Tomáš Zato
1个回答

2

tl;dr: You can’t do that, but check out this other thing you can do with the latest V8 and bluebird:

async function asyncReduce() {
    const sum = await Promise.reduce(
        [1, 2, 3, 4, 5],
        async (m, n) => m + await Promise.delay(200, n),
        0
    );

    console.log(sum);
}
不,无法使Array.prototype.sort接受比较函数异步返回的结果;您必须完全重新实现它。对于其他个别情况,可能会有一些技巧,例如使用协程的forEach(甚至不一定按预期工作,因为每个生成器都会在其第一个yield之前运行,然后从yield继续运行):
function syncForEach() {
    [1, 2, 3, 4, 5].forEach(function (x) {
        console.log(x);
    });
}

function delayed(x) {
    return new Promise(resolve => {
        setTimeout(() => resolve(x), Math.random() * 1000 | 0);
    });
}

function* chain(iterators) {
    for (const it of iterators) {
        yield* it;
    }
}

function* asyncForEach() {
    yield* chain(
        [1, 2, 3, 4, 5].map(function* (x) {
            console.log(yield delayed(x));
        })
    );
}

reduce 函数在自然情况下非常好用(但是当你关注性能时就不是这样了):

function syncReduce() {
    const sum = [1, 2, 3, 4, 5].reduce(function (m, n) {
        return m + n;
    }, 0);

    console.log(sum);
}

function* asyncReduce() {
    const sum = yield* [1, 2, 3, 4, 5].reduce(function* (m, n) {
        return (yield* m) + (yield delayed(n));
    }, function* () { return 0; }());

    console.log(sum);
}

但是,对于所有函数来说,并没有魔法棒可用。
理想情况下,您可以为所有这些函数添加基于承诺的替代实现 - 流行的承诺库(例如bluebird)已经为map和reduce等函数做到了这一点 - 并使用async/await而不是生成器(因为async函数返回承诺):
async function asyncReduce() {
    const sum = await Promise.reduce(
        [1, 2, 3, 4, 5],
        async (m, n) => m + await delayed(n),
        0
    );

    console.log(sum);
}

如果ECMAScript有像Python一样合理的装饰器,你就不需要等待async支持来做这个了:

@Promise.coroutine
function* add(m, n) {
    return m + (yield delayed(n));
}

@Promise.coroutine
function* asyncReduce() {
    const sum = yield Promise.reduce([1, 2, 3, 4, 5], add, 0);

    console.log(sum);
}

...但它并没有实现,所以你需要这样做。或者你可以接受像这样的代码:

const asyncReduce = Promise.coroutine(function* () {
    const sum = yield Promise.reduce([1, 2, 3, 4, 5], Promise.coroutine(function* (m, n) {
        return m + (yield delayed(n));
    }), 0);

    console.log(sum);
});

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