普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月19日技术

JavaScript 异步编程完全指南:从入门到精通

2026年2月19日 08:09

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
        └── 内存泄漏

学习路线建议:

  1. 入门:理解同步/异步 → 事件循环 → 回调
  2. 基础:Promise 创建/消费 → 链式调用 → 错误处理
  3. 进阶:async/await → 并行/串行 → 静态方法
  4. 高级:并发控制 → 取消操作 → Generator/异步迭代
  5. 精通:手写 Promise → 架构设计 → 性能优化 → 响应式编程

CSS Grid 案例

2026年2月19日 02:55

CSS Grid 案例详解

1、min-content、max-content、auto 空间计算

单列 auto 布局:内容宽度决定列宽,剩余空间自动分配

auto-1.png

双列 auto 布局:两列宽度根据内容自动调整

auto-2.png

三列 auto 布局:多列布局时内容宽度的分配方式

auto-3.png

四列 auto 布局:列数增加时的空间分配逻辑

auto-4.png

max-content:列宽等于内容最大宽度,不换行

max-content.png

min-content:列宽等于内容最小宽度,尽可能换行

min-content.png

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

2、stretch 内容填充.png

两个内容自动撑开, 左右排列
<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 填满剩余空间

单列布局,当容器宽度不足时自动调整为单列

3、一列.png

两列布局:容器宽度足够时自动扩展为多列

3、两列.png

三列布局:自适应容器宽度的多列排列

3、三列.png

四列布局:充分利用可用空间的自适应布局

3、铺满四列.png

<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,通过命名区域实现复杂的网格布局

4、grid 命名格子.png

代码

<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、隐形网格

image.png 隐式网格: 超出显式网格范围的项目自动创建新行

代码

<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设置隐式列的宽度

image.png

<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 流向: 左到右、从上到下排列

7、grid-auot-flow 流向.png

column流向: 上到下、从左到右排列

7、grid-auto-flow 流向2.png

<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 图片案例

第一个图片占据两行空间,其他图片自动排列

image.png
<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填补: 自动填补空缺位置,优化空间利用 image.png

<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 完整写法

image.png

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栏布局

经典的页面布局: 包含头部、侧边栏、主内容和底部四个区域 image.png

<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 线条版

image.png 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 伪元素选择器:为元素披上魔法的斗篷

作者 Lee川
2026年2月19日 00:52

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 实现,无需图片。

🚫 伪元素的禁忌与限制

魔法虽强,也有规则:

  1. content是必需的:没有它,伪元素不显现
  2. 某些属性不可用:伪元素不能应用 content属性本身
  3. 不能用于替换元素:如 <img><input><textarea>
  4. SEO 不可见:搜索引擎看不到伪元素的内容
  5. 可访问性注意:屏幕阅读器可能不会读取伪元素内容

💡 最佳实践指南

何时使用伪元素?

适合

  • 纯粹的视觉装饰(图标、线条、形状)
  • 不需要交互的 UI 元素
  • 内容前后的固定标记
  • 不影响语义的样式增强

不适合

  • 重要的交互内容(应使用真实元素)
  • 需要被搜索引擎收录的内容
  • 复杂的、需要维护的动态内容

性能小贴士

css
css
复制
/* 良好实践:减少重绘 */
.decorative::after {
  content: "";
  will-change: transform; /* 提示浏览器优化 */
  transform: translateZ(0); /* 触发 GPU 加速 */
}

/* 避免过度使用 */
/* 每个元素都加伪元素会影响性能 */

🌈 结语:魔法的艺术

伪元素选择器是 CSS 工具箱中的瑞士军刀——小巧、锋利、用途广泛。它们代表了关注点分离的优雅理念:HTML 负责结构,CSS 负责表现。

就像画家不会在画布上固定装饰品,而是在画作上直接绘制光影效果一样,优秀的开发者懂得使用伪元素来增强界面,而不是堆砌冗余的 HTML 标签。

记住:最好的魔法往往是看不见的魔法。当用户觉得“这个界面就是应该长这样”,而不是“这里加了个小图标”时,你就掌握了伪元素的真正精髓。

现在,拿起你的 CSS 魔杖,去创造一些神奇的界面吧!记住这句魔咒: “内容在前,装饰在后,语义清晰,表现灵活” ——这就是伪元素哲学的核心。 ✨

WebMCP 实战指南:让你的网站瞬间变成 AI 的“大脑外挂”

2026年2月19日 08:27

一、 AI 终于不用“瞎猜”你的网页了

我们可以把 WebMCP 想象成一种**“翻译官协议”**:

  • 以前的 AI(视觉模拟派) :就像一个老外在看一份全中文的报纸,他得先拍照,再识别文字,最后猜哪里是按钮。一旦你把按钮从左边挪到右边,他就找不到了。
  • WebMCP(接口直连派) :你的网站现在给 AI 提供了一个**“操作说明书”**。AI 进门后不用看页面长什么样,直接问:“那个‘查询余额’的功能在哪?” 你的网站直接通过 WebMCP 告诉它:“在这里,发个 JSON 给我,我就告诉你结果。”

一句话总结:WebMCP 让网页从“给人看的界面”变成了“给 AI 调用的函数”。


二、 核心能力:WebMCP 的“两把斧”

在实际开发中,WebMCP 提供了两种接入方式:

  1. 宣告式(适合简单动作) :在 HTML 里加个属性,就像给按钮贴个“AI 可读”的标签。
  2. 命令式(适合高级逻辑) :用 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 ManagerTrae/Cursor。WebMCP 的出现解决了前端工程中的“适配地狱”。

1. “一次开发,到处运行”的 AI 能力

  • 旧模式:你得写一个 Chrome 插件给 Gemini 用,再写一个 MCP Server 给 Claude Desktop 用,再给 Cursor 写特定的插件。
  • WebMCP 模式:你只需要在网页里 registerTool。由于 Chrome 是宿主,只要你在 Chrome 里打开这个网页,无论是侧边栏的 Gemini,还是通过 DevTools 接入的 AI Agent,都能识别并使用这套能力。这极大降低了你维护 AI 基础设施 的成本。

八、为什么非用它不可?

  1. 性能屠宰场:不再需要给 AI 发送几万个 DOM 节点的 HTML 文本,只传核心 JSON,Token 消耗节省 90%。
  2. 安全围栏:数据处理在本地浏览器完成。大模型只发指令,不直接接触你的敏感数据库明细。
  3. 开发效率:你不再需要为不同的 AI 插件写不同的适配层,只要符合 WebMCP 标准,所有支持该协议的 AI 助手都能秒懂你的业务。

源码回溯的艺术:SourceMap 底层 VLQ 编码与离线解析架构实战

2026年2月19日 08:26

对于正在自研监控系统的架构师来说,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-map JS 库在反解析时极其耗时,且内存占用极高。

  • Native 级加速:推荐引入由 Rust 编写的解析库(通过 N-API 接入 Node.js)。例如 oxc-sourcemap@jridgewell/trace-mapping。这些库利用二进制层面的位运算,解析速度比传统库快一个数量级。

  • 多级缓存方案

    • L1(内存) :缓存最近解析过的 SourceMap 对象的实例。
    • L2(磁盘缓存) :缓存反解析后的堆栈片段。
    • L3(存储) :原始 .map 文件。

三、 实战避坑:那些年老兵踩过的“暗雷”

  1. 列偏移量的一致性

    有些压缩工具(如早期的 UglifyJS)生成的列号是从 0 开始的,而有些(如某些浏览器报错)是从 1 开始的。在反解析时,必须严格校准这个 0/1 的差异,否则还原出来的代码会错位一个字符。

  2. 异步解析的原子性

    当一个错误高频发生(例如全局报错)时,不要并发去下载同一个 .map 文件。利用 Promise 缓存(Singleflight 模式) 确保同一个版本的 Map 文件只被拉取并解析一次。

  3. 内联(Inline)风险警示

    绝对不要在 webpack.config.js 中使用 evalinline 开头的 devtool 配置。这不仅会暴露源码,还会因为 Base64 字符串嵌入导致 JS 运行速度下降 30% 以上。


💡 结语与下一步

SourceMap 解决了“在哪里报错”的问题。但在监控系统的进阶阶段,我们还需要知道“报错时的上下文(上下文变量、网络请求、用户轨迹)”。

每日一题-计数二进制子串🟢

2026年2月19日 00:00

给定一个字符串 s,统计并返回具有相同数量 01 的非空(连续)子字符串的数量,并且这些子字符串中的所有 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 类型体操练习笔记(二)

2026年2月18日 23:53

进度(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]
}

查了半天只有这个 pr官方文档里也没有明确说明。

