Promise-async/await
基于回调的异步编程
在需要远程请求脚本文件,并执行其中函数时,可以使用异步方式。
setTimeout 函数允许异步执行。
function loadScript(src, callback) {
let script = {src: '', onload: () => {}, onerror: () => {}};
script.src = src;
console.info("开始模拟网络请求...");
// 模拟网络请求延时
script.onload = () => setTimeout(callback, 1000, script);
// 模拟完成后触发
script.onload();
console.info("同步代码块执行完成...");
}
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js ', script => {
console.info(script.src + " 异步执行加载完成");
});
Promise
生产消费模型
- 生产者:执行一些任务,并且有一定的耗时(通过网络加载脚本)
- 消费者:在生产者完成生产后立即获取生产结果(需要用到脚本内函数的方法)
- Promise:连接生产者与消费者的特殊对象,类似于订阅列表,当生产者完成生产时,向所有订阅了的代码开放结果
Promise本质上会更加复杂,并不是简单的生产消费模型。
生产者 executor
语法签名:
let promise = new Promise(function(resolve, reject) {
// executor(生产者代码,“歌手”)
});
- 传入的参数:一个被称为
executor的函数,当Promise创建完成后,此函数自动运行,执行生产者代码 executor参数:resolve和reject两个函数由引擎预先定义,无需自行创建,直接调用即可,两个函数只会成功调用一个,不存在同时被调用。resolve(value)—— 如果任务成功完成并带有结果valuereject(error)—— 如果出现了error,error即为Error对象(允许使用其他非Error类型,但不建议)
Promise 对象内部属性,无法外部访问:
state—— 从"pending",如果resolve被调用时变为"fulfilled",否则reject被调用时变为"rejected"result—— 从undefined,如果resolve(value)被调用时变为value,否则reject(error)被调用时变为error
Promise 最终的状态只能为二者之一,因此只能调用相对应状态的 resolve/reject 函数:
state:"fulfilled",result:valuestate:"rejected",result:error
消费者:then、catch
Promise 只是存在于 executor 和消费函数之间的桥梁,可以使用 then 和 catch 注册消费函数。
then
语法:
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});
// resolve 运行 .then 中的第一个函数
promise.then(
result => console.info(result), // 注册成功的消费函数
error => console.info(error) // 注册失败的消费函数
);
- 当
resolved时,自动运行成功消费函数,忽略失败消费函数 - 当
rejected时,自动运行失败消费函数,忽略成功消费函数
catch
then 方法既可以注册成功函数,也可以注册失败函数,但允许只注册成功函数,然后在 catch 中注册失败函数。
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// .catch(f) 与 promise.then(null, f) 一样
promise.catch(console.info); // 1 秒后显示 "Error: Whoops!"
.catch(console.info)相当于.then(null, console.info)
finally 清理
支持在创建 Promise 对象后执行清理终结的动作,要注意:
finally不传入参数,因此未知Promise是否执行成功- 即使
finally返回内容也会被忽略 finally抛出error时,转到最近的error处理程序中
new Promise((resolve, reject) => {
throw new Error("error");
})
.finally(() => console.info("Promise ready")) // 先触发
.catch(err => console.info(err)); // <-- .catch 显示这个 error
Promise vs. 回调:异步编程模式对比
| 维度 | Promise | 回调 |
|---|---|---|
| 编码顺序 | 先启动异步任务,后通过 .then 处理结果,符合自然阅读顺序 | 必须在调用异步函数前就把回调准备好,顺序倒置 |
| 多次订阅 | 同一次异步操作可多次 .then,每个处理函数独立加入“粉丝列表” | 一个回调只能被调用一次;如需多个处理逻辑需手动封装或层层嵌套 |
| 链式/组合 | 返回新 Promise,可链式 .then 或 Promise.all/race 等组合 | 需要手动管理回调嵌套或额外库,易形成“回调地狱” |
| 错误处理 | 统一 .catch 捕获整条链异常,支持 throw→reject 自动传播 | 每个回调需单独判断 err 参数,遗漏即静默失败 |
| 可读性 | 扁平链式结构,逻辑清晰 | 多层嵌套后缩进爆炸,维护困难 |
Promise让异步代码先执行、后处理,且可多次订阅、可链式组合;回调则必须先处理、后执行,并受限于单次回调的嵌套噩梦。
Promise 链
适用于一个接一个调用异步任务场景。
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
console.info(result); // 1
return result * 2;
}).then(function(result) { // (***)
console.info(result); // 2
return result * 2;
}).then(function(result) {
console.info(result); // 4
return result * 2;
});
then的回调若返回非Promise值,JavaScript 会把它自动包装成立即resolved的Promise,再传给下一环;- 只有返回真正的
Promise时才会等待其状态。因此链式调用始终成立,无需手动return Promise.resolve(x)。
显式返回 Promise
对于真正的
Promise,下一个resolved会等待其状态
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
console.info(result); // 1
return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) { // (**)
console.info(result); // 2
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) {
console.info(result); // 4
});
Thenables
"thenable" 对象:一个具有方法 .then 的任意对象,会被当做一个 Promise 来对待。
因此三方库可以实现一个与 Promise 相兼容的对象,也即是实现自己的 then 方法。
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
console.info(resolve); // function() { native code }
// 1 秒后使用 this.num*2 进行 resolve
setTimeout(() => resolve(this.num * 2), 1000); // (**)
}
}
new Promise(resolve => resolve(1))
.then(result => {
// 引擎会对返回的对象进行检查
// 若存在 then 方法即会进行调用
// 并提供原生的函数 resolve 和 reject 作为参数
return new Thenable(result); // (*)
})
.then(console.info); // 1000ms 后显示 2
Promise 错误处理
当一个 Promise 被 reject 时,控制权将移交至最近的 rejection 处理程序。
比如将 Promise 的 catch 方法附在链尾:
需要 v18 版本的 Node.js 环境才原生支持
fetch,或者使用浏览器
fetch('https://no-such-server.blabla ') // reject
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/ ${user.name}`))
.then(response => response.json())
.catch(err => console.info(err))
隐式捕获
执行者(executor)和 Promise 的处理程序周围有一个“隐式的 try..catch”,因此
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(console.info); // Error: Whoops!
等同于
new Promise((resolve, reject) => {
reject(new Error("Whoops!"));
}).catch(console.info); // Error: Whoops!
因此会自动捕获了 error,并将其变为 rejected Promise,然后将错误移至最近的错误处理程序。
再次抛出
和 try..catch 一样,catch 方法中允许再次将未知的错误抛出。每一次错误都会转到最近的处理程序,处理完成后转到最近的 then 方法。
// 执行流:catch -> catch
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(function(error) { // (*)
if (error instanceof URIError) {
// 处理它
} else {
console.info("Can't handle such error");
throw error; // 再次抛出此 error 或另外一个 error,执行将跳转至下一个 catch
}
}).then(function() {
/* 不在这里运行 */
}).catch(error => { // (**)
console.info(`The unknown error has occurred: ${error}`);
// 不会返回任何内容 => 执行正常进行
}).then(function() {
/* 继续运行 */
});
未处理的 rejection
- 出现
error,Promise状态变为rejected,执行最近的 rejection 处理程序 - 若未找到最近的 rejection 处理程序,引擎会跟踪此类 rejection
- 之后生成全局
error
浏览器中可以注册事件进行监听此类错误:
window.addEventListener('unhandledrejection', function(event) {
// 这个事件对象有两个特殊的属性:
console.info(event.promise); // [object Promise] —— 生成该全局 error 的 promise
console.info(event.reason); // Error: Whoops! —— 未处理的 error 对象
});
new Promise(function() {
throw new Error("Whoops!");
}); // 没有用来处理 error 的 catch
Promise API
所有 API 都返回一个
Promise
Promise.all(promises)
并行等待所有 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(console.info); // 1,2,3(按原顺序,不受结束时间影响)
// 常见技巧:数据数组 → Promise 数组 → Promise.all
let urls = [
'https://api.github.com/users/iliakan ',
'https://api.github.com/users/remy ',
'https://api.github.com/users/jeresig '
];
let requests = urls.map(url => fetch(url));
Promise.all(requests)
.then(responses => responses.forEach(
response => console.info(`${response.url}: ${response.status}`)
))
.catch(err => console.info(err));
// 任一失败,整体立即拒绝
Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(console.info); // Error: Whoops!
一旦有一个
Promise变成rejected,则整体立即rejected,即使其他Promise仍在执行,但不再关心其状态。
Promise.allSettled(promises)
等待所有 Promise 都 settle(无论成败),返回状态对象数组。
/**
* 结果格式:
* - 成功: {status:"fulfilled", value:result}
* - 失败: {status:"rejected", reason:error}
*/
let urls = [
'https://api.github.com/users/iliakan ',
'https://api.github.com/users/remy ',
'https://no-such-url'
];
Promise.allSettled(urls.map(url => fetch(url)))
.then(results => {
results.forEach((result, num) => {
if (result.status == "fulfilled") {
console.info(`${urls[num]}: ${result.value.status}`);
}
if (result.status == "rejected") {
console.info(`${urls[num]}: ${result.reason}`);
}
});
});
Promise.race(promises)
只等待第一个 settled 的 Promise,无论成功或失败。
Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(console.info); // 1
Promise.any(promises)
只等待第一个 fulfilled 的 Promise;若全部失败,返回 AggregateError。
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(console.info); // 1
// 全部失败
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000))
]).catch(error => {
console.log(error.constructor.name); // AggregateError
console.log(error.errors[0]); // Error: Ouch!
});
Promise.resolve(value)
将值包装为立即成功的 Promise,用于兼容性处理。
let cache = new Map();
function loadCached(url) {
if (cache.has(url)) {
return Promise.resolve(cache.get(url)); // 同步值 → Promise
}
return fetch(url)
.then(response => response.text())
.then(text => {
cache.set(url, text);
return text;
});
}
Promise.reject(error)
创建立即失败的 Promise(极少使用)。
Promise.reject(new Error("fail")).catch(console.info); // Error: fail
对比速查
| 方法 | 成功条件 | 失败条件 | 返回值/行为 |
|---|---|---|---|
Promise.all | 全部成功 | 任一失败 | 结果数组 / 首个错误 |
Promise.allSettled | 全部落定 | 无(永不拒绝) | 状态对象数组 |
Promise.race | 首个落定 | 首个落定(若失败) | 首个结果/错误 |
Promise.any | 首个成功 | 全部失败 | 首个结果 / AggregateError |
Promise.resolve | 立即 | - | 成功的 Promise |
Promise.reject | - | 立即 | 失败的 Promise |
Promisification
指将一个接受回调的函数转换为一个返回 Promise 的函数。由于历史原因,许多库函数都是基于回调,因此 Promise 化是有意义的。但不是回调的完全替代。
Promise 装饰器:
// promisify(f, true) 来获取结果数组
function promisify(f, manyArgs = false) {
return function (...args) {
return new Promise((resolve, reject) => {
function callback(err, ...results) { // 我们自定义的 f 的回调
if (err) {
reject(err);
} else {
// 如果 manyArgs 被指定,则使用所有回调的结果 resolve
resolve(manyArgs ? results : results[0]);
}
}
args.push(callback);
f.call(this, ...args);
});
};
}
// 用法:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...);
promisify(f)的形式调用时,返回正常的Promise化的函数promisify(f, true)的形式调用时,返回以回调函数数组为结果resolve的Promise
在 Node.js 中,有一个内建的
Promise化函数util.promisify请记住,一个
Promise可能只有一个结果,但从技术上讲,一个回调可能被调用很多次。因此,
promisification仅适用于调用一次回调的函数。进一步的调用将被忽略。
微任务
微任务队列
promise 的处理程序 .then、.catch 和 .finally 都是异步的,
这三个方法内部的代码会晚于其余正常代码的执行。
let promise = Promise.resolve();
promise.then(() => console.info("promise done!")); // 后显示
console.info("code finished"); // 先显示
内部队列 PromiseJobs(微任务队列):
- 队列(queue)是先进先出的:首先进入队列的任务会首先运行。
- 只有在 JavaScript 引擎中没有其它任务在运行时,才开始执行任务队列中的任务。
promise 准备就绪后, .then、.catch 和 .finally会被放入微任务队列中等待执行。
未处理的 rejection
如果一个 promise 的 error 未被在微任务队列的末尾进行处理,则会出现“未处理的 rejection”。
如果延迟catch方法的处理时间(浏览器中):
let promise = Promise.reject(new Error("Promise Failed!"));
// 再次被catch捕获
setTimeout(() => promise.catch(err => console.info('caught')), 1000);
// 会先被全局捕获
// Error: Promise Failed!
window.addEventListener('unhandledrejection', event => console.info(event.reason));
引擎会检查 promise:
- 如果 promise 中的任意一个出现 “rejected” 状态,unhandledrejection 事件就会被触发
- 由于已经添加了catch方法,1000ms后,被添加到 setTimeout 中的 .catch 也会被触发,对捕获的错误进行显示
async/await
async
允许对函数进行修饰,使其总是返回一个 resolved 的 promise
async function f() {
return 1;
}
f().then(console.info); // 1
等同于显式的返回一个Promise
async function f() {
return Promise.resolve(1);
}
f().then(console.info); // 1
await
让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果
只在 async 函数内工作
// 只在 async 函数内工作
let value = await promise;
示例:
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // 等待,直到 promise resolve (*)
console.info(result); // "done!"
}
f();
- 执行到 (*) 行暂停,在 promise settle 时,拿到 result 作为结果继续往下执行
- await 实际上会暂停函数的执行,直到 promise 状态变为 settled
- 然后以 promise 的结果继续执行
这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务
await接受"thenables"
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
console.info(resolve);
// 1000ms 后使用 this.num*2 进行 resolve
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
}
async function f() {
// 等待 1 秒,之后 result 变为 2
let result = await new Thenable(1);
console.info(result);
}
f();
在类中定义async方法:
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(console.info); // 1(console.info 等同于 result => console.info(result))
Error 处理
await所期望的Promise被reject时,相当于throw一个Error,之后正常try..catch捕获以及再次抛出即可。
或者在Promise后增加catch方法进行捕获
async function f() {
await Promise.reject(new Error("Whoops!"));
}
等同于
async function f() {
throw new Error("Whoops!");
}
async、await与then、catch
- 使用 async/await 时,几乎不会用到 .then,因为await 处理了等待
- 使用常规的 try..catch 代替使用 .catch
- 在代码的顶层时,也就是在所有 async 函数之外,在语法上就不能使用 await 了,此时需要添加 .then/catch 来处理最终的结果或者错误
async/await 与 Promise.all
需要同时等待多个 promise 时,可以用 Promise.all 把它们包装起来,然后使用 await
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);