【问题标题】:What is a nice way to override the value a promise resolves to? [duplicate]什么是覆盖 promise 解析为的值的好方法? [复制]
【发布时间】:2018-06-30 06:11:57
【问题描述】:

我正在编写一个 Node.js 脚本来使用测试数据集填充 SQL 数据库。

在下面代码sn-p所示的promise链中(实际代码有点毛茸茸),函数insertData()需要从前一阶段传递过来的db对象。但是,上一阶段dropAndCreateTables() 内部的异步调用使用db 对象但不返回它。我想出了一个解决方案,将 dropAndCreateTables() 中的 Promise 包装到另一个 Promise 对象中,该对象解析为 db 对象。然而:

  • 我听说在非库代码中使用 Promise() 构造函数是一种反模式,可能会导致微妙且难以诊断的错误
  • 听说嵌套then()-chains 也是一种反模式
  • 它不允许我忽略来自 promiseDrop 的错误(例如,我不在乎表在删除时是否不存在)
  • 很丑

问题:

  • 是否有更简单、更好、更被社会接受的方法来覆盖承诺的返回值? (在本例中,使用 Promise.all() 创建)
  • 有没有办法重组我的代码以使这个问题不会发生? (即我这里不排除“XY问题”的可能)

代码:

const dropAndCreateTables = (db, startClean) => {
  if(startClean) {
    const sqlDrop = fs.readFileSync('drop.sql').toString()
    const promiseDrop = db.raw(sqlDrop)

    const sqlCreate = fs.readFileSync('create.sql').toString()
    const promiseCreate = db.raw(sqlCreate)

    /********* Problems here? ************************************/
    return new Promise((resolve, reject) => {      // Ew?
      Promise.all([promiseDrop, promiseCreate])
        .then(() => {
          resolve(db) // Override the returned value
        })
        .catch(reject)
    })

  }
  return Promise.resolve(db)
}

initDB({ debug: false })
  .then((db) => {
    return dropAndCreateTables(db, START_CLEAN) // Without my hack this does not return `db`
  })
  .then((db) => {
    return insertData(db, DO_UPSERT)  // This needs the `db` object
  })
  .then(() => {
    console.info(`\n${timestamp()} done`)
  })
  .catch(handleError)

【问题讨论】:

  • 确实是复制品。我怎么能错过那个。

标签: javascript node.js ecmascript-6 promise


【解决方案1】:

(答案中途和后面的一些相当重要的注释,请一直阅读到最后。)

是否有更简单、更好且更被社会接受的方式来覆盖承诺的返回值? (在这种情况下,使用Promise.all() 创建)

是的,您只需从 then 处理程序返回一个值,然后返回承诺 then 回报:

return Promise.all([promiseDrop, promiseCreate])
    .then(() => db);

then(和catch)创建承诺。链条中的每个环节都可以改变结果。 thencatch 返回一个新的承诺,该承诺将根据其回调中发生的情况而被履行或拒绝:

  • 如果他们的回调抛出,promise 会拒绝并抛出错误
  • 如果他们的回调返回一个非 thenable 值(例如,promise),则该值实现了 promise
  • 如果他们的回调返回一个 thenable 值,则 Promise 将解析为该 thenable - 它等待另一个 Promise 解决,然后以相同方式解决

(如果“thenable”这个词不熟悉,或者你不清楚“fulfill”和“resolve”之间的区别,我会在我的博客上的this post 中介绍promise 术语。)

我听说在非库代码中使用 Promise() 构造函数是一种反模式,可能会导致微妙且难以诊断的错误

区别不在于库代码与非库代码,而在于尚未承诺可以使用的代码和可以使用的代码之间。如果您已经有合作的承诺,您几乎不想使用new Promise。更多:What is the explicit promise construction antipattern and how do I avoid it?

听说嵌套 then()-chains 也是一种反模式

您几乎不需要嵌套 then 链,因为链中的每个链接都已经具有转换通过它的结果的方法。所以:

// Unnecessary nesting
doSomething()
    .then(a => {
        return doSomethingElse(a * 2)
            .then(b => b * 3);
    })
    .catch(e => { /*...handle error...*/ });

可以更惯用更简单的写法:

doSomething()
    .then(a => doSomethingElse(a * 2))
    .then(b => b * 3);
    .catch(e => { /*...handle error...*/ });

有没有办法重组我的代码以使这个问题不会发生? (即我这里不排除“XY问题”的可能性)

