Skip to content

js学习记录-现代特性

约 4107 字大约 14 分钟

2025-10-23

异步的前情提要

首先需要指出的是 js 是单线程的,毕竟他被设计时是作为浏览器脚本来用的,所以尽量简单;但解析 js 的引擎是多线程的例如 V8。

js 的执行上下文。参考

执行上下文

这是 MDN 里的专有名词可以搜到,就是执行函数中的栈,包含一些当前执行函数的:变量、函数、对象,函数退出时需要返回的地址。js 里有许多回调函数应用,回调执行到暂停点会返回调用栈等到暂停点被调用时再次进入回调函数,所以上下文还保存着回调函数的地址。

执行上下文差不多对应这样一种抽象:调用函数时,为执行函数所创建的栈空间。

每一个函数或者 setTimeout()setInterval() 创建的执行的 js 代码段被称为 任务

js 引擎作为 web 浏览器的脚本都是执行在 HTML 中 <script> 的,网页卡顿暂停会给用户不好的体验,所以 js 脚本一般是非阻塞的,但同时 js 所有代码都运行在唯一一个主线程上,除非使用为 js 提供运行环境提供的特殊 API(Node.js 的 Worker Threads;浏览器的 WebWorker)才能创建新的线程。

js 的事件循环

js 提供的事件循环机制其实是 js 引擎维护了多个任务微任务组成的任务队列。为了方便叙述,下面用一个任务队列和一个微任务队列说明。

  • 任务(有的 js 教程里称为宏任务,估计是和微任务对称):暂时理解为函数或者 evalsetTimeout()setInterval() 执行的代码段

  • 微任务:可以暂时认为为 Promise 的使用 then 时的回调函数执行的任务,和有 async/await 暂停执行又恢复执行的任务

一个例子:

setTimeout(() => console.log("1:这是任务,会在下一个事件循环执行"), 0);

new Promise((resolve) => {
    console.log("2:这是个同步任务");
    resolve("3:这是个微任务,在同步任务执行完后执行")
}).then(console.log);

console.log("4:这是个同步任务,在本轮事件循环之前执行")

// 输出
// 2:这是个同步任务
// 4:这是个同步任务,在本轮事件循环之前执行
// 3:这是个微任务,在同步任务执行完后执行
// 1:这是任务,会在下一个事件循环执行

事件循环机制

尽管 setTimeout 设置的时间为 0,属于任务也会在下轮事件循环开始执行。这几个 console.log 函数都被抽象为一个个任务,其中在 then 中作为回调函数调用的被作为微任务。

正常在代码块中由上至下执行的是任务(或者说同步任务),上一个任务不执行完下一个任务不会执行。

js 依此来维护一个任务队列,也就是函数中的同步代码,只有同步任务执行完——同步任务队列为空,此时才开始从执行微任务。微任务执行完——微任务队列为空才会开始执行下一个事件循环。

通常一个 .js 文件或者被 HTML 包含的多个 <script> 中的 js 文件都在一个事件循环中。

进入下一个事件循环的方式:

  1. setTimeout() / setInterval():目前在 Node.js 环境下唯一手动进入下一个事件循环的方法
  2. I/O 事件的回调:fs.readFile
  3. UI 中点击事件的回调发生、UI 渲染

还有两个分别是浏览器空闲时执行的回调和手动构造任务的方法。

Promise

类似于 C# 的 async await,其返回值 Task<result> 和 C++ 的多线程工具 future/promise(可惜没做成语言特性),C++20 的 co_await 更像,不过 js 是单线程的异步操作不一定需要使用线程来实现。

异步函数不直接返回所需对象,而是返回一个承诺对象。

这个承诺对象自然而然的分三种情况:

  1. 待定
  2. 完成
  3. 失败

MDN 这么介绍的:

待定(pending):初始状态,既没有被兑现,也没有被拒绝。 已兑现(fulfilled):意味着操作成功完成。 已拒绝(rejected):意味着操作失败

这个承诺对象外部是改不了的。

怎么玩?

使用 new 调用构造函数生成 Promise 对象:

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */) {
    resolve(value);
  } else {
    reject(error);
  }
});

Promise 构造函数接受一个函数,其函数传入两个参数,这两个参数是固定式的叫 resolvereject,由 js 引擎控制。这里的 resolvereject 叫什么都行,在构造 Promise 时 js 会自动传入两个函数作为参数供你使用。