形如 { [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" ,我们依次判断它是否为 stringnumbersymbol 都不是则证明是具体属性,否则为索引签名。

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 开头,根据结果返回 truefalse

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 结尾,根据结果返回 truefalse

type EndsWith<T extends string, U extends string> =
  T extends `${string}${U}` ? true : false

50. Medium - 2757 - PartialByKeys

实现一个通用的 PartialByKeys<T, K>,它接收两个类型参数 TK

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>,它接收两个类型参数 TK

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 的属性成为一个新的类型。

直到了 asMapped 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-bigbtn__prise-warning

实现 BEM<B,E,M>,从这三个参数生成字符串并集。其中 B 是字符串文字,EM 是字符串数组(可以为空)。

// 把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>,其中 TU 必须是元组。

就是把所有元组中的第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 是否为一个元组类型

tuple type is another sort of Array type 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.indexOfindexOf<T, U> 接受两个参数,数组 T 和任意类型 U 返回 UT 中第一次出现的下标,不存在返回 -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.joinJoin<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.lastIndexOfLastIndexOf<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)

作者 endlesscheng
2026年2月7日 09:22

题意:子串必须形如 $\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)$。

专题训练

见下面双指针题单的「六、分组循环」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

【计数二进制子串】计数

作者 ikaruga
2020年8月10日 10:00

思路

  1. 对于 000111 来说,符合要求的子串是 000111 0011 01
    1. 不难发现,如果我们找到一段类似 000111 的数据,就可以用来统计答案
    2. 即 这样前面是连续 0/1 后面是连续 1/0 的数据
    3. 这一段的所有 3 个子串,取决于前面 0/1 的个数和后面 1/0 的个数
    4. min(cnt_pre, cnt_cur)

图片.png

  1. 遍历时,当数字再一次改变时(或到达结尾时),意味着一段结束,并能得到这一段前面和后面数字的个数。
    1. 11101 来说,当我们遍历到最后的 1 时,1110 就是一段可以用来统计答案的数据
    2. 而末尾的 01 则是另一段可以用来统计答案的数据

<图片.png,图片.png>

  1. 小技巧,对字符串结尾增加一个字符,可以将判断逻辑写在一个地方

答题

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;
    }
};

致谢

感谢您的观看,如果感觉还不错就点个赞吧,关注我的 力扣个人主页 ,欢迎热烈的交流!

计数二进制子串

2020年8月9日 21:31

方法一:按字符分组

思路与算法

我们可以将字符串 $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)$。
昨天 — 2026年2月18日技术

深度解析 JWT:从 RFC 原理到 NestJS 实战与架构权衡

作者 NEXT06
2026年2月18日 21:22

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 中包含了用户认证所需的所有信息,避免了多次查询数据库。

主要应用场景:

  1. 身份认证(Authorization) :这是最常见的使用场景。一旦用户登录,后续请求将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。
  2. 信息交换(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),即关于实体(通常是用户)和其他数据的声明。声明分为三类:

  1. Registered Claims(注册声明) :一组预定义的、建议使用的权利声明,如:

    • iss (Issuer): 签发者
    • exp (Expiration Time): 过期时间
    • sub (Subject): 主题(通常是用户ID)
    • aud (Audience): 受众
  2. Public Claims(公共声明) :可以由使用 JWT 的人随意定义。

  3. 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 的优缺点与架构权衡

优点

  1. 无状态与水平扩展(Stateless & Scalability) :服务端不需要存储 Session 信息,完全消除了 Session 同步问题,非常适合微服务和分布式架构。
  2. 跨域友好:不依赖 Cookie(尽管可以结合 Cookie 使用),在 CORS 场景下处理更为简单,且天然适配移动端(iOS/Android)开发。
  3. 性能:在不涉及黑名单机制的前提下,验证 Token 只需要 CPU 计算签名,无需查询数据库,减少了 I/O 开销。

