我们设想一下,假如你是一个歌星,你的粉丝总是不分昼夜的向你询问你将要完成的歌曲(注:就像我们总是问杰伦,什么时候发新歌)
为了减轻这种焦虑,你向粉丝们保证,一旦发新歌,马上就告诉他们,然后呢你就给他们一张表,让他们填上他们的邮箱地址,便于及时地告知你的新歌情况,即使遇到非常糟糕的事情,例如,你的录音室着火了,不能发歌,你的粉丝也会被通知到位。
每个人都很开心,一方面因为你的粉丝们不会在逼逼叨叨你,另一方面粉丝再也不会错过你的歌。
与现实中的这种类似的事情相比,在编程中我们也会经常遇到:
这个对比其实很不精确,在java script中,promise其实更复杂,它有更多的特性和限制。好了,让我们言归正传吧。
创建promise 对象实例的语法如下:
let promise = new Promise(function(resolve, reject) {
// executor (the producing code, "singer")
});
传递给new Promise的函数称为executor。一旦new Promise被创建,executor就会立即执行。它包含最终生成结果的生成代码。根据上面的类比:executor就是“歌手”。
这个函数的参数resolve 和reject是JavaScript本身提供的回调。我们的代码只在executor内部
当executor获得结果时,无论是快还是晚,都无所谓,它应该会调用以下回调之一:
综述,当executor运行去尝试完成某项任务时,执行完之后就可能会产生两个结果,执行成功就去调用resolve, 执行失败就去调用reject,产生error。
通过new promise方法创建的promise对象具有以下内部属性
后面我们将会了解 “粉丝”是怎么知道这些变化的。 下面是一个promise构造函数和一个简单的执行器函数的示例,其中“生成代码”需要时间(通过setTimeout):
let promise = new Promise(function(resolve, reject) {
// the function is executed automatically when the promise is constructed
// after 1 second signal that the job is done with the result "done"
setTimeout(() => resolve("done") , 1000);
});
通过执行以上代码,我们可以了解两件事情
经过一秒的处理之后,executor就会调用resolve(“done”)返回结果。这也改变了promise对象的状态
这是一个成功完成的例子,一个“fulfilled promise”的例子
现在举一个executor拒绝promise,带有error的例子
let promise = new Promise(function(resolve, reject) {
// after 1 second signal that the job is finished with an error
setTimeout(() => reject(new Error("Whoops!")) , 1000); });
这个调用reject(...)会使promise对象的状态变动为"rejected" 状态
简而言之,executor应该执行一项任务(通常是些需要花费时间的事情),然后去调用 resolve或者reject,promise 对象就会随之改变相应的状态。
一个promise状态不管是resolved状态还是rejected状态,都统一被叫做settled, 与之相反的就是初始的pending状态
There can be only a single result or an error
executor只能调用resolve或reject一次,然后返回一个最终的状态
所有后续的resolve或reject调用都会被忽略:
let promise = new Promise(function(resolve, reject) {
resolve("done");
reject(new Error("…")); // ignored
setTimeout(() => resolve("…")); // ignored
});
这个意思就是说,当executor执行完一个job,就会返回一个结果或者是一个error。 另一方面, resolve/reject也只0个或一个参数,额外的参数也会被忽略。
Reject with Error objects
万一出了什么问题,executor就会调用reject。这可以返回任何类型的值(比如resolve) 但是我们建议使用error对象(或者是继承error的对象),为啥这样做后面很快就明白了。
Immediately calling resolve/reject
实际上,executor通常以异步方式执行某些操作,然后经过一段时间去调用resolve/reject,但这并不是绝对的, 我们也可以立刻去调用resolve/reject,就像这样:
let promise = new Promise(function(resolve, reject) {
// not taking our time to do the job
resolve(123);// immediately give the result: 123
});
举个例子,当我们开始做一项工作,但看到所有的事情都已经完成并缓存时,可能会发生这种情况。这样是OK的,我们会立即得到一个resolved promise
The state and result are internal
Promise对象的属性state和 result 是内部的。我们无法直接访问它们。我们可以使用这些方法.then/.catch/.finally来访问。下文将对其进行说明。
一个promise 对象充当 executor(producing code 或者 歌手)和 consuming functions(粉丝)之间的桥梁,它将会接收result 或者error。consuming functions就会被注册(订阅)使用.then, .catch and .finally 方法.
最重要、最基本的一个就是.then 语法如下:
promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
);
.then的第一个参数是一个方法,这个方法是当promise是resolved,并且返回了result。
.then的第二个参数是一个方法,这个方法是当promise是rejected,并且返回了error。
举一个成功返回resolved promise的例子
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});
// resolve runs the first function in .then
promise.then(
result => alert(result), // shows "done!" after 1 second
error => alert(error) // doesn't run
);
.then中第一个方法将会执行
再举一个关于rejection的例子:
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// reject runs the second function in .then
promise.then(
result => alert(result), // doesn't run
error => alert(error) // shows "Error: Whoops!" after 1 second
);
如果我们只对成功的执行操作感兴趣,那么在.then方法中,我们可以只传入一个参数:
let promise = new Promise(resolve => {
setTimeout(() => resolve("done!"), 1000);
});
promise.then(alert); // shows "done!" after 1 second
如果我们只关注errors, 那么我们可以设置第一个参数为null : .then(null, errorHandlingFunction)
或者我们可以使用.catch(errorHandlingFunction), 就像下面这样:
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Whoops!" after 1 second
.catch(f)和.then(null, f)的功能是一样的,只是.catch(f)更简略一些
正如try {...} catch {...}语句有个finally一样,在promises里也有一个finally
.finally(f)和.then(f, f)很相似, 这个f不管promise是resolved还是reject,只要是settled,都会执行
finally是执行清理操作一个很好的方式,例如终止我们的加载指示器,当我们不在需要它们,无论结果怎么样。就像这样:
new Promise((resolve, reject) => {
/* do something that takes time, and then call resolve/reject */
})
// runs when the promise is settled, doesn't matter successfully or not
.finally(() => stop loading indicator) // so the loading indicator is always stopped before we process the result/error
.then(result => show result, err => show error)
意思是说,虽然finally(f)并不完全是then(f,f)的别名,但两者之间几乎没有细微的差别。
new Promise((resolve, reject) => {
setTimeout(() => resolve("result"), 2000) })
.finally(() => alert("Promise ready"))
.then(result => alert(result)); // <-- .then handles the result
如果promise有error,finally就会把error传给catch处理:
new Promise((resolve, reject) => {
throw new Error("error");
})
.finally(() => alert("Promise ready"))
.catch(err => alert(err)); // <-- .catch handles the error object
这样的话就非常方便,因为finally并不是用来处理promise结果的,所以它只需把结果传递出去。
在下一章节中,我们将更多地讨论promise chainning 和 方法之间的result-passing。
如果promise是pending状态,.then/catch/finally这些方法就待命,否则一旦promise是settled状态,这些方法就会执行:
// the promise becomes resolved immediately upon creation
let promise = new Promise(resolve => resolve("done!"));
promise.then(alert); // done! (shows up right now)
注意,这与现实生活中的邮箱分发列表相比,promises更有效率。当一个歌手已经发表了他的歌,这时有个人再去那个邮箱列表填上自己的邮箱,就有可能会收不到这首歌。因为在现实生活中,当歌完成后发送信息这个动作在这个人去邮箱列表增加邮箱这个事件之前,
promise要更加灵活,我们可以在任意时间添加处理方法:只要result准备好,这些方法就会被执行
接下来,让我们参考更多的实际例子,来帮助我们更好的了解promise是怎么帮助我们写异步代码的
从先前的章节中,我们已经了解loadScript方法是用来加载一个脚本
以下是基于回调的变形,只是提醒我们
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script); }
接下来让我们用promises来重写它 创建一个loadScript方法将不需要回调。相反,它将会创建并且返回一个promise对象,这个promise对象解决这个加载什么时候完成。外部的代码可以用.then给它添加方法(subscribing functions)
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Script load error for ${src}`));
document.head.append(script);
});
}
使用:
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => alert(`${script.src} is loaded!`), error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('Another handler...'));
与基于回调的模式相比,我们可以立即看到一些好处:
Promises | Callbacks |
---|---|
Promises 让我们按照自然的顺序做事. 首先, 我们执行 loadScript(script), 然后.then 去处理result. | 当调用loadScript(script, callback)时,我们必须要有一个callback方法在我们的处置中。换句话说, 我们必须知道怎么处理result在loadScript被调用之前. |
只要我们愿意,我们就可以调用promise的.then 方法任意次. 每次调用的时候, 我们就是在“subscription list”上增加了一个新的粉丝,一个新的发送动作,更多内容可以看下个章节 : Promises chaining. | 只能调用一次. |
因此,promise给了我们更好的代码流和灵活性。但还有更多。我们将在下一章中看到这一点。