本身不是 X/Y,但您在该代码中存在问题:不能保证 drop 会在 create 之前发生!因此,与其启动两者并让它们并行运行并使用Promise.all 观察结果,不如确保这些操作按顺序进行

// Fairly minimal changes
const dropAndCreateTables = (db, startClean) => {
  if(startClean) {
    const sqlDrop = fs.readFileSync('drop.sql').toString()
    return db.raw(sqlDrop)
      .then(() => {
        const sqlCreate = fs.readFileSync('create.sql').toString()
        return db.raw(sqlCreate);
      })
      .then(() => db);
  }
  return Promise.resolve(db)
}

但是,我不会使用同步文件 I/O。而是

const promisify = require("utils").promisify;
const readWithPromise = promisify(fs.readFile);

然后

const dropAndCreateTables = (db, startClean) => {
  if(startClean) {
    const getDrop = readWithPromise('drop.sql');       // Start this first
    const getCreate = readWithPromise('create.sql');   // Then start this
    return getDrop
      .then(dropSql => db.raw(dropSql))                // Got the drop SQL, run it
      .then(() => getCreate)                           // Make sure we have the create SQl
      .then(createSql => db.raw(createSql))            // Run it
      .then(() => db);
  }
  return Promise.resolve(db)
}

请注意我们如何避免在 I/O 上忙于等待,并且我们可以将 DB 的删除操作与读取创建 SQL 重叠。

【讨论】:

  • You almost never need to nest then chains -> IMO 这是罕见的用例之一
  • 谢谢你,我从你的回答中学到了很多!关于嵌套链:我们都想出了dropAndCreateTables(),它里面有一个链,它嵌套在全局范围内的主链中。生成的dropAndCreateTables() 不是类似于“不必要的嵌套”示例中的a => {} 函数吗?我们无法将链重构出dropAndCreateTables()。因此,我对链式嵌套指南感到困惑。你能对此发表评论吗?
  • @Drop:您的意思是“最小更改”代码?不,这不是 promise 嵌套。这就是承诺链。如果必须的话,在then 处理程序中initiate 一个promise 很好(并且很常见),这就是该代码的作用。
  • @JonasW.:为什么?我在这里根本看不到它有任何好处。
  • @T.J.Crowder 不,我的意思是另一回事。如果我用它的内容(任何版本)替换对dropAndCreateTables() 的调用,它会不会变成与“不必要的嵌套”示例中return doSomethingElse(a * 2).then(b => b * 3); 所示的相同模式?事实上,我试图消除调用,然后消除嵌套,但我不确定如何处理 if 语句。或者这不是我应该消除的那种嵌套? (我在其他更复杂的函数中有更多这种嵌套)
【解决方案2】:

你不需要在返回另一个promise时调用Promise构造函数,你可以这样写:

return Promise.all([promiseDrop, promiseCreate])
    .then(() => db)
    .catch(error => {
       // handle the error or rethrow it
    })

【讨论】:

    【解决方案3】:

    您可能会省略从dropAndCreateTables 解析db,如下所示:

    .then((db) => {
        return dropAndCreateTables(db, START_CLEAN).then(Promise.resolve(db));
    })
    

    【讨论】:

    • 我其实很喜欢这个解决方案。但是,它会正确处理错误吗,尤其是promiseCreate
    【解决方案4】:

    你不应该让 dropAndCreateTables 返回一个 db promise,它没有真正的用例。所以:

    return Promise.all([promiseDrop, promiseCreate]);
    

    就够了。现在是链接部分:

    initDB({ debug: false }).then(async (db) => {
    
      await dropAndCreateTables(db, START_CLEAN);
      await insertData(db, DO_UPSERT);
    
      console.info(`\n${timestamp()} done`)
    }).catch(handleError)
    

    【讨论】:

    • 是的,async/await 确实更好。我昨天尝试了这个并将整个脚本转换为使用async/await。添加错误检查try/catch 子句后,我的脚本变得完全不可读,所以我回滚到then()/catch()。但是,转换此特定部分实际上可能是值得的。我目前正在研究是否应该将每个异步调用包装到 try/catch 中,或者是否可以将它们分组。在后一种情况下,我经常会收到未经处理的拒绝警告。
    • @drop 你只需要抓住它们一次。因此,您实际上只需要最高级别的 1 次捕获。但这取决于您的代码结构。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-06-09
    • 1970-01-01
    • 2013-11-22
    • 1970-01-01
    • 2014-01-01
    • 2014-09-28
    • 2017-06-28
    相关资源
    最近更新 更多