JavaScript Promise

Promise

Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大,并且获得了 JavaScript 的原生支持!

Promise 起源

Pyramid of doom

JavaScript 是单线程执行的,很多的操作都是异步执行的。异步执行可以使用回调函数进行实现:

1
2
doSomething(function(response) { 
})

如果多个操作顺序执行,使用回调实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
doSomething(function(responseOne) {  
doSomethingElse(responseOne, function(responseTwo, err) {
if (err) { handleError(err); }
doMoreStuff(responseTwo, function(responseThree, err) {
if (err) { handleAnotherError(err); }
doFinalThing(responseThree, function(err) {
if (err) { handleAnotherError(err); }
// Complete
}); // end doFinalThing
}); // end doMoreStuff
}); // end doSomethingElse
}); // end doSomething

目前看上去好像也没什么不妥,但如果要改变事件顺序呢?或者需要嵌套的操作更多呢?

Promise 能很清晰的处理上面的问题:

1
2
3
4
5
6
doSomething()  
.then(doSomethingElse)
.catch(handleError)
.then(doMoreStuff)
.then(doFinalThing)
.catch(handleAnotherError)

上面代码要清晰很多,而且不管是修改还是扩展都很容易。

简介

ECMAScript 6 中的 Promise 规范来源于 Promises/A+ 标准。语法上来看,Promise 是一个对象,从它可以获取异步操作的消息。

Promise 的状态

new Promise实例化的 Promise 对象有以下三个状态:

  • Pending,Promise 对象刚被创建后的初始化状态
  • Fulfilled(Resolved),操作成功
  • Rejected,操作失败

其中 Fulfilled 、Rejected 也可以被称为settled状态,Promise 状态有两个特点:

  • 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  • 一旦状态改变,就不会再变,并且任何时候都可以得到变化的结果。

Pending 状态的 Promise 对象可能触发 Fulfilled 状态并传递一个值给相应的状态处理方法,也可能触发 Rejected 状态并传递失败信息:

Promise States

Constructor

创造一个 Promise 实例:

1
2
3
4
5
6
7
8
9
const Promise = new Promise(function(resolve, reject) {
// ... some code

if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});

resolve 和 reject 是两个函数,由 JavaScript 引擎提供,不需要自己部署。resolve 和 reject 函数被调用时,分别将 Promise 的状态改为 fulfilled(完成)或rejected(失败)。

Instance Method

1
2
3
Promise.prototype.then(onFulfilled, onRejected)
Promise.prototype.catch(onRejected)
Promise.prototype.finally(onFinally)

Promise 对象可以调用上面三个方法,这三个方法都是添加回调到当前的 Promise,并且返回一个新的 Promise。回调在 Promise 状态变化时会调用:

  • onFulfilled,状态变成 Fulfilled 时的回调
  • onRejected,状态变成 Rejected 时的回调
  • onFinally,状态变成 Fulfilled 或者 Rejected 时都会回调

如果传递的参数为非函数类型,则会转为null,将会继续透传上一个 promise 的结果。下面代码跟没有.then(null)是等效的:

1
2
3
doSomething()  
.then(null)
.then(doSomethingElse)

因为这三个方法都返回新的 Promise 对象,所以它们可以被链式调用:

Promise Chain

方法返回一个的 Promise 与回调函数的返回值有关:

  • 如果返回一个值,那么返回的 Promise 将会成为 Fulfilled,并且将返回的值作为 onRejected 回调函数的参数值。
  • 如果回调函数抛出一个错误,那么返回的 Promise 将会成为 Rejected,并且将抛出的错误作为 onRejected 回调函数的参数值。
  • 如果返回一个 Fulfilled 状态的 Promise,那么返回的 Promise 也会成为 Fulfilled 状态,保持回调的值一致。
  • 如果返回一个已经是 Rejected 状态的 Promise,那么返回的 Promise 也会成为 Rejected 状态,保持回调的值一致。
  • 如果返回一个未定状态 pending 状态的 Promise,那么返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的。

而 Promise 的调用如下:

Promise Chain Flow

Static Method

Promise.all(iterable)

方法返回一个新的 Promise 对象,该 Promise 对象在 iterable 参数对象里所有的 Promise 对象都成功的时候才会触发成功,一旦有任何一个 iterable 里面的 Promise 对象失败则立即触发该 Promise 对象的失败。

返回的新 Promise 对象在触发成功状态以后,会把一个包含 iterable 里所有 Promise 返回值的数组作为成功回调的返回值,顺序跟 iterable 的顺序一致;如果这个新的 Promise 对象触发了失败状态,它会把 iterable 里第一个触发失败的 Promise 对象的错误信息作为它的失败错误信息。