缺点与挑战

  1. 令牌体积:JWT 包含了 Payload 信息,相比仅存储 ID 的 Cookie,其体积更大,这会增加每次 HTTP 请求的 Header 大小,影响流量。
  2. 撤销难题(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 机制

  1. Access Token:有效期短(如 15 分钟),用于访问业务接口。
  2. 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 黑名单 的组合拳,以在安全性、性能和扩展性之间取得最佳平衡。

MySQL/MariaDB Cheatsheet

Connect and Exit

Open a SQL session and disconnect safely.

Command Description
mysql -u root -p Connect as root (prompt for password)
mysql -u user -p -h 127.0.0.1 Connect to specific host
mysql -u user -p -P 3306 Connect on custom port
sudo mysql Login using Unix socket auth (common on Debian/Ubuntu)
exit Leave MySQL/MariaDB shell

Basic SQL Checks

Run quick checks after connecting.

SQL Description
SELECT VERSION(); Show server version
SHOW DATABASES; List all databases
USE db_name; Switch active database
SHOW TABLES; List tables in current database
SHOW GRANTS FOR 'user'@'host'; Show user privileges

Database Management

Create, inspect, and remove databases.

SQL Description
CREATE DATABASE db_name; Create a database
CREATE DATABASE db_name CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; Create DB with charset/collation
SHOW DATABASES LIKE 'db_name'; Check if DB exists
DROP DATABASE db_name; Delete a database
SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA; List schemas via information schema

User Management

Create users and manage authentication.

SQL Description
CREATE USER 'app'@'localhost' IDENTIFIED BY 'strong_password'; Create local user
CREATE USER 'app'@'%' IDENTIFIED BY 'strong_password'; Create remote-capable user
ALTER USER 'app'@'localhost' IDENTIFIED BY 'new_password'; Change password
DROP USER 'app'@'localhost'; Delete user
SELECT user, host FROM mysql.user; List users and hosts

Privileges

Grant, review, and revoke access.

SQL Description
GRANT ALL PRIVILEGES ON db_name.* TO 'app'@'localhost'; Grant full DB access
GRANT SELECT,INSERT,UPDATE,DELETE ON db_name.* TO 'app'@'localhost'; Grant specific privileges
REVOKE ALL PRIVILEGES ON db_name.* FROM 'app'@'localhost'; Remove DB privileges
SHOW GRANTS FOR 'app'@'localhost'; Review granted privileges
FLUSH PRIVILEGES; Reload grant tables

Backup and Restore

Use mysqldump for exports and imports.

Command Description
mysqldump -u root -p db_name > db_name.sql Backup one database
mysqldump -u root -p --databases db1 db2 > multi.sql Backup multiple databases
mysqldump -u root -p --all-databases > all.sql Backup all databases
mysql -u root -p db_name < db_name.sql Restore into existing database
gunzip < db_name.sql.gz | mysql -u root -p db_name Restore compressed SQL dump

Import and Export Data

Common import/export patterns.

Command Description
mysql -u root -p -e "SHOW DATABASES;" Run one-off SQL from shell
mysql -u root -p db_name -e "SOURCE /path/file.sql" Import SQL file via client
mysql -u root -p -Nse "SELECT NOW();" Non-interactive query output
mysqldump -u root -p db_name table_name > table.sql Export one table
mysql -u root -p db_name < table.sql Import one table dump

Service and Health Checks

Verify daemon state and open ports.

Command Description
systemctl status mysql Check MySQL service status
systemctl status mariadb Check MariaDB service status
sudo systemctl restart mysql Restart MySQL service
sudo systemctl restart mariadb Restart MariaDB service
ss -tulpn | grep 3306 Confirm server is listening

Common Troubleshooting

Quick checks for frequent connection and auth problems.

Issue Check
Access denied for user Verify user/host pair and password, then check SHOW GRANTS
Can't connect to local MySQL server through socket Check service status and socket path in config
Can't connect to MySQL server on 'host' Confirm host/port reachability and firewall rules
Unknown database Verify database name with SHOW DATABASES;
Restore fails on collation/charset Ensure server supports source collation and set utf8mb4 where needed

Related Guides

Use these articles for detailed MySQL/MariaDB workflows.

Guide Description
How to Manage MySQL Databases and Users from the Command Line End-to-end admin workflow
How to Create MySQL Users Accounts and Grant Privileges User and privilege setup
How to Back Up and Restore MySQL Databases with Mysqldump Backup and restore patterns
How to Show a List of All Databases in MySQL Database discovery commands
List (Show) Tables in a MySQL Database Table listing commands
How to Check the MySQL Version Version checks and verification

对象数组的排序与分组:sort / localeCompare / 自定义 compare

作者 SuperEugene
2026年2月18日 13:56

日常开发里,列表、表格、统计几乎都绕不开「对象数组」的排序和分组。本文不讲底层原理,只讲怎么选、为什么选、容易踩哪些坑。适合会写 JS 但概念有点混的同学,也适合想补齐基础的前端老手。

一、Array.sort 到底在干什么

1.1 三个关键点

要点 说明
原地排序 sort() 会直接修改原数组,不会返回新数组
默认行为 不传比较函数时,按字符串逐个字符比较
compare 返回值 负数:a 排前面;0:不变;正数:b 排前面

有没有同学会有这样的疑问:compare 返回值?这是啥? 解释:

  • 这里的 compare 指的是 Array.sort() 方法中传入的比较函数(也就是你后面写的 (a, b) => a - b 这种形式)。
  • 简单说:当你用sort()排序时,传入的这个函数就是 compare,它的作用是告诉 sort() 两个元素(ab)该怎么排,返回值直接决定排序结果,和表格里的说明完全对应。
  • 比如 nums.sort((a, b) => a - b) 中,(a, b) => a - b 就是 compare 比较函数。

1.2 第一个坑:数字数组直接用 sort

const nums = [10, 2, 1];
nums.sort(); // 这一步已经把原数组 nums 改了!以为会得到 [1, 2, 10]
console.log(nums); // 打印的是被修改后的原数组,不是初始值。实际得到 [1, 10, 2] —— 按字符串 "10"、"2"、"1" 比较了!
// ✅ 正确写法
nums.sort((a, b) => a - b);   // 升序 [1, 2, 10]
nums.sort((a, b) => b - a);   // 降序 [10, 2, 1]

:为什么按字符串比较会得到 [1, 10, 2]? sort() 默认的字符串比较规则是「逐字符按 Unicode 码点比较」,不是看数字大小,步骤拆解如下:

  1. 先把数组里的数字都转成字符串:10→"10"、2→"2"、1→"1";
  2. 从第一个字符开始比,字符的 Unicode 码点:"1"(码点 49)< "2"(码点 50);
  3. 具体比较过程:
    • 比较 "1" 和 "10":第一个字符都是 "1"(码点相同),但 "1" 没有第二个字符,所以 "1" < "10";
    • 比较 "10" 和 "2":第一个字符 "1" < "2",所以 "10" < "2"

1.3 第二个坑:原数组被改了

const original = [3, 1, 2];
const sorted = original.sort((a, b) => a - b);

console.log(sorted);   // [1, 2, 3]
console.log(original); // [1, 2, 3] —— 原数组也被改了!

// ✅ 需要保留原数组时,先浅拷贝再排序
const sorted2 = [...original].sort((a, b) => a - b);

二、对象数组按不同字段排序

2.1 按数字排序

const users = [
  { name: '张三', age: 25 },
  { name: '李四', age: 18 },
  { name: '王五', age: 30 }
];

// 按 age 升序
users.sort((a, b) => a.age - b.age);
// 结果:李四(18) → 张三(25) → 王五(30)

// 按 age 降序
users.sort((a, b) => b.age - a.age);

写法记忆:升序 a - b,降序 b - a

2.2 按字符串排序

// 按 name 字母/拼音顺序
users.sort((a, b) => a.name.localeCompare(b.name));

直接用 a.name > b.name ? 1 : -1 可以工作,但遇到中文、大小写、多语言时容易出问题,所以更推荐 localeCompare,后面会细讲。

2.3 按日期排序

日期有两种常见形式:字符串和时间戳。

const orders = [
  { id: 1, date: '2025-02-15' },
  { id: 2, date: '2025-01-20' },
  { id: 3, date: '2025-02-10' }
];

// 方式一:YYYY-MM-DD 格式的字符串可以直接用 localeCompare
orders.sort((a, b) => a.date.localeCompare(b.date));

// 方式二:转时间戳(适用各种日期格式)
orders.sort((a, b) => new Date(a.date) - new Date(b.date));

建议:后端返回的日期如果是 YYYY-MM-DD,用 localeCompare 即可;格式不统一时,统一用 new Date() 转时间戳再比较。

2.4 多字段排序

先按 A 排序,A 相同再按 B 排序,可以用 || 链式比较:

users.sort((a, b) => {
  if (a.age !== b.age) return a.age - b.age;  // 先按年龄
  return a.name.localeCompare(b.name);        // 年龄相同再按姓名
});

// 更简洁的写法
users.sort((a, b) => a.age - b.age || a.name.localeCompare(b.name));

原理a.age - b.age 为 0 时,0 || xxx 会取后面的 localeCompare 结果。

三、localeCompare:字符串排序的正确姿势

3.1 为什么不用 >、< 比较字符串?

const arr = ['张三', '李四', '王五', 'apple', 'Apple'];
arr.sort((a, b) => a > b ? 1 : -1);  // 按 Unicode 比较,中文结果不符合直觉
arr.sort((a, b) => a.localeCompare(b));  // 按语言规则,更符合人类习惯

localeCompare 可以:

  • 中文按拼音
  • 控制大小写敏感
  • 数字按数值比较(如 "10" 在 "2" 后面)

3.2 常用用法

// 指定语言(中文按拼音)
'张三'.localeCompare('李四', 'zh-CN');  // 负数,张在李后面

// 忽略大小写
'apple'.localeCompare('Apple', undefined, { sensitivity: 'base' });  // 0,视为相等

// 数字按数值比较
['10', '2', '1'].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
// 结果:['1', '2', '10']

3.3 兼容性说明

现代浏览器和 Node 都支持 localeCompare。带 options 配置的 localeCompare 写法,在老环境(旧浏览器 / 旧 Node 版本)中可能表现不一致,生产环境建议先小范围验证。

// 忽略大小写(options:{ sensitivity: 'base' })
'apple'.localeCompare('Apple', undefined, { sensitivity: 'base' });
// 数字按数值比较(options:{ numeric: true })
['10','2'].sort((a,b) => a.localeCompare(b, undefined, { numeric: true }));

老环境问题:像旧版 IE、低版本 Node(比如 Node.js 10 以下),对这些options配置支持不完善(比如不识别numeric: true),导致排序结果出错,所以生产环境要先小范围验证。

3.4 补充localeCompareoptions写法 老环境兼容技巧

核心兼容思路:降级处理——先判断环境是否支持localeCompareoptions配置,支持则用带options的简洁写法,不支持则降级为基础写法,保证排序效果一致,且代码简单可直接套用(无需额外引入兼容库)。

场景1:忽略大小写排序(对应options: { sensitivity: 'base' })音标:/sensəˈtɪvəti/

老环境兼容写法(适配旧IE、低版本Node):

// 兼容函数:忽略大小写比较两个字符串
function compareIgnoreCase(a, b) {
  // 先统一转小写,再用基础localeCompare(老环境均支持无options写法)
  const lowerA = a.toLowerCase();
  const lowerB = b.toLowerCase();
  return lowerA.localeCompare(lowerB, 'zh-CN'); // 中文场景可加语言标识
}

// 用法(和带options写法效果一致)
const arr = ['apple', 'Apple', 'Banana', 'banana'];
arr.sort(compareIgnoreCase); // 结果:['apple', 'Apple', 'Banana', 'banana']

场景2:数字字符串按数值排序(对应options: { numeric: true }

老环境兼容写法(避免老环境不识别numeric 音标:/njuːˈmerɪk/ 配置导致排序错乱):

// 兼容函数:数字字符串按数值排序
function compareNumericStr(a, b) {
  // 降级思路:转成数字比较(贴合原文数字排序逻辑,老环境完全支持)
  const numA = Number(a);
  const numB = Number(b);
  return numA - numB; // 升序,降序则改为numB - numA
}

// 用法(和带options写法效果一致)
const arr = ['10', '2', '1', '25'];
arr.sort(compareNumericStr); // 结果:['1', '2', '10', '25']

关键注意点

  • 无需判断环境:上述兼容写法兼容所有环境(老环境正常运行,新环境也不影响效果),不用额外写环境判断代码,简化开发。

  • 生产环境验证:如果老环境占比极低,可直接用带options写法,上线前用老环境(如IE11、Node.js 8)简单测试1个排序案例即可。

四、分组统计:从排序到 groupBy 【分组】

排序和分组是两个不同操作:

  • 排序:改变顺序,不拆分数组
  • 分组:按某个字段把数组拆成多组

JS 没有内置 groupBy,可以用 reduce 实现:

const orders = [
  { id: 1, status: 'paid', amount: 100 },
  { id: 2, status: 'pending', amount: 50 },
  { id: 3, status: 'paid', amount: 200 }
];

const byStatus = orders.reduce((acc, item) => {
  const key = item.status;
  if (!acc[key]) acc[key] = [];
  acc[key].push(item);
  return acc;
}, {});

// 结果:
// {
//   paid: [{ id: 1, ... }, { id: 3, ... }],
//   pending: [{ id: 2, ... }]
// }

分组后再排序

分组后,如果每组内部还要排序:

Object.keys(byStatus).forEach(key => {
  byStatus[key].sort((a, b) => b.amount - a.amount);  // 每组按金额降序
});

分组 + 统计

需要同时统计每组数量或汇总值时:

const stats = orders.reduce((acc, item) => {
  const key = item.status;
  if (!acc[key]) {
    acc[key] = { list: [], total: 0, count: 0 };
  }
  acc[key].list.push(item);
  acc[key].total += item.amount;
  acc[key].count += 1;
  return acc;
}, {});

// 结果示例:{ paid: { list: [...], total: 300, count: 2 }, ... }

五、踩坑速查表

坑点 错误表现 正确写法
数字数组排序错乱 [10, 2, 1].sort()[1, 10, 2] arr.sort((a, b) => a - b)
原数组被修改 排序后原数组也变了 [...arr].sort(...)
中文排序不对 直接用 >< 比较 a.localeCompare(b, 'zh-CN')
多字段排序只写了一层 只按第一个字段排 a.age - b.age || a.name.localeCompare(b.name)
日期格式不统一 字符串比较出错 new Date(a.date) - new Date(b.date)

六、小结

  1. 数字排序:用 (a, b) => a - bb - a,不要用默认 sort()
  2. 字符串排序:优先用 localeCompare,尤其是中文和多语言场景。
  3. 日期排序YYYY-MM-DDlocaleCompare,其他格式用时间戳。
  4. 多字段排序:用 || 串联多个比较。
  5. 分组:用 reducegroupBy,再按需对每组排序或统计。
  6. 保留原数组:排序前先 [...arr] 浅拷贝。

这些写法足够覆盖大部分日常需求,记住上面的速查表,可以少踩很多坑。


以上就是本次的学习分享,欢迎大家在评论区讨论指正,与大家共勉。

我是 Eugene,你的电子学友。

如果文章对你有帮助,别忘了点赞、收藏、加关注,你的认可是我持续输出的最大动力~

UFW Cheatsheet

Basic Commands

Start with status and firewall state.

Command Description
ufw status Show firewall status and rules
ufw status verbose Show detailed status and defaults
sudo ufw enable Enable UFW
sudo ufw disable Disable UFW
sudo ufw reload Reload rules
sudo ufw reset Reset UFW to defaults

Default Policies

Set default inbound and outbound behavior.

Command Description
sudo ufw default deny incoming Deny all incoming by default
sudo ufw default allow outgoing Allow all outgoing by default
sudo ufw default deny outgoing Deny all outgoing by default
sudo ufw default allow incoming Allow all incoming (not recommended on servers)

Allow and Deny Rules

Allow or block traffic by port and protocol.

Command Description
sudo ufw allow 22 Allow port 22 (TCP and UDP)
sudo ufw allow 80/tcp Allow HTTP over TCP
sudo ufw allow 443/tcp Allow HTTPS over TCP
sudo ufw deny 25 Deny SMTP port 25
sudo ufw reject 23 Reject Telnet connections
sudo ufw limit 22/tcp Rate-limit SSH connections

Rule Management

List, delete, and clean specific rules.

Command Description
sudo ufw status numbered List rules with numbers
sudo ufw delete allow 80/tcp Delete matching rule
sudo ufw delete 3 Delete rule by number
sudo ufw delete deny 25 Delete a deny rule

IP-Based Rules

Allow or deny traffic from specific hosts and networks.

Command Description
sudo ufw allow from 203.0.113.10 Allow all traffic from one IP
sudo ufw deny from 203.0.113.10 Block all traffic from one IP
sudo ufw allow from 203.0.113.10 to any port 22 Allow SSH from one IP
sudo ufw allow from 10.0.0.0/24 to any port 3306 Allow MySQL from a subnet
sudo ufw deny from 198.51.100.0/24 to any port 22 proto tcp Deny TCP SSH from subnet

Application Profiles

Use service profiles from /etc/ufw/applications.d/.

Command Description
sudo ufw app list List available application profiles
sudo ufw app info "Nginx Full" Show ports/protocols for profile
sudo ufw allow "OpenSSH" Allow profile rules
sudo ufw deny "Nginx HTTP" Deny profile rules
sudo ufw delete allow "OpenSSH" Remove allowed profile

Logging

Control and inspect UFW logging.

Command Description
sudo ufw logging on Enable logging
sudo ufw logging off Disable logging
sudo ufw logging low Set low log level
sudo ufw logging medium Set medium log level
sudo ufw logging high Set high log level

Common Server Setup

Baseline rules for a web server.

Command Description
sudo ufw default deny incoming Deny incoming by default
sudo ufw default allow outgoing Allow outgoing by default
sudo ufw allow OpenSSH Keep SSH access
sudo ufw allow 80/tcp Allow HTTP
sudo ufw allow 443/tcp Allow HTTPS
sudo ufw enable Activate firewall
sudo ufw status verbose Verify active rules

Troubleshooting

Quick checks for common UFW issues.

Issue Check
SSH access lost after enable Ensure OpenSSH is allowed before ufw enable
Rule did not apply Run sudo ufw reload and re-check with ufw status numbered
Service still unreachable Confirm service is listening (ss -tulpn) and port/protocol match
Rules conflict Check order with ufw status numbered and delete/re-add as needed
UFW not active at boot Verify service state with systemctl status ufw

Related Guides

Use these guides for full UFW workflows.

Guide Description
How to Set Up a Firewall with UFW on Ubuntu 20.04 Full UFW setup on Ubuntu 20.04
How to Set Up a Firewall with UFW on Ubuntu 18.04 UFW setup on Ubuntu 18.04
How to Set Up a Firewall with UFW on Debian 10 UFW setup on Debian 10
How to List and Delete UFW Firewall Rules Rule management and cleanup

Flutter 为什么能运行在 HarmonyOS 上

作者 Bowen_Jin
2026年2月18日 13:42

335328e4fabd7656e8f1e9587269d3a4.jpeg

前言

Flutter 是 Google 推出的跨平台 UI 框架,最初只支持 iOS 和 Android。随着 HarmonyOS 的崛起,Flutter 也能在鸿蒙系统上运行了。这背后到底是怎么实现的呢?本文将从源码层面进行解析。


一、核心原理:Flutter 分层架构

要理解 Flutter 如何在 HarmonyOS 上运行,首先需要了解 Flutter 的架构。Flutter 采用分层设计,从上到下分为三层:

┌─────────────────────────────────┐
│   Framework 层(Dart)           │  ← Flutter 代码
├─────────────────────────────────┤
│   Engine 层(C++)               │  ← 渲染引擎(Impeller)
├─────────────────────────────────┤
│   Embedder 层(平台相关)         │  ← 与操作系统交互(调用 HarmonyOS 原生 API)
└─────────────────────────────────┘

前面两层完全复用现有Dart和C++代码,而 Embedder 层则是为 HarmonyOS 定制的。

关键点:Embedder 层

Embedder 层是 Flutter 能够跨平台运行的关键。它负责:

  • 创建和管理窗口

  • 处理输入事件

  • 调用系统 API

  • 管理渲染 Surface

**不同平台有不同的 Embedder 实现: **

  • Android:platform_view_android.cc

  • iOS:platform_view_ios.mm

  • HarmonyOS:platform_view_ohos.cpp


cc和cpp是标准的C++语言代码后缀

鸿蒙的系统API是C++ 实现的,所以鸿蒙platform_view 使用C++实现进行调用最方便**

二、HarmonyOS Embedder 的核心实现

让我们看看 HarmonyOS Embedder 的核心代码结构:

2.1 平台视图(PlatformViewOHOS)

这是 HarmonyOS Embedder 的核心类,位于: engine/src/flutter/shell/platform/ohos/platform_view_ohos.cpp

class PlatformViewOHOS final : public PlatformView {
 public:
  PlatformViewOHOS(PlatformView::Delegate& delegate,
                   const flutter::TaskRunners& task_runners,
                   const std::shared_ptr<PlatformViewOHOSNapi>& napi_facade,
                   const std::shared_ptr<flutter::OHOSContext>& ohos_context);

  // 通知窗口创建
  void NotifyCreate(fml::RefPtr<OHOSNativeWindow> native_window); 

  // 更新显示尺寸
  void UpdateDisplaySize(int width, int height);

  // 分发平台消息
  void DispatchPlatformMessage(std::string name, void* message, ...);
 private:
  std::shared_ptr<OHOSContext> ohos_context_;  // HarmonyOS 图形上下文
  std::shared_ptr<PlatformViewOHOSNapi> napi_facade_;  // NAPI装饰器(NAPI 是 HarmonyOS 提供的 JavaScript 接口, 用于调用 HarmonyOS 系统 API
  std::unique_ptr<OHOSSurface> ohos_surface_;  // HarmonyOS 渲染 Surface, surface 是渲染的目标画布, 可以是窗口, 也可以是离屏缓冲区
};

**这个类做了什么? **

  1. 继承自 PlatformView(Flutter 的通用平台视图接口)

  2. 持有 HarmonyOS 的图形上下文 OHOSContext

  3. 持有 NAPI装饰器 PlatformViewOHOSNapi(用于调用 HarmonyOS 原生 API)

  4. 管理渲染 Surface OHOSSurface

2.2 Shell 持有者(OHOSShellHolder)

Shell 是 Flutter 引擎的核心,负责管理 Flutter 应用的生命周期、渲染循环、事件处理等, OHOSShellHolder 负责创建和管理 Shell:

class OHOSShellHolder {
 public:
  // 构造函数
  // settings: Flutter 引擎启动参数(如是否启用 Impeller、日志级别等)

  // napi_facade: 与 HarmonyOS 原生层交互的 NAPI 装饰器

  // platform_loop: HarmonyOS 平台线程的 looper,用于投递平台任务
  OHOSShellHolder(const flutter::Settings& settings,
                  std::shared_ptr<PlatformViewOHOSNapi> napi_facade,
                  void* platform_loop);

  // 析构函数:确保 Shell 安全退出并释放所有资源
  ~OHOSShellHolder();
 
  // 启动 Flutter 引擎,加载 Dart 代码并开始渲染
  // hap_asset_provider: HarmonyOS HAP 包资源提供器,用于读取 assets、fonts、kernel_blob 等
  // entrypoint: Dart 入口函数名(默认为 main)

  // libraryUrl: Dart 库 URI(如 package:my_app/main.dart)

  // entrypoint_args: 传给 Dart main 的命令行参数列表

  void Launch(std::unique_ptr<OHOSAssetProvider> hap_asset_provider,
              const std::string& entrypoint,
              const std::string& libraryUrl,
              const std::vector<std::string>& entrypoint_args);
  // 优雅地停止 Flutter Shell,等待所有任务完成后退出
  void Shutdown();
  // 获取 PlatformViewOHOS 的弱引用,用于在平台线程安全地访问平台视图
  fml::WeakPtr<PlatformViewOHOS> GetPlatformView();
  // 设置应用生命周期回调,供 HarmonyOS 通知 Flutter 前后台切换
  void SetLifecycleHandler(std::function<void(AppLifecycleState)> handler);
  // 设置平台消息回调,供 HarmonyOS 主动发消息到 Dart 侧
  void SetPlatformMessageHandler(
      std::function<void(const std::string& channel,
                         const std::vector<uint8_t>& message,
                         std::function<void(std::vector<uint8_t>)> reply)> handler);
  // 向 Dart 侧发送平台消息,支持异步回调
  void SendPlatformMessage(const std::string& channel,
                           const std::vector<uint8_t>& message,
                           std::function<void(std::vector<uint8_t>)> reply = nullptr);
  // 通知 Flutter 引擎窗口尺寸变化,触发重新布局
  void NotifyViewportMetricsChanged(const ViewportMetrics& metrics);
  // 通知 Flutter 引擎内存压力,触发 Dart 侧 GC 或资源释放
  void NotifyLowMemoryWarning();
  // 获取当前 Shell 的运行状态
  enum class ShellState { kNotStarted, kRunning, kShuttingDown, kStopped };
  ShellState GetShellState() const;
  // 返回当前线程安全的 Shell 指针,仅用于调试或测试
  Shell* GetShellUnsafe() const { return shell_.get(); }
 private:
  // 创建并配置 Flutter Shell,内部调用 Shell::Create
  void CreateShell(const flutter::Settings& settings,
                   std::unique_ptr<OHOSAssetProvider> asset_provider);
  // 初始化平台任务执行器,将 HarmonyOS 平台任务映射到 Flutter 的任务队列
  void SetupTaskRunners(void* platform_loop);
  // 注册 HarmonyOS 平台视图到 Shell,完成平台桥接
  void RegisterPlatformView();
  // 加载 Dart AOT 或 Kernel,决定运行模式(Release/Profile 使用 AOT,Debug 使用 Kernel)
  void LoadDartCode(const std::string& entrypoint,
                    const std::string& libraryUrl,
                    const std::vector<std::string>& entrypoint_args);
  // 释放所有资源,顺序:PlatformView → Shell → TaskRunners
  void Teardown();
 private:
  std::unique_ptr<Shell> shell_;                         // Flutter 引擎核心
  std::shared_ptr<PlatformViewOHOSNapi> napi_facade_;  // NAPI 装饰器
  fml::WeakPtrFactory<OHOSShellHolder> weak_factory_;    // 弱引用工厂,防止悬空指针
  ShellState state_ = ShellState::kNotStarted;           // 当前 Shell 状态
  flutter::TaskRunners task_runners_;                    // 跨平台任务队列(UI/GPU/IO/Platform)
  std::mutex state_mutex_;                               // 保护 state_ 的线程安全
};

三、图形渲染适配

Flutter 在 HarmonyOS 上支持三种渲染方式:

3.1 鸿蒙三种渲染方式

enum class OHOSRenderingAPI {
  kSoftware,          // 软件渲染, 基于 CPU 进行渲染, 性能较低, 不依赖于 GPU,适用于简单场景。
  kOpenGLES,          // OpenGL ES 渲染(Skia), 基于 OpenGL ES 进行渲染, 性能较高, 依赖于 GPU, 适用于复杂场景。
  kImpellerVulkan,    // Vulkan 渲染(Impeller), 基于 Vulkan 进行渲染, 性能最高, 依赖于 GPU, 适用于需要高性能渲染的场景。
};

platform_view_ohos.cpp 中,根据渲染方式创建不同的Surface

std::unique_ptr<OHOSSurface> OhosSurfaceFactoryImpl::CreateSurface() {
  switch (ohos_context_->RenderingApi()) {
    case OHOSRenderingAPI::kSoftware:
      return std::make_unique<OHOSSurfaceSoftware>(ohos_context_); // 软件渲染, 基于 CPU 进行渲染, 性能较低, 不依赖于 GPU,适用于简单场景。
    case OHOSRenderingAPI::kOpenGLES:
      return std::make_unique<OhosSurfaceGLSkia>(ohos_context_); // OpenGL ES 渲染(Skia), 基于 OpenGL ES 进行渲染, 性能较高, 依赖于 GPU, 适用于复杂场景。
    case flutter::OHOSRenderingAPI::kImpellerVulkan:
      return std::make_unique<OHOSSurfaceVulkanImpeller>(ohos_context_); // Vulkan 渲染(Impeller), 基于 Vulkan 进行渲染, 性能最高, 依赖于 GPU, 适用于需要高性能渲染的场景。
    default:
      return nullptr;
  }
}

3.2 原生窗口(OHOSNativeWindow)

HarmonyOS 的窗口系统通过 OHNativeWindow 暴露给 Flutter:

class OHOSNativeWindow : public fml::RefCountedThreadSafe<OHOSNativeWindow> {
 public:
  Handle Gethandle() const// 获取 HarmonyOS 原生窗口句柄
  bool IsValid() const;      // 检查窗口是否有效
  SkISize GetSize() const;   // 获取窗口尺寸
 private:
  Handle window_;  // OHNativeWindow*
};

**渲染流程: **

Flutter Engine
    ↓
PlatformViewOHOS
    ↓
OHOSSurface(根据渲染方式创建不同的Surface)
    ↓
OHOSNativeWindow(HarmonyOS 原生窗口)
    ↓
HarmonyOS 图形系统

四、输入事件处理

因为事件处理需要在渲染完成后(VSync同步流程)才能触发, 否则会导致事件处理与渲染不一致的问题。

4.1 VSync 同步

VSync(垂直同步)信号是渲染的关键,它是每次屏幕刷新周期开始时发送的信号,用于同步渲染和显示。

Flutter 需要等待系统的 VSync 信号,才能触发下一帧渲染。

class VsyncWaiterOHOS final : public VsyncWaiter {
 public:
  explicit VsyncWaiterOHOS(const flutter::TaskRunners& task_runners,
                           std::shared_ptr<bool>& enable_frame_cache);

 private:
  OH_NativeVSync* vsync_handle_;  // HarmonyOS VSync 句柄
  void AwaitVSync() override// 等待 VSync 信号
  static void OnVsyncFromOHOS(long long timestamp, void* data); // 接收 HarmonyOS VSync 信号, 通知 Flutter Engine 触发下一帧渲染
};

**工作流程: **

HarmonyOS VSync 信号
    ↓
VsyncWaiterOHOS::OnVsyncFromOHOS
    ↓
通知 Flutter Engine
    ↓
触发下一帧渲染
    ↓
渲染完成
    ↓
触发事件处理

4.2 触摸事件处理

HarmonyOS 的输入事件需要转换为 Flutter 的事件格式:

触摸事件通过 OhosTouchProcessor 处理:

class OhosTouchProcessor {
 public:
  // 处理 HarmonyOS 触摸事件
  void ProcessTouchEvent(const OH_NativeXComponent_TouchEvent* event);
 private:
  // 转换为 Flutter 触摸事件格式
  std::vector<PointerData> ConvertToFlutterTouchEvents(
      const OH_NativeXComponent_TouchEvent* event);
};

五、平台消息通信

Flutter 与 HarmonyOS 的通信通过 Platform Channel 实现:

5.1 NAPI 装饰器(PlatformViewOHOSNapi)

NAPI(Native API)是 HarmonyOS 提供的原生 API 接口:

class PlatformViewOHOSNapi {
 public:
  // 发送平台消息到 HarmonyOS
  void SendPlatformMessage(const std::string& channel,
                           const std::vector<uint8_t>& message);
  // 接收来自 HarmonyOS 的平台消息
  void SetPlatformMessageHandler(
      std::function<void(const std::string&, const std::vector<uint8_t>&)> handler);
 private:
  napi_env env_;  // NAPI 环境
};

5.2 消息处理流程

Flutter 代码(Dart)
    ↓
MethodChannel.invokeMethod
    ↓
PlatformViewOHOS::DispatchPlatformMessage
    ↓
PlatformViewOHOSNapi::SendPlatformMessage
    ↓
HarmonyOS 原生代码(ArkTS/C++)
    ↓
返回结果
    ↓
Flutter 接收响应

六、完整的工作流程

让我们把所有部分串联起来,看看 Flutter 应用在 HarmonyOS 上是如何运行的:

6.1 初始化流程

1. HarmonyOS 应用启动
    ↓
2. 调用 OhosMain::NativeInit(NAPI 入口)
    ↓
3. 创建 OHOSShellHolder
    ↓
4. 创建 PlatformViewOHOS
    ↓
5. 创建 OHOSContext(图形上下文)
    ↓
6. 创建 OHOSSurface(渲染表面)
    ↓
7. 创建 Flutter Shell(引擎)
    ↓
8. 加载 Dart 代码
    ↓
9. 开始渲染

6.2 渲染流程

1. Dart 代码构建 Widget 树
    ↓
2. Framework 层生成 Layer 树
    ↓
3. Engine 层生成 Scene
    ↓
4. Impeller 渲染引擎绘制
    ↓
5. 通过 OHOSSurface 提交绘制指令
    ↓
6. OHOSNativeWindow 接收绘制结果
    ↓
7. HarmonyOS 图形系统显示到屏幕

6.3 事件处理流程

1. 用户触摸屏幕
    ↓
2. HarmonyOS 接收触摸事件
    ↓
3. OhosTouchProcessor 处理
    ↓
4. 转换为 Flutter 触摸事件格式
    ↓
5. PlatformViewOHOS 分发事件
    ↓
6. Framework 层处理事件
    ↓
7. Widget 响应用户操作

七、关键代码示例

7.1 创建 HarmonyOS Embedder

// 创建图形上下文
std::unique_ptr<OHOSContext> CreateOHOSContext(
    const flutter::TaskRunners& task_runners,
    OHOSRenderingAPI rendering_api,
    bool enable_vulkan_validation,
    bool enable_opengl_gpu_tracing,
    bool enable_vulkan_gpu_tracing) {
  switch (rendering_api) {
    case OHOSRenderingAPI::kSoftware:
      return std::make_unique<OHOSContext>(OHOSRenderingAPI::kSoftware);
    case OHOSRenderingAPI::kOpenGLES:
      return std::make_unique<OhosContextGLSkia>(OHOSRenderingAPI::kOpenGLES,
                                                 task_runners);
    case OHOSRenderingAPI::kImpellerVulkan:
      return std::make_unique<OHOSContextVulkanImpeller>(
          enable_vulkan_validation, enable_vulkan_gpu_tracing);
    default:
      return nullptr;
  }
}
// 创建平台视图
PlatformViewOHOS::PlatformViewOHOS(
    PlatformView::Delegate& delegate,
    const flutter::TaskRunners& task_runners,
    const std::shared_ptr<PlatformViewOHOSNapi>& napi_facade,
    const std::shared_ptr<flutter::OHOSContext>& ohos_context)
    : PlatformView(delegate, task_runners),
      napi_facade_(napi_facade),
      ohos_context_(ohos_context) {
  // 创建 Surface 工厂
  surface_factory_ = std::make_shared<OhosSurfaceFactoryImpl>(ohos_context_);
  // 创建渲染 Surface
  ohos_surface_ = surface_factory_->CreateSurface();
  // 预加载 GPU Surface(加速首帧渲染)
  task_runners_.GetRasterTaskRunner()->PostDelayedTask(
      [surface = ohos_surface_]() { surface->PrepareGpuSurface(); },
      fml::TimeDelta::FromMicroseconds(1000));
}

7.2 通知窗口创建

void PlatformViewOHOS::NotifyCreate(
    fml::RefPtr<OHOSNativeWindow> native_window) {
  FML_LOG(INFO) << "NotifyCreate start";
  // 缓存原生窗口
  native_window_ = native_window;
  // 通知 Surface 窗口已创建
  ohos_surface_->SetNativeWindow(native_window);
  // 获取窗口尺寸
  SkISize size = native_window->GetSize();
  // 更新视口尺寸
  UpdateDisplaySize(size.width(), size.height());

  // 通知 Flutter 引擎窗口已创建
  NotifyCreated();
}

7.3 处理平台消息

void PlatformViewOHOS::DispatchPlatformMessage(
    std::string name,
    void* message,
    int messageLength,
    int responseId) {
  // 创建平台消息
  fml::MallocMapping buffer = fml::MallocMapping(
      static_cast<const uint8_t*>(message), messageLength);
  auto platform_message = std::make_unique<PlatformMessage>(
      name,
      std::move(buffer),
      responseId,
      fml::TimePoint::Now());
  // 分发到 Flutter 引擎
  DispatchPlatformMessage(std::move(platform_message));
}

八、为什么 Flutter 能在 HarmonyOS 上运行?

通过上面的代码分析,我们可以总结出以下几个关键原因:

8.1 架构设计优势

Flutter 的分层架构设计使得 Embedder 层可以独立适配不同平台:

  • Framework 层Engine 层是平台无关的

  • 只有 Embedder 层需要针对不同平台实现

8.2 HarmonyOS 提供的开放接口

HarmonyOS 提供了丰富的原生 API,使得 Flutter 可以:

  • 通过 OHNativeWindow 获取窗口句柄

  • 通过 OH_NativeVSync 获取 VSync 信号

  • 通过 NAPI 调用系统能力

  • 通过 XComponent 组件集成 Flutter 视图

8.3 图形接口兼容

HarmonyOS 支持标准的图形接口:

  • OpenGL ES:Skia 渲染引擎可以直接使用

  • Vulkan:Impeller 渲染引擎可以直接使用

  • NativeWindow:提供了跨平台的窗口抽象

8.4 社区共同努力

  • 华为官方和 Flutter 社区共同维护 flutter_flutter 项目

  • 基于 Flutter Engine 源码进行适配

  • 提供完整的开发工具链


从代码层面看,核心就是实现了 PlatformViewOHOSOHOSShellHolderOHOSContext 等类,将 Flutter Engine 与 HarmonyOS 系统连接起来。

**一句话总结:Flutter 通过实现 HarmonyOS 专属的 Embedder 层,将 Flutter Engine 与 HarmonyOS 的窗口系统、图形系统、输入系统对接,从而实现了跨平台运行。 **


九、参考资料

Vue3组件开发中如何兼顾复用性、可维护性与性能优化?

作者 kknone
2026年2月18日 12:33

一、组件开发的基本原则

1.1 单一职责原则

每个组件应专注于完成一个核心功能,避免将过多无关逻辑塞进同一个组件。例如,一个用户信息组件只负责展示用户头像、名称和基本资料,而不处理表单提交或数据请求逻辑。这种设计让组件更易于理解、测试和维护。

1.2 可复用性原则

通过Props、插槽和组合式API提高组件的复用性。例如,一个按钮组件可以通过Props定义不同的尺寸、颜色和状态,通过插槽支持自定义内容,从而在多个页面中重复使用。

1.3 可维护性原则

  • 命名规范:使用有意义的组件名称(如UserAvatar而非Avatar1),Props和事件名称采用kebab-case(如max-count而非maxCount)。
  • 模块化结构:将组件按功能划分到不同目录(如components/UI存放通用UI组件,components/Features存放业务功能组件)。
  • 注释文档:为组件和关键逻辑添加注释,说明组件用途、Props含义和事件触发时机。

二、组件设计的最佳实践

2.1 Props设计规范

使用TypeScript定义Props类型,设置默认值和校验规则,避免传递无效数据导致组件异常。

<template>
  <div class="counter">
    <button @click="decrement">-</button>
    <span>{{ count }}</span>
    <button @click="increment">+</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

// 定义Props类型和默认值
interface Props {
  count?: number;
  step?: number;
  min?: number;
  max?: number;
}

const props = withDefaults(defineProps<Props>(), {
  count: 0,
  step: 1,
  min: 0,
  max: 100
});

const emit = defineEmits<{
  'update:count': [value: number]
}>();

const increment = () => {
  const newValue = props.count + props.step;
  if (newValue <= props.max) {
    emit('update:count', newValue);
  }
};

const decrement = () => {
  const newValue = props.count - props.step;
  if (newValue >= props.min) {
    emit('update:count', newValue);
  }
};
</script>

2.2 自定义事件处理

通过defineEmits定义组件触发的事件,避免直接修改父组件状态,保持数据流的单向性。

<!-- 父组件 -->
<template>
  <Counter :count="count" @update:count="count = $event" />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import Counter from './Counter.vue';

const count = ref(0);
</script>

2.3 灵活使用插槽

通过插槽让组件支持自定义内容,提高组件的灵活性和复用性。

<!-- Card.vue -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header">默认标题</slot>
    </div>
    <div class="card-body">
      <slot>默认内容</slot>
    </div>
    <div class="card-footer">
      <slot name="footer" :current-time="currentTime">
        默认页脚 - {{ currentTime }}
      </slot>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const currentTime = ref(new Date().toLocaleTimeString());
</script>
<!-- 使用Card组件 -->
<template>
  <Card>
    <template #header>
      <h2>用户详情</h2>
    </template>
    <p>这是用户的详细信息...</p>
    <template #footer="{ currentTime }">
      更新时间:{{ currentTime }}
    </template>
  </Card>
</template>

2.4 组合式API的逻辑复用

往期文章归档
免费好用的热门在线工具

将组件逻辑抽离成可复用的Composables,提高代码的可维护性和复用性。

// composables/useCounter.ts
import { ref, computed } from 'vue';

export function useCounter(initialCount = 0, step = 1) {
  const count = ref(initialCount);
  
  const increment = () => count.value += step;
  const decrement = () => count.value -= step;
  const doubleCount = computed(() => count.value * 2);
  
  return { count, increment, decrement, doubleCount };
}
<!-- 在组件中使用 -->
<template>
  <div class="counter">
    <button @click="decrement">-</button>
    <span>{{ count }}</span>
    <span>双倍值:{{ doubleCount }}</span>
    <button @click="increment">+</button>
  </div>
</template>

<script setup lang="ts">
import { useCounter } from '@/composables/useCounter';

const { count, increment, decrement, doubleCount } = useCounter(0, 2);
</script>

三、组件通信的多种实现方式

graph TD
    A[父组件] -->|Props| B[子组件]
    B -->|Events| A
    A -->|Provide| C[深层子组件]
    C -->|Inject| A
    D[Pinia Store] -->|读取/修改| A
    D -->|读取/修改| B
    D -->|读取/修改| C

3.1 父子组件通信:Props与Events

这是最基础的通信方式,父组件通过Props传递数据给子组件,子组件通过Events通知父组件更新状态。

3.2 跨层级通信:Provide与Inject

适用于深层嵌套组件之间的通信,父组件通过provide提供数据,子组件通过inject获取数据。

<!-- 父组件 -->
<script setup lang="ts">
import { provide } from 'vue';
import ChildComponent from './ChildComponent.vue';

provide('theme', 'dark');
</script>
<!-- 深层子组件 -->
<script setup lang="ts">
import { inject } from 'vue';

const theme = inject('theme', 'light'); // 默认值为light
</script>

3.3 全局状态管理:Pinia

对于需要在多个组件之间共享的状态(如用户登录状态、购物车数据),推荐使用Pinia进行全局状态管理。

// stores/counter.ts
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() { this.count++; },
    decrement() { this.count--; }
  },
  getters: {
    doubleCount: (state) => state.count * 2
  }
});
<!-- 在组件中使用 -->
<template>
  <div>
    <span>{{ store.count }}</span>
    <button @click="store.increment">+</button>
  </div>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';

