普通视图
谷歌推出音乐生成模型Lyria 3
三星或将就HBM4约700美元的单价进行谈判
澳大利亚1月季调后失业率报4.1%
JavaScript 异步编程完全指南:从入门到精通
JavaScript 异步编程完全指南:从入门到精通
目录
第一部分:基础概念
├── 1. 为什么需要异步
├── 2. 事件循环机制
└── 3. 任务队列
第二部分:回调函数
├── 4. 回调基础
└── 5. 回调地狱
第三部分:Promise
├── 6. Promise 基础
├── 7. Promise 链式调用
├── 8. Promise 错误处理
├── 9. Promise 静态方法
└── 10. 手写 Promise
第四部分:Async/Await
├── 11. 基本语法
├── 12. 错误处理
└── 13. 常见模式
第五部分:高级异步模式
├── 14. Generator 与异步迭代
├── 15. 并发控制
├── 16. 发布/订阅与事件驱动
└── 17. RxJS 响应式编程简介
第六部分:实战与最佳实践
├── 18. 真实项目场景
├── 19. 性能优化
└── 20. 常见陷阱与调试
第一部分:基础概念
1. 为什么需要异步
1.1 JavaScript 是单线程语言
// JavaScript 只有一个主线程执行代码
// 如果所有操作都是同步的,耗时操作会阻塞后续代码
console.log("开始");
// 假设这是一个同步的网络请求(伪代码),需要3秒
// const data = syncFetch("https://api.example.com/data"); // 阻塞3秒!
console.log("结束"); // 必须等上面完成才能执行
1.2 同步 vs 异步的直观对比
// ============ 同步模型(阻塞)============
// 想象你在餐厅:点菜 → 等厨师做完 → 再点下一道
function syncExample() {
const start = Date.now();
// 模拟同步阻塞(千万别在实际项目中这样做!)
function sleep(ms) {
const end = Date.now() + ms;
while (Date.now() < end) {} // 忙等待,阻塞线程
}
console.log("任务1: 开始");
sleep(2000); // 阻塞2秒
console.log("任务1: 完成");
console.log("任务2: 开始");
sleep(1000); // 阻塞1秒
console.log("任务2: 完成");
console.log(`总耗时: ${Date.now() - start}ms`); // ≈ 3000ms
}
// ============ 异步模型(非阻塞)============
// 想象你在餐厅:点完所有菜 → 哪道先做好就先上
function asyncExample() {
const start = Date.now();
console.log("任务1: 开始");
setTimeout(() => {
console.log(`任务1: 完成 (${Date.now() - start}ms)`);
}, 2000);
console.log("任务2: 开始");
setTimeout(() => {
console.log(`任务2: 完成 (${Date.now() - start}ms)`);
}, 1000);
console.log("两个任务都已发起");
// 输出顺序:
// 任务1: 开始
// 任务2: 开始
// 两个任务都已发起
// 任务2: 完成 (≈1000ms) ← 先完成的先执行
// 任务1: 完成 (≈2000ms)
// 总耗时 ≈ 2000ms(而非3000ms)
}
1.3 常见的异步操作
// 1. 定时器
setTimeout(() => console.log("延迟执行"), 1000);
setInterval(() => console.log("重复执行"), 1000);
// 2. 网络请求
fetch("https://api.github.com/users/octocat")
.then(res => res.json())
.then(data => console.log(data));
// 3. DOM 事件
document.addEventListener("click", (e) => {
console.log("用户点击了", e.target);
});
// 4. 文件读写(Node.js)
const fs = require("fs");
fs.readFile("./data.txt", "utf8", (err, data) => {
console.log(data);
});
// 5. 数据库操作
// db.query("SELECT * FROM users", (err, rows) => { ... });
// 6. Web Workers(浏览器多线程)
// const worker = new Worker("worker.js");
// worker.onmessage = (e) => console.log(e.data);
2. 事件循环机制(Event Loop)
这是理解 JS 异步的核心,必须彻底掌握!
2.1 执行模型全景图
┌─────────────────────────────────────────────────────┐
│ 调用栈 (Call Stack) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 当前正在执行的函数 │ │
│ └─────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────┘
│
▼ 当调用栈为空时
┌─────────────────────────────────────────────────────┐
│ 事件循环 (Event Loop) │
│ 不断检查:调用栈空了吗?队列里有任务吗? │
└───────┬──────────────────────────────┬──────────────┘
│ │
▼ 优先 ▼ 其次
┌───────────────────┐ ┌─────────────────────────┐
│ 微任务队列 │ │ 宏任务队列 │
│ (Microtask Queue) │ │ (Macrotask Queue) │
│ │ │ │
│ • Promise.then │ │ • setTimeout/setInterval │
│ • MutationObserver │ │ • I/O 回调 │
│ • queueMicrotask │ │ • UI 渲染 │
│ • process.nextTick │ │ • setImmediate (Node) │
│ (Node.js) │ │ • requestAnimationFrame │
└───────────────────┘ └─────────────────────────┘
2.2 事件循环执行顺序
console.log("1. 同步代码 - script start");
setTimeout(() => {
console.log("6. 宏任务 - setTimeout");
}, 0);
Promise.resolve()
.then(() => {
console.log("3. 微任务 - Promise 1");
})
.then(() => {
console.log("5. 微任务 - Promise 2");
});
queueMicrotask(() => {
console.log("4. 微任务 - queueMicrotask");
});
console.log("2. 同步代码 - script end");
// 输出顺序(带编号):
// 1. 同步代码 - script start
// 2. 同步代码 - script end
// 3. 微任务 - Promise 1
// 4. 微任务 - queueMicrotask
// 5. 微任务 - Promise 2
// 6. 宏任务 - setTimeout
2.3 事件循环的详细步骤
/*
* 事件循环算法:
*
* 1. 执行全局同步代码(这本身就是一个宏任务)
* 2. 调用栈清空后,检查微任务队列
* 3. 依次执行所有微任务(包括执行过程中新产生的微任务)
* 4. 微任务队列清空后,进行一次 UI 渲染(如果需要)
* 5. 取出一个宏任务执行
* 6. 回到步骤 2
*
* 关键:每执行完一个宏任务,就要清空所有微任务
*/
// 经典面试题:详细分析执行顺序
console.log("script start"); // 同步 → 立即执行
async function async1() {
console.log("async1 start"); // 同步 → 立即执行
await async2();
// await 之后的代码相当于 promise.then 的回调
console.log("async1 end"); // 微任务
}
async function async2() {
console.log("async2"); // 同步 → 立即执行
}
setTimeout(function() {
console.log("setTimeout"); // 宏任务
}, 0);
async1();
new Promise(function(resolve) {
console.log("promise1"); // 同步 → 立即执行
resolve();
}).then(function() {
console.log("promise2"); // 微任务
});
console.log("script end"); // 同步 → 立即执行
/*
* 执行分析:
*
* === 第一轮:执行同步代码(全局宏任务)===
* 调用栈:[global]
* 输出:script start
* 输出:async1 start
* 输出:async2 (async2 函数体是同步的)
* → async1 中 await 后面的代码放入微任务队列
* 输出:promise1 (Promise 构造函数是同步的)
* → then 回调放入微任务队列
* 输出:script end
*
* 此时微任务队列:[async1 end, promise2]
* 此时宏任务队列:[setTimeout]
*
* === 第二轮:清空微任务队列 ===
* 输出:async1 end
* 输出:promise2
*
* === 第三轮:取一个宏任务 ===
* 输出:setTimeout
*
* 最终顺序:
* script start → async1 start → async2 → promise1 →
* script end → async1 end → promise2 → setTimeout
*/
2.4 微任务中产生微任务
// 微任务中可以继续产生微任务,会在同一轮全部执行完
console.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve()
.then(() => {
console.log("promise 1");
// 在微任务中产生新的微任务
Promise.resolve().then(() => {
console.log("promise 1-1");
Promise.resolve().then(() => {
console.log("promise 1-1-1");
});
});
})
.then(() => {
console.log("promise 2");
});
console.log("end");
// 输出:start → end → promise 1 → promise 1-1 → promise 2 → promise 1-1-1 → timeout
// 注意:微任务全部执行完才会执行宏任务 setTimeout
// ⚠️ 危险:无限产生微任务会阻塞渲染
// Promise.resolve().then(function loop() {
// Promise.resolve().then(loop); // 永远清不完微任务,页面卡死!
// });
2.5 Node.js 事件循环(与浏览器的区别)
/*
* Node.js 事件循环有 6 个阶段:
*
* ┌───────────────────────────┐
* │ timers │ ← setTimeout, setInterval
* ├───────────────────────────┤
* │ pending callbacks │ ← 系统级回调(如 TCP 错误)
* ├───────────────────────────┤
* │ idle, prepare │ ← 内部使用
* ├───────────────────────────┤
* │ poll │ ← I/O 回调,在此阶段可能阻塞
* ├───────────────────────────┤
* │ check │ ← setImmediate
* ├───────────────────────────┤
* │ close callbacks │ ← socket.on('close')
* └───────────────────────────┘
*
* 每个阶段之间都会执行 process.nextTick 和 Promise 微任务
* process.nextTick 优先级高于 Promise.then
*/
// Node.js 特有的优先级演示
process.nextTick(() => console.log("1. nextTick"));
Promise.resolve().then(() => console.log("2. promise"));
setTimeout(() => console.log("3. setTimeout"), 0);
setImmediate(() => console.log("4. setImmediate"));
// Node.js 输出:
// 1. nextTick (最高优先级微任务)
// 2. promise (普通微任务)
// 3. setTimeout (timers 阶段)
// 4. setImmediate (check 阶段)
3. 调用栈深入理解
// 调用栈是 LIFO(后进先出)结构
function multiply(a, b) {
return a * b; // 4. multiply 执行完毕,弹出栈
}
function square(n) {
return multiply(n, n); // 3. 调用 multiply,入栈
} // 5. square 执行完毕,弹出栈
function printSquare(n) {
const result = square(n); // 2. 调用 square,入栈
console.log(result); // 6. 调用 console.log
}
printSquare(4); // 1. printSquare 入栈
/*
* 调用栈变化过程:
*
* Step 1: [printSquare]
* Step 2: [printSquare, square]
* Step 3: [printSquare, square, multiply]
* Step 4: [printSquare, square] ← multiply 返回
* Step 5: [printSquare] ← square 返回
* Step 6: [printSquare, console.log]
* Step 7: [printSquare] ← console.log 返回
* Step 8: [] ← printSquare 返回
* 调用栈空 → 事件循环检查队列
*/
// 栈溢出演示
function infiniteRecursion() {
return infiniteRecursion(); // 无限递归
}
// infiniteRecursion();
// RangeError: Maximum call stack size exceeded
第二部分:回调函数
4. 回调函数基础
4.1 什么是回调
// 回调函数:作为参数传递给另一个函数,在适当时机被调用的函数
// === 同步回调 ===
const numbers = [1, 2, 3, 4, 5];
// forEach 的回调是同步执行的
numbers.forEach(function(num) {
console.log(num); // 立即执行
});
console.log("forEach 之后"); // 在所有回调之后
// map 也是同步回调
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// === 异步回调 ===
console.log("请求开始");
// setTimeout 的回调是异步执行的
setTimeout(function callback() {
console.log("1秒后执行"); // 至少1秒后
}, 1000);
console.log("请求已发起"); // 先于回调执行
4.2 Node.js 错误优先回调(Error-First Callback)
const fs = require('fs');
// Node.js 约定:回调的第一个参数是 error
fs.readFile('./config.json', 'utf8', function(err, data) {
if (err) {
// 错误处理
if (err.code === 'ENOENT') {
console.error('文件不存在');
} else {
console.error('读取失败:', err.message);
}
return; // 提前返回,不执行后续逻辑
}
// 成功处理
const config = JSON.parse(data);
console.log('配置:', config);
});
// 自己实现错误优先回调风格
function fetchUserData(userId, callback) {
setTimeout(() => {
if (!userId) {
callback(new Error('userId is required'));
return;
}
// 模拟数据库查询
const user = {
id: userId,
name: 'Alice',
email: 'alice@example.com'
};
callback(null, user); // 第一个参数为 null 表示没有错误
}, 1000);
}
// 使用
fetchUserData(1, function(err, user) {
if (err) {
console.error('获取用户失败:', err.message);
return;
}
console.log('用户信息:', user);
});
4.3 实际应用:事件监听器
// DOM 事件回调
const button = document.getElementById('myButton');
// 点击事件
button.addEventListener('click', function(event) {
console.log('按钮被点击', event.target);
});
// 可以添加多个回调
button.addEventListener('click', handleClick);
button.addEventListener('mouseenter', handleHover);
button.addEventListener('mouseleave', handleLeave);
function handleClick(e) {
console.log('处理点击');
}
function handleHover(e) {
e.target.style.backgroundColor = '#eee';
}
function handleLeave(e) {
e.target.style.backgroundColor = '';
}
// 移除事件监听(必须传入同一个函数引用)
button.removeEventListener('click', handleClick);
// ⚠️ 常见错误:匿名函数无法移除
// button.addEventListener('click', () => {}); // 无法移除这个监听器
5. 回调地狱(Callback Hell)
5.1 问题演示
// 需求:获取用户信息 → 获取用户订单 → 获取订单详情 → 获取物流信息
function getUserInfo(userId, callback) {
setTimeout(() => callback(null, { id: userId, name: 'Alice' }), 300);
}
function getOrders(userId, callback) {
setTimeout(() => callback(null, [{ orderId: 101 }, { orderId: 102 }]), 300);
}
function getOrderDetail(orderId, callback) {
setTimeout(() => callback(null, { orderId, product: 'iPhone', trackingId: 'TK001' }), 300);
}
function getShippingInfo(trackingId, callback) {
setTimeout(() => callback(null, { trackingId, status: '运输中', location: '上海' }), 300);
}
// 😱 回调地狱 - 金字塔形代码
getUserInfo(1, function(err, user) {
if (err) {
console.error('获取用户失败', err);
return;
}
console.log('用户:', user.name);
getOrders(user.id, function(err, orders) {
if (err) {
console.error('获取订单失败', err);
return;
}
console.log('订单数:', orders.length);
getOrderDetail(orders[0].orderId, function(err, detail) {
if (err) {
console.error('获取详情失败', err);
return;
}
console.log('商品:', detail.product);
getShippingInfo(detail.trackingId, function(err, shipping) {
if (err) {
console.error('获取物流失败', err);
return;
}
console.log('物流状态:', shipping.status);
console.log('当前位置:', shipping.location);
// 如果还有更多层嵌套...
// 代码会越来越难以维护
});
});
});
});
5.2 回调地狱的问题
/*
* 回调地狱的三大问题:
*
* 1. 可读性差(Readability)
* - 代码向右缩进,形成"金字塔"
* - 逻辑流程难以追踪
*
* 2. 错误处理困难(Error Handling)
* - 每一层都需要单独处理错误
* - 无法统一 catch
* - 容易遗漏错误处理
*
* 3. 控制反转(Inversion of Control)
* - 把回调交给第三方库,你无法控制:
* - 回调是否会被调用
* - 回调会被调用几次
* - 回调是同步还是异步调用
* - 回调的参数是否正确
*/
// 控制反转的危险示例
function riskyThirdPartyLib(callback) {
// 你无法控制第三方库如何调用你的回调
callback(); // 调用了一次
callback(); // 又调用了一次! ← 可能导致重复计费等严重问题
// 或者根本不调用
// 或者同步调用(不在下一个 tick)
}
5.3 改善回调地狱的方法(不用 Promise)
// 方法1:命名函数 + 扁平化
function handleUser(err, user) {
if (err) return console.error('获取用户失败', err);
console.log('用户:', user.name);
getOrders(user.id, handleOrders);
}
function handleOrders(err, orders) {
if (err) return console.error('获取订单失败', err);
console.log('订单数:', orders.length);
getOrderDetail(orders[0].orderId, handleDetail);
}
function handleDetail(err, detail) {
if (err) return console.error('获取详情失败', err);
console.log('商品:', detail.product);
getShippingInfo(detail.trackingId, handleShipping);
}
function handleShipping(err, shipping) {
if (err) return console.error('获取物流失败', err);
console.log('物流状态:', shipping.status);
}
// 启动链条
getUserInfo(1, handleUser);
// 代码变平了,但函数间的关系不够直观
// 方法2:使用工具库(如 async.js)
const async = require('async');
async.waterfall([
function(cb) {
getUserInfo(1, cb);
},
function(user, cb) {
console.log('用户:', user.name);
getOrders(user.id, cb);
},
function(orders, cb) {
console.log('订单数:', orders.length);
getOrderDetail(orders[0].orderId, cb);
},
function(detail, cb) {
console.log('商品:', detail.product);
getShippingInfo(detail.trackingId, cb);
}
], function(err, shipping) {
if (err) {
console.error('流程出错:', err);
return;
}
console.log('物流状态:', shipping.status);
});
第三部分:Promise
6. Promise 基础
6.1 什么是 Promise
/*
* Promise 是一个代表异步操作最终结果的对象
*
* 三种状态:
* ┌─────────┐ resolve(value) ┌───────────┐
* │ pending │ ──────────────────→ │ fulfilled │
* │ (等待中) │ │ (已成功) │
* └─────────┘ └───────────┘
* │
* │ reject(reason) ┌───────────┐
* └────────────────────────→ │ rejected │
* │ (已失败) │
* └───────────┘
*
* 重要特性:
* 1. 状态一旦改变就不可逆(pending → fulfilled 或 pending → rejected)
* 2. 状态改变后,任何时候都可以获取结果
*/
// 创建 Promise
const promise = new Promise(function(resolve, reject) {
// 这个函数叫做 executor(执行器),立即同步执行
console.log("executor 执行了"); // 同步执行!
// 异步操作
setTimeout(() => {
const success = true;
if (success) {
resolve("操作成功的数据"); // 将 promise 变为 fulfilled
} else {
reject(new Error("操作失败的原因")); // 将 promise 变为 rejected
}
}, 1000);
});
console.log("Promise 创建后"); // 在 executor 之后,在异步回调之前
// 消费 Promise
promise.then(
function onFulfilled(value) {
console.log("成功:", value);
},
function onRejected(reason) {
console.log("失败:", reason.message);
}
);
6.2 将回调转换为 Promise
// 改造之前回调风格的函数
function getUserInfo(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!userId) {
reject(new Error('userId is required'));
return;
}
resolve({ id: userId, name: 'Alice' });
}, 300);
});
}
function getOrders(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([{ orderId: 101 }, { orderId: 102 }]);
}, 300);
});
}
function getOrderDetail(orderId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ orderId, product: 'iPhone', trackingId: 'TK001' });
}, 300);
});
}
function getShippingInfo(trackingId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ trackingId, status: '运输中', location: '上海' });
}, 300);
});
}
// Node.js 提供的通用转换工具
const { promisify } = require('util');
const fs = require('fs');
// 将回调风格的 fs.readFile 转为 Promise 风格
const readFile = promisify(fs.readFile);
readFile('./config.json', 'utf8').then(data => console.log(data));
// 手写 promisify
function myPromisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};
}
6.3 Promise 基本使用
// .then() 处理成功
// .catch() 处理失败
// .finally() 无论成功失败都执行
function fetchData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url.includes("error")) {
reject(new Error(`请求失败: ${url}`));
} else {
resolve({ data: `来自 ${url} 的数据`, status: 200 });
}
}, 500);
});
}
fetchData("https://api.example.com/data")
.then(result => {
console.log("成功:", result.data);
})
.catch(error => {
console.error("失败:", error.message);
})
.finally(() => {
console.log("请求完成(无论成败)");
// 常用于:隐藏 loading、释放资源等
});
7. Promise 链式调用
7.1 链式调用原理
/*
* .then() 返回一个新的 Promise,这是链式调用的关键
*
* 返回值规则:
* 1. return 普通值 → 新 Promise 以该值 resolve
* 2. return Promise → 新 Promise 跟随该 Promise 的状态
* 3. throw 错误 → 新 Promise 以该错误 reject
* 4. 不 return → 新 Promise 以 undefined resolve
*/
// 基本链式调用
Promise.resolve(1)
.then(value => {
console.log(value); // 1
return value + 1; // return 普通值
})
.then(value => {
console.log(value); // 2
return Promise.resolve(value + 1); // return Promise
})
.then(value => {
console.log(value); // 3
// 不 return
})
.then(value => {
console.log(value); // undefined
throw new Error("出错了"); // throw 错误
})
.catch(err => {
console.error(err.message); // "出错了"
return "recovered"; // catch 也可以 return,链继续
})
.then(value => {
console.log(value); // "recovered"
});
7.2 用链式调用解决回调地狱
// 之前的回调地狱,用 Promise 链改写
getUserInfo(1)
.then(user => {
console.log('用户:', user.name);
return getOrders(user.id);
})
.then(orders => {
console.log('订单数:', orders.length);
return getOrderDetail(orders[0].orderId);
})
.then(detail => {
console.log('商品:', detail.product);
return getShippingInfo(detail.trackingId);
})
.then(shipping => {
console.log('物流状态:', shipping.status);
console.log('当前位置:', shipping.location);
})
.catch(err => {
// 统一错误处理!任何一步失败都会到这里
console.error('流程出错:', err.message);
});
// 代码扁平化、错误统一处理、流程清晰
7.3 链式调用中传递数据
// 问题:后面的 .then 需要用到前面多个步骤的数据
// 方案1:闭包(简单但变量多了会乱)
let savedUser;
getUserInfo(1)
.then(user => {
savedUser = user; // 保存到外部变量
return getOrders(user.id);
})
.then(orders => {
console.log(savedUser.name, '有', orders.length, '个订单');
});
// 方案2:逐层传递对象(推荐)
getUserInfo(1)
.then(user => {
return getOrders(user.id).then(orders => ({
user,
orders
}));
})
.then(({ user, orders }) => {
console.log(user.name, '有', orders.length, '个订单');
return getOrderDetail(orders[0].orderId).then(detail => ({
user,
orders,
detail
}));
})
.then(({ user, orders, detail }) => {
console.log(`${user.name} 购买了 ${detail.product}`);
});
// 方案3:async/await(最佳方案,后面会讲)
async function getFullInfo() {
const user = await getUserInfo(1);
const orders = await getOrders(user.id);
const detail = await getOrderDetail(orders[0].orderId);
const shipping = await getShippingInfo(detail.trackingId);
// 所有变量都在同一作用域!
console.log(`${user.name} 购买了 ${detail.product},${shipping.status}`);
}
8. Promise 错误处理
8.1 错误捕获机制
// .catch() 相当于 .then(undefined, onRejected)
// 方式1:.then 的第二个参数
promise.then(
value => console.log(value),
error => console.error(error) // 只能捕获 promise 本身的错误
);
// 方式2:.catch()(推荐)
promise
.then(value => {
// 如果这里抛出错误...
throw new Error("then 中的错误");
})
.catch(error => {
// .catch 可以捕获前面所有 .then 中的错误
console.error(error.message);
});
// 区别演示
const p = Promise.reject(new Error("初始错误"));
// ❌ .then 的第二个参数无法捕获同一个 .then 的第一个参数中的错误
p.then(
value => { throw new Error("then 中的错误"); },
error => console.log("捕获:", error.message) // 捕获的是"初始错误"
);
// ✅ .catch 可以捕获链上任何位置的错误
p.then(value => {
throw new Error("then 中的错误");
}).catch(error => {
console.log("捕获:", error.message); // 可以捕获两种错误
});
8.2 错误传播
// 错误会沿着链向下传播,直到被 catch
Promise.resolve("start")
.then(v => {
console.log("step 1:", v);
throw new Error("step 1 出错");
})
.then(v => {
console.log("step 2:", v); // ❌ 跳过!不执行
})
.then(v => {
console.log("step 3:", v); // ❌ 跳过!不执行
})
.catch(err => {
console.log("捕获错误:", err.message); // "step 1 出错"
return "error handled"; // 错误恢复
})
.then(v => {
console.log("step 4:", v); // ✅ "error handled" — 继续执行
});
8.3 多层错误处理
// 可以在链的不同位置放置 catch
fetchData("/api/users")
.then(users => {
return processUsers(users);
})
.catch(err => {
// 处理获取/处理用户数据的错误
console.warn("用户数据处理失败,使用缓存:", err.message);
return getCachedUsers(); // 降级方案
})
.then(users => {
return fetchData(`/api/users/${users[0].id}/orders`);
})
.catch(err => {
// 处理获取订单的错误
console.warn("订单获取失败:", err.message);
return []; // 返回空数组作为默认值
})
.then(orders => {
renderOrders(orders);
})
.catch(err => {
// 最终的错误兜底
showErrorPage(err);
});
8.4 未处理的 Promise 拒绝
// ⚠️ 危险:没有 catch 的 rejected Promise
const unhandled = Promise.reject(new Error("无人处理的错误"));
// 浏览器控制台会警告:UnhandledPromiseRejectionWarning
// Node.js 15+ 会直接终止进程!
// 全局捕获未处理的 rejection
// 浏览器环境
window.addEventListener('unhandledrejection', event => {
console.error('未处理的 Promise 拒绝:', event.reason);
event.preventDefault(); // 阻止默认的控制台错误输出
// 上报错误到监控系统
reportError({
type: 'unhandledrejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack
});
});
// Node.js 环境
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason);
// 推荐:记录日志后优雅退出
});
process.on('rejectionHandled', (promise) => {
// 之前未处理的 rejection 后来被处理了
console.log('延迟处理的 rejection');
});
9. Promise 静态方法
9.1 Promise.resolve() 和 Promise.reject()
// Promise.resolve() — 创建一个 fulfilled 的 Promise
const p1 = Promise.resolve(42);
p1.then(v => console.log(v)); // 42
// 传入 Promise 会直接返回
const p2 = Promise.resolve(Promise.resolve("hello"));
p2.then(v => console.log(v)); // "hello"(不会嵌套)
// 传入 thenable 对象(有 then 方法的对象)
const thenable = {
then(resolve, reject) {
resolve("from thenable");
}
};
Promise.resolve(thenable).then(v => console.log(v)); // "from thenable"
// Promise.reject() — 创建一个 rejected 的 Promise
const p3 = Promise.reject(new Error("失败"));
p3.catch(err => console.log(err.message)); // "失败"
// 注意:reject 不会解包 Promise
const p4 = Promise.reject(Promise.resolve("嵌套"));
p4.catch(v => console.log(v)); // Promise {<fulfilled>: "嵌套"} ← 注意是 Promise 对象
9.2 Promise.all() — 全部成功才成功
/*
* Promise.all(iterable)
* - 所有 Promise 都 fulfilled → 结果数组(顺序与输入一致)
* - 任何一个 rejected → 立即 rejected(快速失败)
*/
// 并行请求多个 API
const userPromise = fetch('/api/user').then(r => r.json());
const ordersPromise = fetch('/api/orders').then(r => r.json());
const settingsPromise = fetch('/api/settings').then(r => r.json());
Promise.all([userPromise, ordersPromise, settingsPromise])
.then(([user, orders, settings]) => {
// 三个请求都完成后才执行
console.log('用户:', user);
console.log('订单:', orders);
console.log('设置:', settings);
renderDashboard(user, orders, settings);
})
.catch(err => {
// 任何一个失败就到这里
console.error('加载仪表盘失败:', err);
});
// 空数组
Promise.all([]).then(v => console.log(v)); // [](立即 fulfilled)
// 包含非 Promise 值
Promise.all([1, "hello", Promise.resolve(true)])
.then(values => console.log(values)); // [1, "hello", true]
// 实际应用:批量上传文件
async function uploadFiles(files) {
const uploadPromises = files.map(file => {
return fetch('/api/upload', {
method: 'POST',
body: file
});
});
try {
const results = await Promise.all(uploadPromises);
console.log('全部上传成功');
return results;
} catch (err) {
console.error('有文件上传失败:', err);
throw err;
}
}
9.3 Promise.allSettled() — 等所有完成(不论成败)
/*
* Promise.allSettled(iterable) [ES2020]
* - 等待所有 Promise 完成(settled = fulfilled 或 rejected)
* - 永远不会 reject
* - 结果数组中每个元素:
* { status: "fulfilled", value: ... }
* { status: "rejected", reason: ... }
*/
const promises = [
fetch('/api/user'),
fetch('/api/nonexistent'), // 这个会失败
fetch('/api/settings')
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`请求 ${index} 成功:`, result.value);
} else {
console.log(`请求 ${index} 失败:`, result.reason);
}
});
// 过滤出成功的结果
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
// 过滤出失败的结果
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
console.log(`${successful.length} 个成功, ${failed.length} 个失败`);
});
// 实际场景:批量通知(不因某个失败就停止)
async function notifyUsers(userIds) {
const notifications = userIds.map(id => sendNotification(id));
const results = await Promise.allSettled(notifications);
const report = {
total: results.length,
success: results.filter(r => r.status === 'fulfilled').length,
failed: results.filter(r => r.status === 'rejected').length,
errors: results
.filter(r => r.status === 'rejected')
.map(r => r.reason.message)
};
console.log('通知报告:', report);
return report;
}
9.4 Promise.race() — 最快的那个
/*
* Promise.race(iterable)
* - 返回最先 settle 的 Promise 的结果(无论成功失败)
*/
// 超时控制
function fetchWithTimeout(url, timeoutMs = 5000) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`请求超时 (${timeoutMs}ms): ${url}`));
}, timeoutMs);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
// 使用
fetchWithTimeout('https://api.example.com/data', 3000)
.then(response => response.json())
.then(data => console.log('数据:', data))
.catch(err => console.error(err.message));
// 多个数据源竞速
function fetchFromFastestCDN(resource) {
return Promise.race([
fetch(`https://cdn1.example.com/${resource}`),
fetch(`https://cdn2.example.com/${resource}`),
fetch(`https://cdn3.example.com/${resource}`)
]);
}
// 注意:其他未完成的 Promise 不会被取消,只是结果被忽略
9.5 Promise.any() — 第一个成功的
/*
* Promise.any(iterable) [ES2021]
* - 返回第一个 fulfilled 的 Promise
* - 全部 rejected → 返回 AggregateError
*
* vs Promise.race():
* - race: 第一个 settled(无论成败)
* - any: 第一个 fulfilled(忽略 rejected)
*/
// 从多个镜像获取资源
const mirrors = [
fetch('https://mirror1.example.com/data.json'),
fetch('https://mirror2.example.com/data.json'),
fetch('https://mirror3.example.com/data.json')
];
Promise.any(mirrors)
.then(response => {
console.log('从最快的可用镜像获取到数据');
return response.json();
})
.catch(err => {
// AggregateError: All promises were rejected
console.error('所有镜像都不可用');
console.error('错误列表:', err.errors); // 所有错误的数组
});
// 对比 race 和 any
const p1 = new Promise((_, reject) => setTimeout(() => reject('p1 fail'), 100));
const p2 = new Promise((resolve) => setTimeout(() => resolve('p2 success'), 200));
Promise.race([p1, p2]).catch(e => console.log('race:', e)); // "race: p1 fail"
Promise.any([p1, p2]).then(v => console.log('any:', v)); // "any: p2 success"
9.6 Promise.withResolvers() [ES2024]
/*
* Promise.withResolvers() — 将 resolve/reject 提取到外部
* 返回 { promise, resolve, reject }
*/
// 之前的写法
let externalResolve, externalReject;
const promise = new Promise((resolve, reject) => {
externalResolve = resolve;
externalReject = reject;
});
// ES2024 新写法
const { promise: p, resolve, reject } = Promise.withResolvers();
// 实际用途:在其他地方控制 Promise 的状态
class EventEmitter {
#listeners = new Map();
waitFor(eventName) {
const { promise, resolve } = Promise.withResolvers();
this.#listeners.set(eventName, resolve);
return promise;
}
emit(eventName, data) {
const resolve = this.#listeners.get(eventName);
if (resolve) {
resolve(data);
this.#listeners.delete(eventName);
}
}
}
const emitter = new EventEmitter();
emitter.waitFor('data').then(data => console.log('收到:', data));
emitter.emit('data', { message: 'hello' }); // 收到: { message: 'hello' }
9.7 静态方法对比总结
/*
* ┌──────────────────┬───────────────┬─────────────────────────────┐
* │ 方法 │ 何时 resolve │ 何时 reject │
* ├──────────────────┼───────────────┼─────────────────────────────┤
* │ Promise.all │ 全部 fulfilled │ 任一 rejected(快速失败) │
* │ Promise.allSettled│ 全部 settled │ 永不 reject │
* │ Promise.race │ 首个 fulfilled │ 首个 rejected │
* │ Promise.any │ 首个 fulfilled │ 全部 rejected(AggregateError)│
* └──────────────────┴───────────────┴─────────────────────────────┘
*/
// 完整对比示例
const fast = new Promise(resolve => setTimeout(() => resolve('fast'), 100));
const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 500));
const fail = new Promise((_, reject) => setTimeout(() => reject('fail'), 200));
// all: 等全部,一个失败就失败
Promise.all([fast, slow, fail]).catch(e => console.log('all:', e)); // "fail"
// allSettled: 等全部,告诉你每个的结果
Promise.allSettled([fast, slow, fail]).then(r => console.log('allSettled:', r));
// [{status:'fulfilled',value:'fast'}, {status:'fulfilled',value:'slow'}, {status:'rejected',reason:'fail'}]
// race: 第一个完成的(无论成败)
Promise.race([fast, slow, fail]).then(v => console.log('race:', v)); // "fast"
// any: 第一个成功的
Promise.any([fast, slow, fail]).then(v => console.log('any:', v)); // "fast"
10. 手写 Promise(面试高频)
class MyPromise {
static PENDING = 'pending';
static FULFILLED = 'fulfilled';
static REJECTED = 'rejected';
constructor(executor) {
this.status = MyPromise.PENDING;
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
// 处理 resolve 一个 Promise 的情况
if (value instanceof MyPromise) {
value.then(resolve, reject);
return;
}
if (this.status === MyPromise.PENDING) {
this.status = MyPromise.FULFILLED;
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.status === MyPromise.PENDING) {
this.status = MyPromise.REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
// 参数默认值:实现值穿透
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };
const promise2 = new MyPromise((resolve, reject) => {
const fulfilledMicrotask = () => {
queueMicrotask(() => {
try {
const x = onFulfilled(this.value);
this.#resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
};
const rejectedMicrotask = () => {
queueMicrotask(() => {
try {
const x = onRejected(this.reason);
this.#resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
};
if (this.status === MyPromise.FULFILLED) {
fulfilledMicrotask();
} else if (this.status === MyPromise.REJECTED) {
rejectedMicrotask();
} else {
// pending 状态:收集回调
this.onFulfilledCallbacks.push(fulfilledMicrotask);
this.onRejectedCallbacks.push(rejectedMicrotask);
}
});
return promise2;
}
// Promise Resolution Procedure (Promises/A+ 规范核心)
#resolvePromise(promise2, x, resolve, reject) {
// 不能返回自己(防止死循环)
if (promise2 === x) {
reject(new TypeError('Chaining cycle detected'));
return;
}
if (x instanceof MyPromise) {
x.then(resolve, reject);
} else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// 处理 thenable
let called = false;
try {
const then = x.then;
if (typeof then === 'function') {
then.call(x,
y => {
if (called) return;
called = true;
this.#resolvePromise(promise2, y, resolve, reject);
},
r => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(x);
}
} catch (error) {
if (called) return;
called = true;
reject(error);
}
} else {
resolve(x);
}
}
catch(onRejected) {
return this.then(undefined, onRejected);
}
finally(callback) {
return this.then(
value => MyPromise.resolve(callback()).then(() => value),
reason => MyPromise.resolve(callback()).then(() => { throw reason; })
);
}
static resolve(value) {
if (value instanceof MyPromise) return value;
return new MyPromise(resolve => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let count = 0;
const promiseArr = Array.from(promises);
if (promiseArr.length === 0) {
resolve([]);
return;
}
promiseArr.forEach((p, index) => {
MyPromise.resolve(p).then(
value => {
results[index] = value;
count++;
if (count === promiseArr.length) {
resolve(results);
}
},
reject
);
});
});
}
static race(promises) {
return new MyPromise((resolve, reject) => {
for (const p of promises) {
MyPromise.resolve(p).then(resolve, reject);
}
});
}
static allSettled(promises) {
return new MyPromise((resolve) => {
const results = [];
let count = 0;
const promiseArr = Array.from(promises);
if (promiseArr.length === 0) {
resolve([]);
return;
}
promiseArr.forEach((p, index) => {
MyPromise.resolve(p).then(
value => {
results[index] = { status: 'fulfilled', value };
if (++count === promiseArr.length) resolve(results);
},
reason => {
results[index] = { status: 'rejected', reason };
if (++count === promiseArr.length) resolve(results);
}
);
});
});
}
static any(promises) {
return new MyPromise((resolve, reject) => {
const errors = [];
let count = 0;
const promiseArr = Array.from(promises);
if (promiseArr.length === 0) {
reject(new AggregateError([], 'All promises were rejected'));
return;
}
promiseArr.forEach((p, index) => {
MyPromise.resolve(p).then(resolve, reason => {
errors[index] = reason;
if (++count === promiseArr.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
});
});
});
}
}
// 测试
const test = new MyPromise((resolve) => {
setTimeout(() => resolve('hello'), 100);
});
test.then(v => {
console.log(v); // 'hello'
return v + ' world';
}).then(v => {
console.log(v); // 'hello world'
});
第四部分:Async/Await
11. 基本语法
11.1 async 函数
// async 函数始终返回一个 Promise
// 声明方式
async function fetchUser() {
return { name: 'Alice' }; // 自动包装为 Promise.resolve({ name: 'Alice' })
}
// 等价于
function fetchUser() {
return Promise.resolve({ name: 'Alice' });
}
// 箭头函数
const fetchUser2 = async () => ({ name: 'Bob' });
// 类方法
class UserService {
async getUser(id) {
return { id, name: 'Charlie' };
}
}
// 验证返回 Promise
const result = fetchUser();
console.log(result); // Promise {<fulfilled>: { name: 'Alice' }}
console.log(result instanceof Promise); // true
result.then(user => console.log(user)); // { name: 'Alice' }
11.2 await 关键字
/*
* await 做了什么:
* 1. 暂停 async 函数的执行
* 2. 等待 Promise settle
* 3. 如果 fulfilled → 返回 value
* 4. 如果 rejected → 抛出 reason
* 5. 恢复 async 函数的执行
*
* await 只能在 async 函数内使用(或在 ES 模块的顶层)
*/
async function demo() {
console.log("开始");
// await 一个 Promise
const value = await new Promise(resolve => {
setTimeout(() => resolve("异步结果"), 1000);
});
console.log("得到:", value); // 1秒后: "异步结果"
// await 非 Promise 值会立即继续(自动包装为 Promise.resolve)
const num = await 42;
console.log("数字:", num); // 42
// await 一个 rejected Promise 会抛出错误
try {
const fail = await Promise.reject(new Error("出错了"));
} catch (err) {
console.error("捕获:", err.message); // "出错了"
}
console.log("结束");
}
demo();
// 顶层 await(ES Modules 中)
// 在 .mjs 文件或 type:"module" 中可以直接使用
// const data = await fetch('/api/data').then(r => r.json());
11.3 用 async/await 改写 Promise 链
// Promise 链版本
function getFullUserInfo_promise(userId) {
return getUserInfo(userId)
.then(user => {
return getOrders(user.id).then(orders => ({ user, orders }));
})
.then(({ user, orders }) => {
return getOrderDetail(orders[0].orderId)
.then(detail => ({ user, orders, detail }));
})
.then(({ user, orders, detail }) => {
return getShippingInfo(detail.trackingId)
.then(shipping => ({ user, orders, detail, shipping }));
});
}
// async/await 版本 ✨
async function getFullUserInfo(userId) {
const user = await getUserInfo(userId);
const orders = await getOrders(user.id);
const detail = await getOrderDetail(orders[0].orderId);
const shipping = await getShippingInfo(detail.trackingId);
return { user, orders, detail, shipping };
}
// 使用
getFullUserInfo(1).then(info => {
console.log(`${info.user.name} 购买了 ${info.detail.product}`);
console.log(`物流状态: ${info.shipping.status}`);
});
// 或者在另一个 async 函数中
async function main() {
const info = await getFullUserInfo(1);
console.log(info);
}
main();
12. 错误处理
12.1 try/catch
// 最直接的方式
async function fetchUserSafely(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const user = await response.json();
return user;
} catch (error) {
if (error.name === 'TypeError') {
console.error('网络错误:', error.message);
} else {
console.error('请求失败:', error.message);
}
return null; // 返回默认值
} finally {
hideLoadingSpinner();
}
}
// 嵌套 try/catch 处理不同阶段的错误
async function processOrder(orderId) {
let order;
try {
order = await fetchOrder(orderId);
} catch (err) {
console.error('获取订单失败');
throw new Error('ORDER_FETCH_FAILED');
}
try {
await validateOrder(order);
} catch (err) {
console.error('订单验证失败');
throw new Error('ORDER_VALIDATION_FAILED');
}
try {
const result = await submitPayment(order);
return result;
} catch (err) {
console.error('支付失败');
await rollbackOrder(order);
throw new Error('PAYMENT_FAILED');
}
}
12.2 优雅的错误处理模式
// 模式1:Go 风格的错误处理
async function to(promise) {
try {
const result = await promise;
return [null, result];
} catch (error) {
return [error, null];
}
}
// 使用
async function main() {
const [err, user] = await to(getUserInfo(1));
if (err) {
console.error('获取用户失败:', err.message);
return;
}
const [err2, orders] = await to(getOrders(user.id));
if (err2) {
console.error('获取订单失败:', err2.message);
return;
}
console.log(user, orders);
}
// 模式2:包装函数添加错误处理
function withErrorHandler(fn, errorHandler) {
return async function(...args) {
try {
return await fn.apply(this, args);
} catch (error) {
return errorHandler(error, ...args);
}
};
}
const safeGetUser = withErrorHandler(
async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
},
(error, id) => {
console.error(`获取用户 ${id} 失败:`, error);
return null;
}
);
const user = await safeGetUser(123);
// 模式3:装饰器模式(TypeScript/提案阶段)
function catchError(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = async function(...args) {
try {
return await original.apply(this, args);
} catch (error) {
console.error(`${name} 执行出错:`, error);
throw error;
}
};
return descriptor;
}
12.3 重试模式
// 带指数退避的重试
async function retry(fn, options = {}) {
const {
maxRetries = 3,
baseDelay = 1000,
maxDelay = 10000,
backoffFactor = 2,
retryOn = () => true, // 判断是否应该重试
} = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn(attempt);
} catch (error) {
lastError = error;
if (attempt === maxRetries || !retryOn(error)) {
throw error;
}
const delay = Math.min(
baseDelay * Math.pow(backoffFactor, attempt),
maxDelay
);
// 添加随机抖动,避免雷群效应
const jitter = delay * 0.1 * Math.random();
const totalDelay = delay + jitter;
console.warn(
`第 ${attempt + 1} 次失败,${totalDelay.toFixed(0)}ms 后重试:`,
error.message
);
await new Promise(resolve => setTimeout(resolve, totalDelay));
}
}
throw lastError;
}
// 使用
async function fetchWithRetry(url) {
return retry(
async (attempt) => {
console.log(`第 ${attempt + 1} 次尝试请求 ${url}`);
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
{
maxRetries: 3,
baseDelay: 1000,
retryOn: (error) => {
// 只对特定错误重试
return error.message.includes('500') ||
error.message.includes('503') ||
error.name === 'TypeError'; // 网络错误
}
}
);
}
const data = await fetchWithRetry('https://api.example.com/data');
13. Async/Await 常见模式
13.1 串行 vs 并行
// ❌ 串行执行(慢!)— 每个请求等上一个完成
async function serial() {
const start = Date.now();
const user = await fetchUser(); // 等 1 秒
const orders = await fetchOrders(); // 再等 1 秒
const products = await fetchProducts(); // 再等 1 秒
console.log(`串行总耗时: ${Date.now() - start}ms`); // ≈ 3000ms
}
// ✅ 并行执行(快!)— 所有请求同时发出
async function parallel() {
const start = Date.now();
// 先发起所有请求(不 await)
const userPromise = fetchUser();
const ordersPromise = fetchOrders();
const productsPromise = fetchProducts();
// 再等待所有结果
const user = await userPromise;
const orders = await ordersPromise;
const products = await productsPromise;
console.log(`并行总耗时: ${Date.now() - start}ms`); // ≈ 1000ms
}
// ✅ 更推荐用 Promise.all
async function parallelWithAll() {
const start = Date.now();
const [user, orders, products] = await Promise.all([
fetchUser(),
fetchOrders(),
fetchProducts()
]);
console.log(`并行总耗时: ${Date.now() - start}ms`); // ≈ 1000ms
}
// 混合:部分串行,部分并行
async function mixed() {
// 先获取用户(必须先有用户信息)
const user = await fetchUser();
// 然后并行获取用户的订单和收藏(互不依赖)
const [orders, favorites] = await Promise.all([
fetchOrders(user.id),
fetchFavorites(user.id)
]);
return { user, orders, favorites };
}
13.2 循环中的 async/await
const urls = [
'/api/data/1',
'/api/data/2',
'/api/data/3'
];
// ❌ forEach 中的 await 不会等待!
async function badLoop() {
urls.forEach(async (url) => {
const data = await fetch(url); // forEach 不会等这个
console.log(data);
});
console.log("完成"); // 这行会在所有 fetch 之前执行!
}
// ✅ 串行:for...of
async function serialLoop() {
const results = [];
for (const url of urls) {
const response = await fetch(url);
const data = await response.json();
results.push(data);
console.log(`完成: ${url}`);
}
return results; // 按顺序串行执行
}
// ✅ 并行:Promise.all + map
async function parallelLoop() {
const results = await Promise.all(
urls.map(async (url) => {
const response = await fetch(url);
return response.json();
})
);
return results; // 并行执行,结果顺序与 urls 一致
}
// ✅ 控制并发数的并行(后面会详细讲)
async function limitedParallelLoop() {
const limit = 2; // 最多同时2个请求
const results = [];
for (let i = 0; i < urls.length; i += limit) {
const batch = urls.slice(i, i + limit);
const batchResults = await Promise.all(
batch.map(url => fetch(url).then(r => r.json()))
);
results.push(...batchResults);
}
return results;
}
// ✅ for await...of(异步迭代器)
async function* fetchAll(urls) {
for (const url of urls) {
const response = await fetch(url);
yield await response.json();
}
}
async function asyncIteratorLoop() {
for await (const data of fetchAll(urls)) {
console.log(data);
}
}
13.3 条件异步
// 根据条件决定是否执行异步操作
async function getUser(id, options = {}) {
const { useCache = true } = options;
// 有缓存就直接返回(同步路径)
if (useCache) {
const cached = cache.get(`user:${id}`);
if (cached) return cached;
}
// 无缓存则请求(异步路径)
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
cache.set(`user:${id}`, user);
return user;
}
// 竞态条件处理
let currentRequestId = 0;
async function search(query) {
const requestId = ++currentRequestId;
const results = await fetchSearchResults(query);
// 如果在等待期间又发起了新请求,丢弃当前结果
if (requestId !== currentRequestId) {
console.log('过时的结果,已丢弃');
return;
}
displayResults(results);
}
// 更好的方式:使用 AbortController
let currentController = null;
async function searchWithAbort(query) {
// 取消之前的请求
if (currentController) {
currentController.abort();
}
currentController = new AbortController();
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: currentController.signal
});
const results = await response.json();
displayResults(results);
} catch (err) {
if (err.name === 'AbortError') {
console.log('请求已取消');
} else {
throw err;
}
}
}
13.4 async/await 与类
class DataService {
#baseUrl;
#cache = new Map();
constructor(baseUrl) {
this.#baseUrl = baseUrl;
}
// 异步方法
async get(endpoint) {
const url = `${this.#baseUrl}${endpoint}`;
if (this.#cache.has(url)) {
return this.#cache.get(url);
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
this.#cache.set(url, data);
return data;
}
async post(endpoint, body) {
const response = await fetch(`${this.#baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return response.json();
}
// 静态异步方法
static async create(baseUrl) {
const service = new DataService(baseUrl);
// 可以在工厂方法中做异步初始化
await service.get('/health'); // 检查服务是否可用
return service;
}
}
// 使用(注意:constructor 不能是 async 的)
async function main() {
const api = await DataService.create('https://api.example.com');
const users = await api.get('/users');
console.log(users);
}
第五部分:高级异步模式
14. Generator 与异步迭代
14.1 Generator 基础
// Generator 函数:可以暂停和恢复的函数
function* numberGenerator() {
console.log("开始");
yield 1; // 暂停,返回 1
console.log("继续");
yield 2; // 暂停,返回 2
console.log("再继续");
return 3; // 结束
}
const gen = numberGenerator(); // 不会立即执行!
console.log(gen.next()); // "开始" → { value: 1, done: false }
console.log(gen.next()); // "继续" → { value: 2, done: false }
console.log(gen.next()); // "再继续" → { value: 3, done: true }
console.log(gen.next()); // → { value: undefined, done: true }
// yield 可以接收外部传入的值
function* conversation() {
const name = yield "你叫什么名字?";
const age = yield `${name},你多大了?`;
return `${name} 今年 ${age} 岁`;
}
const chat = conversation();
console.log(chat.next()); // { value: "你叫什么名字?", done: false }
console.log(chat.next("Alice")); // { value: "Alice,你多大了?", done: false }
console.log(chat.next(25)); // { value: "Alice 今年 25 岁", done: true }
14.2 Generator 实现异步流程控制
// Generator + Promise = async/await 的前身
function* fetchUserFlow() {
try {
const user = yield getUserInfo(1); // yield 一个 Promise
console.log('用户:', user.name);
const orders = yield getOrders(user.id); // yield 另一个 Promise
console.log('订单数:', orders.length);
return { user, orders };
} catch (err) {
console.error('出错:', err.message);
}
}
// 自动执行器(co 库的简化版)
function run(generatorFn) {
return new Promise((resolve, reject) => {
const gen = generatorFn();
function step(nextFn) {
let result;
try {
result = nextFn();
} catch (err) {
return reject(err);
}
if (result.done) {
return resolve(result.value);
}
// 假设 yield 的都是 Promise
Promise.resolve(result.value).then(
value => step(() => gen.next(value)), // 将结果送回 generator
error => step(() => gen.throw(error)) // 将错误送回 generator
);
}
step(() => gen.next());
});
}
// 使用
run(fetchUserFlow).then(result => {
console.log('最终结果:', result);
});
// 对比 async/await(完全等价!)
async function fetchUserAsync() {
try {
const user = await getUserInfo(1);
console.log('用户:', user.name);
const orders = await getOrders(user.id);
console.log('订单数:', orders.length);
return { user, orders };
} catch (err) {
console.error('出错:', err.message);
}
}
// async/await 本质上就是 Generator + 自动执行器的语法糖!
14.3 异步迭代器(Async Iterator)
// Symbol.asyncIterator 和 for await...of
// 创建异步可迭代对象
const asyncIterable = {
[Symbol.asyncIterator]() {
let i = 0;
return {
async next() {
if (i >= 3) {
return { value: undefined, done: true };
}
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 1000));
return { value: i++, done: false };
}
};
}
};
// 使用 for await...of
async function consume() {
for await (const value of asyncIterable) {
console.log(value); // 每隔1秒: 0, 1, 2
}
}
// 异步生成器(更简洁的写法)
async function* asyncRange(start, end) {
for (let i = start; i <= end; i++) {
// 模拟每个值需要异步获取
await new Promise(resolve => setTimeout(resolve, 500));
yield i;
}
}
async function main() {
for await (const num of asyncRange(1, 5)) {
console.log(num); // 每隔500ms: 1, 2, 3, 4, 5
}
}
// 实际应用:分页获取数据
async function* fetchPages(baseUrl) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}&limit=20`);
const data = await response.json();
yield data.items;
hasMore = data.hasMore;
page++;
}
}
// 使用
async function getAllItems() {
const allItems = [];
for await (const items of fetchPages('/api/products')) {
allItems.push(...items);
console.log(`已获取 ${allItems.length} 个商品`);
}
return allItems;
}
// 实际应用:读取大文件流(Node.js)
const fs = require('fs');
async function processLargeFile(filePath) {
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
let lineCount = 0;
for await (const chunk of stream) {
lineCount += chunk.split('\n').length;
}
console.log(`文件共 ${lineCount} 行`);
}
15. 并发控制
15.1 并发限制器
// 实现一个通用的并发限制器
class ConcurrencyLimiter {
#maxConcurrency;
#running = 0;
#queue = [];
constructor(maxConcurrency) {
this.#maxConcurrency = maxConcurrency;
}
async run(fn) {
// 如果达到并发上限,排队等待
if (this.#running >= this.#maxConcurrency) {
await new Promise(resolve => this.#queue.push(resolve));
}
this.#running++;
try {
return await fn();
} finally {
this.#running--;
// 释放一个排队的任务
if (this.#queue.length > 0) {
const next = this.#queue.shift();
next();
}
}
}
get running() { return this.#running; }
get pending() { return this.#queue.length; }
}
// 使用
async function downloadFiles(urls) {
const limiter = new ConcurrencyLimiter(3); // 最多3个并行
const results = await Promise.all(
urls.map(url =>
limiter.run(async () => {
console.log(`开始下载: ${url} (并行: ${limiter.running})`);
const response = await fetch(url);
const data = await response.json();
console.log(`完成下载: ${url}`);
return data;
})
)
);
return results;
}
15.2 Promise 池
// 更精细的并发控制:Promise 池
async function promisePool(tasks, poolSize) {
const results = [];
const executing = new Set();
for (const [index, task] of tasks.entries()) {
// 创建 Promise 并开始执行
const promise = Promise.resolve().then(() => task()).then(result => {
results[index] = { status: 'fulfilled', value: result };
}).catch(error => {
results[index] = { status: 'rejected', reason: error };
});
executing.add(promise);
// Promise 完成后从执行集合中移除
const clean = promise.then(() => executing.delete(promise));
// 达到池大小限制时,等待一个完成
if (executing.size >= poolSize) {
await Promise.race(executing);
}
}
// 等待剩余的任务完成
await Promise.all(executing);
return results;
}
// 使用
const tasks = Array.from({ length: 20 }, (_, i) => {
return () => new Promise(resolve => {
const delay = Math.random() * 2000;
setTimeout(() => {
console.log(`任务 ${i} 完成 (耗时 ${delay.toFixed(0)}ms)`);
resolve(`result-${i}`);
}, delay);
});
});
const results = await promisePool(tasks, 5);
console.log('所有结果:', results);
15.3 带进度的批量处理
async function batchProcess(items, processor, options = {}) {
const {
concurrency = 5,
onProgress = () => {},
onItemComplete = () => {},
onItemError = () => {},
} = options;
const limiter = new ConcurrencyLimiter(concurrency);
const total = items.length;
let completed = 0;
let failed = 0;
const results = [];
const promises = items.map((item, index) =>
limiter.run(async () => {
try {
const result = await processor(item, index);
results[index] = { success: true, data: result };
onItemComplete(item, result, index);
} catch (error) {
results[index] = { success: false, error };
failed++;
onItemError(item, error, index);
} finally {
completed++;
onProgress({
completed,
failed,
total,
percent: ((completed / total) * 100).toFixed(1)
});
}
})
);
await Promise.all(promises);
return {
results,
summary: { total, completed, failed, success: completed - failed }
};
}
// 使用:批量上传图片
const images = ['img1.jpg', 'img2.jpg', /* ... */ 'img100.jpg'];
const report = await batchProcess(
images,
async (image, index) => {
const formData = new FormData();
formData.append('file', image);
const response = await fetch('/api/upload', { method: 'POST', body: formData });
if (!response.ok) throw new Error(`上传失败: ${response.status}`);
return response.json();
},
{
concurrency: 3,
onProgress: ({ completed, total, percent }) => {
console.log(`进度: ${completed}/${total} (${percent}%)`);
updateProgressBar(percent);
},
onItemError: (image, error) => {
console.warn(`${image} 上传失败:`, error.message);
}
}
);
console.log(`上传完成: ${report.summary.success} 成功, ${report.summary.failed} 失败`);
16. 发布/订阅与事件驱动
16.1 EventEmitter 实现
class AsyncEventEmitter {
#listeners = new Map();
on(event, listener) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, []);
}
this.#listeners.get(event).push(listener);
return this; // 链式调用
}
off(event, listener) {
const listeners = this.#listeners.get(event);
if (listeners) {
const index = listeners.indexOf(listener);
if (index > -1) listeners.splice(index, 1);
}
return this;
}
once(event, listener) {
const wrapper = async (...args) => {
this.off(event, wrapper);
return listener(...args);
};
return this.on(event, wrapper);
}
// 异步 emit:等待所有监听器执行完毕
async emit(event, ...args) {
const listeners = this.#listeners.get(event) || [];
const results = [];
for (const listener of [...listeners]) {
results.push(await listener(...args));
}
return results;
}
// 并行 emit
async emitParallel(event, ...args) {
const listeners = this.#listeners.get(event) || [];
return Promise.all(listeners.map(fn => fn(...args)));
}
// 等待某个事件触发(转为 Promise)
waitFor(event, timeout = 0) {
return new Promise((resolve, reject) => {
let timer;
if (timeout > 0) {
timer = setTimeout(() => {
this.off(event, handler);
reject(new Error(`等待 "${event}" 事件超时 (${timeout}ms)`));
}, timeout);
}
const handler = (data) => {
clearTimeout(timer);
resolve(data);
};
this.once(event, handler);
});
}
}
// 使用
const bus = new AsyncEventEmitter();
// 注册异步监听器
bus.on('order:created', async (order) => {
console.log('发送确认邮件...');
await sendEmail(order.userId, '订单已创建');
});
bus.on('order:created', async (order) => {
console.log('更新库存...');
await updateInventory(order.items);
});
// 触发事件
await bus.emit('order:created', { id: 1, userId: 'u1', items: [...] });
console.log('所有后续处理完成');
// 等待事件
const userData = await bus.waitFor('user:login', 30000);
console.log('用户登录了:', userData);
16.2 异步队列
class AsyncQueue {
#queue = [];
#processing = false;
#concurrency;
#running = 0;
constructor(concurrency = 1) {
this.#concurrency = concurrency;
}
enqueue(task) {
return new Promise((resolve, reject) => {
this.#queue.push({ task, resolve, reject });
this.#process();
});
}
async #process() {
if (this.#running >= this.#concurrency || this.#queue.length === 0) {
return;
}
const { task, resolve, reject } = this.#queue.shift();
this.#running++;
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.#running--;
this.#process(); // 处理下一个
}
}
get size() { return this.#queue.length; }
get pending() { return this.#running; }
// 等待所有任务完成
async drain() {
if (this.#queue.length === 0 && this.#running === 0) return;
return new Promise(resolve => {
const check = () => {
if (this.#queue.length === 0 && this.#running === 0) {
resolve();
} else {
setTimeout(check, 50);
}
};
check();
});
}
}
// 使用:任务队列
const queue = new AsyncQueue(2); // 并发度 2
// 添加任务
for (let i = 0; i < 10; i++) {
queue.enqueue(async () => {
console.log(`开始任务 ${i}`);
await new Promise(r => setTimeout(r, 1000));
console.log(`完成任务 ${i}`);
return `result-${i}`;
}).then(result => {
console.log(`任务结果: ${result}`);
});
}
// 等待所有完成
await queue.drain();
console.log('所有任务已完成');
17. 可取消的异步操作
17.1 AbortController
// AbortController 是 Web API,用于取消异步操作
// 基本用法
const controller = new AbortController();
const { signal } = controller;
// 1. 取消 fetch 请求
fetch('/api/large-data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已取消');
} else {
console.error('请求失败:', err);
}
});
// 5秒后取消
setTimeout(() => controller.abort(), 5000);
// 2. 取消多个操作
const controller2 = new AbortController();
await Promise.all([
fetch('/api/data1', { signal: controller2.signal }),
fetch('/api/data2', { signal: controller2.signal }),
fetch('/api/data3', { signal: controller2.signal }),
]);
// controller2.abort() 会同时取消所有三个请求
// 3. 监听取消信号
signal.addEventListener('abort', () => {
console.log('收到取消信号');
console.log('取消原因:', signal.reason);
});
// 带原因的取消
controller.abort(new Error('用户取消了操作'));
17.2 自定义可取消操作
// 让任何异步操作都可以取消
function cancellable(asyncFn) {
const controller = new AbortController();
const promise = new Promise(async (resolve, reject) => {
// 监听取消
controller.signal.addEventListener('abort', () => {
reject(new DOMException('Operation cancelled', 'AbortError'));
});
try {
const result = await asyncFn(controller.signal);
resolve(result);
} catch (err) {
reject(err);
}
});
return {
promise,
cancel: (reason) => controller.abort(reason)
};
}
// 使用
const { promise, cancel } = cancellable(async (signal) => {
const response = await fetch('/api/data', { signal });
return response.json();
});
// 2秒后取消
setTimeout(cancel, 2000);
try {
const data = await promise;
console.log(data);
} catch (err) {
if (err.name === 'AbortError') {
console.log('已取消');
}
}
// 可取消的延迟
function delay(ms, signal) {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Delay cancelled', 'AbortError'));
});
});
}
// 可取消的重试
async function fetchWithCancelableRetry(url, { signal, maxRetries = 3 } = {}) {
for (let i = 0; i <= maxRetries; i++) {
signal?.throwIfAborted(); // 检查是否已取消
try {
return await fetch(url, { signal });
} catch (err) {
if (err.name === 'AbortError') throw err; // 取消不重试
if (i === maxRetries) throw err;
await delay(1000 * Math.pow(2, i), signal);
}
}
}
18. 响应式编程简介(Observable)
// 简单的 Observable 实现
class Observable {
constructor(subscribe) {
this._subscribe = subscribe;
}
subscribe(observer) {
// 标准化 observer
const normalizedObserver = typeof observer === 'function'
? { next: observer, error: () => {}, complete: () => {} }
: { next: () => {}, error: () => {}, complete: () => {}, ...observer };
const subscription = this._subscribe(normalizedObserver);
return {
unsubscribe: () => {
if (subscription?.unsubscribe) subscription.unsubscribe();
}
};
}
// 操作符
map(fn) {
return new Observable(observer => {
return this.subscribe({
next: value => observer.next(fn(value)),
error: err => observer.error(err),
complete: () => observer.complete()
});
});
}
filter(predicate) {
return new Observable(observer => {
return this.subscribe({
next: value => predicate(value) && observer.next(value),
error: err => observer.error(err),
complete: () => observer.complete()
});
});
}
// 从各种来源创建 Observable
static fromEvent(element, eventName) {
return new Observable(observer => {
const handler = event => observer.next(event);
element.addEventListener(eventName, handler);
return {
unsubscribe: () => element.removeEventListener(eventName, handler)
};
});
}
static fromPromise(promise) {
return new Observable(observer => {
promise
.then(value => {
observer.next(value);
observer.complete();
})
.catch(err => observer.error(err));
});
}
static interval(ms) {
return new Observable(observer => {
let i = 0;
const id = setInterval(() => observer.next(i++), ms);
return { unsubscribe: () => clearInterval(id) };
});
}
}
// 使用示例:搜索框防抖
const searchInput = document.getElementById('search');
const subscription = Observable.fromEvent(searchInput, 'input')
.map(e => e.target.value)
.filter(text => text.length >= 2)
.subscribe({
next: async (query) => {
const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
displayResults(results);
}
});
// 取消订阅
// subscription.unsubscribe();
第六部分:实战与最佳实践
19. 真实项目场景
19.1 完整的 API 客户端
class APIClient {
#baseUrl;
#defaultHeaders;
#interceptors = { request: [], response: [] };
#timeout;
constructor(config = {}) {
this.#baseUrl = config.baseUrl || '';
this.#defaultHeaders = config.headers || {};
this.#timeout = config.timeout || 30000;
}
// 拦截器
addRequestInterceptor(fn) {
this.#interceptors.request.push(fn);
return this;
}
addResponseInterceptor(fn) {
this.#interceptors.response.push(fn);
return this;
}
async #request(method, endpoint, options = {}) {
let config = {
method,
url: `${this.#baseUrl}${endpoint}`,
headers: { ...this.#defaultHeaders, ...options.headers },
body: options.body,
params: options.params,
timeout: options.timeout || this.#timeout,
signal: options.signal,
};
// 执行请求拦截器
for (const interceptor of this.#interceptors.request) {
config = await interceptor(config);
}
// 构建 URL(处理查询参数)
const url = new URL(config.url);
if (config.params) {
Object.entries(config.params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
// 超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), config.timeout);
// 合并 signal
const signal = config.signal
? anySignal([config.signal, controller.signal])
: controller.signal;
try {
const fetchOptions = {
method: config.method,
headers: config.headers,
signal,
};
if (config.body && method !== 'GET') {
fetchOptions.body = JSON.stringify(config.body);
fetchOptions.headers['Content-Type'] = 'application/json';
}
let response = await fetch(url.toString(), fetchOptions);
// 执行响应拦截器
for (const interceptor of this.#interceptors.response) {
response = await interceptor(response);
}
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
error.response = response;
throw error;
}
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
return await response.json();
}
return await response.text();
} finally {
clearTimeout(timeoutId);
}
}
get(endpoint, options) { return this.#request('GET', endpoint, options); }
post(endpoint, body, options) { return this.#request('POST', endpoint, { ...options, body }); }
put(endpoint, body, options) { return this.#request('PUT', endpoint, { ...options, body }); }
patch(endpoint, body, options) { return this.#request('PATCH', endpoint, { ...options, body }); }
delete(endpoint, options) { return this.#request('DELETE', endpoint, options); }
}
// 使用
const api = new APIClient({
baseUrl: 'https://api.example.com',
headers: {
'Accept': 'application/json',
},
timeout: 10000,
});
// 添加认证拦截器
api.addRequestInterceptor(async (config) => {
const token = await getAuthToken();
config.headers['Authorization'] = `Bearer ${token}`;
return config;
});
// 添加日志拦截器
api.addResponseInterceptor(async (response) => {
console.log(`${response.url} → ${response.status}`);
return response;
});
// 调用
try {
const users = await api.get('/users', { params: { page: 1, limit: 20 } });
const newUser = await api.post('/users', { name: 'Alice', email: 'alice@example.com' });
} catch (err) {
if (err.status === 401) {
// 跳转登录
}
}
19.2 缓存与去重
// 请求去重 + 缓存
class RequestCache {
#cache = new Map(); // 结果缓存
#pending = new Map(); // 进行中的请求(去重)
#ttl;
constructor(ttl = 60000) { // 默认缓存1分钟
this.#ttl = ttl;
}
async get(key, fetcher) {
// 1. 检查缓存
const cached = this.#cache.get(key);
if (cached && Date.now() - cached.timestamp < this.#ttl) {
console.log(`[Cache HIT] ${key}`);
return cached.data;
}
// 2. 检查是否有相同的请求正在进行(去重)
if (this.#pending.has(key)) {
console.log(`[Cache DEDUP] ${key}`);
return this.#pending.get(key);
}
// 3. 发起新请求
console.log(`[Cache MISS] ${key}`);
const promise = fetcher().then(data => {
// 成功后缓存结果
this.#cache.set(key, { data, timestamp: Date.now() });
this.#pending.delete(key);
return data;
}).catch(err => {
this.#pending.delete(key);
throw err;
});
this.#pending.set(key, promise);
return promise;
}
invalidate(key) {
this.#cache.delete(key);
}
clear() {
this.#cache.clear();
}
}
// 使用
const cache = new RequestCache(30000); // 30秒缓存
async function getUser(id) {
return cache.get(`user:${id}`, () =>
fetch(`/api/users/${id}`).then(r => r.json())
);
}
// 即使同时调用多次,也只会发一个请求
const [user1, user2, user3] = await Promise.all([
getUser(1), // 发起请求
getUser(1), // 复用同一个请求(去重)
getUser(1), // 复用同一个请求(去重)
]);
// 后续调用使用缓存
const user4 = await getUser(1); // Cache HIT
19.3 WebSocket 封装
class ReconnectableWebSocket {
#url;
#ws = null;
#options;
#reconnectAttempts = 0;
#listeners = new Map();
#messageQueue = [];
#isConnected = false;
constructor(url, options = {}) {
this.#url = url;
this.#options = {
maxReconnectAttempts: 10,
reconnectInterval: 1000,
maxReconnectInterval: 30000,
...options
};
this.#connect();
}
#connect() {
this.#ws = new WebSocket(this.#url);
this.#ws.onopen = () => {
console.log('[WS] 连接成功');
this.#isConnected = true;
this.#reconnectAttempts = 0;
// 发送队列中的消息
while (this.#messageQueue.length > 0) {
const msg = this.#messageQueue.shift();
this.#ws.send(msg);
}
this.#emit('open');
};
this.#ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.#emit('message', data);
// 支持按类型分发
if (data.type) {
this.#emit(`message:${data.type}`, data.payload);
}
} catch {
this.#emit('message', event.data);
}
};
this.#ws.onclose = (event) => {
this.#isConnected = false;
console.log(`[WS] 连接关闭: ${event.code}`);
this.#emit('close', event);
if (event.code !== 1000) { // 非正常关闭
this.#reconnect();
}
};
this.#ws.onerror = (error) => {
console.error('[WS] 错误:', error);
this.#emit('error', error);
};
}
#reconnect() {
if (this.#reconnectAttempts >= this.#options.maxReconnectAttempts) {
console.error('[WS] 达到最大重连次数');
this.#emit('maxReconnectAttemptsReached');
return;
}
const delay = Math.min(
this.#options.reconnectInterval * Math.pow(2, this.#reconnectAttempts),
this.#options.maxReconnectInterval
);
console.log(`[WS] ${delay}ms 后重连 (第 ${this.#reconnectAttempts + 1} 次)`);
setTimeout(() => {
this.#reconnectAttempts++;
this.#connect();
}, delay);
}
send(data) {
const message = typeof data === 'string' ? data : JSON.stringify(data);
if (this.#isConnected) {
this.#ws.send(message);
} else {
this.#messageQueue.push(message); // 离线时先队列中
}
}
// 发送请求并等待响应
request(type, payload, timeout = 5000) {
return new Promise((resolve, reject) => {
const requestId = Math.random().toString(36).slice(2);
const timer = setTimeout(() => {
this.off(`message:${type}:${requestId}`, handler);
reject(new Error(`WebSocket 请求超时: ${type}`));
}, timeout);
const handler = (response) => {
clearTimeout(timer);
resolve(response);
};
this.once(`message:${type}:${requestId}`, handler);
this.send({ type, payload, requestId });
});
}
on(event, handler) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(handler);
return this;
}
off(event, handler) {
this.#listeners.get(event)?.delete(handler);
return this;
}
once(event, handler) {
const wrapper = (...args) => {
this.off(event, wrapper);
handler(...args);
};
return this.on(event, wrapper);
}
#emit(event, ...args) {
this.#listeners.get(event)?.forEach(handler => handler(...args));
}
close() {
this.#options.maxReconnectAttempts = 0;
this.#ws?.close(1000, 'Client closed');
}
}
// 使用
const ws = new ReconnectableWebSocket('wss://api.example.com/ws');
ws.on('open', () => console.log('已连接'));
ws.on('message:chat', (msg) => console.log('收到消息:', msg));
ws.on('message:notification', (notif) => showNotification(notif));
ws.send({ type: 'join', payload: { room: 'general' } });
// 请求-响应模式
const userList = await ws.request('getUserList', { room: 'general' });
20. 性能优化
20.1 防抖与节流
// 防抖(Debounce):等用户停止操作后再执行
function debounce(fn, delay, options = {}) {
const { leading = false, trailing = true } = options;
let timer = null;
let lastArgs = null;
function debounced(...args) {
lastArgs = args;
const callNow = leading && !timer;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (trailing && lastArgs) {
fn(...lastArgs);
lastArgs = null;
}
}, delay);
if (callNow) {
fn(...args);
}
}
debounced.cancel = () => {
clearTimeout(timer);
timer = null;
lastArgs = null;
};
// 返回 Promise 版本
debounced.promise = (...args) => {
return new Promise((resolve) => {
debounced((...result) => resolve(fn(...result)));
});
};
return debounced;
}
// 异步防抖搜索
const debouncedSearch = debounce(async (query) => {
const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
displayResults(results);
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// 节流(Throttle):限制执行频率
function throttle(fn, interval) {
let lastTime = 0;
let timer = null;
return function(...args) {
const now = Date.now();
const remaining = interval - (now - lastTime);
if (remaining <= 0) {
clearTimeout(timer);
timer = null;
lastTime = now;
fn(...args);
} else if (!timer) {
timer = setTimeout(() => {
lastTime = Date.now();
timer = null;
fn(...args);
}, remaining);
}
};
}
// 滚动事件节流
window.addEventListener('scroll', throttle(async () => {
if (isNearBottom()) {
await loadMoreItems();
}
}, 200));
20.2 懒加载与预加载
// 懒加载模式
class LazyLoader {
#loaders = new Map();
#cache = new Map();
register(key, loader) {
this.#loaders.set(key, loader);
}
async get(key) {
// 已缓存
if (this.#cache.has(key)) {
return this.#cache.get(key);
}
const loader = this.#loaders.get(key);
if (!loader) throw new Error(`Unknown resource: ${key}`);
const value = await loader();
this.#cache.set(key, value);
return value;
}
// 预加载(后台提前加载)
preload(...keys) {
return Promise.allSettled(
keys.map(key => this.get(key))
);
}
}
// 使用
const resources = new LazyLoader();
resources.register('heavyModule', () => import('./heavy-module.js'));
resources.register('userProfile', () => fetch('/api/profile').then(r => r.json()));
resources.register('config', () => fetch('/api/config').then(r => r.json()));
// 只有在需要时才加载
const profile = await resources.get('userProfile');
// 路由跳转前预加载下一页的资源
router.beforeEach((to) => {
if (to.name === 'dashboard') {
resources.preload('config', 'userProfile');
}
});
// 图片懒加载
function lazyLoadImages() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
}, { rootMargin: '200px' }); // 提前200px开始加载
document.querySelectorAll('img.lazy').forEach(img => {
observer.observe(img);
});
}
20.3 Web Worker 异步计算
// main.js — 将耗时计算放到 Worker 线程
class WorkerPool {
#workers = [];
#queue = [];
#maxWorkers;
constructor(workerScript, maxWorkers = navigator.hardwareConcurrency || 4) {
this.#maxWorkers = maxWorkers;
for (let i = 0; i < maxWorkers; i++) {
this.#workers.push({
worker: new Worker(workerScript),
busy: false
});
}
}
execute(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
const freeWorker = this.#workers.find(w => !w.busy);
if (freeWorker) {
this.#runTask(freeWorker, task);
} else {
this.#queue.push(task);
}
});
}
#runTask(workerInfo, task) {
workerInfo.busy = true;
const handleMessage = (e) => {
workerInfo.worker.removeEventListener('message', handleMessage);
workerInfo.worker.removeEventListener('error', handleError);
workerInfo.busy = false;
task.resolve(e.data);
// 处理队列中的下一个任务
if (this.#queue.length > 0) {
const nextTask = this.#queue.shift();
this.#runTask(workerInfo, nextTask);
}
};
const handleError = (e) => {
workerInfo.worker.removeEventListener('message', handleMessage);
workerInfo.worker.removeEventListener('error', handleError);
workerInfo.busy = false;
task.reject(e.error || new Error(e.message));
};
workerInfo.worker.addEventListener('message', handleMessage);
workerInfo.worker.addEventListener('error', handleError);
workerInfo.worker.postMessage(task.data);
}
terminate() {
this.#workers.forEach(w => w.worker.terminate());
}
}
// worker.js
// self.onmessage = function(e) {
// const { type, payload } = e.data;
//
// switch (type) {
// case 'heavyComputation':
// const result = performHeavyWork(payload);
// self.postMessage(result);
// break;
// }
// };
// 使用
const pool = new WorkerPool('worker.js', 4);
const results = await Promise.all([
pool.execute({ type: 'heavyComputation', payload: data1 }),
pool.execute({ type: 'heavyComputation', payload: data2 }),
pool.execute({ type: 'heavyComputation', payload: data3 }),
]);
21. 常见陷阱与调试
21.1 常见陷阱
// 陷阱1:忘记 await
async function trap1() {
const promise = fetchData(); // ❌ 忘记 await
console.log(promise); // Promise {<pending>},不是数据!
const data = await fetchData(); // ✅
console.log(data); // 实际数据
}
// 陷阱2:forEach 中使用 async/await
async function trap2() {
const ids = [1, 2, 3];
// ❌ forEach 不会等待 async 回调
ids.forEach(async (id) => {
const data = await fetchData(id);
console.log(data);
});
console.log("完成"); // 在所有 fetchData 之前执行!
// ✅ 使用 for...of
for (const id of ids) {
const data = await fetchData(id);
console.log(data);
}
console.log("完成"); // 在所有 fetchData 之后执行
// ✅ 或 Promise.all + map(并行)
await Promise.all(ids.map(async (id) => {
const data = await fetchData(id);
console.log(data);
}));
console.log("完成");
}
// 陷阱3:async 函数中的返回值
async function trap3() {
// ❌ 在 try/catch 的 catch 中 return 但忘记前面的逻辑可能已执行
try {
const data = await riskyOperation();
updateUI(data);
return data;
} catch (err) {
return null; // 但 updateUI 可能已经部分执行了!
}
}
// 陷阱4:Promise 构造函数中的异步操作
// ❌ 在 Promise 构造函数中使用 async
const badPromise = new Promise(async (resolve, reject) => {
try {
const data = await fetchData();
resolve(data);
} catch (err) {
// 如果这里抛出错误,不会被外部 catch 到!
reject(err);
}
});
// ✅ 直接使用 async 函数
async function goodApproach() {
return await fetchData();
}
// 陷阱5:竞态条件
let currentData = null;
async function trap5(query) {
// ❌ 快速调用可能导致旧请求覆盖新请求
const data = await search(query);
currentData = data; // 如果之前的请求比后面的慢,会覆盖新数据
}
// ✅ 使用请求 ID 或 AbortController
let requestCounter = 0;
async function safeSearch(query) {
const myRequestId = ++requestCounter;
const data = await search(query);
if (myRequestId === requestCounter) {
currentData = data; // 只使用最新请求的结果
}
}
// 陷阱6:内存泄漏
class trap6Component {
constructor() {
this.controller = new AbortController();
}
async loadData() {
try {
const data = await fetch('/api/data', {
signal: this.controller.signal
});
this.render(data);
} catch (err) {
if (err.name !== 'AbortError') throw err;
}
}
// ✅ 组件销毁时取消未完成的请求
destroy() {
this.controller.abort();
}
}
// 陷阱7:错误吞噬
async function trap7() {
// ❌ catch 后不重新抛出,调用者不知道出错了
try {
await riskyOperation();
} catch (err) {
console.error(err); // 只是打印,没有抛出
}
// 调用者以为一切正常...
// ✅ 要么重新抛出,要么返回明确的错误标志
try {
await riskyOperation();
} catch (err) {
console.error(err);
throw err; // 重新抛出让调用者知道
}
}
21.2 调试技巧
// 1. 使用 async stack traces
// Chrome DevTools → Settings → Enable async stack traces
// 2. 给 Promise 打标签
function labeledFetch(label, url) {
const promise = fetch(url).then(r => r.json());
promise.label = label; // 调试用
return promise;
}
// 3. 日志包装器
function traced(fn, name) {
return async function(...args) {
const id = Math.random().toString(36).slice(2, 8);
console.log(`[${name}:${id}] 开始`, args);
const start = performance.now();
try {
const result = await fn.apply(this, args);
const duration = (performance.now() - start).toFixed(2);
console.log(`[${name}:${id}] 完成 (${duration}ms)`, result);
return result;
} catch (err) {
const duration = (performance.now() - start).toFixed(2);
console.error(`[${name}:${id}] 失败 (${duration}ms)`, err);
throw err;
}
};
}
const tracedFetch = traced(
(url) => fetch(url).then(r => r.json()),
'API'
);
await tracedFetch('/api/users');
// [API:k3m2n1] 开始 ["/api/users"]
// [API:k3m2n1] 完成 (234.56ms) [{...}, {...}]
// 4. Promise 状态检查
async function inspectPromise(promise) {
const unique = Symbol();
const result = await Promise.race([promise, Promise.resolve(unique)]);
if (result === unique) {
return 'pending';
}
return 'fulfilled';
}
// 5. 性能监测
class PerformanceTracker {
#marks = new Map();
start(label) {
this.#marks.set(label, performance.now());
}
end(label) {
const start = this.#marks.get(label);
if (!start) throw new Error(`No start mark for: ${label}`);
const duration = performance.now() - start;
this.#marks.delete(label);
console.log(`⏱️ ${label}: ${duration.toFixed(2)}ms`);
return duration;
}
async measure(label, fn) {
this.start(label);
try {
return await fn();
} finally {
this.end(label);
}
}
}
const perf = new PerformanceTracker();
await perf.measure('加载用户数据', async () => {
return fetch('/api/users').then(r => r.json());
});
// ⏱️ 加载用户数据: 156.78ms
22. 完整知识图谱总结
JavaScript 异步编程知识图谱
│
├── 基础概念
│ ├── 单线程模型
│ ├── 事件循环 ★★★
│ │ ├── 调用栈 (Call Stack)
│ │ ├── 宏任务队列 (setTimeout, setInterval, I/O)
│ │ ├── 微任务队列 (Promise.then, queueMicrotask, MutationObserver)
│ │ └── 执行顺序:同步 → 微任务(全部) → 宏任务(一个) → 微任务(全部) → ...
│ └── Node.js 事件循环(6个阶段)
│
├── 回调函数
│ ├── 错误优先回调
│ ├── 回调地狱
│ └── 控制反转问题
│
├── Promise ★★★
│ ├── 三种状态:pending → fulfilled / rejected
│ ├── 链式调用(.then 返回新 Promise)
│ ├── 错误处理(.catch 错误传播)
│ ├── 静态方法
│ │ ├── Promise.all (全部成功)
│ │ ├── Promise.allSettled (全部完成)
│ │ ├── Promise.race (最快)
│ │ ├── Promise.any (第一个成功)
│ │ └── Promise.withResolvers (ES2024)
│ └── 手写 Promise(面试)
│
├── Async/Await ★★★
│ ├── async 函数返回 Promise
│ ├── await 暂停执行等待 Promise
│ ├── 错误处理(try/catch)
│ ├── 串行 vs 并行
│ └── 循环中的 await
│
├── 高级模式
│ ├── Generator + 自动执行器
│ ├── 异步迭代器 (for await...of)
│ ├── 并发控制(限流器、Promise池)
│ ├── AbortController(取消操作)
│ ├── 发布/订阅模式
│ └── Observable(响应式编程)
│
└── 实战技巧
├── 重试机制(指数退避)
├── 请求去重与缓存
├── 防抖与节流
├── 竞态条件处理
├── Web Worker
└── 常见陷阱
├── forEach 中的 async
├── 忘记 await
├── 未处理的 rejection
└── 内存泄漏
学习路线建议:
- 入门:理解同步/异步 → 事件循环 → 回调
- 基础:Promise 创建/消费 → 链式调用 → 错误处理
- 进阶:async/await → 并行/串行 → 静态方法
- 高级:并发控制 → 取消操作 → Generator/异步迭代
- 精通:手写 Promise → 架构设计 → 性能优化 → 响应式编程
CSS Grid 案例
CSS Grid 案例详解
1、min-content、max-content、auto 空间计算
单列 auto 布局:内容宽度决定列宽,剩余空间自动分配
![]()
双列 auto 布局:两列宽度根据内容自动调整
![]()
三列 auto 布局:多列布局时内容宽度的分配方式
![]()
四列 auto 布局:列数增加时的空间分配逻辑
![]()
max-content:列宽等于内容最大宽度,不换行
![]()
min-content:列宽等于内容最小宽度,尽可能换行
![]()
min-content 最小宽度,max-content 完整内容宽度, auto 自动分配剩余空间 列宽等于内容最小宽度,尽可能换行
<body>
<div class="grid">
<div class="item">内容1</div>
<div class="item">内容2</div>
<div class="item">内容3</div>
<div class="item">内容4</div>
</div>
</body>
<style>
.grid {
display: grid;
/* 左(自动分配剩余)
右(内容最大或最小) */
grid-template-columns: auto max-content;
/* 单列,两列,三列,四列 */
grid-template-columns: auto;
grid-template-columns: auto auto;
grid-template-columns: auto auto auto;
grid-template-columns: auto auto auto auto;
grid-template-columns: auto min-content;
/* 加间距,方便看列的边界 */
grid-column-gap: 5px;
grid-row-gap: 5px;
background-color: #c1c1c1;
padding: 5px;
}
.item {
/* 加背景,直观区分列 */
border: 1px solid #000;
padding: 2px;
}
</style>
<!--
★ auto 特性:优先适应父容器宽度,剩余空间自动分配,内容超出时会换行
★ max-content:列宽=内容不换行时的最大宽度(内容不会折行)
★ min-content:列宽=内容能折行时的最小宽度(内容尽可能折行收缩)
★ 多列 auto:列数=auto的个数,列宽优先适配自身内容,剩余空间平分
-->
2、space-between
![]()
<div class="container">
<div class="item">用户123</div>
<div class="item">很多内容</div>
<div class="item">内容</div>
<div class="item">很多内容很多内容</div>
</div>
<style>
.container {
width: 300px;
display: grid;
grid-template-columns: auto auto;
/** 两个内容自动撑开, 左右排列 **/
justify-content: space-between;
background-color: blueviolet;
}
.item {
background-color: skyblue;
}
</style>
3、auto-fit、minmax 填满剩余空间
单列布局,当容器宽度不足时自动调整为单列
![]()
两列布局:容器宽度足够时自动扩展为多列
![]()
三列布局:自适应容器宽度的多列排列
![]()
四列布局:充分利用可用空间的自适应布局
![]()
<main>
<div></div>
<div></div>
<div></div>
<div></div>
</main>
<style>
main {
display: grid;
grid-gap: 5px;
/* 1、minmax 定义尺寸范围 */
/* 2、
auto-fill 在宽度足够的条件下预留了空白
auto-fit 在宽度足够的条件下充分利用空间
*/
grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
border: 3px dashed;
}
div {
background-color: deepskyblue;
height: 100px;
}
</style>
4、grid 命名格子
使用grid-template-areas,通过命名区域实现复杂的网格布局
![]()
代码
<div class="container">
<div class="item putao">葡萄</div>
<div class="item longxia">龙虾</div>
<div class="item yangyu">养鱼</div>
<div class="item xigua">西瓜</div>
</div>
<style>
.container {
height: 400px;
display: grid;
/* grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(4, 1fr);
grid-template-areas:
"葡萄 葡萄 葡萄"
"龙虾 养鱼 养鱼"
"龙虾 养鱼 养鱼"
"西瓜 西瓜 西瓜"
; */
/* 简写 */
grid: "葡萄 葡萄 葡萄" 1fr "龙虾 养鱼 养鱼" 1fr "龙虾 养鱼 养鱼" 1fr "西瓜 西瓜 西瓜" 1fr / 1fr 1fr 1fr;
}
.putao {
grid-area: 葡萄;
background-color: greenyellow;
}
.longxia {
grid-area: 龙虾;
background-color: plum;
}
.yangyu {
grid-area: 养鱼;
background-color: slategray;
}
.xigua {
grid-area: 西瓜;
background-color: crimson;
}
.container .item {
display: flex;
align-items: center;
justify-content: center;
}
</style>
5、隐形网格
隐式网格: 超出显式网格范围的项目自动创建新行
代码
<div class="container">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5隐式网格</div>
</div>
<style>
.container {
display: grid;
grid: 1fr 1fr/1fr 1fr;
grid-auto-rows: 100px;
}
.item:nth-of-type(1) {
background-color: rgb(239, 255, 170);
}
.item:nth-of-type(2) {
background-color: rgb(182, 184, 255);
}
.item:nth-of-type(3) {
background-color: rgb(255, 195, 253);
}
.item:nth-of-type(4) {
background-color: rgb(210, 255, 179);
}
.item:nth-of-type(5) {
background-color: rgb(185, 185, 185);
}
</style>
6、隐式网格2
隐式网格的列布局: grid-auto-columns设置隐式列的宽度
![]()
<div class="container2">
<div class="item-a">a</div>
<div class="item-b">b</div>
</div>
<style>
.container2 {
display: grid;
grid-template: 1fr 1fr/1fr 1fr;
/* 额外列(第 3 列 +)宽度固定 60px; */
grid-auto-columns: 60px;
border: 1px solid #000;
}
.item-b {
/* 让 item-b 占第 3 列(第 3-4 根列线之间) */
grid-column: 3/4;
background-color: aqua;
}
</style>
7、grid-auto-flow 流向
默认row 流向: 左到右、从上到下排列
![]()
column流向: 上到下、从左到右排列
![]()
<div class="container">
<div class="item">格子1</div>
<div class="item">格子2</div>
<div class="item">格子3</div>
<div class="item">格子4</div>
<div class="item">格子5</div>
<div class="item">格子6</div>
<div class="item">格子7</div>
<div class="item">格子8</div>
<div class="item">格子9</div>
</div>
<style>
.container{
display: grid;
line-height: 40px;
background-color: skyblue;
/* 默认流向是左往右,再下一行左往右,重复前面方式 */
grid-template-columns: 1fr 1fr;
grid-auto-flow: row;
/* 换一个方向,上往下 */
/* grid-template-rows: 1fr 1fr;
grid-auto-flow: column; */
}
.item{
outline: 1px dotted;
}
</style>
8、grid-auto-flow 图片案例
第一个图片占据两行空间,其他图片自动排列
<div class="container">
<div class="item"><img src="./图片/shu.webp"></div>
<div class="item"><img src="./图片/1.webp"></div>
<div class="item"><img src="./图片/2.webp"></div>
<div class="item"><img src="./图片/3.webp"></div>
<div class="item"><img src="./图片/4.webp"></div>
</div>
<style>
.container {
display: grid;
background-color: skyblue;
/* 1. 修复:去掉 grid-auto-flow: column,用默认 row 按行排列 */
/* 2. 修复:用 minmax(0, 1fr) 防止单元格被图片撑大 */
grid-template:
"a . ." minmax(0, 1fr)
"a . ." minmax(0, 1fr)
/ minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
grid-gap: 6px;
}
/* 第一个元素占网格区域 a(2行1列) */
.container .item:first-child {
grid-area: a;
}
/* 修复:图片不溢出、不变形 */
.container img {
display: block;
width: 100%;
height: 100%;
object-fit: cover; /* 按比例裁剪,填满单元格 */
}
</style>
9、dense 填补
dense填补: 自动填补空缺位置,优化空间利用
![]()
<div class="container">
<div class="item">各自1</div>
<div class="item">各自2</div>
<div class="item">各自3</div>
<div class="item">各自4</div>
<div class="item">各自5</div>
<div class="item">各自6</div>
<div class="item">各自7</div>
<div class="item">各自8</div>
<div class="item">各自9</div>
</div>
<style>
.container {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-auto-flow: dense;
}
.container .item {
outline: 1px dotted;
}
/* 模拟空缺位置 */
.container .item:first-child {
grid-column-start: 2;
}
</style>
10、grid 简写
<style>
/*
1、 none 表示设置所有子项属性值为初始化值
grid:none
2、template
单独:
grid-template-rows:100px 300px;
grid-template-columns:3fr 1fr;
简写:
grid:100px 300px / 3fr 1fr;
4、auto-flow
单独
grid-template-rows: 100px 300px;
grid-auto-flow: column;
grid-auto-columns: 200px;
合并
grid: 100px 300px / auto-flow 200px;
3、template-areas
完整:
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(4, 1fr);
grid-template-areas:
"葡萄 葡萄 葡萄"
"龙虾 养鱼 养鱼"
"龙虾 养鱼 养鱼"
"西瓜 西瓜 西瓜";
简写:
grid:
"葡萄 葡萄 葡萄" 1fr
"龙虾 养鱼 养鱼" 1fr
"龙虾 养鱼 养鱼" 1fr
"西瓜 西瓜 西瓜" 1fr
/ 1fr 1fr 1fr;
*/
</style>
11、place-items 完整写法
![]()
place-items:项目在单元格内的居中对齐方式
<div class="container">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
</div>
<style>
.container {
display: grid;
grid: 1fr 1fr/1fr 1fr;
height: 300px;
background-color: skyblue;
/* 单个,类似flex
justify-content: center;
align-items: center;
*/
/* 合并 */
place-items: center right;
}
.item{
outline: 1px solid;
}
</style>
12、layout栏布局
经典的页面布局: 包含头部、侧边栏、主内容和底部四个区域
![]()
<style>
.grid-container {
width: 500px;
height: 400px;
margin: 20px auto;
display: grid;
/* 精简核心:去掉重复线名,换行简化,保留关键命名 */
grid-template:
[r1] "header header header" 1fr [r2]
[r2] "sidebar main main" 2fr [r3]
[r3] "sidebar footer footer" 1fr [r4]
/ [c1] 150px [c2] 1fr [c3] 1fr [c4];
gap: 8px;
}
/* 子项定位(极简写法) */
.grid-container>div{display: flex;justify-content: center;align-items: center;}
.header { grid-area: header; background: #409eff; color: #fff; }
.sidebar { grid-row: r2/r4; grid-column: c1/c2; background: #67c23a; color: #fff; }
.main { grid-area: main; background: #e6a23c; color: #fff; }
.footer { grid-row: r3/r4; grid-column: c2/c4; background: #f56c6c; color: #fff; }
</style>
<body>
<div class="grid-container">
<div class="header">头部</div>
<div class="sidebar">侧边栏</div>
<div class="main">主内容</div>
<div class="footer">底部</div>
</div>
</body>
<!-- Grid 布局示例,含命名网格线 + 区域命名,3 行 3 列分头部 / 侧边 / 主内容 / 底部 -->
13、grid-area 线条版
grid重叠: 图片和标题在同一网格单元格内叠加
<figure>
<img src="./图片/13.png">
<figcaption>自然风景</figcaption>
</figure>
<style>
figure {
display: grid;
}
img {
width: 100%;
}
figure>img,
figure>figcaption {
grid-area: 1 / 1 / 2 / 2;
}
figure>figcaption {
align-self: end;
text-align: center;
background: #0009;
color: #fff;
line-height: 2;
}
</style>
附录
参考资源
- 《CSS新世界》- 张鑫旭
CSS 伪元素选择器:为元素披上魔法的斗篷
CSS 伪元素选择器:为元素披上魔法的斗篷
在 CSS 的魔法世界中,有一项特别的能力——伪元素选择器。它就像哈利·波特的隐形斗篷,让你不必修改 HTML 结构,就能凭空创造出新的视觉元素。今天,就让我们一起揭开这项魔法技艺的神秘面纱。
🎭 什么是伪元素选择器?
伪元素(Pseudo-element)是 CSS 提供的一种特殊选择器,允许你选择元素的特定部分,或者在元素内容周围插入虚拟的 DOM 节点。它们不是真正的 HTML 元素,而是通过 CSS 渲染出来的“影子元素”。
最核心的魔法咒语有两个:
-
::before- 在元素内容之前创建伪元素 -
::after- 在元素内容之后创建伪元素
📝 语法小贴士:现代 CSS 推荐使用双冒号
::(如::before),以区别于伪类的单冒号:。但单冒号:before也仍然有效。
🔮 基础咒语:content 属性
要施展伪元素魔法,必须先念出核心咒语——**content** 属性。没有它,伪元素就不会显形。
css
css
复制
.魔法帽子::before {
content: "🎩"; /* 必须的咒语! */
margin-right: 8px;
}
content可以接受多种“魔法材料”:
-
字符串文本:
content: "→ ";(添加箭头) -
空字符串:
content: "";(纯装饰元素) -
属性值:
content: attr(data-tip);(读取 HTML 属性) -
计数器:
content: counter(chapter);(自动编号) -
图片:
content: url(icon.png);
✨ 实战魔法秀
魔法一:优雅的装饰线条
代码示例:
css
css
复制
.card .header::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 80rpx;
border-bottom: 4rpx solid #000;
}
这是标题装饰线的经典用法。想象一下,你的标题下方自动长出了一条精致的小横线,就像绅士西装上的口袋巾,既优雅又不过分张扬。
魔法二:自动化的引用标记
css
css
复制
blockquote::before {
content: "“"; /* 开引号 */
font-size: 3em;
color: #e74c3c;
vertical-align: -0.4em;
margin-right: 10px;
}
blockquote::after {
content: "”"; /* 闭引号 */
font-size: 3em;
color: #e74c3c;
vertical-align: -0.4em;
margin-left: 10px;
}
现在你的 <blockquote>元素会自动戴上红色的巨大引号,仿佛是文学作品中的点睛之笔。
魔法三:视觉引导箭头
css
css
复制
.dropdown::after {
content: "▾"; /* 向下箭头 */
display: inline-block;
margin-left: 8px;
transition: transform 0.3s;
}
.dropdown.open::after {
transform: rotate(180deg); /* 点击时箭头翻转 */
}
导航菜单的交互指示器就此诞生!用户点击时,箭头会优雅地旋转,指示状态变化。
魔法四:清浮动(经典技巧)
css
css
复制
.clearfix::after {
content: "";
display: block;
clear: both;
}
这个古老的魔法曾拯救了无数布局。它在浮动元素后面插入一个看不见的“清扫工”,确保父元素能正确包裹子元素。
🎨 伪元素的艺术:超越 ::before 和 ::after
除了最常用的两个,伪元素家族还有其他成员:
::first-letter- 首字母魔法
css
css
复制
article p::first-letter {
font-size: 2.5em;
float: left;
line-height: 1;
margin-right: 8px;
color: #2c3e50;
font-weight: bold;
}
让段落首字母变得像中世纪手抄本一样华丽,瞬间提升文章的视觉档次。
::first-line- 首行高亮
css
css
复制
.poem::first-line {
font-variant: small-caps; /* 小型大写字母 */
letter-spacing: 1px;
color: #8e44ad;
}
诗歌的首行会以特殊样式呈现,就像歌剧中主角的第一次亮相。
::selection- 选择区域染色
css
css
复制
::selection {
background-color: #3498db;
color: white;
text-shadow: none;
}
用户选中文本时,背景会变成优雅的蓝色,而不是默认的灰蓝色。
::placeholder- 输入框占位符美化
css
css
复制
input::placeholder {
color: #95a5a6;
font-style: italic;
opacity: 0.8;
}
让表单的提示文字更加柔和友好。
⚡ 伪元素的超能力
能力一:Z 轴分层
伪元素拥有独立的堆叠上下文,可以创造出精美的多层效果:
css
css
复制
.button {
position: relative;
background: #3498db;
color: white;
padding: 12px 24px;
border: none;
}
.button::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(135deg, transparent 30%, rgba(255,255,255,0.3) 100%);
border-radius: inherit;
z-index: 1;
}
这个按钮表面有一层半透明的渐变光泽,就像刚打过蜡的汽车漆面。
能力二:动画与过渡
伪元素完全可以动起来!
css
css
复制
.loading::after {
content: "";
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid #ddd;
border-top-color: #3498db;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
一个简约而不简单的加载动画,无需任何额外的 HTML 标签。
能力三:复杂的图形绘制
利用边框技巧,伪元素可以绘制各种形状:
css
css
复制
.tooltip::before {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
/* 绘制三角形 */
border: 6px solid transparent;
border-top-color: #333;
}
这是工具提示框的小箭头,纯 CSS 实现,无需图片。
🚫 伪元素的禁忌与限制
魔法虽强,也有规则:
-
content是必需的:没有它,伪元素不显现 -
某些属性不可用:伪元素不能应用
content属性本身 -
不能用于替换元素:如
<img>、<input>、<textarea>等 - SEO 不可见:搜索引擎看不到伪元素的内容
- 可访问性注意:屏幕阅读器可能不会读取伪元素内容
💡 最佳实践指南
何时使用伪元素?
✅ 适合:
- 纯粹的视觉装饰(图标、线条、形状)
- 不需要交互的 UI 元素
- 内容前后的固定标记
- 不影响语义的样式增强
❌ 不适合:
- 重要的交互内容(应使用真实元素)
- 需要被搜索引擎收录的内容
- 复杂的、需要维护的动态内容
性能小贴士
css
css
复制
/* 良好实践:减少重绘 */
.decorative::after {
content: "";
will-change: transform; /* 提示浏览器优化 */
transform: translateZ(0); /* 触发 GPU 加速 */
}
/* 避免过度使用 */
/* 每个元素都加伪元素会影响性能 */
🌈 结语:魔法的艺术
伪元素选择器是 CSS 工具箱中的瑞士军刀——小巧、锋利、用途广泛。它们代表了关注点分离的优雅理念:HTML 负责结构,CSS 负责表现。
就像画家不会在画布上固定装饰品,而是在画作上直接绘制光影效果一样,优秀的开发者懂得使用伪元素来增强界面,而不是堆砌冗余的 HTML 标签。
记住:最好的魔法往往是看不见的魔法。当用户觉得“这个界面就是应该长这样”,而不是“这里加了个小图标”时,你就掌握了伪元素的真正精髓。
现在,拿起你的 CSS 魔杖,去创造一些神奇的界面吧!记住这句魔咒: “内容在前,装饰在后,语义清晰,表现灵活” ——这就是伪元素哲学的核心。 ✨
WebMCP 实战指南:让你的网站瞬间变成 AI 的“大脑外挂”
一、 AI 终于不用“瞎猜”你的网页了
我们可以把 WebMCP 想象成一种**“翻译官协议”**:
- 以前的 AI(视觉模拟派) :就像一个老外在看一份全中文的报纸,他得先拍照,再识别文字,最后猜哪里是按钮。一旦你把按钮从左边挪到右边,他就找不到了。
- WebMCP(接口直连派) :你的网站现在给 AI 提供了一个**“操作说明书”**。AI 进门后不用看页面长什么样,直接问:“那个‘查询余额’的功能在哪?” 你的网站直接通过 WebMCP 告诉它:“在这里,发个 JSON 给我,我就告诉你结果。”
一句话总结:WebMCP 让网页从“给人看的界面”变成了“给 AI 调用的函数”。
二、 核心能力:WebMCP 的“两把斧”
在实际开发中,WebMCP 提供了两种接入方式:
- 宣告式(适合简单动作) :在 HTML 里加个属性,就像给按钮贴个“AI 可读”的标签。
- 命令式(适合高级逻辑) :用 JavaScript 编写具体的执行函数,适合处理复杂计算。
三、 实战:WebMCP 的具体使用方法
目前,你可以在 Chrome Canary (v145+) 中通过以下步骤实现一个“AI 自动分析监控日志”的功能。
1. 开启实验室开关
在浏览器地址栏输入:chrome://flags/#enable-webmcp,将其设置为 Enabled 并重启。
2. 定义“说明书” (mcp-config.json)
在你的网站根目录放置一个配置文件,告诉 AI 你有哪些能力。
JSON
{
"tools": [
{
"name": "get_frontend_error_logs",
"description": "获取当前页面的前端错误日志详情",
"parameters": {
"type": "object",
"properties": {
"limit": { "type": "number", "description": "返回的日志数量" }
}
}
}
]
}
3. JavaScript 具体实现逻辑
在你的网页脚本中,注册这个工具的具体执行逻辑。
JavaScript
// 检查浏览器是否支持 WebMCP
if ('modelContext' in navigator) {
// 注册工具
navigator.modelContext.registerTool('get_frontend_error_logs', async (args) => {
// 1. 从你的监控系统中提取数据
const logs = window.__MY_MONITOR_LOGS__.slice(0, args.limit || 5);
// 2. 返回给 AI
return {
content: [
{
type: "text",
text: JSON.stringify(logs, null, 2)
}
]
};
});
console.log("WebMCP 工具已就绪,AI 现在可以直接读取你的日志了!");
}
四、 极致的 Token 优化:从“读图”到“读字典”
web系统经常有成千上万行的表格数据,让 AI 截图识别简直是灾难。
1. 结构化数据的降维打击
- 视觉识别:1 张 1080P 的截图可能消耗 1000+ Tokens,且 AI 经常看错行。
-
WebMCP:你返回一个
JSON.stringify(summary)仅需几十个 Tokens。 -
工程技巧:在
registerTool的返回结果中,你可以预先对数据进行特征提取(比如只返回异常波动点),将原本需要 AI 自己总结的过程,在本地通过高性能 JS 预处理完成,进一步压榨 Token 成本。
五、 交互新范式:从“被动响应”到“双向协同”
在你的实战代码中,展示了 AI 如何“主动”调数据,但 WebMCP 的真正威力在于它构建了一个双向总线。
1. 订阅模式:让 AI 实时盯盘
在金融监控或 K 线分析场景,AI 不应该总在问“现在价格是多少”,而应该是当价格波动超过阈值时,网站主动推送到 AI 上下文。
- 扩展用法:利用 WebMCP 的资源订阅(Resources Subscription)机制。
-
代码逻辑:通过
navigator.modelContext.updateResource(),当监控系统发现异常流量或金融数据触发对冲点时,自动将上下文注入 AI 的实时缓存,实现“无感预警”。
六、 安全深度防御:权限隔离
你一定关心:万一 AI 乱发指令怎么办?WebMCP 引入了显式授权与沙箱隔离。
1. 权限最小化原则
WebMCP 不会默认给 AI 权限去读你的整个 LocalStorage 或 Cookie。
-
层级控制:每一个
registerTool注册的功能,都会在 Chrome 侧边栏显示详细的权限说明。 -
人类确认 (HITL) :对于涉及敏感操作(如:执行转账、删除线上日志)的 Tool,WebMCP 支持声明
userApproval: "required"。当 AI 尝试调用时,浏览器会跳出原生确认框,这种系统级的阻断是任何第三方插件无法模拟的安全保障。
七、 架构解耦:一套标准,适配所有 AI 终端
你目前在研究 AI Prompt Manager 和 Trae/Cursor。WebMCP 的出现解决了前端工程中的“适配地狱”。
1. “一次开发,到处运行”的 AI 能力
- 旧模式:你得写一个 Chrome 插件给 Gemini 用,再写一个 MCP Server 给 Claude Desktop 用,再给 Cursor 写特定的插件。
-
WebMCP 模式:你只需要在网页里
registerTool。由于 Chrome 是宿主,只要你在 Chrome 里打开这个网页,无论是侧边栏的 Gemini,还是通过 DevTools 接入的 AI Agent,都能识别并使用这套能力。这极大降低了你维护 AI 基础设施 的成本。
八、为什么非用它不可?
- 性能屠宰场:不再需要给 AI 发送几万个 DOM 节点的 HTML 文本,只传核心 JSON,Token 消耗节省 90%。
- 安全围栏:数据处理在本地浏览器完成。大模型只发指令,不直接接触你的敏感数据库明细。
- 开发效率:你不再需要为不同的 AI 插件写不同的适配层,只要符合 WebMCP 标准,所有支持该协议的 AI 助手都能秒懂你的业务。
源码回溯的艺术:SourceMap 底层 VLQ 编码与离线解析架构实战
对于正在自研监控系统的架构师来说,SourceMap 绝不仅是一个调试工具,它是线上治理的“黑匣子”。
如果你的监控系统只能报出 at a.js:1:1234 这种“天书”,那它和盲人摸象没有区别。要实现“一眼定位代码行”,不仅需要理解其底层的编码协议,更要构建一套工业级的自动化闭环体系,在确保源码安全的同时,抗住海量错误冲击下的解析压力。
一、 协议拆解:SourceMap 为什么要搞得这么复杂?
混淆压缩(Minification)的目的是为了极致的传输性能,而 SourceMap 的目的是为了极致的调试体验。
1. 为什么不能直接记录映射表?
假设你的源码有 10,000 行,如果简单地用 JSON 记录每一行每一列的对应关系,这个 .map 文件可能会达到几十 MB。为了解决体积问题,SourceMap 引入了三个层级的压缩逻辑:
-
层级一:分组压缩。它将
mappings字段按行(用分号;分隔)和位置点(用逗号,分隔)进行切分。 -
层级二:相对偏移。不记录绝对坐标
[100, 200],而是记录相对于前一个点的增量[+5, +10]。 - 层级三:VLQ 编码。将这些增量数字转换成极短的字符序列。
2. 揭秘 VLQ (Variable-Length Quantity) 编码
VLQ 是一种针对整数的变长编码方案。它的核心思想是:用 6 位(一个 Base64 字符)作为基本单元,其中 1 位表示是否有后续单元,1 位表示正负号,剩下 4 位存数值。
- 极致紧凑:对于小的数字(如偏移量通常很小),它只需要 1 个字符就能表示。这让数万个映射点压缩到几百 KB 成为可能。
二、 工业级离线解析架构:安全性与性能的博弈
作为架构师,你必须坚守一条底线:SourceMap 永远不能出现在生产环境的 CDN 上。一旦泄露,混淆后的代码将毫无秘密可言。
1. CI/CD 流程中的“双轨制”
在自动化构建流程中,我们需要建立一套同步机制:
-
外轨(公开) :生成的
.js文件正常发布,但通过配置(如 Webpack 的hidden-source-map)移除文件末尾的//# sourceMappingURL=声明,确保浏览器不会尝试加载它。 -
内轨(私有) :生成的
.map文件通过 API 自动上传到监控系统的私有存储服务器(如 MinIO 或 S3) 。 - 关联键(Release ID) :每个构建版本必须生成一个唯一的版本号(可以是 Git Commit Hash),并同时注入到前端 SDK 和存储文件名中,确保解析时能“对号入座”。
2. 后端解析引擎:性能瓶颈的突破
如果监控系统并发量极高,解析过程会成为 CPU 黑洞。
-
V8 的局限:传统的
source-mapJS 库在反解析时极其耗时,且内存占用极高。 -
Native 级加速:推荐引入由 Rust 编写的解析库(通过 N-API 接入 Node.js)。例如
oxc-sourcemap或@jridgewell/trace-mapping。这些库利用二进制层面的位运算,解析速度比传统库快一个数量级。 -
多级缓存方案:
- L1(内存) :缓存最近解析过的 SourceMap 对象的实例。
- L2(磁盘缓存) :缓存反解析后的堆栈片段。
-
L3(存储) :原始
.map文件。
三、 实战避坑:那些年老兵踩过的“暗雷”
-
列偏移量的一致性:
有些压缩工具(如早期的 UglifyJS)生成的列号是从 0 开始的,而有些(如某些浏览器报错)是从 1 开始的。在反解析时,必须严格校准这个
0/1的差异,否则还原出来的代码会错位一个字符。 -
异步解析的原子性:
当一个错误高频发生(例如全局报错)时,不要并发去下载同一个
.map文件。利用 Promise 缓存(Singleflight 模式) 确保同一个版本的 Map 文件只被拉取并解析一次。 -
内联(Inline)风险警示:
绝对不要在
webpack.config.js中使用eval或inline开头的devtool配置。这不仅会暴露源码,还会因为 Base64 字符串嵌入导致 JS 运行速度下降 30% 以上。
💡 结语与下一步
SourceMap 解决了“在哪里报错”的问题。但在监控系统的进阶阶段,我们还需要知道“报错时的上下文(上下文变量、网络请求、用户轨迹)”。
每日一题-计数二进制子串🟢
给定一个字符串 s,统计并返回具有相同数量 0 和 1 的非空(连续)子字符串的数量,并且这些子字符串中的所有 0 和所有 1 都是成组连续的。
重复出现(不同位置)的子串也要统计它们出现的次数。
示例 1:
输入:s = "00110011" 输出:6 解释:6 个子串满足具有相同数量的连续 1 和 0 :"0011"、"01"、"1100"、"10"、"0011" 和 "01" 。 注意,一些重复出现的子串(不同位置)要统计它们出现的次数。 另外,"00110011" 不是有效的子串,因为所有的 0(还有 1 )没有组合在一起。
示例 2:
输入:s = "10101" 输出:4 解释:有 4 个子串:"10"、"01"、"10"、"01" ,具有相同数量的连续 1 和 0 。
提示:
1 <= s.length <= 105-
s[i]为'0'或'1'
TypeScript 类型体操练习笔记(二)
进度(90 /188)
其中标记 ※ 的是我认为比较难或者涉及新知识点的题目
刷题也许没有什么意义,但是喜欢一个人思考一整天的灵光一现,也喜欢看到新奇的答案时的恍然大悟,仅此而已。
42. Medium - 1130 - ReplaceKeys ※
实现一个类型 ReplaceKeys,用于替换联合类型中的键,如果某个类型不包含该键则跳过替换。该类型接受三个参数。
一开始我只是想这么写,我想分布式条件类型 + Pick + Omit 来实现。
type ReplaceKeys<U, T, Y> = U extends any
? Omit<U, T & keyof U> & Pick<Y, T & keyof U & keyof Y>
: any
理论上 case1 是能通过的,但是一直报错。然后我又试了一下,看来判断的 Equal 不认为这两种是相等的:
type T1 = { a: number }
type T2 = { b: number }
type E = Equal<T1 & T2, { a: number, b: number }> // false
不过还是有办法的,我们可以通过一层映射把交叉类型拍平:
type IntersectionToObj<T> = {
[K in keyof T]: T[K]
}
type E1 = Equal<IntersectionToObj<T1 & T2>, { a: number, b: number }> // true
不过我试了下第二个 case 还是不太好实现,那就直接用映射类型来解决。
利用分布式特性处理联合元素,然后遍历 U 的属性然后按要求进行处理即可。
type ReplaceKeys<U, T, Y> = U extends any
? {
[K in keyof U]: K extends T ? (K extends keyof Y ? Y[K] : never) : U[K]
}
: never // 不会进入这个分支
但是看到别人的答案我又开始困惑了:
type ReplaceKeys<U, T, Y> = {
[K in keyof U]: K extends T ? (K extends keyof Y ? Y[K] : never) : U[K]
}
形如
{ [P in keyof T]: X }的映射类型(其中T是类型参数)被称为 isomorphic mapped type(同构映射类型),因为它会产生一个与T具有相同结构的类型。通过此 PR,我们使同构映射类型的实例化在联合类型上具有分布性。
43. Medium - 1367 - Remove Index Signature 移除索引签名 ※
实现 RemoveIndexSignature<T>,移除一个对象类型的索引签名。
索引签名(Index Signature) 是 TypeScript 中用于描述对象中未明确声明的属性的类型。它允许你定义一个对象可以有任意数量的属性,只要这些属性的键和值符合指定的类型。
interface StringDictionary {
[key: string]: string; // 索引签名
// 表示该对象可以有任意多个属性,键必须是 string 类型,值也必须是 string 类型。
}
和索引签名对应的是具体属性,这两种也可以混合使用,但是具体属性的类型必须是索引签名类型的子类型:
interface MixedType {
// 具体属性
name: string;
age: number;
// 索引签名
[key: string]: string | number; // 必须包含具体属性的类型
}
要处理这个问题,就要针对索引签名的特点,他是一个宽泛的类型(string/number/symbol),而具体属性是一个字面量类型,比如 "name" ,我们依次判断它是否为 string,number 或 symbol 都不是则证明是具体属性,否则为索引签名。
type RemoveIndexSignature<T> = {
[K in keyof T as
string extends K
? never
: number extends K
? never
: symbol extends K
? never
: K
]: T[K]
}`
在评论区看到一个很天才的解法
type RemoveIndexSignature<T, P = PropertyKey> = {
[K in keyof T as P extends K? never : K extends P ? K : never]: T[K]
}
其中 PropertyKey 上是 TypeScript 的内置类型 type PropertyKey = string | number | symbol;。它的判断过程如下:
P extends K ? never : (K extends P ? K : never) /* P = string | number | symbol */
// becomes
(string | number | symbol) extends K ? never : (K extends P ? K : never)
// becomes
| string extends K ? never : (K extends string ? K : never)
| number extends K ? never : (K extends number ? K : never)
| symbol extends K ? never : (K extends symbol ? K : never)
本质上和我们上面的写法是一样的,但是利用条件类型的分布性,一下子判断了三种类型。୧(๑•̀◡•́๑)૭
44. Medium - 1978 - Percentage Parser 百分比解析器
实现类型 PercentageParser。根据规则 /^(\+|\-)?(\d*)?(\%)?$/ 匹配类型 T。
匹配的结果由三部分组成,分别是:[正负号, 数字, 单位],如果没有匹配,则默认是空字符串。
type Sign = '+' | '-'
type PercentageParser<A extends string> =
A extends `${infer F}%`
/** 存在 % */
? F extends `${infer S extends Sign}${infer N}`
? [S, N, '%']
: ['', F, '%']
/** 不存在 % */
: A extends `${infer S extends Sign}${infer N}`
? [S, N, '']
: ['', A, '']
题目不难,加几个分支判断就可以了。或者这样写优雅一点(大概):
type SignParser<A extends string> = A extends `${infer S extends '+' | '-'}${infer N}` ? [S, N] : ['', A]
type PercentageParser<A extends string> = A extends `${infer F}%` ? [...SignParser<F>, '%'] : [...SignParser<A>, '']
45. Medium - 2070 - Drop Char 删除字符
从字符串中剔除指定字符。
type DropChar<S, C> = S extends `${infer F}${infer R}`
? F extends C
? DropChar<R, C>
: `${F}${DropChar<R, C>}`
: ''
没有新的知识点,简单题。
46. Medium - 2257 - MinusOne 减一 ※
给定一个正整数作为类型的参数,要求返回的类型是该数字减 1。
有点意思的一道题目,没有新的知识点,但是类似于算法中的模拟题。需要递归加不同情况的判断,复杂度较高。
我先想到了一个比较搞的办法,生成长度为 T 的数组,然后移除一个元素,再获取数组长度。
type MakeArray<T extends number, R extends any[] = []> =
R['length'] extends T ? R : MakeArray<T, [...R, any]>
type MinusOne<T extends number> =
MakeArray<T> extends [infer _F, ...infer R] ? R['length'] : never
1000 以内是可行的,但是再大就会出现错误:
type A = MakeArray<1101> // error: 类型实例化过深,且可能无限。ts(2589)
那么只能换一种方法,通过模拟减法的方式实现,枚举最后一位即可,如果最后一位大于 0 则只需要操作最后一位,否则需要递归处理:
type MinusOne2String<T extends string> =
T extends `${infer F}0` // 如果最后一位是0,则把此位改为9,然后递归处理(题目限定了是正数)
? `${MinusOne2String<F>}9`
: T extends `${infer F}9` // 其他情况直接把最后一位减一
? `${F}8`
: T extends `${infer F}8`
? `${F}7`
: T extends `${infer F}7`
? `${F}6`
: T extends `${infer F}6`
? `${F}5`
: T extends `${infer F}5`
? `${F}4`
: T extends `${infer F}4`
? `${F}3`
: T extends `${infer F}3`
? `${F}2`
: T extends `${infer F}2`
? `${F}1`
: T extends `${infer F}1`
? `${F}0`
: '0'
// 100-1=099 这种情况需要删除前导零
type removeLeadZero<T extends string> =
T extends '0' ? '0' : T extends `0${infer R}` ? removeLeadZero<R> : T
// 删除前导零后,转换为数字类型
type MinusOne<T extends number> =
removeLeadZero<MinusOne2String<`${T}`>> extends `${infer X extends number}` ? X : 0
47. Medium - 2595 - PickByType
从 T 中选择可赋值给 U 的属性类型集合。
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never ]: T[K]
}
送分题,知识点前面的题目都有涉及。
48. Medium - 2688 - StartsWith
实现 StartsWith<T, U>,接收两个 string 类型参数,然后判断 T 是否以 U 开头,根据结果返回 true 或 false。
type StartsWith<T extends string, U extends string> =
T extends `${U}${infer F}` ? true : false
送分题+1,模板字符串类型基础。
49. Medium - 2693 - EndsWith
实现 EndsWith<T, U>,接收两个 string 类型参数,然后判断 T 是否以 U 结尾,根据结果返回 true 或 false。
type EndsWith<T extends string, U extends string> =
T extends `${string}${U}` ? true : false
50. Medium - 2757 - PartialByKeys
实现一个通用的 PartialByKeys<T, K>,它接收两个类型参数 T 和 K。
K 指定应设置为可选的 T 的属性集。当没有提供 K 时,它就和普通的 Partial<T> 一样使所有属性都是可选的。
前面已经讲过 IntersectionToObj 这个小技巧,这里就比较简单了,其中 Partial 是内置的工具类型,可以把一个对象类型的全部属性都变成可选。
type IntersectionToObj<T> = {
[K in keyof T]: T[K]
}
type PartialByKeys<T , K extends keyof T = keyof T> =
IntersectionToObj< Omit<T, K> & Partial<Pick<T, K>> >
51. Medium - 2759 - RequiredByKeys
实现一个通用的 RequiredByKeys<T, K>,它接收两个类型参数 T 和 K。
K 指定应设为必选的 T 的属性集。当没有提供 K 时,它就和普通的 Required<T> 一样使所有的属性成为必选的。
和 PartialByKeys 本质上没有什么区别,Required 也是内置的工具类型,可以把一个对象类型的全部属性,用于将一个类型 T 中的所有属性转换为必填属性(即移除其可选性 ?)。
type IntersectionToObj<T> = {
[K in keyof T]: T[K]
}
type RequiredByKeys<T, K extends keyof T = keyof T> =
IntersectionToObj<Omit<T, K> & Required<Pick<T, K>>>
52. Medium - 2793 - Mutable ※
实现一个通用的类型 Mutable<T>,使类型 T 的全部属性可变(非只读)。
这题不难,但是涉及到映射类型的一个语法,之前没有涉及过。mapped-types
type Mutable<T extends object> ={
-readonly [K in keyof T]: T[K]
}
53. Medium - 2852 - OmitByType
从类型 T 中选择不可赋值给 U 的属性成为一个新的类型。
直到了 as 和 Mapped Types 的用法,这也很简单,和之前的 Omit 没什么区别。
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K]
}
54. Medium - 2946 - ObjectEntries
实现 Object.entries 的类型版本。
首先这题需要应用分布式条件类型,所以需要先构造一个由类型key组成的联合类型 U 然后 U extends ... 触发分布式。
type ObjectEntries<T, U = keyof T> =
U extends keyof T ? [U, T[U]] : never
不过有个case 过不去。
type eq = Equal<ObjectEntries<Partial<Model>>, ModelEntries> // false
type o = ObjectEntries<Partial<Model>>
// ["name", string | undefined] | ["age", number | undefined] | ["locations", string[] | null | undefined]
可以看到由于 Partial 导致每个类型都多了一个 undefined。很明显这里需要 Required,但是需要先了解一下它的特性。
type r1 = Required<{ key?: undefined }> // {key: never}
type r2 = Required<{ key: undefined }> // {key: undefined}
type r3 = Required<{ key: string | undefined }> // {key: string | undefined}
type r4 = Required<{ key?: string | undefined }> // {key:string}
可以看到在存在 ? 时,Required 会删除类型中的 undefined,否则不会。
而此题的要求是:如果类型存在 ? 就删除 undefined,但是如果类型只有 undefined 则不处理。我只能说,题本身不难,但是描述的不清楚,只能看用例。
type ObjectEntries<T, U = keyof T> =
U extends keyof T
? [U, [T[U]] extends [undefined] ? undefined : Required<T>[U]]
: never
55. Medium - 3062 - Shift
实现类型版本的 Array.shift。
type Shift<T extends any[]> = T extends [infer F, ...infer R] ? [...R] : []
infer 的基础应用,在最前面的 First of Array 就了解过了。
56. Medium - 3188 - Tuple to Nested Object
给一个只包含字符串类型的元组 T 和一个类型 U 递归构建一个对象。
type TupleToNestedObject<T extends string[], U> =
T extends [infer F extends string, ...infer R extends string[]]
? Record<F, TupleToNestedObject<R, U>> : U
每次提取数组中第一个元素,然后把该元素作为键,递归构造的对象作为值。
57. Medium - 3192 - Reverse
实现类型版本的数组反转 Array.reverse
type Reverse<T extends any[]> = T extends [infer F, ...infer R]
? [...Reverse<R>, F]
: []
使用递归的方式,每次都把第一个元素移到最后一个。
58. Medium - 3196 - Flip Arguments
实现 lodash 中 _.flip 函数的类型版本。
类型转换函数 FlipArguments<T> 要求函数类型 T,并返回一个新的函数类型,该类型具有与 T 相同的返回类型但参数顺序颠倒。
type FlipArguments<T extends Function> =
T extends (...args: infer P) => infer R ? (...args: Reverse<P>) => R : never
通过 infer 获取函数的参数和返回值,并且通过上一题实现的 Reverse 将参数反转。
59. Medium - 3243 - FlattenDepth
递归展开数组至指定深度
首先需要实现一个铺平一次的函数,这个比较简单
type FlattenOnce<A extends any[]> = A extends [infer F, ...infer R]
? F extends any[] ? [...F, ...FlattenOnce<R>] : [F, ...FlattenOnce<R>]
: []
TypeScript 中无法进行数字计算,我们可以通过邪修实现,这点我们在前面的 MinusOne 已经实现,所以这里直接引用 MinusOne 就可以了。
type FlattenDepth<T extends any[], depth extends number = 1> =
depth extends 0 // 判断深度为零,则已经不需要铺平了
? T
: FlattenOnce<T> extends T // 判断是否铺平前后的结果一致,一致则不需要再处理了
? T
: FlattenDepth<FlattenOnce<T>, MinusOne<depth>>
当然我们可以用之前在里面用过的用数组记录数字的方法,只不过 TypeScript 中数组长度有限制,当然这一题中是没有问题的,嵌套最多才 5 层
type FlattenDepth<T extends any[], depth = 1, depArr extends any[] = []> =
depArr['length'] extends depth
? T
: FlattenOnce<T> extends T ? T : FlattenDepth<FlattenOnce<T>, depth, [...depArr, any]>
60. Medium - 3326 - BEM style string
使用块(Block)、元素(Element)、修饰符(Modifier)命名(BEM)是 CSS 中类的一种流行命名约定。
例如,块组件表示为 btn,依赖于块的元素表示为 btn__price,更改块样式的修饰符表示为 btn-big 或 btn__prise-warning。
实现 BEM<B,E,M>,从这三个参数生成字符串并集。其中 B 是字符串文字,E 和 M 是字符串数组(可以为空)。
// 把A和B连接,先把B处理为联合类型,然后用S连接
type JoinWithSeparator<A extends string, B extends string[], S extends string, B2Union extends string = B[number]> =
B2Union extends any ? `${A}${S}${B2Union}` : never
type BEM<B extends string, E extends string[], M extends string[]> =
E['length'] extends 0 // 判断E是否为空
? JoinWithSeparator<B, M, '--'> // 如果E为空, B和M连接
: M['length'] extends 0 // 判断M是否为空
? JoinWithSeparator<B, E, '__'> // E不为空,M为空,把B和M连接
: JoinWithSeparator<JoinWithSeparator<B, E, '__'>, M, '--'> // 都不为空,先把B和E连接,然后再加上M
需要写一个辅助工具类型 JoinWithSeparator 用于连接一个字符和数组,逻辑有点小复杂,已经加了完整注释。
61. Medium - 3376 - InorderTraversal
实现二叉树中序遍历的类型版本。
如果会中序遍历二叉树这题就不难了,不会的可以先学学数据结构。
type InorderTraversal<T extends TreeNode | null> =
T extends null
? []
: [...InorderTraversal<T['left']>, T['val'], ...InorderTraversal<T['right']>]
比较麻烦的是,T extends null 语法无法判断第二个分支中 T 不为空,所以可以反过来,判断 T 是否为 TreeNode。
type InorderTraversal<T extends TreeNode | null> =
T extends TreeNode
? [
...InorderTraversal<T['left']>,
T['val'],
...InorderTraversal<T['right']>
] : []
62. Medium - 4179 - Flip
实现 just-flip-object 的类型版本(把类型的键和值类型反转)。
type Flip<T> = {
[K in keyof T as T[K] extends number | string | boolean ? `${T[K]}` : never]: K
}
为了保证 T[K] 类型正确,加了一个 extends number | string | boolean 的限制。
63. Medium - 4182 - Fibonacci Sequence 斐波那契序列 ※
实现一个通用的 Fibonacci<T>,它接受一个数字 T 并返回其相应的斐波纳契数。
序列开始:1、1、2、3、5、8、13、21、34、55、89、144...
首先斐波纳契公式 f(n)=f(n-1)+f(n-2) 可以递归实现。由于 TypeScript 类型无法使用加法,所以我们通过数组的元素个数来变向进行计算,至于减法可以复用之前实现的 MinusOne。
type FibonacciArray<T extends number, A extends any[] = []> =
T extends 1
? [any]
: T extends 2
? [any]
: [...FibonacciArray<MinusOne<MinusOne<T>>>, ...FibonacciArray<MinusOne<T>>]
type Fibonacci<T extends number> = FibonacciArray<T>['length']
看了下别人的答案,优化空间还是很大的,下面是正向计算,Index 表示计算到了第 n 个数字,Cur 表示 f(n),Prev 表示 f(n-1)
type Fibonacci<
T extends number,
Index extends any[] = [any, any],
Cur extends any[] = [any],
Prev extends any[] = [any]
> =
T extends 1 | 2
? 1
: Index['length'] extends T
? Cur['length']
: Fibonacci<T, [...Index, any], [...Cur, ...Prev], Cur>
64. Medium - 4260 - AllCombinations ※
实现 AllCombinations<S> 类型,该类型返回使用 S 中的字符所组成的所有字符串,每个字符最多使用一次。
type AllCombinations<S extends string, P extends string = ''> =
S extends `${infer F}${infer R}`
? '' | F | `${F}${AllCombinations<`${R}${P}`>}` // S[0] 开头的所有排列情况
| AllCombinations<R, `${P}${F}`> // 除了 S[0] 开头以外的所有情况
: ''
很有意思的题目,实现一个字符串中字符的所有组合。我的解法是 AllCombinations<S, P> 表示获取字符串 ${S}${P} 中,以 S 每个字母开头的全排列组合。
所以 AllCombinations<S, ''> 就是答案,而它等于 S[0] 为开头的所有情况,再加上 AllCombinations<S.split(1), S[0]>(伪代码示例)
而 S[0] 为开头的所有情况,就是求 S[0] 连接剩余字符的全排列,也就是 AllCombinations<S.split(1), ''>
65. Medium - 4425 - Greater Than
在本次挑战中,你需要实现一个类似 T > U 的类型: GreaterThan<T, U>
负数无需考虑。
这种题我可以说不难,就是有点恶心。我不喜欢!下面的代码我加了注释,应该可以看懂。
type LengthOfString<S extends string> = Split<S>['length'];
type FirstOfString<S extends string> = S extends `${infer F}${infer R}`
? F
: never;
type RestOfString<S extends string> = S extends `${infer F}${infer R}`
? R
: never;
type Split<S extends string> = S extends `${infer F}${infer R}`
? [F, ...Split<R>]
: [];
// 比较10以内数字的大小
type GreaterThanDigit<
T extends string,
U extends string,
D extends string = '9876543210'
> = D extends `${infer F}${infer R}` // 从大到小依次比较每一个数字
? F extends U // 如果先匹配了U 则证明T≤U 返回false
? false
: F extends T // 如果先匹配了T 则证明T>U 返回true
? true
: GreaterThanDigit<T, U, R> // 再尝试匹配下一个数字
: false;
type GreaterThanString<
T extends string,
U extends string,
LEN_T extends number = LengthOfString<`${T}`>, // T的长度
LEN_U extends number = LengthOfString<`${U}`>, // U的长度
FIRST_T extends string = FirstOfString<`${T}`>, // T的长度
FIRST_U extends string = FirstOfString<`${U}`> // U的长度
> = LEN_U extends LEN_T // 判断长度是否相同
? LEN_T extends 1 // 判断相同,长度是否为1
? GreaterThanDigit<FIRST_T, FIRST_U> // 长度为1 直接比较首位
: FIRST_T extends FIRST_U // 长度相同,且长度不为1,依次比较每一位,先判断首位
? GreaterThanString<RestOfString<`${T}`>, RestOfString<`${U}`>> // 首位相同 则比较下一位
: GreaterThanDigit<FIRST_T, FIRST_U> // 首位不同,则比较大小
: GreaterThan<LEN_T, LEN_U>; // 如果长度不相同,则长度大的数字更大
type GreaterThan<T extends number, U extends number> = GreaterThanString<
`${T}`,
`${U}`
>;
66. Medium - 4471 - Zip
在这个挑战中,你需要实现一个类型 Zip<T, U>,其中 T 和 U 必须是元组。
就是把所有元组中的第1项组成结果中的第1项,所有元组中的第2项组成结果中的第2项....所有元组中的第n项组成结果中的第n项,比较简单。
type Zip<T extends any[], U extends any[]> = T extends [infer TF, ...infer TR]
? U extends [infer UF, ...infer UR]
? [[TF, UF], ...Zip<TR, UR>]
: []
: [];
67. Medium - 4484 - IsTuple ※
实现类型 IsTuple, 传入类型 T 并返回类型 T 是否为一个元组类型。
A tuple type is another sort of
Arraytype that knows exactly how many elements it contains, and exactly which types it contains at specific positions.元组类型 是另一种“数组”类型,它确切地知道它包含多少元素,以及它在特定位置包含哪些类型。
T extends readonly any[] 判断 T 为数组和元素,添加 readonly 可以兼容 readonly [1] 这种情况。
根据定义可以知道,元组类型的长度是固定的,所以 Tuple['length'] 是一个具体的数字,而数组 A['length'] 是 number。
因此,可以通过 number extends T['length'] 来判断 T 是否为元组而不是数组。
type IsTuple<T> = [T] extends [never]
? false
: T extends readonly any[]
? number extends T['length']
? false
: true
: false;
68. Medium - 4499 - Chunk
你知道 lodash 吗?Chunk 是其中一个非常有用的函数,现在让我们实现它。Chunk<T,N> 接受两个必需的类型参数,T 必须是元组,N 必须是大于等于 1 的整数。
type Chunk<
T extends any[],
N extends number,
Result extends any[] = [],
Current extends any[] = []
> = T extends [infer F, ...infer R] // 判断是否还有元素
? Current['length'] extends N // 有元素,判断当前的块已经满了
? Chunk<R, N, [...Result, Current], [F]> // 如果当前的块已经满了,把它放进结果数组里
: Chunk<R, N, Result, [...Current, F]> // 没有满,就把元素放进当前块
: Current['length'] extends 0 // T中所有元素都处理完了,判断当前块中是否有元素
? Result // 当前块为空的,直接返回结果
: [...Result, Current]; // 否则把当前块放进结果,再返回
69. Medium - 4518 - Fill
Fill 是一个通用的 JavaScript 函数,现在来实现它的类型版本。Fill<T, N, Start?, End?> 接受 4 个参数, T 是一个元组,N 是任意类型, Start and End 是大于等于 0 的整数。把 T 在 [Start.End] 范围内的元素都替换为 N。
type Fill<
T extends unknown[], // 原数组
N, // 要填充的类型
Start extends number = 0, // 开始下标
End extends number = T['length'], // 结束下标
Result extends unknown [] = [], // 结果数组
In extends boolean = false // 是否在[Start,End]范围内
> = T extends [infer F, ...infer R] // T是否存在第一个元素
? Result['length'] extends End // 先判断是否为结束下标
? Fill<R, N, Start, End, [...Result, F], false> // 是结束下标,则证明已经填充完了,后面填充T的内容就行
: Result['length'] extends Start // 不是,判断是否为开始下标
? Fill<R, N, Start, End, [...Result, N], true> // 是开始下标,则填充N,用IN=true表示已经在范围内
: In extends true // 判断是否在[Start,End]范围内
? Fill<R, N, Start, End, [...Result, N], true> // 如果在范围内 则用N填充
: Fill<R, N, Start, End, [...Result, F], false> // 不在范围内 用T中内容填充
: Result // 处理完成,返回结果
70. Medium - 4803 - Trim Right
实现 TrimRight<T>,它接收确定的字符串类型并返回一个新的字符串,其中新返回的字符串删除了原字符串结尾的空白字符串。
type TrimRight<S extends string> =
S extends `${infer F}${' '|'\n'|'\t'}` ? TrimRight<F> : S
71. Medium - 5117 - Without
实现一个像 Lodash.without 函数一样的泛型 Without<T, U>,它接收数组类型的 T 和数字或数组类型的 U 为参数,会返回一个去除 U 中元素的数组 T。
Equal 是玄学,别问,用就完事了。
type Includes<T extends readonly unknown[], U> =
T extends [infer F, ...infer Rest]
? Equal<F, U> extends true ? true : Includes<Rest, U>
: false;
type Without<T extends any[], U extends any> = T extends [infer F, ...infer R]
? U extends any[]
? Includes<U, F> extends true // 如果U是数组类型,使用Includes判断是否包含
? Without<R, U>
: [F, ...Without<R, U>]
: F extends U // 如果U不是数组,直接判断
? Without<R, U>
: [F, ...Without<R, U>]
: []
评论区看到了更好的解法,先转成联合在判断是否包含
type ToUnion<T extends any> = T extends any[] ? T[number] : T;
type Without<T extends any[], U extends any> =
T extends [infer F, ...infer R]
? F extends ToUnion<U>
? Without<R, U>
: [F, ...Without<R, U>]
: [];
72. Medium - 5140 - Without
实现 Math.trunc 的类型版本,它接受字符串或数字,并通过删除所有小数来返回数字的整数部分。
简单的模板字符串模式匹配,注意 '-.3' 这种删除小数点后面的内容后需要手动补 0。
type Trunc<T extends number | string> =
`${T}` extends `${infer F}.${infer _}`
? F extends '-' | ''
? `${F}0`
: F
: `${T}`;
73. Medium - 5153 - IndexOf
实现类型版本的 Array.indexOf,indexOf<T, U> 接受两个参数,数组 T 和任意类型 U 返回 U 在 T 中第一次出现的下标,不存在返回 -1。
type IndexOf<T extends any[], U, Pre extends any[] = []> =
T extends [infer F, ...infer R]
? Equal<F, U> extends true
? Pre['length']
: IndexOf<R, U, [...Pre, F]>
: -1
因为 TypeScript 无法进行计算,所以思路还是一样,用一个数组 Pre 记录已经遍历了几个数字,用 Pre['length'] 计数。
74. Medium - 5310 - Join
实现类型版本的 Array.join,Join<T, U>接受一个数组 T 字符串或数字类型 U,返回 T 中的所有元素用 U 连接的字符串,U 默认为 ','。
type Join<
T extends any[],
U extends string | number = ',',
Pre extends string = ''
> = T extends [infer F, ...infer R]
? Pre extends ''
? Join<R, U, `${F & string}`>
: Join<R, U, `${Pre}${U}${F & string}`>
: Pre;
其中 F & string 因为 F 的类型是 any 但是只有一部分类型可以反正模板字符串中,所以这里类型会把报错,通过 & string 限制为 string。
75. Medium - 5317 - LastIndexOf
实现类型版本的 Array.lastIndexOf, LastIndexOf<T, U> 接受数组 T, any 类型的 U, 如果 U 存在于 T 中, 返回 U 在数组 T 中最后一个位置的索引, 不存在则返回 -1
type LastIndexOf<
T extends any[],
U,
Pre extends any[] = [],
Result extends number = -1
> = T extends [infer F, ...infer R]
? Equal<F, U> extends true // 判断当前的值是否为U
? LastIndexOf<R, U, [...Pre, F], Pre['length']> // 如果是,更新Result,然后继续处理
: LastIndexOf<R, U, [...Pre, F], Result>
: Result
76. Medium - 5360 - Unique 数组去重
实现类型版本的 Lodash.uniq 方法,Unique 接收数组类型 T, 返回去重后的数组类型。
type Includes<T extends readonly unknown[], U> =
T extends [infer F, ...infer Rest]
? Equal<F, U> extends true ? true : Includes<Rest, U>
: false;
type Unique<
T extends any[],
Result extends any[] = [],
> = T extends [infer F, ...infer R]
? Includes<Result, F> extends true
? Unique<R, Result>
: Unique<R, [...Result, F]>
: Result;
顺便评论区看到另一个,算是利用分布式条件类型更简洁的实现了 Includes 。
type Unique<T, U = never> =
T extends [infer F, ...infer R]
? true extends (U extends U ? Equal<U, [F]> : never)
? Unique<R, U>
: [F, ...Unique<R, U | [F]>]
: []
77. Medium - 5821 - MapTypes 映射类型
实现 MapTypes<T, R>,把对象 T 中的类型根据 R 做转换。比如 R 为 { mapFrom: string, mapTo: boolean } 表示把 T 中的所有 string 类型改为 boolean。
// 根据类型映射的定义 和 原类型 获取映射后的类型
type getType<T extends { mapFrom: any; mapTo: any }, P> = T extends any
? T['mapFrom'] extends P // 利用分布式条件类型,依次判断是否匹配mapFrom类型
? T['mapTo'] // 符合的返回对应的mapTo类型
: never // 不符合返回never 其他类型和never联合只会剩下其他类型
: never;
type MapTypes<T, R extends { mapFrom: any; mapTo: any }> = {
// { mapFrom: T[K]; mapTo: any } 是否可以赋值给R
[K in keyof T]: { mapFrom: T[K]; mapTo: any } extends R
? getType<R, T[K]> // 证明他的类型匹配了mapFrom 需要返回对应的mapTo
: T[K]; // 否则不需要调整
};
78. Medium - 7544 - Construct Tuple 构造元组 ※
构造一个给定长度的元组。
这题简直太水了,递归就可以 TypeScript 最多递归到999层,所以最后一个 case Expect<Equal<ConstructTuple<1000>['length'], 1000>> 会失败。
// 生成元组,但是TS递归只能到999
type ConstructTuple<
N extends string | number,
Result extends any[] = []
> = `${Result['length']}` extends `${N}`
? Result
: ConstructTuple<N, [...Result, unknown]>;
但是,让 9999 成功才是有趣的问题,我开始一直想二分,把自己困住了,后来发现简单的按位计算就可以。参考github.com/type-challe…
// 生成元组,但是TS递归只能到999
type ConstructTupleSimple<
N extends string | number,
Result extends any[] = []
> = `${Result['length']}` extends `${N}`
? Result
: ConstructTupleSimple<N, [...Result, unknown]>;
// 把数组T中的元素数量*10
type Multi10<T extends any[]> = [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]
// 从左到右依次计算 例321 = (3*10+2)*10+1
type ConstructTuple<
L extends number | string,
Result extends any[] = []
> = `${L}` extends `${infer F}${infer R}`
? ConstructTuple<R, [...Multi10<Result>, ...ConstructTupleSimple<F>]>
: Result;
79. Medium - 8640 - Number Range
构造指定范围内所有数字的联合。
好像在前面做过类似的题目……通过 Arr 辅助记录遍历数字,Result 记录结果,InRange 记录是否在范围内。
type NumberRange<L, H, Arr extends any[] = [], Result = never, InRange = false> =
Arr['length'] extends L
? NumberRange<L, H, [...Arr, unknown], L | Result, true>
: Arr['length'] extends H
? Result | H
: InRange extends true
? NumberRange<L, H, [...Arr, unknown], Arr['length'] | Result, InRange>
: NumberRange<L, H, [...Arr, unknown], Result, InRange>
也有更直观的解法:
type ConstructUnion<
N extends string | number,
Result extends any[] = []
> = `${Result['length']}` extends `${N}`
? Result[number]
: ConstructUnion<N, [...Result, Result['length']]>;
type NumberRange<L extends number, H extends number> =
| Exclude<ConstructUnion<H>, ConstructUnion<L>>
| L
| H;
80. Medium - 8767 - Combination ※
给定一个字符串数组,执行排列和组合。
// 计算T中每个元素开头的 T和P中所有元素组成的全部组合
type Combination<T extends string[], P extends string[] = []> =
T extends [infer F extends string, ...infer R extends string[]] // 先计算F开头的所有情况
? `${F} ${Combination<[...R, ...P]>}` // 首个单词是F 然后连接 其他单词的全排列
//(在模板字符串中的联合类型会自动生成所有情况的模板字符串结果的联合)
| Combination<R, [...P, F]> // 继续计算单个单词剩余单词的情况
| F // 单个单词
: never
这种题我每次都要花一个小时做出来,头痛。看了下别人的解法很nb,利用联合类型分布式遍历 T,少了一次递归。
type Combination<T extends string[], All = T[number], Item = All>
= Item extends string
? Item | `${Item} ${Combination<[], Exclude<All, Item>>}`
: never
81. Medium - 8987 - Subsequence ※
给定一个唯一元素数组,返回所有可能的子序列。
type UnionAddT<U extends any[], T> = U extends any ? [T, ...U] : never
type Subsequence<T extends any[]> = T extends [infer F, ...infer R]
? Subsequence<R> // 不包含F的所有子序列
| UnionAddT<Subsequence<R>, F> // 包含F的所有子序列
: []
这题不难,加了重点标识因为新学了一个语法:当 ... 展开运算符应用到联合类型时,会对联合类型的每个成员分别展开,然后将结果再组成联合类型。
type Subsequence<T extends unknown[]> = T extends [infer X, ...infer Y]
? [X, ...Subsequence<Y>] | Subsequence<Y>
: [];
82. Medium - 9142 - CheckRepeatedChars
实现类型 CheckRepeatedChars<S> 返回 S 中是否有重复字符。
type CheckRepeatedChars<
T extends string,
visited = never
> = T extends `${infer F}${infer R}`
? F extends visited
? true
: CheckRepeatedChars<R, visited | F>
: false;
83. Medium - 9286 - FirstUniqueCharIndex
给一个字符串 S 找到第一个不重复字符的下标,不存在返回 -1。 (灵感来自 leetcode 387)(笑死力扣都来了)
type GetRepectChars<T extends string, Once = never, Repeated = never> =
T extends `${infer F}${infer R}`
? F extends Once
? GetRepectChars<R, Once, Repeated | F>
: GetRepectChars<R, Once | F, Repeated>
: Repeated
type FirstUniqueCharIndex<T extends string, Repeated = GetRepectChars<T>, Index extends any[] = []> =
T extends `${infer F}${infer R}`
? F extends Repeated
? FirstUniqueCharIndex<R, Repeated, [...Index, F]>
: Index['length']
: -1
84. Medium - 9616 - Parse URL Params
实现类型层面的解析器,把 URL 中的参数字符串解析为一个联合。
type ParseUrlParams<T extends string> = T extends `${infer F}/${infer R}`
? F extends `:${infer P}`
? P | ParseUrlParams<R>
: ParseUrlParams<R>
: T extends `:${infer P}`
? P
: never
85. Medium - 9896 - GetMiddleElement
通过实现一个 GetMiddleElement 方法,获取数组的中间元素,用数组表示
如果数组的长度为奇数,则返回中间一个元素 如果数组的长度为偶数,则返回中间两个元素
type GetMiddleElement<T extends any[]> =
T['length'] extends 0 | 1 | 2
? T
: T extends [infer _L, ...infer M, infer _R]
? GetMiddleElement<M>
: []
简单,每次删除前后两个元素,对长度为 0 1 2 的数组特殊处理。
86. Medium - 9898 - Appear only once
找出目标数组中只出现过一次的元素。例如:输入 [1,2,2,3,3,4,5,6,6,6],输出 [1,4,5]。
// 判断联合类型T中是否存在U
type Includes<T, U> = true extends (T extends any ? Equal<T, U> : never)
? true
: false;
type FindEles<
T extends any[],
Pre extends any[] = [],
Res extends any[] = []
> = T extends [infer F, ...infer R]
? Includes<[...Pre, ...R][number], F> extends true // 如果F前后组成的数组是否包含F
? FindEles<R, [...Pre, F], Res> // 包含F 则证明不唯一 结果不添加F
: FindEles<R, [...Pre, F], [...Res, F]> // 不包含F 则证明唯一 结果添加F
: Res; // 遍历结束 返回结果
87. Medium - 9989 - Count Element Number To Object
通过实现一个 CountElementNumberToObject 方法,统计数组中相同元素的个数。
// 把数组拍平,然后把其中never元素删除
type Flatten<A extends any[]> = A extends [infer F, ...infer R] // 判断A存在第一个元素F
? [F] extends [never]
? [...Flatten<R>]
: F extends any[]
? [...Flatten<F>, ...Flatten<R>]
: [F, ...Flatten<R>]
: [];
type CountElementNumberToObject<
T extends any[],
// 辅助计数的对象,用数组计数
Aux extends Record<string | number, any[]> = {},
// T中有嵌套数组 把T拍平
F extends (number | string)[] = Flatten<T>
> = F extends [
infer L extends number | string, // 取第一个元素
...infer R extends (number | string)[]
]
? CountElementNumberToObject<
R,
{
[K in keyof Aux | L]: K extends L // 遍历Aux中key
? L extends keyof Aux // 遍历到L了,判断如果L是Aux中的key
? [...Aux[K], unknown] // 就在对应数组中添加一个元素
: [unknown] // 不在就新创建一个数组,添加一个元素
: Aux[K]; // 其他key不做处理
}
>
: {
[K in keyof Aux]: Aux[K]['length']; // 把结果映射为数组的长度
};
88. Medium - 10969 - Integer ※
请完成类型 Integer<T>,类型 T 继承于 number,如果 T 是一个整数则返回它,否则返回 never。
type OnlyZero<T> = T extends `${infer F}${infer R}`
? F extends '0'
? OnlyZero<R>
: false
: true;
type ToNumber<T> = T extends `${infer N extends number}` ? N : never;
type Integer<T> = T extends number
? number extends T
? never
: `${T}` extends `${infer Int}.${infer Deci}`
? OnlyZero<Deci> extends true
? ToNumber<Int>
: never
: T
: never;
虽然也不算难,但是一看评论区天塌了。
type Integer<T extends string | number> = number extends T
? never
: `${T}` extends `${string}.${string}`
? never
: T;
// 或者这样,因为 bigint 只能是整数
type Integer<T extends number> = `${T}` extends `${bigint}` ? T : never
我自己试了一下数字转字符串,发现对于多余的小数点后面的 0 会被删除。
type x = `${1.0}` // "1"
type x1 = `${1.2}` // "1.2"
type x2 = `${1.200}` // "1.2"
89. Medium - 16259 - ToPrimitive ※
把对象中类型为字面类型(标签类型)的属性,转换为基本类型。
这题可以枚举类型实现,不过就没啥意思了。看到一种神奇的解法,用到了 valueOf。
type ToPrimitive<T> = T extends (...args: any[]) => any
? Function
: T extends object
? { [K in keyof T]: ToPrimitive<T[K]> }
: T extends { valueOf: () => infer R }
? R
: T;
JavaScript 中每个包装对象都有 valueOf() 方法:
-
String.prototype.valueOf()返回string -
Number.prototype.valueOf()返回number - ...
在 TypeScript 类型系统中,我们可以利用这个特性:
interface String {
/** Returns the primitive value of the specified object. */
valueOf(): string;
// ... 其他方法
}
// string 字面量类型
type Test1 = 'Tom' extends { valueOf: () => infer R } ? R : never
// 'Tom' 是 string 类型,string 有 valueOf(): string
// R = string ✅
type Test2 = 30 extends { valueOf: () => infer R } ? R : never
// 30 是 number 类型,number 有 valueOf(): number
// R = number ✅
90. Medium - 17973 - DeepMutable
实现一个通用的 DeepMutable ,它使对象的每个属性,及其递归的子属性 - 可变。
type DeepMutable<T extends object> = {
-readonly [K in keyof T]: T[K] extends (...args: any) => any
? T[K]
: T[K] extends object ? DeepMutable<T[K]> : T[K]
}
一次遍历,简洁写法(Python/Java/C++/C/Go/JS/Rust)
题意:子串必须形如 $\underbrace{\texttt{0}\cdots \texttt{0}}{k\ 个\ \texttt{0}}\underbrace{\texttt{1}\cdots \texttt{1}}{k\ 个\ \texttt{1}}$ 或者 $\underbrace{\texttt{1}\cdots \texttt{1}}{k\ 个\ \texttt{1}}\underbrace{\texttt{0}\cdots \texttt{0}}{k\ 个\ \texttt{0}}$。只能有一段 $\texttt{0}$ 和一段 $\texttt{1}$,不能是 $\texttt{00111}$(两段长度不等)或者 $\texttt{010}$(超过两段)等。
例如 $s = \texttt{001110000}$,按照连续相同字符,分成三组 $\texttt{00},\texttt{111},\texttt{0000}$。
- 在前两组中,我们可以得到 $2$ 个合法子串:$\texttt{0011}$ 和 $\texttt{01}$。
- 在后两组中,我们可以得到 $3$ 个合法子串:$\texttt{111000}$、$\texttt{1100}$ 和 $\texttt{10}$。
一般地,遍历 $s$,按照连续相同字符分组,计算每一组的长度。设当前这组的长度为 $\textit{cur}$,上一组的长度为 $\textit{pre}$,那么当前这组和上一组,能得到 $\min(\textit{pre},\textit{cur})$ 个合法子串,加到答案中。
###py
class Solution:
def countBinarySubstrings(self, s: str) -> int:
n = len(s)
pre = cur = ans = 0
for i in range(n):
cur += 1
if i == n - 1 or s[i] != s[i + 1]:
# 遍历到了这一组的末尾
ans += min(pre, cur)
pre = cur
cur = 0
return ans
###java
class Solution {
public int countBinarySubstrings(String S) {
char[] s = S.toCharArray();
int n = s.length;
int pre = 0;
int cur = 0;
int ans = 0;
for (int i = 0; i < n; i++) {
cur++;
if (i == n - 1 || s[i] != s[i + 1]) {
// 遍历到了这一组的末尾
ans += Math.min(pre, cur);
pre = cur;
cur = 0;
}
}
return ans;
}
}
###cpp
class Solution {
public:
int countBinarySubstrings(string s) {
int n = s.size();
int pre = 0, cur = 0, ans = 0;
for (int i = 0; i < n; i++) {
cur++;
if (i == n - 1 || s[i] != s[i + 1]) {
// 遍历到了这一组的末尾
ans += min(pre, cur);
pre = cur;
cur = 0;
}
}
return ans;
}
};
###c
#define MIN(a, b) ((b) < (a) ? (b) : (a))
int countBinarySubstrings(char* s) {
int pre = 0, cur = 0, ans = 0;
for (int i = 0; s[i]; i++) {
cur++;
if (s[i] != s[i + 1]) {
// 遍历到了这一组的末尾
ans += MIN(pre, cur);
pre = cur;
cur = 0;
}
}
return ans;
}
###go
func countBinarySubstrings(s string) (ans int) {
n := len(s)
pre, cur := 0, 0
for i := range n {
cur++
if i == n-1 || s[i] != s[i+1] {
// 遍历到了这一组的末尾
ans += min(pre, cur)
pre = cur
cur = 0
}
}
return
}
###js
var countBinarySubstrings = function(s) {
const n = s.length;
let pre = 0, cur = 0, ans = 0;
for (let i = 0; i < n; i++) {
cur++;
if (i === n - 1 || s[i] !== s[i + 1]) {
// 遍历到了这一组的末尾
ans += Math.min(pre, cur);
pre = cur;
cur = 0;
}
}
return ans;
};
###rust
impl Solution {
pub fn count_binary_substrings(s: String) -> i32 {
let s = s.as_bytes();
let n = s.len();
let mut pre = 0;
let mut cur = 0;
let mut ans = 0;
for i in 0..n {
cur += 1;
if i == n - 1 || s[i] != s[i + 1] {
// 遍历到了这一组的末尾
ans += pre.min(cur);
pre = cur;
cur = 0;
}
}
ans
}
}
复杂度分析
- 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $s$ 的长度。
- 空间复杂度:$\mathcal{O}(1)$。
专题训练
见下面双指针题单的「六、分组循环」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府
【计数二进制子串】计数
思路
- 对于
000111来说,符合要求的子串是000111001101- 不难发现,如果我们找到一段类似
000111的数据,就可以用来统计答案 - 即 这样前面是连续
0/1后面是连续1/0的数据 - 这一段的所有 3 个子串,取决于前面
0/1的个数和后面1/0的个数 - 即
min(cnt_pre, cnt_cur)
- 不难发现,如果我们找到一段类似
![]()
- 遍历时,当数字再一次改变时(或到达结尾时),意味着一段结束,并能得到这一段前面和后面数字的个数。
- 如
11101来说,当我们遍历到最后的1时,1110就是一段可以用来统计答案的数据 - 而末尾的
01则是另一段可以用来统计答案的数据
- 如
<
,
>
- 小技巧,对字符串结尾增加一个字符,可以将判断逻辑写在一个地方
答题
class Solution {
public:
int countBinarySubstrings(string s) {
int ans = 0;
char last = '-';
int cnt_pre = 0;
int cnt_cur = 0;
s += '-';
for (auto c : s) {
if (last != c) {
last = c;
ans += min(cnt_pre, cnt_cur);
cnt_pre = cnt_cur;
cnt_cur = 0;
}
cnt_cur++;
}
return ans;
}
};
致谢
感谢您的观看,如果感觉还不错就点个赞吧,关注我的 力扣个人主页 ,欢迎热烈的交流!
计数二进制子串
方法一:按字符分组
思路与算法
我们可以将字符串 $s$ 按照 $0$ 和 $1$ 的连续段分组,存在 $\textit{counts}$ 数组中,例如 $s = 00111011$,可以得到这样的 $\textit{counts}$ 数组:$\textit{counts} = {2, 3, 1, 2}$。
这里 $\textit{counts}$ 数组中两个相邻的数一定代表的是两种不同的字符。假设 $\textit{counts}$ 数组中两个相邻的数字为 $u$ 或者 $v$,它们对应着 $u$ 个 $0$ 和 $v$ 个 $1$,或者 $u$ 个 $1$ 和 $v$ 个 $0$。它们能组成的满足条件的子串数目为 $\min { u, v }$,即一对相邻的数字对答案的贡献。
我们只要遍历所有相邻的数对,求它们的贡献总和,即可得到答案。
不难得到这样的实现:
###C++
class Solution {
public:
int countBinarySubstrings(string s) {
vector<int> counts;
int ptr = 0, n = s.size();
while (ptr < n) {
char c = s[ptr];
int count = 0;
while (ptr < n && s[ptr] == c) {
++ptr;
++count;
}
counts.push_back(count);
}
int ans = 0;
for (int i = 1; i < counts.size(); ++i) {
ans += min(counts[i], counts[i - 1]);
}
return ans;
}
};
###Java
class Solution {
public int countBinarySubstrings(String s) {
List<Integer> counts = new ArrayList<Integer>();
int ptr = 0, n = s.length();
while (ptr < n) {
char c = s.charAt(ptr);
int count = 0;
while (ptr < n && s.charAt(ptr) == c) {
++ptr;
++count;
}
counts.add(count);
}
int ans = 0;
for (int i = 1; i < counts.size(); ++i) {
ans += Math.min(counts.get(i), counts.get(i - 1));
}
return ans;
}
}
###JavaScript
var countBinarySubstrings = function(s) {
const counts = [];
let ptr = 0, n = s.length;
while (ptr < n) {
const c = s.charAt(ptr);
let count = 0;
while (ptr < n && s.charAt(ptr) === c) {
++ptr;
++count;
}
counts.push(count);
}
let ans = 0;
for (let i = 1; i < counts.length; ++i) {
ans += Math.min(counts[i], counts[i - 1]);
}
return ans;
};
###Go
func countBinarySubstrings(s string) int {
counts := []int{}
ptr, n := 0, len(s)
for ptr < n {
c := s[ptr]
count := 0
for ptr < n && s[ptr] == c {
ptr++
count++
}
counts = append(counts, count)
}
ans := 0
for i := 1; i < len(counts); i++ {
ans += min(counts[i], counts[i-1])
}
return ans
}
###C
int countBinarySubstrings(char* s) {
int n = strlen(s);
int counts[n], counts_len = 0;
memset(counts, 0, sizeof(counts));
int ptr = 0;
while (ptr < n) {
char c = s[ptr];
int count = 0;
while (ptr < n && s[ptr] == c) {
++ptr;
++count;
}
counts[counts_len++] = count;
}
int ans = 0;
for (int i = 1; i < counts_len; ++i) {
ans += fmin(counts[i], counts[i - 1]);
}
return ans;
}
###Python
class Solution:
def countBinarySubstrings(self, s: str) -> int:
counts = []
ptr, n = 0, len(s)
while ptr < n:
c = s[ptr]
count = 0
while ptr < n and s[ptr] == c:
ptr += 1
count += 1
counts.append(count)
ans = 0
for i in range(1, len(counts)):
ans += min(counts[i], counts[i - 1])
return ans
###C#
public class Solution {
public int CountBinarySubstrings(string s) {
List<int> counts = new List<int>();
int ptr = 0, n = s.Length;
while (ptr < n) {
char c = s[ptr];
int count = 0;
while (ptr < n && s[ptr] == c) {
ptr++;
count++;
}
counts.Add(count);
}
int ans = 0;
for (int i = 1; i < counts.Count; i++) {
ans += Math.Min(counts[i], counts[i - 1]);
}
return ans;
}
}
###TypeScript
function countBinarySubstrings(s: string): number {
const counts: number[] = [];
let ptr = 0, n = s.length;
while (ptr < n) {
const c = s[ptr];
let count = 0;
while (ptr < n && s[ptr] === c) {
ptr++;
count++;
}
counts.push(count);
}
let ans = 0;
for (let i = 1; i < counts.length; i++) {
ans += Math.min(counts[i], counts[i - 1]);
}
return ans;
}
###Rust
impl Solution {
pub fn count_binary_substrings(s: String) -> i32 {
let mut counts = Vec::new();
let bytes = s.as_bytes();
let n = bytes.len();
let mut ptr = 0;
while ptr < n {
let c = bytes[ptr];
let mut count = 0;
while ptr < n && bytes[ptr] == c {
ptr += 1;
count += 1;
}
counts.push(count);
}
let mut ans = 0;
for i in 1..counts.len() {
ans += counts[i].min(counts[i - 1]);
}
ans
}
}
这个实现的时间复杂度和空间复杂度都是 $O(n)$。
对于某一个位置 $i$,其实我们只关心 $i - 1$ 位置的 $\textit{counts}$ 值是多少,所以可以用一个 $\textit{last}$ 变量来维护当前位置的前一个位置,这样可以省去一个 $\textit{counts}$ 数组的空间。
代码
###C++
class Solution {
public:
int countBinarySubstrings(string s) {
int ptr = 0, n = s.size(), last = 0, ans = 0;
while (ptr < n) {
char c = s[ptr];
int count = 0;
while (ptr < n && s[ptr] == c) {
++ptr;
++count;
}
ans += min(count, last);
last = count;
}
return ans;
}
};
###Java
class Solution {
public int countBinarySubstrings(String s) {
int ptr = 0, n = s.length(), last = 0, ans = 0;
while (ptr < n) {
char c = s.charAt(ptr);
int count = 0;
while (ptr < n && s.charAt(ptr) == c) {
++ptr;
++count;
}
ans += Math.min(count, last);
last = count;
}
return ans;
}
}
###JavaScript
var countBinarySubstrings = function(s) {
let ptr = 0, n = s.length, last = 0, ans = 0;
while (ptr < n) {
const c = s.charAt(ptr);
let count = 0;
while (ptr < n && s.charAt(ptr) === c) {
++ptr;
++count;
}
ans += Math.min(count, last);
last = count;
}
return ans;
};
###Go
func countBinarySubstrings(s string) int {
var ptr, last, ans int
n := len(s)
for ptr < n {
c := s[ptr]
count := 0
for ptr < n && s[ptr] == c {
ptr++
count++
}
ans += min(count, last)
last = count
}
return ans
}
###C
int countBinarySubstrings(char* s) {
int ptr = 0, n = strlen(s), last = 0, ans = 0;
while (ptr < n) {
char c = s[ptr];
int count = 0;
while (ptr < n && s[ptr] == c) {
++ptr;
++count;
}
ans += fmin(count, last);
last = count;
}
return ans;
}
###Python
class Solution:
def countBinarySubstrings(self, s: str) -> int:
ptr, n = 0, len(s)
last, ans = 0, 0
while ptr < n:
c = s[ptr]
count = 0
while ptr < n and s[ptr] == c:
ptr += 1
count += 1
ans += min(count, last)
last = count
return ans
###C#
public class Solution {
public int CountBinarySubstrings(string s) {
int ptr = 0, n = s.Length;
int last = 0, ans = 0;
while (ptr < n) {
char c = s[ptr];
int count = 0;
while (ptr < n && s[ptr] == c) {
ptr++;
count++;
}
ans += Math.Min(count, last);
last = count;
}
return ans;
}
}
###TypeScript
function countBinarySubstrings(s: string): number {
let ptr = 0, n = s.length;
let last = 0, ans = 0;
while (ptr < n) {
const c = s[ptr];
let count = 0;
while (ptr < n && s[ptr] === c) {
ptr++;
count++;
}
ans += Math.min(count, last);
last = count;
}
return ans;
}
###Rust
impl Solution {
pub fn count_binary_substrings(s: String) -> i32 {
let bytes = s.as_bytes();
let n = bytes.len();
let mut ptr = 0;
let mut last = 0;
let mut ans = 0;
while ptr < n {
let c = bytes[ptr];
let mut count = 0;
while ptr < n && bytes[ptr] == c {
ptr += 1;
count += 1;
}
ans += count.min(last);
last = count;
}
ans
}
}
复杂度分析
- 时间复杂度:$O(n)$。
- 空间复杂度:$O(1)$。
腾讯宣布:元宝DAU超5000万,MAU达1.14亿
深度解析 JWT:从 RFC 原理到 NestJS 实战与架构权衡
1. 引言
HTTP 协议本质上是无状态(Stateless)的。在早期的单体应用时代,为了识别用户身份,我们通常依赖 Session-Cookie 机制:服务端在内存或数据库中存储 Session 数据,客户端浏览器通过 Cookie 携带 Session ID。
然而,随着微服务架构和分布式系统的兴起,这种有状态(Stateful)的机制暴露出了明显的弊端:Session 数据需要在集群节点间同步(Session Sticky 或 Session Replication),这极大地限制了系统的水平扩展能力(Horizontal Scaling)。
为了解决这一痛点,JSON Web Token(JWT)应运而生。作为一种轻量级、自包含的身份验证标准,JWT 已成为现代 Web 应用——特别是前后端分离架构与微服务架构中——主流的身份认证解决方案。本文将从原理剖析、NestJS 实战、架构权衡及高频面试考点四个维度,带你全面深入理解 JWT。
2. 什么是 JWT
JWT(JSON Web Token)是基于开放标准 RFC 7519 定义的一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。
核心特性:
- 紧凑(Compact) :体积小,可以通过 URL 参数、POST 参数或 HTTP Header 发送。
- 自包含(Self-contained) :Payload 中包含了用户认证所需的所有信息,避免了多次查询数据库。
主要应用场景:
- 身份认证(Authorization) :这是最常见的使用场景。一旦用户登录,后续请求将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。
- 信息交换(Information Exchange) :利用签名机制,确保发送者的身份是合法的,且传输的内容未被篡改。
3. JWT 的解剖学:原理详解
一个标准的 JWT 字符串由三部分组成,通过点(.)分隔:Header(请求头).Payload(载荷).Signature(签名信息)。
3.1 Header(头部)
Header 通常包含两部分信息:令牌的类型(即 JWT)和所使用的签名算法(如 HMAC SHA256 或 RSA),一般会有多种算法,如果开发者无选择,那么默认是HMAC SHA256算法。
JSON
{
"alg": "HS256",
"typ": "JWT"
}
该 JSON 被 Base64Url 编码后,构成 JWT 的第一部分。
3.2 Payload(负载)
Payload 包含声明(Claims),即关于实体(通常是用户)和其他数据的声明。声明分为三类:
-
Registered Claims(注册声明) :一组预定义的、建议使用的权利声明,如:
- iss (Issuer): 签发者
- exp (Expiration Time): 过期时间
- sub (Subject): 主题(通常是用户ID)
- aud (Audience): 受众
-
Public Claims(公共声明) :可以由使用 JWT 的人随意定义。
-
Private Claims(私有声明) :用于在同意使用这些定义的各方之间共享信息,如 userId、role 等。
架构师警示:
Payload 仅仅是进行了 Base64Url 编码(Encoding) ,而非 加密(Encryption) 。
这意味着,任何截获 Token 的人都可以通过 Base64 解码看到 Payload 中的明文内容。因此,严禁在 Payload 中存储密码、手机号等敏感信息。
3.3 Signature(签名)
签名是 JWT 安全性的核心。它是对前两部分(编码后的 Header 和 Payload)进行签名,以防止数据被篡改。
生成签名的公式如下(以 HMAC SHA256 为例):
Code
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
原理解析:
服务端持有一个密钥(Secret),该密钥绝不能泄露给客户端。当服务端收到 Token 时,会使用同样的算法和密钥重新计算签名。如果计算出的签名与 Token 中的 Signature 一致,说明 Token 是由合法的服务端签发,且 Payload 中的内容未被篡改(完整性校验)。
4. 实战:基于 NestJS 实现 JWT 认证
NestJS 是 Node.js 生态中优秀的企业级框架。下面演示如何使用 @nestjs/jwt 和 @nestjs/passport 实现标准的 JWT 认证流程。
4.1 依赖安装
Bash
npm install --save @nestjs/passport passport @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt
4.2 Module 配置
在 AuthModule 中注册 JwtModule,配置密钥和过期时间。
TypeScript
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: 'YOUR_SECRET_KEY', // 生产环境请使用环境变量
signOptions: { expiresIn: '60m' }, // Token 有效期
}),
],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
4.3 Service 层:签发 Token
实现登录逻辑,验证用户信息通过后,生成 JWT。
TypeScript
// auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
}
4.4 Strategy 实现:解析 Token
编写策略类,用于解析请求头中的 Bearer Token 并进行验证。
TypeScript
// jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false, // 拒绝过期 Token
secretOrKey: 'YOUR_SECRET_KEY', // 需与 Module 中配置一致
});
}
async validate(payload: any) {
// passport 会自动把返回值注入到 request.user 中
return { userId: payload.sub, username: payload.username };
}
}
4.5 Controller 使用:路由保护
TypeScript
// app.controller.ts
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller('profile')
export class ProfileController {
@UseGuards(AuthGuard('jwt'))
@Get()
getProfile(@Request() req) {
return req.user; // 这里是通过 JwtStrategy.validate 返回的数据
}
}
5. 深度分析:JWT 的优缺点与架构权衡
优点
- 无状态与水平扩展(Stateless & Scalability) :服务端不需要存储 Session 信息,完全消除了 Session 同步问题,非常适合微服务和分布式架构。
- 跨域友好:不依赖 Cookie(尽管可以结合 Cookie 使用),在 CORS 场景下处理更为简单,且天然适配移动端(iOS/Android)开发。
- 性能:在不涉及黑名单机制的前提下,验证 Token 只需要 CPU 计算签名,无需查询数据库,减少了 I/O 开销。
缺点与挑战
- 令牌体积:JWT 包含了 Payload 信息,相比仅存储 ID 的 Cookie,其体积更大,这会增加每次 HTTP 请求的 Header 大小,影响流量。
- 撤销难题(Revocation) :这是 JWT 最大 的痛点。JWT 一旦签发,在有效期内始终有效。服务端无法像 Session 那样直接删除服务器端数据来强制用户下线。
6. 面试高频考点与解决方案(进阶)
在面试中,仅仅展示如何生成 JWT 是远远不够的,面试官更关注安全性与工程化挑战。
问题 1:JWT 安全吗?如何防范攻击?
-
XSS(跨站脚本攻击) :如果将 JWT 存储在 localStorage 或 sessionStorage,恶意 JS 脚本可以轻松读取 Token。
- 解决方案:建议将 Token 存储在标记为 HttpOnly 的 Cookie 中,这样 JS 无法读取。
-
CSRF(跨站请求伪造) :如果使用 Cookie 存储 Token,则会面临 CSRF 风险。
- 解决方案:使用 SameSite=Strict 属性,或配合 CSRF Token 防御。如果坚持存储在 localStorage 并通过 Authorization Header 发送,则天然免疫 CSRF,但需重点防范 XSS。
-
中间人攻击:由于 Header 和 Payload 是明文编码。
- 解决方案:必须强制全站使用 HTTPS。
问题 2:如何实现注销(Logout)或强制下线?
既然 JWT 是无状态的,如何实现“踢人下线”?这实际上是无状态与管控性之间的权衡。
-
方案 A:黑名单机制(Blacklist)
- 将用户注销或被封禁的 Token ID (jti) 存入 Redis,设置过期时间等于 Token 的剩余有效期。
- 每次请求验证时,先校验签名,再查询 Redis 是否在黑名单中。
- 权衡:牺牲了部分“无状态”优势(引入了 Redis 查询),但获得了即时的安全管控。
-
方案 B:版本号/时间戳控制
- 在 JWT Payload 中加入 token_version。
- 在数据库用户表中也存储一个 token_version。
- 当用户修改密码或注销时,增加数据库中的版本号。
- 权衡:每次验证都需要查询数据库比对版本号,退化回了 Session 的模式,性能开销大。
问题 3:Token 续签(Refresh Token)机制是如何设计的?
为了解决 JWT 有效期过长不安全、过短体验差的问题,业界标准做法是 双 Token 机制:
- Access Token:有效期短(如 15 分钟),用于访问业务接口。
- Refresh Token:有效期长(如 7 天),仅用于换取新的 Access Token。
流程设计:
- 客户端请求接口,若 Access Token 过期,服务端返回 401。
- 客户端捕获 401,携带 Refresh Token 请求 /refresh 接口。
- 服务端验证 Refresh Token 合法(且未在黑名单/数据库中被禁用),签发新的 Access Token。
- 关键点:Refresh Token 通常需要在服务端(数据库)持久化存储,以便管理员可以随时禁用某个 Refresh Token,从而间接实现“撤销”用户的登录状态。
7. 结语
JWT 并不是银弹。它通过牺牲一定的“可控性”换取了“无状态”和“扩展性”。
在架构选型时:
- 如果你的应用是小型单体,且对即时注销要求极高,传统的 Session 模式可能更简单有效。
- 如果你的应用是微服务架构,或者需要支持多端登录,JWT 是不二之选。
- 在构建企业级应用时,切勿盲目追求纯粹的无状态。推荐使用 JWT + Access/Refresh Token 双令牌 + Redis 黑名单 的组合拳,以在安全性、性能和扩展性之间取得最佳平衡。