【问题标题】:how to turn Child_process.spawn's "Promise" syntax to "async/await" syntax如何将 Child_process.spawn 的“Promise”语法转换为“async/await”语法
【发布时间】:2020-02-22 12:04:25
【问题描述】:

所以我有这段代码,我正在尝试全面了解 async/await 语法。下面是Promise版本的代码:

function callToolsPromise(req) {
    return new Promise((resolve, reject) => {
        let pipshell = 'pipenv';
        let args = ['run', 'tools'];
        req.forEach(arg => {
            args.push(arg)
        });
        tool = spawn(pipshell, args);
        tool.on('exit', (code) => {
            if (code !== 0) {
                tool.stderr.on('data', (data) => {
                    reject(data);
                });
            } else {
                tool.stdout.on ('data', (data) => {
                    resolve(JSON.parse(data)):
                });
            }
        });
    })
}

我有一些 python 代码我想在 tools/__main__.py 中执行,所以这就是我调用“pipenv”的原因。

这是我尝试以 async/await 方式编写它(实际上有效):

async function callToolsAsync(req) {
    let pipshell = 'pipenv';
    let args = ['run', 'tools'];
    req.forEach(arg => {
        args.push(arg)
    });
    let tool = spawn(pipshell, args);
    for await (const data of tool.stdout) {
        return data
    }
}

但我所做的只是从我有for await... 循环的某人的示例中复制和粘贴。

因此,我一直在尝试重写相同的代码,以便我能够真正理解它,但我已经失败了好几天了。

有没有其他方法可以在不使用for await... 循环的情况下使用 async/await 方式编写此代码?

除了使用.then 语法之外,我也不知道如何访问数据:

callToolsAsync(['GET','mailuser'])
.then(console.log)

我还能如何访问来自resolve(data) 的“数据”?

非常感谢。

【问题讨论】:

  • 仅在exit 事件之后等待stdout/stderr 上的数据似乎是一个非常糟糕的主意

标签: javascript node.js asynchronous child-process


【解决方案1】:

在 Node.js 中使用async/await 编写代码可能没有更好的方法。节点流专门实现了一个异步迭代器来允许这样做。

这个article on 2ality有更深入的解释和讨论。 Symbol.asyncIterator 和 for-await...of 上的 MDN 文章更普遍地介绍了异步迭代。

一旦决定使用异步迭代,就必须在 async 函数中使用它,该函数将返回一个承诺。

虽然在返回的 Promise 上使用 then 子句是获取数据的完全正常方式,但您也可以 await callToolsAsync(req) 的结果 - 当然前提是调用编码在 async 中函数使await 处于有效上下文中。


以下代码experiment 获取stdio 和stderr 输出,以及子进程的退出代码。它不使用 Python 或解析数据。

ma​​in.js(输入node main.js运行)

// main.js
async function spawnChild() {
    const { spawn } = require('child_process');
    const child = spawn('node', ["child.js"]);

    let data = "";
    for await (const chunk of child.stdout) {
        console.log('stdout chunk: '+chunk);
        data += chunk;
    }
    let error = "";
    for await (const chunk of child.stderr) {
        console.error('stderr chunk: '+chunk);
        error += chunk;
    }
    const exitCode = await new Promise( (resolve, reject) => {
        child.on('close', resolve);
    });

    if( exitCode) {
        throw new Error( `subprocess error exit ${exitCode}, ${error}`);
    }
    return data;
}

spawnChild().then(
    data=> {console.log("async result:\n" + data);},
    err=>  {console.error("async error:\n" + err);}
);

child.js

// child.js
console.log( "child.js started"); //  stdout
setTimeout( finish, 1000);
function finish() {
    console.log( "child.js: finish() call");  //  stdout 
    console.error("child exit using code 1"); //  stderr
    process.exit(1);
}

这表明

  • 控制台警告,可读流的异步迭代在节点中仍处于试验阶段,
  • for await (chunk of stream) 循环似乎一直循环到流关闭 - 在这种情况下意味着 await 将等待当时没有可用数据的打开流。
  • 从它们的管道中检索stdout 和stderr 内容,并且可以不按特定顺序获取退出代码,因为检索是异步的。
  • 合并从另一个进程通过管道到达的数据块不是可选的 - 来自子进程的控制台日志是单独通过的。

【讨论】:

  • 您能否提供一个“监控进程退出代码或出现在stderr上的数据”的示例?
  • 感谢您的精彩解释。这澄清了我的很多问题,例如运行多个 for await 循环的行为。谢谢你的澄清。
  • 这个“for await (const chunk of child.stdout)”不会阻塞,那么?意思是我会同时得到标准输出和标准错误?
  • @ed22 await ... of 构造不会退出循环,直到正在读取的流关闭或在循环体内执行break 语句,从而阻止同时监视两个流。它不会阻止子进程写入 stdio 输出 - 写入的数据存储在流的管道中,直到读取。如果需要在子进程完成之前监视 stderr 和 sdout 并采取措施,那么在使用异步迭代器读取流之间交替可能比其他解决方案更复杂。
【解决方案2】:

重要的是要了解async/await 和promise 相同,只是语法不同。

所以每个异步函数都会返回一个承诺!

所以假设你有一个返回承诺的函数:

function foo() {
  return new Promise(resolve => setTimeout(() => resolve("done"), 1000));
}

有两种消费价值的方式。

承诺风格:

function test() {
  foo().then(value => console.log(value));
}

或异步等待:

async function test() {
  const value = await foo();
  console.log(value);
}

现在重要的是要了解您原来的callToolsPromise 函数是不是承诺风格。在处理承诺时,您从不致电new Promise。基本上new Promise 的整个想法是将异步非承诺代码(因此非异步/等待相同)转换为承诺(因此异步/等待)。

现在异步不代表async/await,而是一个更笼统的概念。处理异步的另一种常用方法是回调。

所以tool.on('exit', (code) => { 是异步的,但既不是 Promise 也不是 async/await。

所以包装 new Promise 基本上是用来将其转换为 Promise 风格的函数,可以用作 Promise 或与 async/await 一起使用。


关于这个 sn-p 的最后一句话:

for await (const data of tool.stdout) {
    return data
}

这有点问题。虽然节点流是异步生成器,但它将在第一个块之后返回,并在流接收到多个块时立即中断。 因此,您要么应该将 return 替换为 yield 并返回一个异步生成器,要么在循环完成后将缓冲区与循环和 return 连接起来。

【讨论】:

  • @Lux 我很感谢你有见地的解释。但我不喜欢“因为不知道,所以无效”的态度。与其假设代码会返回什么,我建议先对其进行测试...
  • 谢谢,这就是我的 AFAIK 存在的原因。但是,您肯定需要合并这些块。循环内的return 只有在只有 1 个块时才会起作用。
  • 你说得对,我很抱歉。感谢您的建议。修复了我的代码:)
  • 越来越多地回到这个问题,这让我意识到这个答案中的重要概念。谢谢@Lux。祝你有美好的一天。
【解决方案3】:

我使用await-spawn解决了这个问题

https://www.npmjs.com/package/await-spawn

child_process.spawn() wrapped in a Promise for doing async/await.

【讨论】:

  • 谢谢。一个非常优雅且易于使用的解决方案。非常适合我的用例。
猜你喜欢
  • 1970-01-01
  • 2019-11-08
  • 2019-08-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-25
  • 1970-01-01
相关资源
最近更新 更多