const store = useCounterStore();
</script>

四、组件性能优化策略

4.1 异步组件与懒加载

使用defineAsyncComponent实现组件懒加载,减少初始包体积,提高页面加载速度。

<template>
  <Suspense>
    <AsyncChart />
    <template #fallback>
      <div>图表加载中...</div>
    </template>
  </Suspense>
</template>

<script setup lang="ts">
import { defineAsyncComponent } from 'vue';

const AsyncChart = defineAsyncComponent(() => import('./Chart.vue'));
</script>

4.2 使用Memoization减少重渲染

使用memo包裹组件,只有当Props发生变化时才重新渲染组件。

<script setup lang="ts">
import { memo } from 'vue';
import ExpensiveComponent from './ExpensiveComponent.vue';

const MemoizedComponent = memo(ExpensiveComponent);
</script>

4.3 虚拟列表处理大量数据

对于包含大量数据的列表,使用虚拟列表技术只渲染可见区域的元素,提高页面性能。推荐使用vue-virtual-scroller库:

npm install vue-virtual-scroller
<template>
  <RecycleScroller
    class="scroller"
    :items="largeList"
    :item-size="50"
  >
    <template v-slot="{ item }">
      <div class="list-item">{{ item }}</div>
    </template>
  </RecycleScroller>