Promise.race(iterable)

方法返回一个新的 Promise 对象,将与第一个完成的 Promise 相同的完成方式被完成。

Promise.resolve(value)

将被 Promise 对象解析的参数,可以是值,可以是一个Promise对象,也可以是一个thenable:

1
2
3
Promise.resolve(value);
Promise.resolve(Promise);
Promise.resolve(thenable);

返回一个状态由给定 value 决定的 Promise 对象:

  1. 如果该值是一个 Promise 对象,则直接返回该对象
  2. 如果该值是 thenable(即,带有 then 方法的对象),返回的 Promise 对象的最终状态由 then 方法执行决定
  3. 该 value 为空,基本类型或者不带 then 方法的对象,返回的 Promise 对象状态为 fulfilled,并且将该 value 传递给对应的 then 方法。

通常而言,如果你不知道一个值是否是 Promise 对象,使用 Promise.resolve(value) 来返回一个 Promise 对象,这样就能将该 value 以Promise 对象形式使用。

Promise.reject(reason)

方法返回一个用 reason 拒绝的 Promise。

特殊用法及错误用法

列举了一些对 Promise 的特殊用法及常见的一些错误用法,部分参考文档 We have a problem with promises

常见错误

the promisey pyramid of doom

pyramid of doom 是可以用 Promise 来避免的,但如果使用不当,比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
remotedb.allDocs({
include_docs: true,
attachments: true
}).then(function (result) {
var docs = result.rows;
docs.forEach(function(element) {
localdb.put(element.doc).then(function(response) {
alert("Pulled doc with id " + element.doc._id + " and added to local db.");
}).catch(function (err) {
if (err.name == 'conflict') {
localdb.get(element.doc._id).then(function (resp) {
localdb.remove(resp._id, resp._rev).then(function (resp) {
// et cetera...

then 中又嵌套了 Promise,运行顺序上就看起来很混乱,应该像下面这种就很清晰了:

1
2
3
4
5
6
7
8
9
10
11
12
13
remotedb.allDocs(...)
.then(function (resultOfAllDocs) {
return localdb.put(...);
})
.then(function (resultOfPut) {
return localdb.get(...);
})
.then(function (resultOfGet) {
return localdb.put(...);
})
.catch(function (err) {
console.log(err);
});

promise 中怎么使用 forEach()?

很自然的会想到如下写法:

1
2
3
4
5
6
7
8
// I want to remove() all docs
db.allDocs({include_docs: true}).then(function (result) {
result.rows.forEach(function (row) {
db.remove(row.doc);
});
}).then(function () {
// I naively believe all docs have been removed() now!
});

但是 then 中的返回是undefined,并且不会等待所有db.remove执行完,也就是当第二个 then 的回调调用时,不能确定所有文档执行了db.remove。而为了解决这个问题,我们可以使用上面介绍的Promise.all()

1
2
3
4
5
6
7
db.allDocs({include_docs: true}).then(function (result) {
return Promise.all(result.rows.map(function (row) {
return db.remove(row.doc);
}));
}).then(function (arrayOfResults) {
// All docs have really been removed() now!
});

除了能等待所有db.remove()执行完,第二个 then 回调函数的参数是 result 的数组。

忘记使用 .catch()

不使用.catch()意味着所以错误、异常将被吞噬,你根本感知不到,当出现问题时很难调试。我们应该使用.catch()进行异常处理或者是记录异常事件:

1
2
3
4
5
somePromise().then(function () {
return anotherPromise();
}).then(function () {
return yetAnotherPromise();
}).catch(console.log.bind(console)); // <-- this is badass

使用副作用调用而非直接返回

1
2
3
4
5
6
somePromise().then(function () {
someOtherPromise();
}).then(function () {
// Gee, I hope someOtherPromise() has resolved!
// Spoiler alert: it hasn't.
});

上面代码有什么问题呢?

  • 第二个 then 的回调以及之后可能的 catch,无法获得 someOtherPromise 的结果
  • someOtherPromise 是一个异步调用,在执行第二个 then 回调时,不能确保已经执行完

正确的做法应该是:

1
2
3
4
5
6
7
somePromise().then(function () {
return someOtherPromise();
}).then(function (result) {
// Ha, I got a rsult!
}).catch(function (err) {
// Boo, I got an error!
});;

then(resolveHandler).catch(rejectHandler)then(resolveHandler, rejectHandler)的区别

当使用then(resolveHandler, rejectHandler)时,rejectHandler 将不会 catch 由 resolveHandler 抛出的异常:

1
2
3
4
5
6
7
8
9
10
11
somePromise().then(function () {
throw new Error('oh noes');
}).catch(function (err) {
// I caught your error! :)
});

somePromise().then(function () {
throw new Error('oh noes');
}, function (err) {
// I didn't catch your error! :(
});

then 传入参数为非函数

如下代码最终打印为”foo”:

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

其等效于:

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

特殊用法

resolve(promise)

我们可以在resolve()方法中传递另外一个 Promise 对象:

1
2
3
4
5
6
7
8
9
10
11
12
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})

p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail

p1 的状态会传递给 p2,也就是说,p1 的状态决定了 p2 的状态。如果 p1 的状态是 pending,那么 p2 的回调函数就会等待 p1 的状态改变;如果 p1 的状态已经是 Fulfilled 或者 Rejected ,那么 p2 的回调函数将会立刻执行。

Promise.resolve() 和 Promise. reject() 的使用

1
2
3
new Promise(function (resolve, reject) {
resolve(someSynchronousValue);
}).then(/* ... */);

上面代码可以简化为:

1
Promise.resolve(someSynchronousValue).then(/* ... */);

也可以用于对可能抛出异常的方法进行封装,封装成新的 Promise 之后就可以使用 .catch 来捕获:

1
2
3
4
5
6
function somePromiseAPI() {
return Promise.resolve().then(function () {
doSomethingThatMayThrow();
return 'foo';
}).then(/* ... */);
}

Promise.reject()也类似:

1
Promise.reject(new Error('some awful error'));

顺序执行多个 promise

你可能想到了Promise.all(),但这个是并行的。也可能想到如下代码:

1
2
3
4
5
6
7
function executeSequentially(promises) {
var result = Promise.resolve();
promises.forEach(function (promise) {
result = result.then(promise);
});
return result;
}

因为 Promise 创建后就已经开始执行了,所以上述代码也是达不到效果的。我们可以利用 promise factories:

1
2
3
4
5
6
7
function executeSequentially(promiseFactories) {
var result = Promise.resolve();
promiseFactories.forEach(function (promiseFactory) {
result = result.then(promiseFactory);
});
return result;
}

promiseFactory 类似:

1
2
3
function myPromiseFactory() {
return somethingThatCreatesAPromise();
}

获取多个 Promise 的结果

例如:

1
2
3
4
5
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// dangit, I need the "user" object too!
});

当然最简单的方法是用变量进行保存。也可以用如下方法:

1
2
3
4
5
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id).then(function (userAccount) {
// okay, I have both the "user" and the "userAccount"
});
});

可以对上述代码进行封装,使逻辑更为清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function onGetUserAndUserAccount(user, userAccount) {
return doSomething(user, userAccount);
}

function onGetUser(user) {
return getUserAccountById(user.id).then(function (userAccount) {
return onGetUserAndUserAccount(user, userAccount);
});
}

getUserByName('nolan')
.then(onGetUser)
.then(function () {
// at this point, doSomething() is done, and we are back to indentation 0
});

一道题

下面四个用法有什么区别?当然这里有个前提,doSomething()doSomethingElse()返回的是 Promise 对象

1
2
3
4
5
6
7
8
9
10
11
doSomething().then(function () {
return doSomethingElse();
});

doSomething().then(function () {
doSomethingElse();
});

doSomething().then(doSomethingElse());

doSomething().then(doSomethingElse);

#1

1
2
3
doSomething().then(function () {
return doSomethingElse();
}).then(finalHandler);

调用顺序如下:

1
2
3
4
5
6
doSomething
|-----------------|
doSomethingElse(undefined)
|------------------|
finalHandler(resultOfDoSomethingElse)
|------------------|

#2

1
2
3
doSomething().then(function () {
doSomethingElse();
}).then(finalHandler);

调用顺序如下:

1
2
3
4
5
6
doSomething
|-----------------|
doSomethingElse(undefined)
|------------------|
finalHandler(undefined)
|------------------|

#3

1
2
doSomething().then(doSomethingElse())
.then(finalHandler);

调用顺序如下:

1
2
3
4
5
6
doSomething
|-----------------|
doSomethingElse(undefined)
|---------------------------------|
finalHandler(resultOfDoSomething)
|------------------|

当然如果 doSomethingElse() 返回的是一个函数,并且函数又返回一个 Promise 那就另当别论了,可以敲个代码试一试。

#4

1
2
doSomething().then(doSomethingElse)
.then(finalHandler);

调用顺序如下:

1
2
3
4
5
6
doSomething
|-----------------|
doSomethingElse(resultOfDoSomething)
|------------------|
finalHandler(resultOfDoSomethingElse)
|------------------|
Cotin Yang wechat
欢迎订阅我的微信公众号 CotinDev
小小地鼓励一下吧~😘