其中你写了一堆逻辑和操作之后根据条件判断,使用 resolve 把该 promise 的状态置为完成,或者使用 reject 把状态置为失败。其中 resolve/reject 的参数就是 Promise 传递出去的结果,相当于返回值的概念。

同时 resolve(new Promise...) 兑现的内容也可以是一个新的 Promise。

注意

Promise 的状态在第一次调用 resolve/reject 后就已确定,后续再调用 resolve/reject 都不会启效果。

返回值怎么拿到呢?

使用 then() 函数。

then 函数有两个重载:

  • then(onFulfilled)
  • then(onFulfilled, onRejected)

重载 1 的参数允许传入一个回调函数,这个回调函数的参数就是在 Promise 中被 resolve(兑现)的值。也就是说使用 then 时 js 会调用你传入的回调函数,并且回调函数传入的参数正是你在 Promise 中 resolve 的值,resolve 不仅将状态置为完成,并且表示你要将其参数兑现。

重载 2 类似 1,只不过允许传入失败时回调函数处理。

const promise = new Promise(
    (resolve, reject) => {
        resolve(10);
    }
);

promise.then((value) => console.log(value));
// 输出
// 10

同时 then 函数也会返回一个新 Promise 对象,其中兑现的内容或者拒绝原因靠传入的回调函数的 return 语句决定,而不是靠 resolve/reject 函数了。

// 箭头函数单个表达式就是返回值
Promise.resolve(10).then(value => value + 10).then(console.log);

// 用 return 语句表示要兑现的内容
Promise.resolve('第二个Promise')
    .then(message => {
        console.log(message);
        return message + 10;
    }).then(console.log);

// 没有 return 语句,调用第二个 then 时传入回调函数 console.log 的参数为 undefined
Promise.resolve('第三个Promise')
    .then(message => {
        console.log(message);
    }).then(console.log);

// 输出
// 第二个Promise
// 第三个Promise
// 20
// 第二个Promise10
// undefined

return 的语句的表达式不是 Promise 对象,会隐式的包装成 Promise 对象以便链式调用 then

如果主动返回一个 Promise 对象,那么被返回/兑现的内容为这个新 Promise 需要兑现(在内部 resolve)的内容。

Promise.resolve('已解决的对象').then((string) => {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log("在下一个事件循环")
            resolve('在下一个事件循环被兑现')
        }, 1)
    })
}).then((string) => {
    console.log("等上一个任务执行?")
    console.log(string)
})
console.log('本轮事件循环结束')

// 输出
// 本轮事件循环结束
// 在下一个事件循环
// 等上一个任务执行?
// 在下一个事件循环被兑现

这里传入第二个 then 的回调函数的参数(被兑现的值)就是返回的 Promise 里被 resolve 的值。每经过一个 then 函数的调用 Promise 的状态一定是非待定的,也就是要么完成,要么失败。只要是待定状态(未完成状态)then 的回调都不会调用,传入给回调函数的参数不可能是 Promise 对象(thenable 对象)。

thenable 对象

thenable 对象 可以理解为实现了 Thenable 接口即 then 函数的对象,其行为也要和 Promise 类似。

错误处理

  • then(reject)
  • Promise.try
  • catch
  • finally

Promise 的错误可以冒泡,在 then 调用链里传播。

有些人不建议使用 then 函数的第二个重载,即 then(resolveFun, rejectFun),要处理错误直接使用 catch(rejectFun)

可以认为 catch(rejectFun) === then(undefined, rejectFun)

Promise 调用链会吞错误,不 catch 就不会把错误传播出去。

同样的 catch 方法为了链式调用,返回的也是 Promise 对象(thenable 对象)。

finally 的回调函数不接受任何参数——finally 调用应该和 Promise 是否成功无关。

几个典型场景

1. 需要等待多个 Promise 的结果:Promise.all

这是一个典型场景,假如异步读取三个不同文件,后续工作需要所有准备工作完毕才能开始进行,则使用 all 将多个 promise 合成为一个 promise。这样所有 Promise 的状态都完成了这个新合成的 Promise 的状态才会转变为已完成,其中只要有一个 Promise 的状态为失败则合成的整个 Promise 状态都是失败的。

const p1 = new Promise(resolve => {
    setTimeout(() => {
        console.log("task A resolve")
        console.info(new Date().toLocaleTimeString())
        resolve('taskA')
    }, 1)
});
const p2 = new Promise(resolve => {
    setTimeout(() => {
        console.log('task B resolve')
        console.info(new Date().toLocaleTimeString())
        resolve('task B')
    }, 1000)
});
const p3 = new Promise(resolve => {
    setTimeout(() => {
        console.log('task C resolve')
        console.info(new Date().toLocaleTimeString())
        resolve('task C')
    }, 2000);
});