</template>

<script setup lang="ts">
import { RecycleScroller } from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';

const largeList = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
</script>

五、常见问题排查与调试技巧

5.1 Props类型不匹配问题

问题:父组件传递的Props类型与子组件定义的类型不匹配(如传递字符串"5"而非数字5)。 解决:在父组件中转换数据类型,或在子组件中使用类型转换:

// 子组件中处理
const safeCount = Number(props.count);

5.2 事件绑定错误

问题:父组件监听的事件名称与子组件emit的事件名称不一致(如子组件emit('updateCount'),父组件监听update:count)。 解决:统一事件名称,使用kebab-case规范:

// 子组件
emit('update:count', newValue);

// 父组件
<Counter @update:count="handleUpdate" />

5.3 响应式数据更新不及时

问题:直接修改数组索引或对象属性,Vue无法检测到变化:

// 错误写法
const list = ref([1,2,3]);
list.value[0] = 4; // Vue无法检测到

// 正确写法
list.value.splice(0, 1, 4);

六、课后Quiz

问题1:如何在Vue3中实现跨层级组件通信?请至少列举两种方式并说明适用场景。

答案解析:

  1. Provide/Inject:适用于深层嵌套组件之间的通信(如主题设置、全局配置)。父组件通过provide提供数据,子组件通过inject获取数据。优点是无需逐层传递Props,缺点是可能导致组件耦合度升高。
  2. Pinia状态管理:适用于全局状态共享(如用户登录状态、购物车数据)。通过Pinia Store统一管理状态,任何组件都可以读取和修改Store中的数据。优点是状态管理集中化,缺点是需要额外引入Pinia库。
  3. Event Bus:使用mitt库创建事件总线,组件之间通过发布/订阅事件通信。但Vue3官方不推荐使用,建议优先使用Pinia。

