JavaScript 异步和期约
概述
- JavaScript 是单线程的,因此异步机制对于避免阻塞主线程(如 UI 呈现)和耗时的操作(如网络请求、文件读取和计时器)非常重要。
- 回调函数是早期的异步实现,但容易出现“回调地狱”问题。
- ES6 添加了一个正式的 Promise 引用类型,允许您优雅地定义和组织异步逻辑。
- ES2017(或 ES8) 引入了基于承诺的语法糖 async/await,使异步代码更接近同步风格。
异步编程
同步和异步
同步:任务按照代码的顺序逐一执行(执行顺序与代码书写顺序一致),每个任务必须等上一个任务完成后才能开始(阻塞)。
更准确地讲,同步行为是指令按顺序严格执行,执行后变量的值也能立即从寄存器或内存获取,同步代码的执行状态可以说是一目了然,可以被清晰地分析。
异步:任务不需要等待其他任务完成即可开始,耗时操作可以在后台处理,主线程继续执行其他任务。
异步行为与系统中断类似,中断是计算机系统中一种机制,允许外部事件或硬件设备打断当前正在运行的程序,并执行一段特定的处理代码(中断服务程序,ISR),当前进程会被暂时挂起,中断处理完成后恢复执行。
异步行为无法知晓代码的执行状态,就像一个黑盒。
对于需要暂停或等待的任务,异步比同步更加高效,因此同步操作适合短时间内可以完成的简单任务,异步操作则适合如网络请求、文件读写等耗时的任务。
旧异步编程模式:回调
在早期的 JavaScript 中,只支持定义回调函数来表明异步操作完成。回调是指通过将函数作为参数传递,在任务完成后调用。下面逐步演示使用回调的异步操作。
(1)异步操作:
function double(value) { setTimeout(console.log, 1000, value); } double(3);
(2)传递回调函数作为参数并获得异步操作的返回值:
function double(value, callback) { setTimeout(() => callback(value), 1000); } double(3, (x) => console.log(`Success: ${x}`);
(3)传递失败回调:
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);
(4)回调地狱
然而使用这种方式获取的异步返回值只在回调函数内可用,回调结束后也就销毁了。而且,如果异步返回值又依赖另一个异步返回值,就会形成嵌套的回调,即回调地狱:
function double(value, callback) { setTimeout(() => callback(value*2), 1000); } double(3, (x)=> double(x, (y) => double(y, (z) => console.log(`Success: ${z}`) ) );
Promise
Promise 可以看作是对未知状态的一个约定,对不存在结果的一个替代。
Promise 基础
new Promise 创建期约
创建新期约时必须传入执行器(executor)函数作为参数,哪怕是空函数,如果不提供执行器函数,就会抛出 SyntaxError。
let p = new Promise((resolve, reject) => {}); setTimeout(console.log, 0, p); // Promise <pending>
Promise 的三个状态
- Pending(待定):初始状态,既未完成,也未失败。
- Fulfilled 或者 Resolved(成功):Promise 成功完成,返回结果。
- Rejected(拒绝):Promise 被拒绝,返回拒绝原因。
let p = new Promise((resolve, reject) => { resolve(Promise.reject('foo')); }); setTimeout(console.log, 0, p); // Promise {<rejected>: 'foo'}
在待定状态下,Promise 可以落定(settle)为代表成功的兑现状态,或者代表失败的拒绝状态,无论落定为哪种状态都是不可逆的。并且 Promise 的状态是私有的,不能直接被外部 JS 检测到,于是也不能被修改。
Promise 的两大用途
Promise 主要有两大用途:
- 表示一个异步操作。前面已经说过了异步操作的三种状态,对于一些用例来说,状态表示已经足够。比如,请求成功,请求状态转为
Resolved
或Fulfilled
;请求失败,Promise状态转为Rejected
。 - 生成一个值。Promise状态改变后,程序会获取到一个值。Promise 成功,该值为解决值;Promise 失败,该值为拒绝理由,一般为 Error 对象。
上述两大用途都是通过执行器函数实现的,下节具体介绍。
执行器函数的作用
(1)初始化 Promise 的异步行为
Promise 执行器函数中的代码是同步的、立即执行的。通常用于处理网络请求、文件读取等异步操作。
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() 会把状态切换为拒绝且会抛出错误。
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() 中的哪个被调用,状态转换都不可撤销了,继续修改状态会静默失败。
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。
class Thenable{ then(onFulfilled, onRejected){ ... } }
then()
简介:Promise.prototype.then()
是为 Promise 实例添加处理程序的主要方法。
签名:then(onFulfilled, onRejected)
参数:onFulfilled 是 Promise 成功时调用的回调函数;onRejected 是 Promise 失败时调用的回调函数。如果想只提供 onRejected 参数,那就要在 onResolved 参数的位置上传入 undefined 或 null,避免创建多余的对象。
返回值:then() 返回一个新的 Promise 实例,这使得可以链式调用 then()。这个新 Promise 实例的解决方式与 Promise.resolve() 相同。
示例:
在 Promise 的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由。
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实例。
示例:
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() 中出现冗余代码,只要 Promise 落定(无论是成功还是拒绝),它都会被调用。
签名:finally(onFinally)
参数:
onFinally
函数在 Promise落定后执行,不接受参数。
返回值:finally() 返回一个新 Promise 实例,新 Promise 实例的状态与原 Promise 一致,若 onFinally 抛出错误或者返回被拒绝的 Promise,新 Promise 实例将以该值为原因落定为拒绝态。
示例:
// 原 Promise 已解决 const originalResolved = Promise.resolve("resolved"); const newFromResolved = originalResolved.finally(() => { console.log("onFinally executed"); }); // 立即检查新 Promise 状态 → pending console.log(newFromResolved); // Promise { <pending> } setTimeout(() => { // 异步检查新 Promise 状态 → 跟随原 Promise console.log(newFromResolved); // Promise { <fulfilled>: "resolved" } }, 0);
// onFinally 抛出错误 const original = Promise.resolve("resolved"); const newRejected = original.finally(() => { throw "error from onFinally"; // return Promise.reject("error from onFinally"); }); // 立即检查新 Promise 状态 → pending console.log(newRejected); // Promise { <pending> } setTimeout(() => { // 异步检查新 Promise 状态 → 拒绝(因 onFinally 错误) console.log(newRejected); // Promise { <rejected>: "error from onFinally" } }, 0);
Promise 的静态方法
Promise.resolve()
简介:Promise.resolve()
通过一定规则将给定参数“resolve”为一个 Promise。
签名:Promise.resolve(value)
参数:
value
:可以是任意值。
返回值:
- 如果
value
是 Promise,那么直接返回它; - 如果
value
是 Thenable(拥有then()
),Promise.resolve()
会为Thenable.then()
提供两个回调函数(新 Promise 的resolve()
、reject()
)作为参数;
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 });
- 如果
value
是嵌套 Thenable,那么Promise.resolve()
会打平它:
const innerThenable = { then: function(onFulfilled) { onFulfilled(42); } }; const outerThenable = { then: function(onFulfilled) { onFulfilled(innerThenable); // 返回另一个 thenable } }; Promise.resolve(outerThenable) .then(value => console.log(value)); // 输出 42(递归展开)
示例:
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 的拒绝原因。
示例:
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 值,则这些值会被立即解析。
示例:
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 });
Promise 代码的执行顺序
Promise 代码的执行顺序取决于 JavaScript 中事件循环的机制:
- 在 JavaScript 中,首先会执行所有同步代码,也就是栈中立即执行的代码。只有同步代码执行完,事件循环才会开始处理异步代码。
- Promise 的回调(.then() 或 .catch())是异步执行的,只有当 Promise 状态改变(从 pending 到 resolved 或 rejected)之后,回调才会被加入到微任务队列中。微任务(Promise 的回调、MutationObserver 等) 在当前执行栈清空后、宏任务开始前执行。
- 宏任务(如 setTimeout、setInterval、I/O 操作等) 在微任务队列执行后才执行。
示例:
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'); // 同步代码,立即执行
输出:
Start Promise started End Promise resolved setTimeout
Promise 的错误处理
当Promise 中抛出错误时注意如下几点:
- Promise 的状态转为拒绝;
- 错误无法被同步的 try...catch 捕获;
- 错误不会阻止后续代码执行。
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
async/await
async/await 是在 ES8 规范中引入的新特性,旨在通过语法和行为上的改进,使 JavaScript 能够以更接近同步代码的方式执行异步操作,让我们更方便地编写异步代码,避免依赖回调函数和过度嵌套的 .then()
调用。
async
async 关键字用于声明异步函数,可以用在函数声明、函数表达式、箭头函数和方法上。
使用 async
关键字让函数成为异步函数,但函数内部仍按同步方式执行。
异步函数返回的值会被隐式地包装在一个 Promise 里,但这与 Promise.resolve 又有所不同,对于 Promise 类型的返回值,Promise.resolve 会返回相同的引用,异步函数会返回一个新的 Promise。
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 的错误不会被异步函数捕获:
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 对象被拒绝,那么会抛出拒绝原因。
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();
示例
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#examples
Control flow effect of await
await 暂停当前函数执行,依赖于 await 的代码会被推入微任务队列(microtask queue),但不会阻塞主线程,主线程继续执行其他任务。即使等待的值是已解决的 Promise 或者非 Promise,也会是这个行为。
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
这相当于:
function foo(name) { return new Promise((resolve) => { console.log(name, "start"); resolve(console.log(name, "middle")); }).then(() => { console.log(name, "end"); }); }
注意点
(1)await 只能被用在异步函数或者模块的顶部。
// 假设这是一个模块文件,例如 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 不能出现在嵌套的同步函数内。
// 不允许:await出现在了同步函数中 async function foo() { const syncFn = () => { return await Promise.resolve('foo'); }; console.log(syncFn()); }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?