console.log('等待任务返回');
const all_promies = Promise.all([p1, p2, p3]);

all_promies.then(values => console.log(values)).catch(err => console.warn(err));

// 输出
// 等待任务返回
// task A resolve
// 19:00:46
// task B resolve
// 19:00:47
// task C resolve
// 19:00:48
// [ 'taskA', 'task B', 'task C' ]

此时这个合成的 Promise 对象传递给回调函数的参数是一个数组,可以使用解包语法。

2. 对时间有要求的竞争任务:Promise.race

合成 Promise 的状态由第一个返回/兑现的 Promise 决定。例子直接偷 MDN

function sleep(time, value, state) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (state === "兑现") {
                return resolve(value);
            } else {
                return reject(new Error(value));
            }
        }, time);
    });
}

const p1 = sleep(500, "", "兑现");
const p2 = sleep(100, "", "兑现");

Promise.race([p1, p2]).then((value) => {
    console.log(value); // "二"
    // 两个都会兑现,但 p2 更快
});

const p3 = sleep(100, "", "兑现");
const p4 = sleep(500, "", "拒绝");

Promise.race([p3, p4]).then(
    (value) => {
        console.log(value); // "三"
        // p3 更快,所以它兑现
    },
    (error) => {
        // 不会被调用
    },
);

const p5 = sleep(500, "", "兑现");
const p6 = sleep(100, "", "拒绝");

Promise.race([p5, p6]).then(
    (value) => {
        // 不会被调用
    },
    (error) => {
        console.error(error.message); // "六"
        // p6 更快,所以它拒绝
    },
);

最快返回的 Promise 的状态决定了合成的 Promise 的状态。

3. 对状态不在特别在意,多个任务只要有一个成功的就好:Promise.any

只有所有任务都被拒绝了才会返回 AggregateError,其中 error 属性表明被拒绝原因的数组。

const pErr = new Promise((resolve, reject) => {
    reject("总是失败");
});

const pSlow = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, "最终完成");
});

const pFast = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, "很快完成");
});

Promise.any([pErr, pSlow, pFast]).then((value) => {
    console.log(value);
    // pFast 第一个兑现
});
// 打印:
// 很快完成

剩余几个静态函数

方法说明
Promise.resolve(value)直接返回一个已完成/兑现的 Promise 对象,兑现内容为传入的参数 value
Promise.reject(error)直接返回一个失败的 Promise 对象,拒绝理由为传入的参数 error
Promise.allSettled(iterable)传入的 iterable 对象中的所有 Promise 都确定时(要么成功要么失败)返回一个数组对象里面保存了所有 Promise 的状态和值

从 MDN 偷的例子:

Promise.allSettled([
    Promise.resolve(33),
    new Promise((resolve) => setTimeout(() => resolve(66), 0)),
    99,
    Promise.reject(new Error("一个错误")),
]).then((values) => console.log(values));

// [
//   { status: 'fulfilled', value: 33 },
//   { status: 'fulfilled', value: 66 },
//   { status: 'fulfilled', value: 99 },
//   { status: 'rejected', reason: Error: 一个错误 }
// ]

这几个函数的参数都可以传 Promise(原封不动返回)、thenable 对象(调用 then 时会调用 thenable 对象实现的 then),也可以不传参直接返回对应状态无兑现值/拒绝理由的 Promise;一般都会传原始值。

async / await

很多语言都有这两个关键字,js 里这两个关键字就是 Promise / then 常见用法的语法糖

和其他语言的关键字一样,async 放在需要声明的函数前,看例子就知道怎么写了。同时 await 只能在 async 函数里使用,async 具有传染性,基本上每个设计这两个关键字的语言都有这些特性。

通过使用语法糖的形式将返回值包装成 Promise 对象;await 语句返回兑现的值,而不是 Promise。

let count = 0;

// 模拟异步任务1:1秒后增加 count 并打印
function task1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            for (let i = 0; i < 1000; i += 1) {
                if (i % 250 == 0) console.log(`[task1] 正在处理 ${i}`);
            }
            count += 10;
            console.log(`[task1] 完成,count += 10,当前 count = ${count}`);
            resolve('task1-result');
        }, 1000);
    });
}