七、常见报错解决方案

7.1 Props类型不匹配警告

错误信息[Vue warn]: Invalid prop: type check failed for prop "count". Expected Number, got String. 原因:父组件传递的Props类型与子组件定义的类型不匹配。 解决:在父组件中传递正确类型的数据,或在子组件中转换类型:

// 父组件
<Counter :count="5" /> <!-- 使用v-bind传递数字 -->

// 子组件
const safeCount = Number(props.count);

7.2 未定义的属性或方法错误

错误信息[Vue warn]: Property "increment" was accessed during render but is not defined on instance. 原因:在<script setup>中未正确导出变量或方法,或在选项式API中未在methods中定义方法。 解决:在<script setup>中确保变量和方法是顶级声明(自动导出),或在选项式API中添加到methods对象中。

7.3 生命周期钩子调用错误

错误信息[Vue warn]: Invalid hook call. Hooks can only be called inside the body of a setup() function. 原因:在setup函数外部调用了组合式API钩子(如onMounted)。 解决:确保所有钩子函数在setup函数内部调用:

<script setup lang="ts">
import { onMounted } from 'vue';

onMounted(() => {
  console.log('组件挂载完成');
});
</script>

八、参考链接

面试官 : “ 请问你实际开发中用过 函数柯理化 吗? 能讲一下吗 ?”

