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 可以看作是对未知状态的一个约定,对不存在结果的一个替代。

关键里程碑2010Promises/A规范发布(CommonJS项目)2012Promises/A+规范分叉<br/>(解决Promises/A不足,明确细节)2015ES6 正式支持 Promise类型<br/>(实现Promises/A+ 规范)Promise 规范发展时间轴

Promise 基础

new Promise 创建期约

创建新期约时必须传入执行器(executor)函数作为参数,哪怕是空函数,如果不提供执行器函数,就会抛出 SyntaxError。

let p = new Promise((resolve, reject) => {});
setTimeout(console.log, 0, p); // Promise <pending>

Promise 的三个状态

resolve()
resolve 或 reject()
Pending
Fulfilled
Rejected
  • 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 主要有两大用途:

  1. 表示一个异步操作。前面已经说过了异步操作的三种状态,对于一些用例来说,状态表示已经足够。比如,请求成功,请求状态转为ResolvedFulfilled;请求失败,Promise状态转为Rejected
  2. 生成一个值。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:可以是任意值。

返回值

  1. 如果 value 是 Promise,那么直接返回它;
  2. 如果 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
});
  1. 如果 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 中事件循环的机制:

  1. 在 JavaScript 中,首先会执行所有同步代码,也就是栈中立即执行的代码。只有同步代码执行完,事件循环才会开始处理异步代码。
  2. Promise 的回调(.then() 或 .catch())是异步执行的,只有当 Promise 状态改变(从 pending 到 resolved 或 rejected)之后,回调才会被加入到微任务队列中。微任务(Promise 的回调、MutationObserver 等) 在当前执行栈清空后、宏任务开始前执行。
  3. 宏任务(如 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 中抛出错误时注意如下几点:

  1. Promise 的状态转为拒绝;
  2. 错误无法被同步的 try...catch 捕获;
  3. 错误不会阻止后续代码执行。
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());
}
posted @   dawnkylin  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示