【问题标题】:Avoiding javascript callback and promise hell避免 javascript 回调和承诺地狱
【发布时间】:2016-08-09 23:31:00
【问题描述】:

我有许多异步方法要执行,我的程序流程可能会根据每个方法返回而发生很大变化。下面的逻辑就是一个例子。我无法使用 Promises 以易于阅读的方式编写它。你会怎么写呢?

Ps:欢迎更复杂的流程。

Ps2:is_business 是一个预定义的标志,我们在其中说明我们是在编写“商业用户”还是“个人用户”。

begin transaction
update users
if updated
    if is_business
        update_business
        if not updated
            insert business
        end if
    else
        delete business
    end if
else
    if upsert
        insert user
        if is_business
            insert business
        end if
    end if
end if
commit transaction

【问题讨论】:

  • 我认为这是一个公平的问题。所有这些操作都应该是异步的吗?
  • 是的@mooiamaduck。实际上我正在做的数据库插入/更新都是异步的。
  • 逻辑有问题。 if is_busines { *...* } else { *delete business* }。如果不是业务,您为什么要删除业务。同样,*update business* if not updated *insert business*。您应该更新业务或插入业务。
  • 您不需要使用任何库。在 ES7 引入缓解异步和等待之前,您可以使用标准 ES6 承诺/生成器实现简单的 Promise.coroutine() 方法,并在生成器函数中处理异步工作流,就好像它是同步的一样。如果我今天晚些时候能找到一些时间,我会试着举个例子。
  • @Redu 了解它的工作原理很好,但我不知道为什么我会自己实现它只是说我做到了,当有像 bluebird 的 Promise.coroutineco 这样的选项时图书馆可用。

标签: javascript node.js promise


【解决方案1】:

Promise 的好处在于,它们在同步代码和异步代码之间做了一个简单的类比。举例说明(使用 Q 库):

同步:

var thisReturnsAValue = function() {
  var result = mySynchronousFunction();
  if(result) {
    return getOneValue();
  } else {
    return getAnotherValue();
  }
};

try {
  var value = thisReturnsAValue();
  console.log(value);
} catch(err) {
  console.error(err);
}

异步:

var Q = require('q');

var thisReturnsAPromiseForAValue = function() {
  return Q.Promise(function() {
    return myAsynchronousFunction().then(function(result) {
      if(result) {
        // Even getOneValue() would work here, because a non-promise
        // value is automatically cast to a pre-resolved promise
        return getOneValueAsynchronously();
      } else {
        return getAnotherValueAsynchronously();
      }
    });
  });
};

thisReturnsAPromiseForAValue().then(function(value) {
  console.log(value);
}, function(err) {
  console.error(err);
});

你只需要习惯这样的想法,返回值总是作为 then-callbacks 的参数访问,并且链式 Promise 等同于组合函数调用 (f(g(h(x)))) 或按顺序执行函数 (var x2 = h(x); var x3 = g(x2);) .基本上就是这样!当你引入分支时,事情会变得有点棘手,但你可以从这些首要原则中弄清楚要做什么。因为 then-callbacks 接受 Promise 作为返回值,所以你可以通过返回 another Promise 异步操作来改变你异步获得的值,该异步操作会根据旧的值解析为新值,并且父 Promise 将在新的解决之前不要解决!当然,您可以从 if-else 分支中返回这些承诺。

上面示例中展示的另一件非常好的事情是,promise(至少是符合 Promises/A+ 的)以同样类似的方式处理异常。 “引发”的第一个错误会绕过非错误回调并冒泡到第一个可用的错误回调,很像 try-catch 块。

对于它的价值,我认为尝试使用手工制作的 Node.js 风格的回调和 async 库来模仿这种行为是它自己的特殊地狱:)。

遵循这些准则,您的代码将变为(假设所有函数都是异步的并返回承诺):

beginTransaction().then(function() {
  // beginTransaction() has run
  return updateUsers(); // resolves the boolean value `updated`
}).then(function(updated) {
  // updateUsers() has "returned" `updated`
  if(updated) {
    if(isBusiness) {
      return updateBusiness().then(function(updated) {
        if(!updated) {
          return insertBusiness();
        }
        // It's okay if we don't return anything -- it will
        // result in a promise which immediately resolves to
        // `undefined`, which is a no-op, just like a missing
        // else-branch
      });
    } else {
      return deleteBusiness();
    }
  } else {
    if(upsert) {
      return insertUser().then(function() {
        if(isBusiness) {
          return insertBusiness();
        }
      });
    }
  }
}).then(function() {
  return commitTransaction();
}).done(function() {
  console.log('all done!');
}, function(err) {
  console.error(err);
});

【讨论】:

  • 非常感谢@mooiamaduck。这正是我所期望的。我已经理解它是如何工作的,但不幸的是,我认为它仍然很难阅读。我发现制作单个回调函数并在 switch/case 中使用标志更容易理解。
  • 我觉得这更自然,所以我想这是一个品味问题。
  • 让我害怕的是非承诺代码需要转换为承诺代码才能与其他承诺代码一起使用。这意味着,一旦您开始使用一些 Promise,它们就会在您的整个代码库中溢出,直到您编写的几乎所有内容都是基于 Promise 的。这不一定是坏事,但是想想如果你遇到死胡同会发生什么,由于某种原因,代码根本不能基于某种原因的承诺,这是令人恐惧的。也许这永远不会发生?
【解决方案2】:

解决方案是@mooiamaduck 回答和@Kevin 评论的混合。

使用 Promise、ES6 生成器和 co 库使代码更加清晰。我在阅读 postgresql 节点库示例 (pg) 时发现了一个很好的示例。在下面的示例中,pool.connectclient.query 是返回 Promises 的异步操作。我们可以在获取result 之后轻松添加if/else,然后进行更多异步操作,使代码看起来像同步。

co(function * () {
  var client = yield pool.connect()
  try {
      yield client.query('BEGIN')
      var result = yield client.query('SELECT $1::text as name', ['foo'])
      yield client.query('INSERT INTO something(name) VALUES($1)', [result.rows[0].name])
      yield client.query('COMMIT')
      client.release()
  } catch(e) {
    // pass truthy value to release to destroy the client
    // instead of returning it to the pool
    // the pool will create a new client next time
    // this will also roll back the transaction within postgres
    client.release(true)
  }
})

【讨论】:

    猜你喜欢
    • 2019-09-18
    • 2017-02-09
    • 2017-05-08
    • 1970-01-01
    • 2020-12-24
    • 2016-03-27
    • 1970-01-01
    • 2021-01-31
    • 1970-01-01
    相关资源
    最近更新 更多