Promise
在web开发中,异步编程是个难度较大的挑战
异步操作,即允许cpu在等待该异步操作完成的同时,转而去进行其他操作;异步程序意味着耗时的操作不会让程序中的其他部分暂停。
在我们的生活中,也有很多异步的例子。例如,打扫房间的时候等待洗碗机洗完碗或者等待洗衣机洗完衣服,在等待期间,我们可以自由地去做其他事情
同样的,web开发者也利用了异步操作,例如发送网络请求、查询数据库这种比较耗时的操作,但js允许我们在等待它们的同时去执行其他操作
这一节会教给你如何通过promise处理异步操作,promise是es6引入的
什么是Promise
Promise是代表异步操作最终结果的对象。Promise对象的状态通常为以下三种之一:
- Pending(暂停):表示操作尚未完成
- FulFilled(完成):操作已成功完成,promise现在有一个resolved值,例如,一个请求的promise可能会解析成一个值为json对象的结果
- Rejected(失败):操作失败,promise现在有一个失败原因,这个原因通常是某种错误类型
如果一个promise不是pending,那么它就是settled(已确定),我们可以把洗碗机的状态比作promise的状态:
- Pending:洗碗机正在运行但还没有洗完
- fulfilled:洗碗机已经完成工作并且洗的很干净
- Rejected:洗碗机出现了故障(例如没有放肥皂),返回了不干净的餐具
如果我们的洗碗机promise是fulfilled,我们就可以执行接下来的操作,例如将餐具从洗碗机中取出,如果是rejected,我们可以执行其他可选择步骤,例如重新装上肥皂运行,或者手洗
所有的promise最终都会是settled,我们需要完成根据promise是否成功后执行的逻辑
构建一个Promise对象
创建Promise对象,我们使用new关键词和promise 构造器
const executorFunction = (resolve, reject) => { };
const myFirstPromise = new Promise(executorFunction);
promise构造器有一个excutorFunction的函数参数,当构造器被调用的时候,这个函数会自动运行,执行器参数通常会开始一个异步操作
例如:
const executorFunction = (resolve, reject) => {
if (someCondition) {
resolve('I resolved!');
} else {
reject('I rejected!');
}
}
const myFirstPromise = new Promise(executorFunction);
在我们的例子中,myFirstPromise根据一个简单的条件判断执行resolve或rejects,但是在实际中,promise是根据异步操作的结果来决定的
例如,数据库请求会有两种结果,一种是成功且带着查询到的数据,一种是失败且抛出错误。在这个练习中,我们将构造一个promise,使异步更容易理解
const inventory = {
sunglasses: 1900,
pants: 1088,
bags: 1344
};
// Write your code below:
const myExecutor = (resolve, reject)=> {
if(inventory.sunglasses > 0){
resolve('Sunglasses order processed.');
}else{
reject('That item is sold out.');
}
};
function orderSunglasses(){
return new Promise(myExecutor);
};
const orderPromise = orderSunglasses();
console.log(orderPromise);
setTimeout() 函数
比知道如何构建promise更重要的是如何调用和使用promise,例如如何处理Promise返回的异步操作对象
Promise开始的时候是pending,但最终一定会是settle
为了练习,我们会给你几个函数,它们会在一会儿后返回settle的Promise。我们将会使用一个Node Api(浏览器提供,类似API),setTimeOut(),它会延迟执行回调函数
setTimeout()有两个参数,回调函数和延迟的时间
const delayedHello = () => {
console.log('Hi! This is an asynchronous greeting!');
};
setTimeout(delayedHello, 2000);
至少两秒后,delayHello会被调用,但为什么是至少两秒而不是精确两秒?
这个延迟是在执行一种异步,在delay期间,我们的程序不是停止执行了。js异步使用了一种叫事件循环的机制,两秒后,delayedHello()会被添加到代码队列中等待运行。在它执行前,程序中的任何同步代码都会先执行。接下来,任何队列中它前面的代码都会运行。
意味着,delayHello()可能会在超过两秒后才会运行
我们是如何使用setTimeout()来构建异步promise的:
const returnPromiseFunction = () => {
return new Promise((resolve, reject) => {
setTimeout(( ) => {resolve('I resolved!')}, 1000);
});
};
const prom = returnPromiseFunction();
- prom最初的状态是pending
console.log("This is the first line of code in app.js.");
// Keep the line above as the first line of code
// Write your code here:
const usingSTO = () => {
console.log('Hello World');
};
setTimeout(usingSTO, 2132);
resolve 和 reject
resolve 和 reject 是两个由 JavaScript 提供的、你可以调用的函数,用来告诉 Promise 结果是“成功”还是“失败”。
更具体一点:它们是“状态控制器”
当你写:
new Promise((resolve, reject) => {
// 你写的代码在这里运行
});
这段函数(executor 函数)里发生的事情是由你控制的。
JavaScript 引擎会在内部自动帮你做两件事:
-
创建一个“还没完成的承诺”(
pending状态的 Promise); -
自动生成两个函数:
resolve:调用后这个 Promise 就变成“成功状态(fulfilled)”;reject:调用后这个 Promise 就变成“失败状态(rejected)”。
然后这两个函数被传给你,让你来“决定这个承诺最后是成功还是失败”。
你就像是坐在一间工厂办公室里:
-
JavaScript 是工厂老板,给你派来两个按钮(函数):
- 一个按钮叫
resolve:按一下就说明产品合格; - 一个按钮叫
reject:按一下就说明产品不合格。
- 一个按钮叫
只要写 new Promise((resolve, reject) => { ... }),JavaScript 就会自动把这两个函数给我
调用Promise
异步Promise的首个状态是Pending,但是我们可以保证它最终是settle。我们怎么告诉计算机之后会发生什么?
Peomise提供了一个then()方法,它允许做出指令:“我有一个promise,当它转为settle之后,我想要……”
在洗碗机的例子中,洗碗机将会这样运行then:
- 如果promise错误,这意味着盘子没有洗干净,我们要加肥皂水然后再次启动洗碗机
- 如果promise成功,这意味着盘子干净了,我们要将盘子放起来
then是一个高阶函数,它接受两个高阶函数作为回调,我们将回调函数称为handlers,当promise变成settle时,相应的处理程序将使用该值调用
- 第一个handler,有时称为
onFulfilled,是成功handler,应该包括解析promise的逻辑 - 第二个handler,有时称为
onRejected,是失败handler,应该包括promise被拒绝时的处理逻辑
我们调用then时可以包含一个或两个甚至没有handler,then很灵活,但如果没有提供handler,then不会抛出错误,而是返回与promise(已settle)返回的值
.then的重要特性就是它始终会返回一个promise
成功和失败回调函数
处理成功的promise(即已解析的promise),我们可以在该promise上调用then,并传入一个成功handler
const prom = new Promise((resolve, reject) => {
resolve('Yay!');
});
const handleSuccess = (resolvedValue) => {
console.log(resolvedValue);
};
prom.then(handleSuccess); // Prints: 'Yay!'
- resolve()会使promise的状态从pending变fulfilled,并将Yay作为结果值返回给then
- reject()相反
在典型的promise应用场景中,我们无法得知promise是会resolve还是reject,所以我们需要为这两种情况提供相应的处理逻辑,通过then将成功回调和失败回调传入
let prom = new Promise((resolve, reject) => {
let num = Math.random();
if (num < .5 ){
resolve('Yay!');
} else {
reject('Ohhh noooo!');
}
});
const handleSuccess = (resolvedValue) => {
console.log(resolvedValue);
};
const handleFailure = (rejectionReason) => {
console.log(rejectionReason);
};
prom.then(handleSuccess, handleFailure);
解析:
- prom是随机生成resolv和reject的promise
- then有两个handler入参
例子:
const {checkInventory} = require('./library.js');
const order = [['sunglasses', 1], ['bags', 2]];
// Write your code below:
const handleSuccess = (resolvedValue) => {
console.log(resolvedValue);
}
const handleFailure = (rejectionReason) => {
console.log(rejectionReason);
}
checkInventory(order).then(handleSuccess, handleFailure);
使用Catch
如果没有提供对应的handler,then会返回一个与调用它的Promise具有相同Settle值的promise.这种实现方式使我们能够将成功和失败的逻辑分开,但我们也可以不用再同一个then中传入两个处理程序,而是将成功的程序带到第一个then,失败的带到第二个then,例如:
prom
.then((resolvedValue) => {
console.log(resolvedValue);
})
.then(null, (rejectionReason) => {
console.log(rejectionReason);
});
由于js对空格不敏感,我们约定链式调用每部分都要放在哪新的一行上,为了使代码更清晰,我们可以用另一种Promise方式:catch()
catch方法只接受一个参数onRejected,当Promise被拒绝时,此失败处理将会被调用,并接受调用原因,使用catch的效果与实现then仅包含一个失败处理程序的情况相同:
prom
.then((resolvedValue) => {
console.log(resolvedValue);
})
.catch((rejectionReason) => {
console.log(rejectionReason);
});
例如: app.js:
const {checkInventory} = require('./library.js');
const order = [['sunglasses', 1], ['bags', 2]];
const handleSuccess = (resolvedValue) => {
console.log(resolvedValue);
};
const handleFailure = (rejectReason) => {
console.log(rejectReason);
};
// Write your code below:
checkInventory(order)
.then(handleSuccess)
.catch(handleFailure);
lib.js:
const inventory = {
sunglasses: 0,
pants: 1088,
bags: 1344
};
const checkInventory = (order) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
let inStock = order.every(item => inventory[item[0]] >= item[1]);
if (inStock) {
resolve(`Thank you. Your order was successful.`);
} else {
reject(`We're sorry. Your order could not be completed because some items are sold out.`);
}
}, 1000);
});
};
module.exports = {checkInventory};
链式调用多个Promise
在异步编程中的一种常见模式就是多个操作彼此依赖,或者必须按某种顺序执行
例如我们向数据库发出一个请求,然后使用返回的数据进行另一个请求,以此类推
让我们用洗衣服来说明:
我们将脏衣服放在洗衣机里,如果衣服洗干净了,就将它们放到烘干机里,烘干之后,我们再叠整齐把衣服放起来
这一串promise的过程被称为组合,promise在设计时就考虑到了组合,以下代码是一个简单的Promise链:
firstPromiseFunction()
.then((firstResolveVal) => {
return secondPromiseFunction(firstResolveVal);
})
.then((secondResolveVal) => {
console.log(secondResolveVal);
});
- 我们调用firstPromiseFunction, 它返回一个promise
- 接着调用then,并将一个匿名函数作为成功处理程序
- 在success handler内部,我们返回一个新的promise(调用第二个函数secondPromiseFunction得到的结果),该函数使用第一个promise的解析值作为参数
- 调用第二个then来处理第二个promise的结算逻辑
- 在这个then的内部,有一个成功处理程序来将第二个promise的解析值记录到控制台
为了使链式调用正常工作,我们必须return promise secondPromiseFunction(firstResolveVal),这确保了第一个then的返回值是我们的第二个promise,而不是默认返回一个与初始promise具有相同settle值的新promise
例子:
const {checkInventory, processPayment, shipOrder} = require('./library.js');
const order = {
items: [['sunglasses', 1], ['bags', 2]],
giftcardBalance: 79.82
};
checkInventory(order)
.then((resolvedValueArray) => {
// Write the correct return statement here:
return processPayment(resolvedValueArray);
})
.then((resolvedValueArray) => {
// Write the correct return statement here:
return shipOrder(resolvedValueArray);
})
.then((successMessage) => {
console.log(successMessage);
})
.catch((errorMessage) => {
console.log(errorMessage);
});
## 避免常规错误
promise composition 比之前的嵌套回调可读性更强,但仍然容易出错,我们来讨论promise composition中的两个常见错误
### 嵌套promise而不是调用
```js
returnsFirstPromise()
.then((firstResolveVal) => {
return returnsSecondValue(firstResolveVal)
.then((secondResolveVal) => {
console.log(secondResolveVal);
})
})
忘记返回promise
returnsFirstPromise()
.then((firstResolveVal) => {
returnsSecondValue(firstResolveVal)
})
.then((someVal) => {
console.log(someVal);
})
- 在第一个then中,我们忘了返回第二个promise
- 当我们调用第二个then时,它应当去处理第二个promise的逻辑,但是因为我们什么也没返回,这个
.then()实际上是在一个与原始 Promise 具有相同已解决值的 Promise 上被调用
因为忘了返回promise不会报错,如果出错会让调试变得很棘手
promise.all()
如果使用得当,promise composition是一种处理异步操作互相一类或执行顺序的绝佳方法
那么如果我们处理多个promise,且不用关心它们的顺序呢?
去想象一下我们打扫房间时的场景,需要将衣服烘干,将垃圾桶清理干净,让洗碗机工作,我们需要将这些所有的工作都完成,但是不用按照特定的顺序。此外,由于它们都是异步执行的,因此它们实际上应该同时执行
为了最大化效率,我们应该使用并发,即多个异步操作同时进行。使用promise,我们可以通过promise.all()来实现这一点
promise.all()接受promise数组作为参数,返回一个promise对象。该promise将以以下两种方式之一落定(settle):
- 如果所有promise都resolves,返回的promise也会是resolved并返回一个数组,其中包含参数数组中每个promise的解析值
- 如果存在promise是reject,返回的promise也立即会是reject并返回promise是reject的原因,这种行文有时会被称为快速失败
例如:
let myPromises = Promise.all([returnsPromOne(), returnsPromTwo(), returnsPromThree()]);
myPromises
.then((arrayOfValues) => {
console.log(arrayOfValues);
})
.catch((rejectionReason) => {
console.log(rejectionReason);
});
检查供应商是否有存货:
const {checkAvailability} = require('./library.js');
const onFulfill = (itemsArray) => {
console.log(`Items checked: ${itemsArray}`);
console.log(`Every item was available from the distributor. Placing order now.`);
};
const onReject = (rejectionReason) => {
console.log(rejectionReason);
};
// Write your code below:
const checkSunglasses = checkAvailability('sunglasses','Favorite Supply Co.');
const checkPants = checkAvailability('pants','Favorite Supply Co.');
const checkBags = checkAvailability('bags','Favorite Supply Co.');
Promise.all([checkSunglasses, checkPants ,checkBags])
.then(onFulfill)
.catch(onReject);