1、promise是什么
- “生产者代码(producing code)”会做一些事,并需要一些时间。例如,通过网络加载数据的代码。
- “消费者代码(consuming code)”想要在生产者代码完成工作的第一时间就能获取其工作成果。许多函数可能都需要这个结果。
- Promise是将“生产者代码”和“消费者代码”连接在一起的一个特殊JavaScript对象。
Promise对象的构造器(constructor)语法如下
:
let promise = new Promise(function(resolve, reject) {
// executor(生产者代码)
});
其中,传递给new Promise的函数被称为executor。当new Promise被创建,executor会自动执行。它包含最终应产出结果的生产者代码。
参数resolve
和reject
是由JavaScript自身提供的回调,我们自己的代码仅在executor内部。
当executor获得结果,应该调用以下回调之一:
- resolve(value) - 任务成功完成并带有结果value
- reject(error) - 如果出现了error,error 即为error对象
由new Promise构造器返回的promise对象具有以下的内部属性:
- state - 最初是pending,然后在resolve被调用时变为fulfilled,或者在reject被调用时变为rejected。
- result - 最初时undefined,然后在resolve(value)被调用时变为value,或者在reject(error)被调用时变为error。
与最初的"pedding" promise相反,一个resolved或rejected的promise都会被称为"settled"。
promise对象中state和result属性都是内部的。无法直接访问,但可以对其使用.then/.catch/.finally
方法。
2、消费者:then,catch
promise对象是executor和consumer之间的连接,后者将接收result或error,可以通过.then和.catch方法注册消费函数。
2.1 then
最基础最重要的就是then
promise.then (
function(result) {/* handle a successful result */},
function(error) {/* handle an error */}
);
then的两个参数都是函数。
- 第一个参数将在promise resolved且接收到结果后执行;
- 第二个参数将在promise rejected且接收到error信息后执行。
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});
// resolve 运行 .then的第一个函数
promise.then(
result => alert(result), // 1s后显示done
error => alert(error) // 不运行
);
如果是reject的情况:
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("oh no, it's an error!")), 1000);
});
如果只对成功完成的情况感兴趣,则可以只为.then提供一个函数参数:
promise.then(alert);
2.2 catch
如果只对error感兴趣,那么可以将第一个函数参数设置为null:
.then(null, errorHandlingFunction)。
或者使用.catch(errorHandlingFunction)
。
promise.catch(alert);
// catch(f) 与 .then(null, f)一样
3、清理 finally
像常规try {...} catch {...}
中的finally子句一样,promise中也有finally。
调用.finally(f)
类似于.then(f, f)
, 因为当promise 状态为settled时,f 就会执行:无论promise被resolve还是reject。
finally的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。例如,停止加载指示器,关闭不再需要的连接等。
new Promise((resolve, reject) => {
// executor code... 之后调用可能会 resolve,也可能会reject
})
// 在promise为 settled 时运行(无论是resolved还是rejected)
.finally(() => stop loading indicator)
// 所以加载指示器 loading indicator始终会在我们继续之前停止
.then(result => show result, err => show error)
finally(f) 和 then(f, f)执行有重要的区别:
- finally处理程序没有参数,finally中,我们并不知道promise是否成功。
- finally处理程序将result或error传递给下一个合适的处理程序。
例如上述例子中,传递给了then。
也就是说,finally并不会处理promise的结果,无论结果如何,都会进行常规的清理。 - finally处理程序也不应该返回任何内容。如果返回了,返回值也会默认被忽略。
此规则唯一例外是当finally处理程序抛出error,这个error(不是任何之前的结果)会被转到下一个处理程序。
4、Promise链
可以通过.then处理程序链
进行传递result。
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return result * 2;
}).then(function(result) {
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
})
每个对.then
的调用都会返回一个新的promise,因此可以在其之上调用下一个.then。
当然,也可以将多个.then添加到一个promise上,但这并不是promise链chaining。
then处理程序中,还可以创建并返回一个promise:
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
})
}).then(function(result) {
alert(result); // 2
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
})
}).then(function(result) {
alert(result); // 4
})
以上程序每隔1s弹出一个数字,then处理程序返回一个promise对象,会将result传递给下一个then处理程序。
确切地说,处理程序返回的不完全是一个promise,而是返回一个被称为“thenable”对象—— 一个具有.then方法的任意对象,它会被当作一个promise来对待。
5、fetch
promise经常被用于网络请求,fetch方法用于从远程服务器加载用户信息。
let promise = fetch(url);
执行这条语句,向url发出网络请求并返回一个promise。当远程服务器返回header(是在 全部相应加载完成前)时,该promise使用一个response对象来进行resolve。
为了读取完整的响应,我们应该调用 response.text() 方法
:当全部文字内容从远程服务器下载完成后,会返回一个promise,该promise以刚刚下载完成的这个文本作为result进行resolve。
fetch('url')
.then(function(response) {
// 当url加载完成时,response.text()会返回一个新的promise对象
return response.text();
}).then(function(text) {
alert(text);
})
从fetch返回的response对象还包括response.json()方法,该方法读取远程数据并将其解析为JSON。
fetch('url')
.then(response => response.json())
.then(user => alert(user.name));
6、then vs catch
promise.then(f1).catch(f2);
vs
promise.then(f1, f2);
第一个例子中f1如果出现error,会被catch处理;但第二个例子不会。
因为error是沿着链传递的,第二个代码的f1下面没有链。换句话说,.then将result/error传递给下一个.then/.catch。
7、使用promise进行错误处理
Promise链在错误处理中十分强大,当一个promise被reject时,控制权将移交至最近的rejection处理程序。
.catch不必是立即的,可以在一个或多个.then之后出现。捕获所有error最简单的方法是,将.catch附加到链的末尾。通常情况下,.catch不会被触发,但是如果上述任意一个promise被reject(网络问题或者无效的json或其他),.catch就会捕获它。
隐式try…catch
Promise的执行者(executor)和promise的处理程序(handler)周围有一个隐式的try…catch,如果发生异常,则会被捕获,并被视为rejection进行处理。
new Promise((resolve, reject) => {
throw new Error("oh no, it's an error!");
}).catch(alert);
以上代码和下面的代码工作上完全相同:
new Promise((resolve, reject) => {
reject(new Error("oh no, it's an error"));
}).catch(alert);
在executor周围的隐式try…catch自动捕获了error,并将其变为rejected promise。
这不仅仅发生在executor函数中,同样也发生在其handler中。如果在.then处理程序中throw,promise对象也会被rejected,控制权移交至最近的error处理程序。
new Promise(function(resolve, reject) {
setTimeout(() => {
throw new Error("Whoops!");
}, 1000);
}).catch(alert);
但是上述的.catch不会被触发,因为函数代码周围虽然有隐式的try…catch,所有的同步错误都会得到处理。但是!上述代码中,错误并不是在executor运行时生成的,而是稍后生成的,因此promise无法进行处理。
8、Promise API
8.1 Promise.all
假设我们希望并行执行多个promise,并等待所有promise都准备就绪。例如,并行下载几个URL并等到所有内容都下载完毕后再对它们进行处理。
let promise = Promise.all(iterable);
这里接受一个可迭代对象,通常是一个数组项为promise的数组,并返回一个新的promise。当所有给定的promise都resolve时,新的promise才会resolve,并将其结果数组作为新promise的结果。
Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)),
new Promise(resolve => setTimeout(() => resolve(2), 2000)),
new Promise(resolve => setTimeout(() => resolve(3), 1000))
]).then(alert); // 1,2,3
结果数组中元素的顺序与其在源promise中的顺序相同。即使第一个promise花费了最长的时间才resolve,但它仍是结果数组中的第一个。
一个常见的技巧是,将一个任务数组映射到一个promise数组,然后将其包装到Promise.all:
let urls = [
'url1',
'url2',
'url3'
];
// 将每个url映射到fetch到Promise中
let requests = urls.map(url => fetch(url));
// Promise.all 等待所有任务都 resolved
Promise.all(requests)
.then(responses => responses.forEach(
response => alert(`${response.url}: ${response.status}`)
));
在Promise.all()中,如果任意一个promise被reject,则返回的promise会立即reject,并带有对应被reject的那个promise的error。也就是说,如果出现了error,那么其他的promise就会被忽略掉。
Promise.all(iterable)也允许参数是非promise的常规值,该参数将被按原样传递给结果数组。
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
}),
2,
3
]).then(alert); // 1,2,3
8.2 Promise.allSettled
该API等待所有的promise都被settle(无论结果如何)后,返回一个结果数组:
- 对于成功的响应:{status: “fulfilled”, value: result}
- 对于error:{status: “rejected”, reason: error}
8.3 Promise.race
只等待第一个settled的promise并获取其结果/error。
8.4 Promise.any
只等待第一个fulfilled的promise,并将其返回。如果给出的promise都rejected,则返回rejected的promise和AggregateError错误类型的error实例—— 一个特殊的error对象,在其errors属性中存储着所有promise error。
8.5 Promise.resolve/reject
使用给定的value创建一个resolved/rejected的promise。
9、微任务Microtask
Promise的处理程序.then, .catch, .finally都是异步的。
即便一个promise立即被resolve,这些处理程序下面的代码也会在这些处理程序之前就被执行。
let promise = Promise.resolve();
promise.then(() => alert("promise done!"));
alert("code done!");
以上代码会先弹出"code done!“,然后再弹出"promise done!”。这是因为微任务队列Microtask queue。
异步任务需要适当的管理。为此,ECMA标准规定了一个内部队列PromiseJobs
,通常被称为“微任务队列”。
队列queue是先进先出的:首先进入队列的任务会首先执行。
只有在js引擎中没有其他任务运行时,才开始执行任务队列中的任务。
简单来说,当一个promise准备就绪时,.then/catch/finally处理程序就会被放入队列,但不会立即被执行。js引擎执行完当前代码后,才会从队列中获取任务并执行。
如果需要确保一段代码在处理程序之后被执行,可以将它添加到链式调用的.then中。
10、Async/await
10.1 Async function
在函数前的async就表达了一个简单的事情:这个函数总是返回一个promise,其他值将自动被包装在一个resolved的promise中。
async function f() {
return 1;
}
f().then(alert); // 1
也可以显式返回一个promise,与上述一样:
async function f() {
return Promise.resolve(1);
}
f().then(alert);
10.2 Await
只能在async函数内工作。
关键字await让js引擎等待直到promise完成settle并返回结果。
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // 等待,直到promise resolve
alert(result); // "done!"
}
f();
函数执行时,直到promise settle,拿到result作为结果继续往下执行。await实际上会暂停函数的执行,直到promise状态变为settled,然后以promise的结果继续执行。该行为不会耗费任何CPU资源,因为js引擎可以同时处理其他任务,比如:执行其他脚本,处理事件等。
相比于promise.then,它只是获取promise的结果的一个更优雅的语法。