概述
- JavaScript 是单线程的,因此异步机制对于避免阻塞主线程(如 UI 呈现)和耗时的操作(如网络请求、文件读取和计时器)非常重要。
- 回调函数是早期的异步实现,但容易出现"回调地狱"问题。
- ES6 添加了一个正式的 Promise 引用类型,允许您优雅地定义和组织异步逻辑。
- ES2017引入了基于承诺的语法糖async/await,使异步代码更接近同步风格。
异步编程
同步和异步
同步:任务按照代码的顺序逐一执行(执行顺序与代码书写顺序一致),每个任务必须等上一个任务完成后才能开始(阻塞)。
更准确地讲,同步行为是指令按顺序严格执行,执行后变量的值也能立即从寄存器或内存获取,同步代码的执行状态可以说是一目了然,可以被清晰地分析。
异步:任务不需要等待其他任务完成即可开始,耗时操作可以在后台处理,主线程继续执行其他任务。
异步行为与系统中断类似,中断是计算机系统中一种机制,允许外部事件或硬件设备打断当前正在运行的程序,并执行一段特定的处理代码(中断服务程序,ISR),当前进程会被暂时挂起,中断处理完成后恢复执行。
异步行为无法知晓代码的执行状态,就像一个黑盒。
对于需要暂停或等待的任务,异步比同步更加高效,因此同步操作适合短时间内可以完成的简单任务,异步操作则适合如网络请求、文件读写等耗时的任务。
旧异步编程模式:回调
在早期的 JavaScript 中,只支持定义回调函数来表明异步操作完成。回调是指通过将函数作为参数传递,在任务完成后调用。下面逐步演示使用回调的异步操作。
(1)异步操作:
javascript
function double(value) {
setTimeout(console.log, 1000, value);
}
double(3);
(2)传递回调函数作为参数并获得异步操作的返回值:
javascript
function double(value, callback) {
setTimeout(() => callback(value), 1000);
}
double(3, (x) => console.log(`Success: ${x}`);
(3)传递失败回调:
javascript
function double(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== 'number') {
throw 'Must provide number as first argument';
}
success(value);
} catch (e) {
failure(e);
}
}, 1000);
}
const successCallback = (x) => console.log(`Success: ${x}`);
const failureCallback = (e) => console.log(`Failure: ${e}`);
double(3, successCallback, failureCallback);
double('b', successCallback, failureCallback);
到这儿已经初见 Promise 端倪了。
(4)回调地狱
然而使用这种方式获取的异步返回值只在回调函数内可用,回调结束后也就销毁了。而且,如果异步返回值又依赖另一个异步返回值,就会形成嵌套的回调,即回调地狱:
javascript
function double(value, callback) {
setTimeout(() => callback(value*2), 1000);
}
double(3, (x)=>
double(x, (y) =>
double(y, (z) => console.log(`Success: ${z}`)
)
);
Promise
Promise 可以看作是对未知状态的一个约定,对不存在结果的一个替代。
(解决 Promises/A 不足,明确细节) 2015 : ES6 正式支持 Promise 类型
(实现 Promises/A+ 规范)
Promise 基础
new Promise 创建期约
创建新期约时必须传入执行器(executor)函数作为参数,哪怕是空函数,如果不提供执行器函数,就会抛出 SyntaxError。
javascript
let p = new Promise((resolve, reject) => {});
setTimeout(console.log, 0, p); // Promise <pending>
Promise 的三个状态
- Pending:初始状态,既未完成,也未失败。
- Fulfilled 或 Resolved(已完成):操作成功完成,返回结果。
- Rejected(已失败):操作失败,返回原因。
在待定状态下,Promise 可以落定(settle)为代表成功的兑现状态,或者代表失败的拒绝状态,无论落定为哪种状态都是不可逆的。并且 Promise 的状态是私有的,不能直接被外部 JS 检测到,于是也不能被修改。
Promise 的两大用途
Promise 主要有两大用途。
- 表示一个异步操作。前面已经说过了异步操作的三种状态,对于一些用例来说,状态表示已经足够。比如,请求成功,请求状态转为
Resolved
或Fulfilled
;请求失败,Promise状态转为Rejected
。 - 生成一个值。Promise状态改变后,程序会获取到一个值。Promise 成功,该值为解决值;Promise 失败,该值为拒绝理由,一般为 Error 对象。
上述两大用途都是通过执行器函数实现的,下节具体介绍。
执行器函数的作用
(1)初始化 Promise 的异步行为
Promise 执行器函数中的代码是同步的、立即执行的。通常用于处理网络请求、文件读取等异步操作。
javascript
const promise = new Promise((resolve, reject) => {
console.log('执行器函数同步执行');
setTimeout(() => {
resolve('异步操作完成');
}, 1000);
});
console.log('Promise 已创建');
promise.then(console.log);
// 执行器函数同步执行
// Promise 已创建
// 异步操作完成
(2)控制 Promise 状态的转换
控制 Promise 状态的转换是通过调用执行器函数的两个函数参数 resolve() 和 reject() 实现的。调用 resolve() 会把状态切换为兑现,调用 reject() 会把状态切换为拒绝且会抛出错误。
javascript
let p1 = new Promise((resolve, reject) => resolve());
setTimeout(console.log, 0, p1); // Promise <resolved>
let p2 = new Promise((resolve, reject) => reject());
setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught error (in promise)
执行器函数是同步执行的,因为执行器函数是 Promise 的初始化程序。
无论 resolve() 和 reject() 中的哪个被调用,状态转换都不可撤销了,继续修改状态会静默失败。
javascript
let p = new Promise((resolve, reject) => {
resolve();
reject(); // 没有效果
});
setTimeout(console.log, 0, p); //Promise<resolved>
(3)传递解决值和拒绝理由
resolve(value)
用于将操作的结果值传递给后续的 .then()
。
reject(reason)
用于将错误或失败的原因传递给后续的 .catch()
。
Promise 的实例方法
Promise 的实例方法是连接外部同步代码与内部异步代码之间的桥梁。
这些方法可以访问异步操作返回的数据,处理 Promise 成功和失败的结果,连续对 Promise 求值,或者添加只有 Promise 进入终止状态时才会执行的代码。
Thenable 接口
在 ECMAScript 暴露的异步结构中,任何对象都有一个 then() 方法,这个方法被认为实现了 Thenable 接口。
只要对象实现了 .then()
方法(一个接受两个参数的函数 onFulfilled 和 onRejected),它就被认为是一个 Thenable。
Thenable 对象可以通过 Promise.resolve() 转换为真正的 Promise,并且可以与原生 Promise 进行交互。
从 Thenable 到 Promise 的过程: 当你将一个 Thenable 对象传递给 Promise
构造函数时,Promise
会通过调用该对象的 then()
方法,并传递给它两个回调函数(resolve
和 reject
),来"转换"它为一个真正的 Promise
。如果这个 Thenable
对象调用 resolve
或 reject
,那么它就会变成一个已解决或已拒绝的 Promise
。
javascript
let thenable = {
then: function(resolve, reject) {
setTimeout(() => resolve('Done!'), 1000); // 模拟异步操作
}
};
let promise = new Promise((resolve, reject) => {
thenable.then(resolve, reject); // 将Thenable对象转化为Promise
});
promise.then((value) => {
console.log(value); // 输出 "Done!" after 1 second
});
then()
简介:Promise.prototype.then() 是为Promise实例添加处理程序的主要方法。
签名:then(onFulfilled, onRejected)
参数:onFulfilled 是 Promise 成功时调用的回调函数;onRejected 是 Promise 失败时调用的回调函数。如果想只提供 onRejected 参数,那就要在 onResolved 参数的位置上传入 undefined 或 null,避免创建多余的对象。
返回值:then() 返回一个新的 Promise 实例,这使得可以链式调用 then()。这个新 Promise 实例是通过 Promise.resolve() 包装返回值生成的。
示例:
在 Promise 的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由。
javascript
const promise = new Promise((resolve, reject) => {
let success = false;
if (success) {
resolve("Success!");
} else {
reject("Failure!");
}
});
let p = promise.then(null, (error) => {
throw new Error(error);
});
setTimeout(console.log, 0, p);
// Promise {<rejected>: Error: Failure!
// Uncaught (in promise) Error: Failure!
catch()
简介:catch() 用于给Promise添加拒绝处理程序,是一个语法糖,相当于 then(null, onRejected)。
签名:catch(onRejected)
参数:
- onRejected:拒绝处理程序。
返回值:与 then 一样,使用 Promise.resolve() 包装处理程序返回值生成的新Promise实例。
示例:
javascript
const promise = new Promise((resolve, reject) => {
let success = false;
if (success) {
resolve("Success!");
} else {
reject("Failure!");
}
});
let p = promise.catch((value) => value);
setTimeout(console.log, 0, p); // Promise {<fulfilled>: 'Failure!'}
finally()
简介:finally() 是为了避免 onResolved() 和 onRejected() 中出现冗余代码。
签名:finally(onFinally())
参数:
- onFinally() 在 Promise 转换为解决或拒绝状态时都会执行。
返回值:finally() 返回一个新 Promise 实例,该 Promise 实例大多数情况下表现为父Promise的传递,除了onFinally() 返回一个待定 Promise 或者抛出了错误(显示抛出或返回一个拒绝 Promise),则会返回相应的 Promise(待定或拒绝)。
示例:
javascript
let p1 = Promise.resolve("foo");
// 这里都会原样后传
let p2 = p1.finally();
let p3 = p1.finally(() => undefined);
let p4 = p1.finally(() => {});
let p5 = p1.finally(() => Promise.resolve());
let p6 = p1.finally(() => "bar");
let p7 = p1.finally(() => Promise.resolve("bar"));
let p8 = p1.finally(() => Error("qux"));
setTimeout(console.log, 0, p2); // Promise <resolved>: foo
setTimeout(console.log, 0, p3); // Promise <resolved>: foo
setTimeout(console.log, 0, p4); // Promise <resolved>: foo
setTimeout(console.log, 0, p5); // Promise <resolved>: foo
setTimeout(console.log, 0, p6); // Promise <resolved>: foo
setTimeout(console.log, 0, p7); // Promise <resolved>: foo
setTimeout(console.log, 0, p8); // Promise <resolved>: foo
let p9 = p1.finally(() => new Promise(() => {}));
let p10 = p1.finally(() => Promise.reject());
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p9); // Promise <pending>
setTimeout(console.log, 0, p10); // Promise <rejected>: undefined
let p11 = p1.finally(() => {
throw "baz";
});
// Uncaught (in promise) baz
setTimeout(console.log, 0, p11); // Promise <rejected>: baz
Promise 代码的执行顺序
Promise 代码的执行顺序取决于 JavaScript 中事件循环的机制:
- 在 JavaScript 中,首先会执行所有同步代码,也就是栈中立即执行的代码。只有同步代码执行完,事件循环才会开始处理异步代码。
- Promise 的回调(.then() 或 .catch())是异步执行的,只有当 Promise 状态改变(从 pending 到 resolved 或 rejected)之后,回调才会被加入到微任务队列中。微任务(Promise 的回调、MutationObserver 等) 在当前执行栈清空后、宏任务开始前执行。
- 宏任务(如 setTimeout、setInterval、I/O 操作等) 在微任务队列执行后才执行。
示例:
javascript
console.log('Start'); // 同步代码,立即执行
const promise = new Promise((resolve, reject) => {
console.log('Promise started'); // 同步代码,立即执行
resolve('Promise resolved'); // 设置 Promise 完成
});
promise.then((result) => {
console.log(result); // 微任务,Promise 回调
});
setTimeout(() => {
console.log('setTimeout'); // 宏任务
}, 0);
console.log('End'); // 同步代码,立即执行
输出:
javascript
Start
Promise started
End
Promise resolved
setTimeout
Promise 的错误处理
Note the following when an error is thrown in a Promise:
- Promise 中抛出错误会导致Promise的状态转为拒绝;
- Promise 中抛出的错误无法被同步的 try...catch 捕获;
- Promise 中抛出错误不会阻止后续代码执行。
javascript
try {
let promise = new Promise((resolve, reject) => {
throw new Error('error');
});
setTimeout(console.log, 0, promise);
} catch (e) {
console.log(e);
}
// Promise {<rejected>: Error: error
// Uncaught (in promise) Error: error
Promise 合成
Promise 类提供两个将多个 Promise 实例组合成一个 Promise 的静态方法:Promise.all() 和 Promise.race()。而合成后的 Promise 的行为取决于内部 Promise 的行为。
Promise.all()
简介:Promise.all() 方法接收一个可迭代对象(如数组),其中的元素是 Promise 对象或值,并返回一个新的 Promise。
签名:Promise.all(iterable)
参数:
- iterable:一个可迭代对象(如数组或 Set),其中的元素是 Promise 对象或其他值。非 Promise 的值会被 Promise.resolve() 包装成 Promise。
返回值:返回一个新的 Promise。当所有传入的 Promise 都成功(fulfilled)时,返回的 Promise 将被解决(fulfilled),其值是所有 Promise 解析值组成的数组。如果任意一个传入的 Promise 被拒绝(rejected),返回的 Promise 立即被拒绝,其原因就是第一个被拒绝的 Promise 的拒绝原因。
示例:
javascript
Promise.all([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]).then((value) => {
console.log(value);
}); // [1, 2, 3]
Promise.all([
Promise.resolve(1),
Promise.reject(2),
Promise.reject(3)
]).then((value) => {
console.log(value);
}).catch((error) => {
console.log(error);
}); // 2
Promise.race()
简介:Promise.race() 方法接受一个可迭代对象(如数组或 Set),其中的元素是 Promise 对象或值,并返回一个新的 Promise。这个新的 Promise 将由输入的第一个完成(无论是解决 fulfilled 还是拒绝 rejected)的 Promise 决定其状态和返回值。
签名:Promise.race(iterable)
参数:
- iterable:一个可迭代对象(如数组或 Set),其中的元素是 Promise 对象或其他值。非 Promise 的值会被 Promise.resolve() 包装成 Promise。
返回值:一旦 iterable 中的某个 Promise 最先完成(无论成功或失败),返回的 Promise 会采用该完成的状态和返回值。如果传入的 iterable 是空的,返回的 Promise 处于 pending 状态。如果传入的 iterable 中包含非 Promise 值,则这些值会被立即解析。
示例:
javascript
Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
reject(2)
}, 500)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
reject(3)
}, 300)
})
]).then(value => {
console.log(value); // Uncaught (in promise) 3
});
async/await
async/await 是在 ES8 规范中引入的新特性,旨在通过语法和行为上的改进,使 JavaScript 能够以更接近同步代码的方式执行异步操作,让我们更方便地编写异步代码,避免依赖回调函数和过度嵌套的 .then()
调用。
async
async 关键字用于声明异步函数,可以用在函数声明、函数表达式、箭头函数和方法上。
使用 async
关键字让函数成为异步函数,但函数内部仍按同步方式执行。
异步函数返回的值会被隐式地包装在一个 Promise 里,但这与 Promise.resolve 又有所不同,对于 Promise 类型的返回值,Promise.resolve 会返回相同的引用,异步函数会返回一个新的 Promise。
javascript
const p = new Promise((res, rej) => {
res(1);
});
async function asyncReturn() {
return p;
}
function basicReturn() {
return Promise.resolve(p);
}
console.log(p === basicReturn()); // true
console.log(p === asyncReturn()); // false
与在 Promise 处理程序中一样,在异步函数中抛出错误会返回拒绝的 Promise。不过,拒绝 Promise 的错误不会被异步函数捕获:
javascript
async function foo() {
console.log(1);
Promise.reject(3);
}
// Attach a rejected handler to the returned promise
foo().catch(console.log);
console.log(2);
// 1
// 2
// Uncaught(inpromise): 3
await
简介
await 操作符被用于等待(或者说是解包,unwrap)一个 Promise 并获取它的解决值,同时暂停异步函数执行。等该 Promise 落定(settled)后,异步函数恢复执行,await 表达式的值变成这个 Promise 的落定值。
语法
await expression
expression 可以是一个 Promise、一个 Thenable 对象或者任意非 Thenable 值。expression 的解包方式与 Promise.resolve 相同,总会转换为一个原生 Promise 然后等待它。
- 若 expression 是 Promise,则它直接被使用;
- 若 expression 是 Thenable,则 Thenable 会转换为 Promise;
- 若 expression 是 non-Thenable,则 non-Thenable 会被转换为 fulfilled promise。
返回值
promise 或者 thenable 对象的解决值,或者如果表达式不是 thenable,那么返回表达式本身的值。
异常
如果 promise 或者 thenable 对象被拒绝,那么会抛出拒绝原因。
javascript
async function f() {
// 使用 try...catch
// try {
// const response = await Promise.reject(30);
// } catch (e) {
// console.error(e); // 30
// }
// 链接 .catch 处理程序
const response = await Promise.reject(30).catch((e) => {
console.error(e); // 30
return "default desponse";
});
}
f();
示例
developer.mozilla.org/en-US/docs/...
✨Control flow effect of await
await 暂停当前函数执行,依赖于 await 的代码会被推入微任务队列(microtask queue),但不会阻塞主线程,主线程继续执行其他任务。即使等待的值是已解决的 Promise 或者非 Promise,也会是这个行为。
javascript
async function foo(name) {
console.log(name, "start");
await console.log(name, "middle");
console.log(name, "end");
}
foo("First");
foo("Second");
// First start
// First middle
// Second start
// Second middle
// First end
// Second end
这相当于:
javascript
function foo(name) {
return new Promise((resolve) => {
console.log(name, "start");
resolve(console.log(name, "middle"));
}).then(() => {
console.log(name, "end");
});
}
注意点
(1)await 只能被用在异步函数或者模块的顶部。
javascript
// 假设这是一个模块文件,例如 topLevelAwait.mjs
// 你可以直接使用 await 在模块顶层
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();
console.log(data);
模块的顶部(the top level of module)不是指视觉位置上的顶部,而是说模块的顶级作用域,不包含在任何函数、类、对象内部。
(2)异步函数的特质不会扩展到嵌套函数。因此,await 不能出现在嵌套的同步函数内。
javascript
// 不允许:await出现在了同步函数中
async function foo() {
const syncFn = () => {
return await Promise.resolve('foo');
};
console.log(syncFn());
}