「译」 Promise 反面模式

✍🏼 写于 2016年03月04日   
❗️ 注意:离本文创建时间已经过去了 天,请注意时效性

最近在看Promise相关的东西,看到了这篇文章,觉得很不错,遂记录下来。

Promises本身是很简单的,前提是你得找得到头绪,下面是几个关于Promise的容易困惑的知识点来验证你是否真的掌握了Promise。其中的几个真的曾经让我抓狂过。

嵌套Promises

你有一捆的Promises互相嵌套着:

1
2
3
4
5
loadSomething().then(function(something) {
  loadAnotherthing().then(function(another) {
    DoSomethingOnThem(something, another);
  });
});

你这么做的原因是你需要处理这两个Promises的结果,所以你不能链式调用他们因为then()方法只接受上一个then()返回的结果。(意思是这两个Promise是需要同时处理没有先后关系的,但是then却有个先后关系,如果前者throw error直接进入catch处理环节)

呵呵,其实你这么写的真正原因是你不知道all()方法:

解决这种丑陋写法的方案:

1
2
3
4
q.all([loadSomething(), loadAnotherThing()])
  .spread(function(something, another) {
    DoSomethingOnThem(something, another);
  });

更简洁了。q.all()返回一个promise对象,并将这个结果结合成一个数组并传递给resolve方法供之后的then方法调用,spread()方法将会分割这个数组为几个数组长度的参数传递给其中的DoSomethingOnThem函数。

(注:Promise.all()接受一个数组作为参数,数组元素为promise,元素之前没有先后顺序,同时执行,最后传递给then方法的值为各个promise方法return值的数组。这里作者使用的是node中的一个模块q作为示例)

中断的链式调用

  假设你有这样一段代码:

1
2
3
4
5
6
7
function anAsyncCall() {
  var promise = doSomethingAsync();
  promise.then(function() {
    somethingComplicated();
  });
  return promise;
}

这段代码的问题是,出现在somethingComplicated()函数的error都不会被捕获。Promises意味着能够链式调用(不然还叫什么then,直接done就行了)每一个被调用的then()方法返回一个新的promise,这个新的promise是会被下一个then()方法继续调用的。正常来说,最后一个调用应该是catch()方法,出现在链式调用任何地方的任何error都会被它捕获并处理。

在上面的的代码中,链式调用在你返回第一个promise而不是返回一个then处理后的新的的promise给最后一个then调用的时候中断了(即then不改变原有的promise,它只处理它,然后返回一个新的promise)。 解决这个问题的方案:

1
2
3
4
5
6
function anAsyncCall() {
  var promise = doSomethingAsync();
  return promise.then(function() {
    somethingComplicated()
  });
}

记住,总是返回最后一个then()的结果(以能够使用链式调用)。

混乱的集合

你有一组元素的数组,你想对这个数组的每个元素之执行一些异步操作。所以你发现你需要做一些涉及到递归调用的事情。

1
2
3
4
5
6
7
8
9
10
11
function workMyCollection(arr) {
  var resultArr = [];
  function _recursive(idx) {
    if (idx >= resultArr.length) return resultArr;
    return doSomethingAsync(arr[idx]).then(function(res) {
      resultArr.push(res);
      return _recursive(idx + 1);
    });
  }
  return _recursive(0);
}

额。。这段代码不是很直观,问题的关键在与,当你不知道有多长的链式调用的时候,链式调用就变成一个意见痛苦的事情。除非你知道(JavaScript ES5+原生的数组方法)map()reduce()

解决方案: 记住,q.all参数是一个由promise构成的数组,同时它会把结果放到一个数组中并传给resolve方法。我们可以简单的使用数组元素的map方法来对每个数组中的元素执行这个异步调用方法,像下面这样:

1
2
3
4
5
function workMyCollection(arr) {
  return q.all(arr.map(function(item) {
    return doSomethingAsync(item);
  }));
}