2026年2月18日 11:29

一、先搞懂:柯里化到底是什么?

核心定义:柯里化是把接收多个参数的函数,转换成一系列只接收单个参数的函数,并持续返回新函数,直到所有参数都被传入后,才执行最终逻辑并返回结果。

用 “人话” 说:原本要一次性传完所有参数的函数,现在可以 “分批传”,传一个参数就返回一个新函数等着接下一个,直到传完为止。

对比:普通函数 vs 柯里化函数

// 普通函数:一次性传所有参数
function add(a, b, c) {
  return a + b + c;
}
add(1, 2, 3); // 6

// 柯里化函数:分批次传参数
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}
curriedAdd(1)(2)(3); // 6(传一个参数,返回新函数,直到传完3个)

二、手动实现一个通用柯里化函数

你不用为每个函数单独写柯里化逻辑,这里写一个通用的 curry 工具函数,能把任意多参数函数转换成柯里化函数:

// 通用柯里化函数
function curry(fn) {
  // 保存原函数的参数个数
  const argsLength = fn.length;
  
  // 递归接收参数
  function curried(...args) {
    // 1. 如果已传参数 >= 原函数需要的参数,执行原函数
    if (args.length >= argsLength) {
      return fn.apply(this, args);
    }
    // 2. 否则,返回新函数,继续接收参数
    return function(...newArgs) {
      return curried.apply(this, [...args, ...newArgs]);
    };
  }
  
  return curried;
}