// 模拟异步任务2:1秒后增加 count 并打印
function task2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            for (let i = 0; i < 1000; i += 1) {
                if (i % 250 == 0) console.log(`[task2] 正在处理 ${i}`);
            }
            count += 20;
            console.log(`[task2] 完成,count += 20,当前 count = ${count}`);
            resolve('task2-result');
        }, 1000);
    });
}

// async 函数,清晰地表达异步流程
async function runTasks() {
    console.log('=== 开始执行异步任务 ===');
    console.log(`初始 count = ${count}`);

    console.log('--- 准备执行 task1 ---');
    const result1 = await task1();  // 等待 task1 的 Promise 完成
    console.log(`task1 返回:${result1}`);

    console.log('--- 准备执行 task2 ---');
    const result2 = await task2();  // 等待 task2 的 Promise 完成
    console.log(`task2 返回:${result2}`);

    console.log(`=== 所有任务完成,最终 count = ${count} ===`);
    return count;
}

// 执行并观察
const finalResultPromise = runTasks();

// 同步代码,此时任务还没开始或进行中
console.log(`[同步代码] 当前 count = ${count}`);  // 输出 0
console.log('[同步代码] 等待异步任务完成...');

// 通过 .then 查看最终结果(等同于 await)
finalResultPromise.then((finalCount) => {
    console.log(`[Promise.then] 最终返回的 count 是:${finalCount}`);
});

// 输出
// === 开始执行异步任务 ===
// 初始 count = 0
// --- 准备执行 task1 ---
// [同步代码] 当前 count = 0
// [同步代码] 等待异步任务完成...
// [task1] 正在处理 0
// [task1] 正在处理 250
// [task1] 正在处理 500
// [task1] 正在处理 750
// [task1] 完成,count += 10,当前 count = 10
// task1 返回:task1-result
// --- 准备执行 task2 ---
// [task2] 正在处理 0
// [task2] 正在处理 250
// [task2] 正在处理 500
// [task2] 正在处理 750
// [task2] 完成,count += 20,当前 count = 30
// task2 返回:task2-result
// === 所有任务完成,最终 count = 30 ===
// [Promise.then] 最终返回的 count 是:30

这一段差不多就是上面 async/await 的解糖形式,输出一样的:

let count = 0;
function task1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            for (let i = 0; i < 1000; i += 1) {
                if (i % 250 == 0) console.log(`[task1] 正在处理 ${i}`);
            }
            count += 10;
            console.log(`[task1] 完成,count += 10,当前 count = ${count}`);
            resolve('task1-result');
        }, 1000);
    });
}

// 模拟异步任务2:1秒后增加 count 并打印
function task2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            for (let i = 0; i < 1000; i += 1) {
                if (i % 250 == 0) console.log(`[task2] 正在处理 ${i}`);
            }
            count += 20;
            console.log(`[task2] 完成,count += 20,当前 count = ${count}`);
            resolve('task2-result');
        }, 1000);
    });
}

console.log('=== 开始执行异步任务 ===');
console.log(`初始 count = ${count}`);
task1().then((result1) => {
    console.log(`task1 返回:${result1}`);

    console.log('--- 准备执行 task2 ---');
    return task2();
}).then((result2) => {
    console.log(`task2 返回:${result2}`);
    console.log(`=== 所有任务完成,最终 count = ${count} ===`);
    return count;
}).then((finalCount) => {
    console.log(`[Promise.then] 最终返回的 count 是:${finalCount}`);
});
console.log(`[同步代码] 当前 count = ${count}`);  // 输出 0
console.log('[同步代码] 等待异步任务完成...');

对异步函数使用 await 的实际值:

let x = await dosth()

不使用 await 返回的是 Promise 对象,后续用 then 函数获取真正的值或者再次使用 await 等待。

// 任务先启动
let x = dosth()
// 干点别的
x.then(() => {
    // 开始处理结果
})
// 或者真正需要 dosth() 的时候在这里等待 x 的结果
let result = await x
// 下面处理结果

async/await 与 Promise 的关系

整个 async 函数被 await 划分成几个部分。await 表达式后跟的函数调用类似于 Promise 中 setTimeout 的部分。当然这个例子不明显,setTimeout 只是延迟执行不是真正开线程并行。

await 之后的部分可以认为是传入 then 的回调函数的代码,在 then 中返回第二个 Promise(task2),await task2 的后续部分同样可以被认为是 then 的回调函数的代码。

这样一个 async 函数作为 Promise 的语法糖实际是 Promise 和 then 拆分成多个部分。MDN 也有一个例子

async 函数中能使用 try/catch/finally