不像开始那个并不是什么解决方案的递归调用,这段代码将同步调用数组中的每个元素传递给一个异步调用函数。明显在时间上更有效率一些。 如果你需要按顺序返回promises,你可以使用reduce

1
2
3
4
5
6
7
function workMyCollection(arr) {
  return arr.reduce(function(promise, item) {
    return promise.then(function(result) {
      return doSomethingAsyncWithResult(item, result);
    });
  }, q());
}

看起来不是很简单明了,但是确实比最开始的那个简洁多了。(Not quite as tidy, but certainly tidier.)

幽灵Promise

有一个确定的方法(意思是已经在开始执行Promise时就给出此方法,而不是在执行中由结果来确定的方法—译者注),有时候需要异步调用,有时候又不需要。因此你为了应对这两种情况只创建了一个promise仅仅是为了保持异步和非异步的情况下代码一致(以便于抽象和解耦—译者注),即使这种情况实际只可能出现其中一种。

1
2
3
4
5
6
7
8
var promise;
if (asyncCallNeeded)
  promise = doSomethingAsync();
else
  promise = Q.resolve(42);
promise.then(function() {
  doSomethingCool();
});

以上这段代码在反面模式中并不算最糟糕的地方,但是却应该写的更清晰一些—用Q()来包裹valuepromise。Q()方法即接受一个值也接受一个promise作为参数:

1
2
3
4
5
6
7
8
9
Q(asyncCallNeeded ? doSomethingAsync() : 42)
  .then(
  function(value){
    doSomethingGood();
  })
  .catch(
    function(err) {
      handleTheError();
    });

备注:开始的时候我在这个情况下建议使用Q.when(),多亏了Kris Kowal同学在评论中的建议把我从错误中拯救出来。不要使用Q.when(),只使用Q()就够了,后者更清晰一些。

饥渴的错误处理函数

小节标题意思是在then中同时设置fulfilledrejected,以期能够使用rejected函数处理同样作为then函数参数的fulfilled中的错误,但是这是不可能的,fulfilled中的error只能传递给下一个then()而不能在当前被rejected函数处理,所以这小节的标题为『过度渴望』—它虽然渴望处理错误,但是错误永远不会传递给它让他处理—译者注

then()方法接受两个参数,对fulfilled状态的操作函数和对rejected状态操作函数。你可能写过下面这种代码:

1
2
3
4
5
6
7
somethingAsync.then(
  function() {
    return somethingElseAsync();
  },
  function(err) {
    handleMyError(err);
});

这么写的问题是,发生在fulfilled状态的的error不会传递给错误处理函数。 解决这个问的的方法是,确保错误处理函数在一个独立的then方法中:

1
2
3
4
5
6
7
8
somethingAsync
  .then(function() {
    return somethingElseAsync();
  })
  .then(null,
    function(err) {
        handleMyError(err);
    });

或者使用catch()

1
2
3
4
5
6
7
somethingAsync
  .then(function() {
      return somethingElseAsync();
  })
  .catch(function(err) {
      handleMyError(err);
  });

这样可以确保任何发生在链式调用中的error都能得到处理。

被遗忘的Promise

你调用一个方法,返回一个promise,然而你忘记了这个promise,然后又创建了一个promise

1
2
3
4
5
6
7
8
9
var deferred = Q.defer();
doSomethingAsync().then(function(res) {
  res = manipulateMeInSomeWay(res);
  deferred.resolve(res);
}, function(err) {
  deferred.reject(err);
});

return deferred.promise;

这段代码真的是把promsie的简洁特性抛弃的一干二净—有太多无用的代码了。 解决方案是,仅仅返回promise即可:

1
2
3
return doSomethingAsync().then(function(res) {
  return manipulateMeInSomeWay(res);
});
- EOF -
本文最先发布在: 「译」 Promise 反面模式 - Xheldon Blog