我正在努力将基于回调的遗留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
答案 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
模块解决了一些使其变得更加直观的复杂性。