Promise 踩坑记

在工作中遇到Promise的很多坑,一直想着对这些知识点进行总结。

javascript的异步处理,大多会想到通过回调函数来解决,但回调带来的问题也比较明显:

  1. 在多层嵌套的回调函数中,无法判断什么时候完成异步,需要在外层作用域声明一些变量,这样容易被其他函数或变量修改。
  2. 因为异步函数在新的栈中运行,无法获取到之前栈的信息,之前的栈也无法捕获新的栈中抛出的错误,无法用try-catch处理错误。

Promise可以将异步处理模块化,规范化。使得代码更简洁,优雅,可读性更好。

基础用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var promise = new Promise((resolve, reject)=>{
// ... some code

if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
})
promise.then((value) => {
// success
}.catch((err)=>{
// fail
})

Promise 构造函数内有个执行器,将要进行的异步操作放入执行器中,Promise 实例一旦创建,执行器立即执行,执行器执行完毕后改变实例的状态,接着去执行相应的函数。Promise 的状态只能由执行器改变

Promise 实例的特点:

  1. 对象状态不受外部影响,只有内部执行器可以改变状态。
  2. 状态一旦改变,就不会再变,任何时候都可以得到这个结果。

Promise的链式调用

Promise支持链式调用,因为调用.then方法每次都会返回新的 Promise 对象
状态响应函数(即 then 方法)的返回值可以是以下三种

  1. 一个 Promise 对象
  2. 一个同步的值或者是 undefined
  3. 同步的 throw 一个错误
  • 如果返回的是一个 Promise 对象(即异步操作),那后一个回调函数(.then()方法),会等待该 Promise 对象状态发送变化,才会去执行。
  • 如果返回一个同步的值,会默认“立刻”执行下一个.then(),如果不返回值,会传入一个 undefined,但不影响接下来的执行。同时将同步的值转化为 Promise 风格的代码。
  • 如果.then 方法里 throw new Error(), 会被 catch 捕获到。

Promise.resolve()

会返回一个 fulfilled 的 Promise 实例或原始的 Promise 实例。

Promise.resolve():当参数为空,返回一个状态为 fulfilled 的 Promise 实例。
Promise.resolve(object):当参数是 Promise 无关的值,同上,不过 fulfilled 响应函数会得到这个值。
Promise.resolve(promise):当参数是 Promise 实例,则返回该实例,不做任何修改。
Promise.resolve(thenable):当参数是.thenable,立刻执行 then 函数。

Promise.reject()

返回一个使用接收到的值进行了 reject 的新的 Promise 对象。
Promise.resolve() 的区别是:当参数为 Promise 对象时,返回一个全新的 Promise 对象。

Promise使用常见错误

注意一:.then 方法每次返回一个新的 Promise 对象。区分链式调用和非链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1: 对同一个 Promise 对象同时调用 `then` 方法
var aPromise = new Promise(function (resolve) {
resolve(100);
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
console.log(value); // => 100
})
// 2: 对 `then` 进行 promise chain 方式进行调用
var bPromise = new Promise(function (resolve) {
resolve(100);
});
bPromise.then(function (value) {
return value * 2;
}).then(function (value) {
return value * 2;
}).then(function (value) {
console.log(value); // => 100 * 2 * 2
});

下面是一个由 then 方法导致的比较容易出错的例子

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

这种错误也是 Promise 的反模式,更多反模式参考这篇文章

这样会存在很多问题: 首先在 somethingComplicated 方法中产生的异常不会被外部捕获,此外,也不能得到 then 的返回值。当最后返回的是第一个 Promise 而不是 Promise 调用 then 方法后的结果,Promise 链也随机断掉。

正确的做法应该是:

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

注意二:.then 方法执行完必须将结果 return 出来。

.then 方法支持链式调用,不 return 的话,then 里的方法就会返回 undefined。所以要养成在 then 方法内部永远显式的调用 return 或throw 的习惯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function test(){
return new Promise(function (resolve) {
resolve(100);
}).then(function (value) {
Promise.reject(value * 2);
// return Promise.reject(value * 2);
});
}
test().then(function (value) {
console.log('true')
console.log(value);
}, function(value){
console.log('false')
console.log(value);
});
// true undefined
// 如果.then将结果return出来,结果是
// false 200

注意三:catch 和 then 的区别

catch 是 then 的语法糖,它是then(null, rejection)的别名,也就是当 Promise 对象状态变成 rejected 时会执行 catch,但是catch 调用完之后还是会返回一个 Promise 实例。

如果用 then,第一个回调抛出来的错误,第二个回调函数(then)不会捕获

1
2
3
4
5
somePromise().then(function () {
throw new Error('error');
}).catch(function (err) {
// I caught your error! :)
});

1
2
3
4
5
somePromise().then(function () {
throw new Error('error');
}, function (err) {
// I didn't catch your error! :(
});

即:当使用 then(resolveHandler, rejectHandler),rejectHandler 不会捕获在 resolveHandler 中抛出的错误。
所以最好也不用 then 的第二个回调,转而用 catch 方法。

问题: .catch() 后面如果跟着 then() 会怎么处理?
.catch() 会返回 Promise 实例,如果不抛出错误,后面跟着 .then() 会执行,如果在 catch 中抛出错误,后面的 .then() 会跳过去,直接走向最后的 .catch()。

注意四:永远传递一个函数到then方法里

看下面代码打印出什么?

1
2
3
4
5
Promise.resolve('foo')
.then(Promise.resolve('bar'))
.then(function (result) {
console.log(result);
});

结果为:foo

当then方法接收一个非函数参数时,会解释为then(null),导致之前的 Promise 的结果丢失,发生 Promise 穿透。

mdn上解释:

如果忽略针对某个状态的回调函数参数,或者提供非函数 (nonfunction) 参数,那么 then 方法将会丢失关于该状态的回调函数信息,但是并不会产生错误。如果调用 then 的 Promise 的状态(fulfill 或 rejection)发生改变,但是 then 中并没有关于这种状态的回调函数,那么 then 将创建一个没有经过回调函数处理的新 Promise 对象,这个新 Promise 只是简单地接受调用这个 then 的原 Promise 的终态作为它的终态。

所以,如果

1
2
3
4
5
6
Promise.resolve('foo')
.then(()=>{
return Promise.resolve('bar')
}).then(function (result) {
console.log(result); // bar
});

最后

最佳实践:

  • 回调函数中一定要使用 return 语句,避免丢失状态和结果。
  • 在最后一定调用 catch 方法,用来捕获整体的异常。
  • 永远传递函数给 then 方法。

参考文章:

JavaScript Promise迷你书

谈谈使用Promise时候的一些反模式

Promise Anti-patterns