Promise是实现异步编程的一种重要方式,在Angular的使用过程中比较简单,在这里深入学习。
Promise起源
异步编程
- 回调函数
- 事件监听
- 发布订阅
- Promise对象
回调函数问题
- 金字塔问题
- 剥夺了我们使用 return 和 throw 这些关键字的能力
- 整个代码流程都是基于副作用的
- 堆栈
简单来说就是一个函数除了会返回一个值之外,还会修改函数以外的状态如全局变量等等。实际上所有异步调用都可以视为带有副作用的行为。
Promise基础
基本API如下
function Promise(resolver) {}
Promise.prototype.then = function() {}
Promise.prototype.catch = function() {}
Promise.resolve = function() {}
Promise.reject = function() {}
Promise.all = function() {}
Promise.race = function() {}
新手错误
Promise版金字塔
出现的原因就是像使用回调一样使用 promises。比如有多个promise需要执行且有先后顺序,常见错误代码如下:
remotedb.allDocs().then(function(resultOfAllDocs){
localdb.put(resultOfAllDocs).then(function(resultOfPut){
localdb.get(resultOfPut).then(function(resultOfGet){
localdb.put(resultOfGet).then(function(resultOfPut){
//...
})
})
})
})
正确的风格应该如下:
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);
});
这种写法被称为 composing promises ,是 promises 的强大能力之一。每一个函数只会在前一个 promise 被调用并且完成回调后调用,并且这个函数会被前一个 promise 的输出调用
。
用了 promises 后怎么用 forEach
例子:删除所有doc后执行某操作,下面是错误代码
// 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!
});
问题在于第一个函数实际上返回的是 undefined,这意味着第二个方法不会等待所有 documents 都执行 db.remove()
。实际上他不会等待任何事情,并且可能会在任意数量的文档被删除后执行!
这里需要 Promise.all(),具体功能点有:
- Promise.all()会以一个 promises 数组为输入,并且返回一个新的 promise。这个新的 promise 会在数组中所有的 promises 都成功返回后才返回。
- Promise.all() 会将执行结果组成的数组返回到下一个函数。
- 一旦数组中的 promise 任意一个返回错误,Promise.all() 也会返回错误。
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!
});
忘记使用 .catch()
即使你坚信不会出现异常,添加一个 catch() 总归是更加谨慎的。如果你的假设最终被发现是错误的,它会让你的生活更加美好。
使用副作用调用而非返回
考虑下面代码问题
somePromise().then(function () {
someOtherPromise();
}).then(function () {
// Gee, I hope someOtherPromise() has resolved!
// Spoiler alert: it hasn't.
});
这是一个一旦你理解了它,就会避免所有我提及的古怪的错误的技巧。promises 的奇妙在于给予我们以前的 return 与 throw。 每一个 promise 都会提供给你一个 then() 函数 (或是 catch(),实际上只是 then(null, ...) 的语法糖)。当我们在 then() 函数内部时:
somePromise().then(function () {
// I'm inside a then() function!
// 1.return 另一个 promise
// 2.return 一个同步的值 (或者 undefined)
// 3.throw 一个同步异常
});
1.返回另一个 promise
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// I got a user account!
});
注意到我是 return
第二个 promise,这个 return
非常重要。如果我没有写 return
,getUserAccountById()
就会成为一个副作用,并且下一个函数将会接收到 undefined
而非 userAccount
。
2.返回一个同步值 (或者 undefined)
返回 undefined 通常是错误的,但是返回一个同步值实际上是将同步代码包裹为 promise 风格代码的一种非常赞的手段。
getUserByName('nolan').then(function (user) {
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // returning a synchronous value!
}
return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
// I got a user account!
});
不幸的是,有一个不便的现实是在 JavaScript 中无返回值函数在技术上是返回 undefined,这就意味着当你本意是返回某些值时,你很容易会不经意间引入副作用。
出于这个原因,我个人养成了在 then() 函数内部 永远返回或抛出
的习惯。我建议你也这样做。
3.抛出同步异常
谈到 throw,这是让 promises 更加赞的一点。比如我们希望在用户已经登出时,抛出一个同步异常。如果用户已经登出,我们的 catch() 会接收到一个同步异常,并且如果 后续的 promise 中出现异步异常,他也会接收到。如果是使用回调风格,这个错误很可能就会被吃掉,但是使用 promises,我们可以轻易的在 catch() 函数中处理它了。
进阶错误
不知道 Promise.resolve()
promises 在封装同步与异步代码时
非常的有用。然而,如果你发现你经常写出下面的代码:
new Promise(function (resolve, reject) {
resolve(someSynchronousValue);
}).then(/* ... */);
你会发现使用 Promise.resolve 会更加简洁:
Promise.resolve(someSynchronousValue).then(/* ... */);
Promise.resolve方法对于参数是有要求的,下面是三种形式:
- Promise.resolve(value);
- Promise.resolve(promise);
- Promise.resolve(thenable);
- 这三种形式都会产生一个新的Promise。
- 第一种形式提供了自定义Promise的值的能力。
- 第二种形式,提供了创建一个Promise的副本的能力。
- 第三种形式,是将一个类似Promise的对象转换成一个真正的Promise对象。
和new Promise()
的区别看例子:
var foo = {
then: (resolve, reject) => resolve('foo')
};
var promise=Promise.resolve(foo);
-->
var promise=new Promise(function(resovle,reject){
foo.then(resovle,reject);
})
promise.then(function(data){
console.log(data);//'foo'
})
catch() 与 then(null, ...) 并非完全等价
区别就是:当你使用 then(resolveHandler, rejectHandler) 这种形式时,rejectHandler
并不会捕获由 resolveHandler
引发的异常。
promises vs promises factories
当我们希望执行一个个的执行一个 promises 序列,即类似 Promise.all() 但是并非并行的执行所有 promises。代码如下:
function executeSequentially(promises) {
var result = Promise.resolve();
promises.forEach(function (promise) {
result = result.then(promise);
});
return result;
}
上面的代码本身没有任何问题,关键在于参数的传入,这也是为什么本节的标题是promises
和promises
工厂,由于依照 promises 规范,一旦一个 promise 被创建,它就被执行了。
,因此传入的不能是已经创建好的Promise对象,而是一些返回Promise对象的函数。DEMO如下:
function executeSequentially(promises) {
var result = Promise.resolve();
promises.forEach(function (promise) {
result = result.then(promise);
});
return result;
}
function myPromiseFactory(fun){
return function(){
return new Promise(fun);
}
}
var promises=[
myPromiseFactory(function(resovle,reject){
setTimeout(function(){
console.log(1);
resovle(1);
},2000);
}),
myPromiseFactory(function(resovle,reject){
setTimeout(function(){
console.log(2);
resovle(2);
},1000);
})
];
executeSequentially(promises);
如果我希望获得两个 promises 的结果怎么办
如果我们希望同时获得这两个 promises 的输出。举例来说:
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// dangit, I need the "user" object too!
});
方法如下:
- 避免金字塔问题,将一个Promise的输入存在一个更高的作用域中的变量里,这样第二个就能访问到两个Promise的输入。
- 抛弃成见,拥抱金字塔
getUserByName('nolan').then(function (user) { return getUserAccountById(user.id).then(function (userAccount) { // okay, I have both the "user" and the "userAccount" }); });
- 一旦缩进开始成为问题,你可以通过 Javascript 开发者从远古时期就开始使用的技巧,将函数抽离到一个命名函数中:
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 });
promises 穿透
下面的结果会是什么呢?
Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) {
console.log(result);
});
如果你认为它会打印出 bar,那么你就错了。它实际上打印出来的是 foo!
发生这个的原因是如果你像 then() 传递的并非是一个函数(比如 promise),它实际上会将其解释为 then(null),这就会导致前一个 promise 的结果会穿透下面。
你可以直接传递一个 promise 到 then() 函数中,但是它并不会按照你期望的去执行。then() 是期望获取一个函数,因此你希望做的最可能是:
Promise.resolve('foo').then(function () {
return Promise.resolve('bar');
}).then(function (result) {
console.log(result);
});