避免javascript回调和承诺地狱

Avoiding javascript callback and promise hell

本文关键字:承诺 地狱 回调 javascript 避免      更新时间:2023-09-26

我有许多异步方法要执行,我的程序流可能会根据每个方法的返回而发生很大变化。下面的逻辑就是一个例子。我无法使用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

承诺的好处是它们在同步代码和异步代码之间做了一个简单的类比。为了说明(使用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);
});

你只需要习惯这样的想法,即返回值总是作为回调函数的参数访问,并且链接承诺等同于组合函数调用(f(g(h(x))))或按顺序执行函数(var x2 = h(x); var x3 = g(x2);)。本质上就是这样!当你引入分支时,事情会变得有点棘手,但你可以从这些基本原理中找出该怎么做。因为then回调函数接受promise作为返回值,你可以通过返回另一个promise来改变异步获得的值,异步操作将根据旧值解析为新值,并且直到新值解析后父承诺才会解析!当然,你也可以在if-else分支中返回这些承诺。

在上面的例子中展示的另一个非常好的东西是承诺(至少是那些符合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答案和@Kevin评论的组合。

使用promises、ES6生成器和co库使代码更加清晰。我发现一个很好的例子,当阅读postgresql节点库的例子(pg)。在下面的示例中,pool.connectclient.query是返回promise的异步操作。我们可以在获得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)
  }
})