// 测试:给加法函数做柯里化
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);

// 支持多种传参方式(核心优势)
console.log(curriedAdd(1)(2)(3)); // 6(逐个传)
console.log(curriedAdd(1, 2)(3)); // 6(分批传)
console.log(curriedAdd(1)(2, 3)); // 6(混合传)
console.log(curriedAdd(1, 2, 3)); // 6(一次性传)

三、柯里化的核心价值(为什么要用?)

  1. 参数复用:提前固定部分参数,生成新函数,避免重复传参。示例:固定 “税率” 参数,复用计算逻辑

    // 原函数:计算税后价格(价格 + 税率)
    const calculateTax = (taxRate, price) => price * (1 + taxRate);
    // 柯里化后,固定税率为10%
    const calculateTax10 = curry(calculateTax)(0.1);
    // 后续只用传价格,不用重复传税率
    calculateTax10(100); // 110
    calculateTax10(200); // 220
    
  2. 延迟执行:先收集参数,不立即执行,等参数凑齐后再执行。示例:表单提交前收集多个字段,凑齐后再验证提交

    const submitForm = (name, phone, address) => {
      console.log(`提交:${name} ${phone} ${address}`);
    };
    const curriedSubmit = curry(submitForm);
    
    // 分步收集参数(比如用户分步填写表单)
    const step1 = curriedSubmit("张三"); // 收集姓名,未执行
    const step2 = step1("13800138000"); // 收集手机号,未执行
    step2("北京市"); // 收集地址,参数凑齐,执行 → 输出:提交:张三 13800138000 北京市
    
  3. 适配函数参数:把多参数函数转换成单参数函数,适配只接收单参数的场景(比如 React 的高阶组件、数组的 map/filter 等)。示例:适配数组 map 的单参数回调

    // 原函数:乘以指定倍数
    const multiply = (multiplier, num) => num * multiplier;
    const curriedMultiply = curry(multiply);
    
    // 固定倍数为2,生成单参数函数
    const double = curriedMultiply(2);
    // 适配 map 的单参数回调
    [1,2,3].map(double); // [2,4,6]
    

四、常见误区

❌ 误区:“柯里化就是把函数拆成只传一个参数的函数,必须链式调用 (a)(b)(c)”

✅ 纠正:柯里化的核心是 “参数分批传递 + 延迟执行”,支持任意分批方式(比如 (a,b)(c)、(a)(b,c)),不一定非要逐个传。

❌ 误区:“柯里化能提升性能”

✅ 纠正:柯里化本质是多了层函数嵌套,性能略有损耗,它的价值是提升代码复用性和可读性,而非性能。

总结

  1. 柯里化核心:把多参数函数转成 “单参数函数链”,支持参数分批传递,凑齐后执行;
  2. 实现关键:通过闭包保存已传参数,递归判断参数是否凑齐,凑齐则执行原函数;
  3. 核心用途:参数复用、延迟执行、适配单参数场景。

基于高德地图JS的旅游足迹,可嵌入个人博客中

作者 AomanHao
2026年2月18日 10:45

一、足迹地图效果

制作最基础的旅行足迹地图,显示效果见下图,可以查看下面的 Demo 演示,显示标记地点的名称和经纬度,并在地图上用红点显示

足迹footprint - AomanHao的博客空间

以前的足迹地图因为地图不合规,显示效果也不太好,如下图

二、足迹地图制作

教大家如何将制作好的足迹地图嵌入到我们自己的博客中,基于 高德地图 (AMap) 来实现这个功能,因为它对中国地图的支持非常完善,且接入简单。整个页面会包含:

  1. 中国地图的基础展示
  2. 已去过地点的标记(带经纬度显示)

2.1 高德地图 Key 获取

前往 高德开放平台 注册账号,创建应用即可获取(免费)。

1)注册高德开放平台,注册个人认证开发者

2)创建新应用,选择web应用,选择web端JS相关

3)把生成的key复制,替换到代码中高德地图key

2.2 高德地图 Key 应用

把生成的key复制,找到到代码中高德地图key的地方,替换上

html文件在本地浏览器可以直接预览

2.3 线上部署

将该文件放到博客的静态资源目录(如 static/pages/footprint_lite.html)。

在博客导航栏添加链接指向该页面即可。

三、后记

1、因为是静态网页展示,足迹地点需要手动离线更新,然后把新文件覆盖到博客部署文件地址上。

2、考虑足迹更新频次很低,静态更新地址完全是OK的


四、代码链接

足迹代码链接:FootPrint/基于高德地图JS at main · AomanHao/FootPrint · GitHub

推荐用lite版本


我的个人博客主页,欢迎访问

我的CSDN主页,欢迎访问

我的GitHub主页,欢迎访问

我的知乎主页,欢迎访问

O(1) 做法原理讲解(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2026年2月18日 08:26

为了做到 $\mathcal{O}(1)$ 时间,我们需要快速判断所有相邻比特位是否都不同

如何判断不同?用哪个位运算最合适?

异或运算最合适。对于单个比特的异或,如果两个数不同,那么结果是 $1$;如果两个数相同,那么结果是 $0$。

如何对所有相邻比特位做异或运算?

例如 $n = 10101$,可以把 $n$ 右移一位,得到 $01010$,再与 $10101$ 做异或运算,计算的就是相邻比特位的异或值了。

如果异或结果全为 $1$,就说明所有相邻比特位都不同。

如何判断一个二进制数全为 $1$?

这相当于判断二进制数加一后,是否为 231. 2 的幂

设 $x$ 为 (n >> 1) ^ n,如果 (x + 1) & x 等于 $0$,那么说明 $x$ 全为 $1$。

class Solution:
    def hasAlternatingBits(self, n: int) -> bool:
        x = (n >> 1) ^ n
        return (x + 1) & x == 0
class Solution {
    public boolean hasAlternatingBits(int n) {
        int x = (n >> 1) ^ n;
        return ((x + 1) & x) == 0;
    }
}
class Solution {
public:
    bool hasAlternatingBits(int n) {
        uint32_t x = (n >> 1) ^ n;
        return ((x + 1) & x) == 0;
    }
};
bool hasAlternatingBits(int n) {
    uint32_t x = (n >> 1) ^ n;
    return ((x + 1) & x) == 0;
}
func hasAlternatingBits(n int) bool {
x := n>>1 ^ n
return (x+1)&x == 0
}
var hasAlternatingBits = function(n) {
    const x = (n >> 1) ^ n;
    return ((x + 1) & x) === 0;
};
impl Solution {
    pub fn has_alternating_bits(n: i32) -> bool {
        let x = (n >> 1) ^ n;
        (x + 1) & x == 0
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(1)$。
  • 空间复杂度:$\mathcal{O}(1)$。

专题训练

见下面位运算题单的「一、基础题」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

❌
❌