「译」 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
5
6
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
6
7
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
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
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
8
somethingAsync.then(
function () {
return somethingElseAsync();
},
function (err) {
handleMyError(err);
}
);

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

1
2
3
4
5
6
7
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
10
11
12
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