将结果集回调函数与生成器/迭代器包装在一起

时间:2019-01-29 14:27:06

标签: javascript node.js ecmascript-6

我正在努力将基于回调的遗留API转换为异步库。但是我只是不愿意让“结果集”作为生成器(Node 10.x)工作。

原始API的工作方式如下:

api.prepare((err, rs) => {
    rs.fetchRows(
        (err, row) => {
            // this callback is called as many times as rows exist
            console.log("here's a row:", row);
        },
        () => {
            console.log("we're done, data exausted");
        }
    );
});

但这是我要使用的方式:

const wrapped = new ApiWrapper(api);
const rs = await wrapped.prepare({});
for (let row of rs.rows()) {
    console.log("here's a row:", row);
}

let row;
while(row = await rs.next()) {
    console.log("here's a row:", row);
}

我以为我可以通过生成器来控制它,但是看来您不能在回调中使用yield。如果您考虑一下,这实际上似乎是合乎逻辑的。

class ApiWrapper {
    constructor(api) {
        this.api = api;
    }
    prepare() {
        return new Promise((resolve, reject) => {
            this.api.prepare((err, rs) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(rs);
                }
            });
        });
    }
    *rows() {
        this.api.fetchRows((err, row) => {
            if (err) {
                throw err;
            } else {
                yield row; // nope, not allowed here
            }
        });
    }
    next() { ... }
}

那我有什么选择?

重要:我不想在数组中存储任何内容然后进行迭代,我们在这里谈论的是数百万行数据。

修改 我可以使用stream.Readable模拟我想要的行为,但是它警告我这是一个实验功能。这是我尝试使用stream解决的问题的基于数组的简化版本:

const stream = require('stream');
function gen(){
    const s = new stream.Readable({
        objectMode: true,
        read(){
            [11, 22, 33].forEach(row => {
                this.push({ value: row });
            });
            this.push(null)
        }
    });
    return s;
}

for await (let row of gen()) {
    console.log(row);
}

// { value: 11 }
// { value: 22 }
// { value: 33 }

(node:97157) ExperimentalWarning: Readable[Symbol.asyncIterator] is an experimental feature. This feature could change at any time

1 个答案:

答案 0 :(得分:1)

我终于意识到我需要与async/await兼容的Go频道相似的内容。基本上,答案是同步异步迭代器和回调,使它们在消耗next()迭代时彼此等待。

我发现最好的(Node) native 解决方案是使用stream作为迭代器,Node 10.x支持该迭代器,但将其标记为实验性的。我还尝试使用p-defer NPM模块来实现它,但是事实证明,这比我预期的要复杂得多。最终遇到了https://www.npmjs.com/package/@nodeguy/channel模块,这正是我所需要的:

const Channel = require('@nodeguy/channel');

class ApiWrapper {
    // ...
    rows() {
        const channel = new Channel();
        const iter = {
            [Symbol.asyncIterator]() {
                return this;
            },
            async next() {
                const val = await channel.shift();
                if (val === undefined) {
                    return { done: true };
                } else {
                    return { done: false, value: val };
                }
            }
        };

        this.api.fetchRows(async (err, row) => {
            await channel.push(row);
        }).then(() => channel.close());

        return iter;
    }
}

// then later

for await (let row of rs.rows()) {
     console.log(row)
}

请注意,每个迭代函数核心next()rows()都有一个await来限制可以跨通道推送多少数据,否则产生的回调可能最终被推送数据不可控制地进入通道队列。这个想法是,回调应先等待迭代器next()消耗数据,然后再推送更多。

这是一个更独立的示例:

const Channel = require('@nodeguy/channel');

function iterating() {
    const channel = Channel();

    const iter = {
        [Symbol.asyncIterator]() {
            return this;
        },
        async next() {
            console.log('next');
            const val = await channel.shift();
            if (val === undefined) {
                return { done: true };
            } else {
                return { done: false, value: val };
            }
        }
    };

    [11, 22, 33].forEach(async it => {
        await channel.push(it);
        console.log('pushed', it);
    });

    console.log('returned');
    return iter;
}

(async function main() {
    for await (let it of iterating()) {
        console.log('got', it);
    }
})();

/*
returned
next
pushed 11
got 11
next
pushed 22
got 22
next
pushed 33
got 33
next
*/

就像我说的那样,可以使用Streams和/或Promises来实现这一点,但是Channel模块解决了一些使其变得更加直观的复杂性。