普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月19日掘金 前端

Tailwind CSS vs UnoCSS 深度对比

作者 ElevenSylvia
2026年2月19日 17:35

Tailwind CSS vs UnoCSS 深度对比

完整技术指南:从架构设计到生产实践的全面对比分析

目录

  1. 概述
  2. 核心架构深度对比
  3. 性能基准测试
  4. 生态系统全景分析
  5. 开发体验详解
  6. 配置系统对比
  7. 实战案例
  8. 最佳实践
  9. 常见问题与解决方案
  10. 迁移指南
  11. 未来发展趋势
  12. 总结与建议

1. 概述

1.1 什么是 Tailwind CSS?

Tailwind CSS 是由 Adam Wathan 在 2017 年创建的实用优先(Utility-First)CSS 框架。它提供了一套完整的预定义原子类系统,让开发者通过组合类名来构建界面,而不是编写传统的 CSS。

核心设计理念:

  • Utility-First: 使用预定义的单一功能类
  • Design System: 内置完整的设计系统约束
  • Responsive: 原生支持响应式设计
  • Customizable: 高度可定制但受限于设计系统

版本演进:

v0.x (2017) → v1.0 (2019) → v2.0 (2020) → v3.0 (2021) → v4.0 (2024)

1.2 什么是 UnoCSS?

UnoCSS 是由 Anthony Fu 在 2021 年创建的即时原子化 CSS 引擎。它是一个轻量级的 CSS 生成工具,可以在开发服务器运行时即时生成所需的 CSS,无需预编译。

核心设计理念:

  • Instant: 即时生成,无需等待
  • On-demand: 按需生成,只输出使用的样式
  • Atomic: 原子化 CSS,最小化样式冗余
  • Engine: 可插拔的 CSS 引擎而非框架

架构特点:

UnoCSS = CSS 引擎 + 预设(Presets)+ 规则引擎

1.3 设计哲学对比

维度 Tailwind CSS UnoCSS
定位 CSS 框架 CSS 引擎
方法论 约束设计系统 灵活生成器
输出方式 预编译生成 即时按需生成
生态策略 大而全 小而美
学习曲线 平缓 陡峭但灵活

2. 核心架构深度对比

2.1 编译流程对比

Tailwind CSS 编译流程
┌─────────────────────────────────────────────────────────────────┐
│                    Tailwind CSS 编译流程                         │
└─────────────────────────────────────────────────────────────────┘

[1] 解析配置文件
    ↓
    tailwind.config.js
    - content: 扫描文件路径
    - theme: 设计系统配置
    - plugins: 插件列表

[2] 扫描内容文件
    ↓
    使用 fast-glob 扫描指定路径
    提取所有 class 属性中的字符串

[3] JIT 引擎匹配
    ↓
    将扫描到的类名与核心插件匹配
    生成对应的 CSS 声明

[4] 生成 CSS
    ↓
    按顺序输出:
    - @layer base (Preflight)
    - @layer components
    - @layer utilities

[5] 后处理
    ↓
    - Autoprefixer
    - CSS Nano (生产环境)
    - 输出到指定文件

实际编译示例:

// 输入:HTML 文件
// <div class="flex p-4 text-blue-500">

// 编译过程
tailwindcss -i ./src/input.css -o ./dist/output.css --watch

// 生成的 CSS(简化)
.flex {
  display: flex;
}
.p-4 {
  padding: 1rem;
}
.text-blue-500 {
  --tw-text-opacity: 1;
  color: rgb(59 130 246 / var(--tw-text-opacity));
}
UnoCSS 编译流程
┌─────────────────────────────────────────────────────────────────┐
│                     UnoCSS 编译流程                              │
└─────────────────────────────────────────────────────────────────┘

[1] 初始化引擎
    ↓
    uno.config.ts
    - presets: 预设列表
    - rules: 自定义规则
    - shortcuts: 快捷方式

[2] 中间件拦截(Vite/Webpack)
    ↓
    拦截模块请求
    - 虚拟模块: virtual:uno.css
    - CSS 注入点

[3] 即时解析
    ↓
    当文件变化时:
    - 解析文件内容
    - 提取类名
    - 匹配规则引擎
    - 即时生成 CSS

[4] 动态生成
    ↓
    每个请求实时生成:
    - 无需持久化文件
    - 按需计算
    - 缓存优化

[5] 响应返回
    ↓
    直接注入到 DOM
    或通过 HMR 更新

实际编译示例:

// uno.config.ts
import { defineConfig, presetUno } from 'unocss'

export default defineConfig({
  presets: [presetUno()]
})

// 在 main.ts 中
import 'virtual:uno.css'

// 开发服务器即时响应
// 类名在访问时即时解析

2.2 类名生成机制详解

Tailwind CSS 的生成逻辑
// 核心生成逻辑(简化版)
const corePlugins = {
  flex: () => ({
    '.flex': { display: 'flex' }
  }),
  
  padding: () => ({
    '.p-1': { padding: '0.25rem' },
    '.p-2': { padding: '0.5rem' },
    '.p-4': { padding: '1rem' },
    // ... 预定义值
  }),
  
  textColor: (theme) => {
    const colors = theme('colors')
    return Object.entries(colors).reduce((acc, [key, value]) => {
      if (typeof value === 'string') {
        acc[`.text-${key}`] = { color: value }
      } else {
        Object.entries(value).forEach(([shade, color]) => {
          acc[`.text-${key}-${shade}`] = { 
            color: `rgb(${color} / var(--tw-text-opacity))` 
          }
        })
      }
      return acc
    }, {})
  }
}

动态值支持:

<!-- 使用任意值 -->
<div class="w-[100px] h-[calc(100vh-4rem)] top-[117px]">
  支持任意值语法
</div>

<!-- 使用 CSS 变量 -->
<div class="bg-[var(--my-color)]">
  使用 CSS 变量
</div>
UnoCSS 的生成逻辑
// UnoCSS 规则引擎
export interface Rule {
  // 匹配模式:字符串或正则
  matcher: string | RegExp
  
  // 生成函数
  generator: (match: RegExpMatchArray) => CSSObject | string | undefined
  
  // 元数据
  meta?: {
    layer?: string
    sort?: number
  }
}

// 示例规则
const rules: Rule[] = [
  // 静态规则
  ['m-1', { margin: '0.25rem' }],
  ['m-2', { margin: '0.5rem' }],
  
  // 动态规则
  [/^m-(\d+)$/, ([, d]) => ({ margin: `${d / 4}rem` })],
  
  // 复杂规则
  [/^text-(.*)$/, ([, color], { theme }) => {
    const value = theme.colors?.[color]
    if (value) {
      return { color: value }
    }
  }],
]

UnoCSS 预设系统:

// @unocss/preset-mini 核心逻辑
export const presetMini = (): Preset => ({
  name: '@unocss/preset-mini',
  
  rules: [
    // Display
    ['block', { display: 'block' }],
    ['flex', { display: 'flex' }],
    ['grid', { display: 'grid' }],
    ['hidden', { display: 'none' }],
    
    // Position
    [/^position-(.*)$/, ([, v]) => ({ position: v })],
    
    // 简写
    [/^(.*)-(\d+)$/, handleNumberValue],
    [/^(.*)-(px|rem|em|%)$/, handleUnitValue],
  ],
  
  shortcuts: [
    // 组合类
    ['btn', 'px-4 py-2 rounded inline-block'],
    ['btn-primary', 'btn bg-blue-500 text-white'],
  ],
  
  theme: {
    colors: {
      primary: '#3b82f6',
      // ...
    }
  }
})

2.3 架构优劣分析

Tailwind CSS 架构特点

优势:

  1. 确定性输出:每次构建生成一致的 CSS 文件
  2. 预编译优化:可以在构建时进行深度优化
  3. 缓存友好:生成的 CSS 文件可被 CDN 缓存
  4. 生态成熟:大量工具链支持预编译模式

劣势:

  1. 构建开销:需要扫描文件并生成完整 CSS
  2. 配置局限:动态值需要特殊语法支持
  3. 包体积:即使只使用少量类,也可能有较大配置文件
// 实际构建时间分析(1000 组件项目)
const buildMetrics = {
  initialBuild: '2.5s',     // 首次构建
  incrementalBuild: '150ms', // 增量构建
  cssOutput: '45KB',        // 输出大小(gzip)
  configParsing: '80ms'     // 配置解析
}
UnoCSS 架构特点

优势:

  1. 即时响应:开发服务器启动几乎瞬间完成
  2. 按需生成:只生成实际使用的 CSS
  3. 内存效率:无需持久化 CSS 文件
  4. 动态规则:正则表达式规则支持无限扩展

劣势:

  1. 运行时依赖:需要开发服务器支持
  2. 构建复杂度:不同构建工具需要不同配置
  3. 调试难度:动态生成的 CSS 较难追踪来源
// 性能指标(1000 组件项目)
const performanceMetrics = {
  coldStart: '50ms',        // 冷启动
  hotReload: '5ms',         // 热更新
  memoryUsage: '12MB',      // 内存占用
  ruleMatching: '0.1ms'     // 单规则匹配
}

3. 性能基准测试

3.1 测试环境配置

# 测试环境
硬件:
  CPU: Intel i9-12900K
  RAM: 32GB DDR5
  SSD: NVMe Gen4

软件:
  Node.js: 20.x
  OS: Windows 11 / macOS 14 / Ubuntu 22.04

项目规模:
  组件数: 1,000
  页面数: 50
  类名使用: 15,000+
  文件大小: ~2MB (源码)

3.2 开发服务器性能

启动时间对比
测试方法:10 次冷启动取平均值

┌────────────────────────────────────────────────────┐
│              开发服务器启动时间(秒)               │
├────────────────────────────────────────────────────┤
│                                                    │
│  Tailwind CSS v3.x                                 │
│  ████████████████████████████████████░░░░░  1.85s  │
│                                                    │
│  UnoCSS v0.58                                      │
│  ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  0.21s  │
│                                                    │
│  性能提升:8.8x                                    │
└────────────────────────────────────────────────────┘

详细数据:

指标 Tailwind CSS UnoCSS 提升倍数
冷启动 1850ms 210ms 8.8x
热启动 450ms 50ms 9.0x
配置重载 320ms 30ms 10.7x
内存占用 156MB 23MB 6.8x
HMR(热更新)性能
测试场景:修改单个组件文件

┌────────────────────────────────────────────────────┐
│                 HMR 响应时间(毫秒)                │
├────────────────────────────────────────────────────┤
│                                                    │
│  Tailwind CSS                                      │
│  █████████████████████████████████████████  145ms  │
│                                                    │
│  UnoCSS                                            │
│  ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   12ms  │
│                                                    │
│  性能提升:12.1x                                   │
└────────────────────────────────────────────────────┘

不同场景的 HMR 性能:

修改类型 Tailwind CSS UnoCSS 差异分析
修改类名 145ms 12ms UnoCSS 即时响应
添加类名 160ms 8ms 无需重新扫描
删除类名 140ms 15ms 清理速度快
修改内容 120ms 180ms* *包含页面重渲染
配置文件 350ms 35ms UnoCSS 规则热重载

3.3 构建性能对比

生产构建时间
构建配置:Vite 5.x + 代码分割 + 压缩

┌────────────────────────────────────────────────────┐
│              生产构建时间(秒)                     │
├────────────────────────────────────────────────────┤
│                                                    │
│  Tailwind CSS                                      │
│  ██████████████████████████████████░░░░░░░  4.2s   │
│                                                    │
│  UnoCSS                                            │
│  █████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░  1.8s   │
│                                                    │
│  性能提升:2.3x                                    │
└────────────────────────────────────────────────────┘

构建阶段详细分析:

// Tailwind CSS 构建时间分解
const tailwindBuildBreakdown = {
  configLoad: '80ms',
  contentScan: '450ms',      // 扫描所有文件
  classGeneration: '320ms',  // 生成 CSS
  postcssProcess: '180ms',   // PostCSS 处理
  minification: '120ms',     // 压缩
  writeFile: '50ms',         // 写入文件
  total: '1200ms'
}

// UnoCSS 构建时间分解
const unocssBuildBreakdown = {
  engineInit: '15ms',
  moduleParse: '200ms',      // 解析模块
  classExtraction: '80ms',   // 提取类名
  cssGeneration: '45ms',     // 生成 CSS
  optimization: '30ms',      // 优化
  total: '370ms'
}

3.4 输出产物对比

CSS 文件大小
项目规模:50 页面,使用 850 个唯一类名

┌────────────────────────────────────────────────────┐
              输出 CSS 大小(KB)                    
├────────────────────────────────────────────────────┤
                                                    
  Tailwind CSS (完整构建)                           
  原始: ████████████████████████████████████████    
  gzip: ███████████████████░░░░░░░░░░░░░░░░░░░░░    
  Brotli: █████████████████░░░░░░░░░░░░░░░░░░░░░    
                                                    
  UnoCSS (按需构建)                                 
  原始: █████████████████░░░░░░░░░░░░░░░░░░░░░░░    
  gzip: ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░    
  Brotli: ██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░    
└────────────────────────────────────────────────────┘

详细数据:

压缩方式 Tailwind CSS UnoCSS 节省
原始 45.2 KB 28.6 KB 36.7%
Gzip 8.4 KB 5.2 KB 38.1%
Brotli 6.8 KB 4.1 KB 39.7%
运行时内存占用
// 开发服务器内存占用(监控 30 分钟)
const memoryProfile = {
  tailwind: {
    initial: '156 MB',
    peak: '245 MB',
    stable: '189 MB',
    trend: '缓慢增长'
  },
  unocss: {
    initial: '23 MB',
    peak: '38 MB',
    stable: '28 MB',
    trend: '稳定'
  }
}

3.5 浏览器性能

解析性能测试
测试方法:Chrome DevTools Performance 面板
测试场景:首次加载包含 1000 个 utility class 的页面

┌────────────────────────────────────────────────────┐
│              CSS 解析时间(毫秒)                   │
├────────────────────────────────────────────────────┤
│                                                    │
│  Tailwind CSS                                      │
│  解析: ████████████████████████████░░░░░░░░  18ms  │
│  应用: ██████████████████████░░░░░░░░░░░░░░  12ms  │
│  总时间: 30ms                                      │
│                                                    │
│  UnoCSS                                            │
│  解析: ██████████████████████░░░░░░░░░░░░░░  12ms  │
│  应用: ██████████████████░░░░░░░░░░░░░░░░░░   8ms  │
│  总时间: 20ms                                      │
│                                                    │
│  性能提升:1.5x                                    │
└────────────────────────────────────────────────────┘

性能影响因素:

  1. CSS 选择器复杂度

    • Tailwind CSS: 大量单一类选择器
    • UnoCSS: 类似结构,但数量更少
  2. CSS 变量使用

    • Tailwind CSS: 重度使用 CSS 变量(--tw-*)
    • UnoCSS: 可选,默认较少使用
  3. 特异性(Specificity)

    • 两者都使用单一类选择器
    • 特异性相同(0,1,0)

4. 生态系统全景分析

4.1 Tailwind CSS 生态系统

官方工具链
┌─────────────────────────────────────────────────────────────────┐
│                  Tailwind CSS 官方生态系统                        │
└─────────────────────────────────────────────────────────────────┘

核心框架
├── tailwindcss@3.x              # 核心框架
│   ├── JIT 引擎                  # Just-in-Time 编译
│   ├── Preflight                # CSS Reset
│   └── 核心插件系统              # 40+ 核心插件
│
├── @tailwindcss/cli             # CLI 工具
│   ├── 构建命令                  # npx tailwindcss
│   ├── 监听模式                  # --watch
│   └── 配置文件初始化            # tailwindcss init
│
└── tailwindcss@4.x (Beta)       # 下一代版本
    ├── Rust 引擎                 # 性能提升 10x
    ├── 原生 CSS 导入             # @import "tailwindcss"
    └── 零配置启动                # 无需配置文件

官方插件
├── @tailwindcss/typography      # 排版样式
│   ├── prose 类                 # 富文本样式
│   └── 自定义配置               # 颜色、间距调整
│
├── @tailwindcss/forms           # 表单元素样式
│   ├── 基础输入框样式            # form-input
│   ├── 选择框样式               # form-select
│   └── 单选/复选框              # form-checkbox
│
├── @tailwindcss/aspect-ratio    # 宽高比
│   ├── aspect-video             # 16:9
│   ├── aspect-square            # 1:1
│   └── 自定义比例               # aspect-[4/3]
│
├── @tailwindcss/line-clamp      # 文本截断
│   ├── line-clamp-1 ~ 6         # 行数控制
│   └── line-clamp-none          # 取消截断
│
└── @tailwindcss/container-queries # 容器查询
    ├── @container               # 容器声明
    └── @md/container            # 容器断点

官方 UI 库
├── Tailwind UI                  # 官方付费组件库
│   ├── 500+ 组件                # React + Vue
│   ├── 应用页面模板              # 完整页面
│   └── 营销页面模板              # Landing pages
│
└── Headless UI                  # 无样式组件
    ├── Combobox                 # 组合框
    ├── Dialog                   # 对话框
    ├── Disclosure               # 展开/折叠
    ├── Listbox                  # 列表选择
    ├── Menu                     # 下拉菜单
    ├── Popover                  # 弹出层
    ├── Radio Group              # 单选组
    ├── Switch                   # 开关
    ├── Tabs                     # 标签页
    └── Transition               # 过渡动画
第三方生态系统
第三方 UI 组件库(按流行度排序)

1. shadcn/ui ⭐ 55k+
   - 可复制粘贴的组件
   - 基于 Radix UI
   - TypeScript + Tailwind
   
2. DaisyUI ⭐ 32k+
   - 语义化类名
   - 30+ 组件
   - 主题系统

3. Flowbite ⭐ 6k+
   - 500+ 组件
   - Figma 设计文件
   - React/Vue/Angular/Svelte

4. Preline UI ⭐ 4k+
   - 250+ 示例
   - 深色模式
   - 高级组件

5. Meraki UI ⭐ 3k+
   - 免费组件
   - RTL 支持
   - Alpine.js 集成

工具库
├── tailwind-merge              # 合并冲突类名
│   └── twMerge('px-2 py-1', 'p-3') = 'p-3'
│
├── clsx + tailwind-merge       # 条件类名 + 合并
│   └── cn() 函数模式
│
├── class-variance-authority    # 组件变体管理
│   └── cva() 函数
│
├── tailwindcss-animate         # 动画扩展
│   └── animate-fade-in 等
│
├── tailwind-scrollbar          # 滚动条样式
│
├── @tailwindcss/typography     # 排版样式
│
└── tailwindcss-debug-screens   # 调试断点显示

开发工具
├── VS Code 插件
│   ├── Tailwind CSS IntelliSense    # 官方插件
│   │   ├── 自动补全
│   │   ├── 悬停预览
│   │   ├── 语法高亮
│   │   └── 类名排序
│   ├── Headwind                     # 类名排序
│   └── Tailwind Shades                # 颜色生成
│
├── Prettier 插件
│   └── prettier-plugin-tailwindcss  # 自动排序
│
├── ESLint 插件
│   └── eslint-plugin-tailwindcss    # 规则检查
│
└── Chrome 扩展
    └── Tailwind CSS Devtools        # 样式调试

4.2 UnoCSS 生态系统

官方预设系统
┌─────────────────────────────────────────────────────────────────┐
│                     UnoCSS 预设系统                             │
└─────────────────────────────────────────────────────────────────┘

核心预设
├── @unocss/preset-uno           # 默认预设(推荐)
│   ├── 基于 Windi CSS           # 兼容 Tailwind
│   ├── 包含所有基础工具          # 完整的 utility set
│   └── 自动检测深色模式          # prefers-color-scheme
│
├── @unocss/preset-wind          # Tailwind 兼容
│   ├── 完全兼容 Tailwind v3     # 类名 1:1 映射
│   ├── 相同的设计系统           # 颜色、间距一致
│   └── 迁移友好                 # 零成本迁移
│
├── @unocss/preset-mini           # 最小预设
│   ├── 最精简的核心             # ~3KB
│   ├── 无默认主题               # 完全自定义
│   └── 适合高级用户             # 需要配置
│
└── @unocss/preset-rem-to-px      # rempx
    └── 自动转换单位              # 适合移动端

扩展预设
├── @unocss/preset-icons          # 图标预设(核心)
│   ├── 100+ 图标集              # Iconify 支持
│   ├── 按需加载                 # 只用到的图标
│   ├── 多种使用方式             
│   │   ├── <div class="i-mdi-home" />      # CSS 图标
│   │   ├── <div i-mdi-home />            # Attributify
│   │   └── <div class="i-[mdi--home]" />  # 动态
│   └── 自定义图标集             
│       └── collections: { custom: {...} }
│
├── @unocss/preset-attributify    # 属性化模式
│   ├── <div m-4 p-2 bg-blue />  # 类名作为属性
│   ├── 前缀支持                 
│   │   └── <div uno-m-4 />      # 避免冲突
│   └── 布尔属性                 
│       └── <button disabled bg-gray-500 />
│
├── @unocss/preset-typography     # 排版预设
│   └── prose 类                 # 类似 @tailwindcss/typography
│
├── @unocss/preset-web-fonts      # Web 字体
│   ├── Google Fonts             # 内置支持
│   ├── Bunny Fonts              # 隐私友好
│   └── 自定义字体提供商          
│
├── @unocss/preset-tagify         # 标签化
│   └── <tag-red-500 />          # 组件化类名
│
└── @unocss/preset-scrollbar       # 滚动条
    └── 类似 tailwind-scrollbar   

社区预设
├── @unocss/preset-daisy          # DaisyUI 兼容
├── @unocss/preset-forms          # 表单预设
├── @unocss/preset-chinese        # 中文排版
└── @unocss/preset-autoprefixer   # Autoprefixer
工具与集成
┌─────────────────────────────────────────────────────────────────┐
│                     UnoCSS 工具链                               │
└─────────────────────────────────────────────────────────────────┘

构建工具集成
├── Vite (官方)
│   └── npm i -D unocss
│   import UnoCSS from 'unocss/vite'
│   plugins: [UnoCSS()]
│
├── Webpack
│   └── npm i -D @unocss/webpack
│
├── Rollup
│   └── npm i -D @unocss/rollup
│
├── Nuxt (官方模块)
│   └── npm i -D @unocss/nuxt
│   modules: ['@unocss/nuxt']
│
├── Astro
│   └── npm i -D @unocss/astro
│   integrations: [UnoCSS()]
│
├── Svelte/SvelteKit
│   └── npm i -D @unocss/svelte-scoped
│
└── 其他
    ├── @unocss/esbuild
    ├── @unocss/rspack
    └── @unocss/farm

CLI 工具
├── @unocss/cli
│   ├── npx unocss "src/**/*" -o output.css
│   ├── --watch 模式
│   └── --minify 压缩
│
├── @unocss/eslint-plugin
│   └── ESLint 规则检查
│
├── @unocss/runtime
│   └── 浏览器运行时生成(CDN 使用)
│
└── @unocss/inspector
    └── 可视化调试工具

VS Code 扩展
├── UnoCSS (官方)
│   ├── 自动补全
│   ├── 悬停预览
│   ├── 颜色预览
│   └── 跳转到定义
│
└── UnoCSS  snippets
    └── 代码片段

4.3 生态系统对比矩阵

类别 Tailwind CSS UnoCSS 胜出
UI 组件库 ⭐⭐⭐⭐⭐ ⭐⭐ Tailwind
官方插件 ⭐⭐⭐⭐⭐ ⭐⭐⭐ Tailwind
工具链成熟度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ Tailwind
IDE 支持 ⭐⭐⭐⭐⭐ ⭐⭐⭐ Tailwind
图标集成 ⭐⭐ ⭐⭐⭐⭐⭐ UnoCSS
配置灵活性 ⭐⭐⭐ ⭐⭐⭐⭐⭐ UnoCSS
现代工具链支持 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ UnoCSS
预设丰富度 ⭐⭐ ⭐⭐⭐⭐⭐ UnoCSS

5. 开发体验详解

5.1 IDE 支持对比

VS Code 功能对比
┌─────────────────────────────────────────────────────────────────┐
│                     VS Code 功能对比                            │
└─────────────────────────────────────────────────────────────────┘

Tailwind CSS IntelliSense (官方)
├── 功能完整度: ⭐⭐⭐⭐⭐
├── 
│   ✅ 自动补全(上下文感知)
│   ✅ 悬停预览(CSS 代码)
│   ✅ 颜色预览(内联方块)
│   ✅ 类名排序(自动)
│   ✅ 语法高亮
│   ✅ 错误提示
│   ✅ 配置文件跳转
│   ✅ 自定义值支持
│   
└── 安装量: 8M+

UnoCSS (官方)
├── 功能完整度: ⭐⭐⭐⭐
├── 
│   ✅ 自动补全
│   ✅ 悬停预览
│   ✅ 颜色预览
│   ✅ 跳转到预设
│   ✅ 快捷方式支持
│   
│   ❌ 类名排序(需配合 Prettier)
│   ❌ 自定义规则预览(有限)
│   
└── 安装量: 800K+

实际使用体验对比:

功能 Tailwind UnoCSS 差异说明
补全速度 ~50ms ~30ms UnoCSS 更快
补全精度 极高 Tailwind 更智能
悬停信息 完整 基本 Tailwind 显示更多
颜色预览 优秀 良好 两者都很好
自定义值 完整支持 部分支持 Tailwind 更强
快捷键 Cmd+K Cmd+G Tailwind 独有
WebStorm 支持
Tailwind CSS
├── 原生支持                       # 内置插件
├── 自动配置检测                   # 开箱即用
├── 完整的代码洞察                 # 导航、重构
└── 智能补全                       # 项目感知

UnoCSS
├── 社区插件                       # 非官方
├── 基本支持                       # 有限的补全
└── 需手动配置                     # 不如 Tailwind 完善

5.2 代码示例深度对比

案例 1:卡片组件

Tailwind CSS 实现:

// Card.jsx
import { twMerge } from 'tailwind-merge'
import { clsx, type ClassValue } from 'clsx'

// 工具函数(常用模式)
function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

export function Card({ 
  children, 
  className,
  variant = 'default',
  size = 'md',
  interactive = false,
  ...props 
}) {
  return (
    <div
      className={cn(
        // 基础样式
        'rounded-lg border bg-card text-card-foreground shadow-sm',
        
        // 尺寸变体
        size === 'sm' && 'p-3',
        size === 'md' && 'p-6',
        size === 'lg' && 'p-8',
        
        // 颜色变体
        variant === 'default' && 'border-border bg-white',
        variant === 'outline' && 'border-2 border-dashed',
        variant === 'ghost' && 'border-transparent bg-transparent',
        variant === 'destructive' && 'border-red-500 bg-red-50',
        
        // 交互状态
        interactive && [
          'cursor-pointer',
          'transition-all duration-200',
          'hover:shadow-md hover:border-gray-300',
          'active:scale-[0.98]',
          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2'
        ],
        
        // 传入的类名覆盖className
      )}
      {...props}
    >
      {children}
    </div>
  )
}

UnoCSS 实现:

// Card.jsx
// 使用 Attributify 预设 + 快捷方式

// uno.config.ts 中定义
const shortcuts = {
  'card': 'rounded-lg border bg-card text-card-foreground shadow-sm',
  'card-sm': 'card p-3',
  'card-md': 'card p-6',
  'card-lg': 'card p-8',
  'card-interactive': 'cursor-pointer transition-all duration-200 hover:shadow-md hover:border-gray-300 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
}

// 组件使用
export function Card({ 
  children, 
  className,
  variant = 'default',
  size = 'md',
  interactive = false,
  ...props 
}) {
  const variantStyles = {
    default: 'border-border bg-white',
    outline: 'border-2 border-dashed',
    ghost: 'border-transparent bg-transparent',
    destructive: 'border-red-500 bg-red-50'
  }
  
  return (
    <div
      class={[
        `card-${size}`,
        variantStyles[variant],
        interactive && 'card-interactive',
        className
      ].filter(Boolean).join(' ')}
      {...props}
    >
      {children}
    </div>
  )
}

// 或者使用 Attributify 模式
export function CardAttributify({ children, ...props }) {
  return (
    <div 
      p-6 rounded-lg border bg-white shadow-sm
      hover:shadow-md transition-shadow
      {...props}
    >
      {children}
    </div>
  )
}
案例 2:表单输入组件

Tailwind CSS 实现:

// Input.jsx
import { forwardRef } from 'react'
import { cn } from '@/lib/utils'

export const Input = forwardRef(({
  className,
  type = 'text',
  error,
  disabled,
  ...props
}, ref) => {
  return (
    <input
      type={type}
      className={cn(
        // 基础样式
        'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2',
        'text-sm ring-offset-background file:border-0 file:bg-transparent',
        'file:text-sm file:font-medium placeholder:text-muted-foreground',
        
        // 焦点状态
        'focus-visible:outline-none focus-visible:ring-2',
        'focus-visible:ring-ring focus-visible:ring-offset-2',
        
        // 禁用状态
        disabled && 'cursor-not-allowed opacity-50',
        
        // 错误状态
        error && [
          'border-red-500',
          'focus-visible:ring-red-500',
          'placeholder:text-red-300'
        ],
        
        // 过渡动画
        'transition-colors duration-200',
        
        className
      )}
      disabled={disabled}
      ref={ref}
      {...props}
    />
  )
})
Input.displayName = 'Input'

UnoCSS 实现(使用 @unocss/preset-forms):

// Input.jsx
// 使用 preset-forms 预设

export const Input = forwardRef(({
  className,
  type = 'text',
  error,
  disabled,
  ...props
}, ref) => {
  return (
    <input
      type={type}
      class={cn(
        // 使用预设的表单样式
        'form-input',
        
        // 自定义覆盖
        'w-full h-10 px-3 py-2',
        'rounded-md border border-gray-300',
        'text-sm placeholder-gray-400',
        'focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20',
        'transition-all duration-200',
        
        // 状态
        disabled && 'opacity-50 cursor-not-allowed',
        error && 'border-red-500 focus:border-red-500 focus:ring-red-500/20',
        
        className
      )}
      disabled={disabled}
      ref={ref}
      {...props}
    />
  )
})
案例 3:响应式导航栏

Tailwind CSS 实现:

// Navbar.jsx
export function Navbar() {
  const [isOpen, setIsOpen] = useState(false)
  
  return (
    <nav className="bg-white shadow-md">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="flex justify-between h-16">
          {/* Logo */}
          <div className="flex items-center">
            <a href="/" className="text-xl font-bold text-gray-800">
              Logo
            </a>
          </div>
          
          {/* Desktop Menu */}
          <div className="hidden md:flex items-center space-x-4">
            {['首页', '产品', '关于', '联系'].map((item) => (
              <a
                key={item}
                href="#"
                className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium transition-colors"
              >
                {item}
              </a>
            ))}
            <button className="bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-blue-700 transition-colors">
              登录
            </button>
          </div>
          
          {/* Mobile Menu Button */}
          <div className="flex items-center md:hidden">
            <button
              onClick={() => setIsOpen(!isOpen)}
              className="text-gray-600 hover:text-gray-900 p-2"
            >
              <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                {isOpen ? (
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
                ) : (
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
                )}
              </svg>
            </button>
          </div>
        </div>
        
        {/* Mobile Menu */}
        <div className={`md:hidden ${isOpen ? 'block' : 'hidden'}`}>
          <div className="px-2 pt-2 pb-3 space-y-1">
            {['首页', '产品', '关于', '联系'].map((item) => (
              <a
                key={item}
                href="#"
                className="text-gray-600 hover:text-gray-900 hover:bg-gray-50 block px-3 py-2 rounded-md text-base font-medium"
              >
                {item}
              </a>
            ))}
          </div>
        </div>
      </div>
    </nav>
  )
}

UnoCSS 实现:

// Navbar.jsx
export function Navbar() {
  const [isOpen, setIsOpen] = useState(false)
  
  return (
    <nav bg-white shadow-md>
      <div max-w-7xl mx-auto px-4 sm:px-6 lg:px-8>
        <div flex justify-between h-16>
          {/* Logo */}
          <div flex items-center>
            <a href="/" text-xl font-bold text-gray-800>
              Logo
            </a>
          </div>
          
          {/* Desktop Menu */}
          <div hidden md:flex items-center space-x-4>
            {['首页', '产品', '关于', '联系'].map((item) => (
              <a
                key={item}
                href="#"
                text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium transition-colors
              >
                {item}
              </a>
            ))}
            
            <button 
              bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium 
              hover:bg-blue-700 transition-colors
            >
              登录
            </button>
          </div>
          
          {/* Mobile Menu Button */}
          <div flex items-center md:hidden>
            <button
              onClick={() => setIsOpen(!isOpen)}
              text-gray-600 hover:text-gray-900 p-2
            >
              <div className={isOpen ? 'i-mdi-close' : 'i-mdi-menu'} text-2xl>
              </div>
            </button>
          </div>
        </div>
        
        {/* Mobile Menu */}
        <div md:hidden block={isOpen}>
          <div px-2 pt-2 pb-3 space-y-1>
            {['首页', '产品', '关于', '联系'].map((item) => (
              <a
                key={item}
                href="#"
                text-gray-600 hover:text-gray-900 hover:bg-gray-50 block px-3 py-2 rounded-md text-base font-medium
              >
                {item}
              </a>
            ))}
          </div>
        </div>
      </div>
    </nav>
  )
}

5.3 Attributify 模式详解

UnoCSS 的 Attributify 预设是其独特功能,可以将类名作为 HTML 属性使用。

传统写法 vs Attributify:

<!-- 传统 Tailwind/UnoCSS -->
<div class="m-4 p-4 bg-blue-500 text-white rounded-lg shadow-md hover:shadow-lg transition-shadow">
  传统写法
</div>

<!-- UnoCSS Attributify 模式 -->
<div
  m-4
  p-4
  bg-blue-500
  text-white
  rounded-lg
  shadow-md
  hover:shadow-lg
  transition-shadow
>
  Attributify 写法
</div>

<!-- 分组写法(更清晰) -->
<div
  m="4"
  p="4"
  bg="blue-500"
  text="white"
  rounded="lg"
  shadow="md hover:lg"
  transition="shadow"
>
  分组写法
</div>

<!-- 复杂示例 -->
<button
  flex items-center justify-center
  gap-2
  px-6 py-3
  bg="blue-600 hover:blue-700"
  text="white"
  font="medium"
  rounded="md"
  transition="all duration-200"
  disabled:opacity-50
  cursor="pointer disabled:not-allowed"
>
  提交
</button>

Attributify 配置:

// uno.config.ts
import { defineConfig, presetUno, presetAttributify } from 'unocss'

export default defineConfig({
  presets: [
    presetUno(),
    presetAttributify({
      // 前缀(可选)
      prefix: 'uno-',
      
      // 前缀(可选)
      prefixedOnly: false,
      
      // 忽略的属性
      ignoreAttributes: ['label']
    })
  ]
})

5.4 图标集成对比

Tailwind CSS 图标方案
// 方案 1:使用 SVG 图标
import { HomeIcon } from '@heroicons/react/24/outline'

function IconDemo() {
  return (
    <div className="flex items-center gap-2">
      <HomeIcon className="w-6 h-6 text-blue-500" />
      <span>首页</span>
    </div>
  )
}

// 方案 2:使用图标字体(如 Font Awesome)
// 需要单独引入 CSS
function FontDemo() {
  return (
    <div className="flex items-center gap-2">
      <i className="fas fa-home text-blue-500 text-xl"></i>
      <span>首页</span>
    </div>
  )
}

// 方案 3:内联 SVG
function InlineSvgDemo() {
  return (
    <div className="flex items-center gap-2">
      <svg className="w-6 h-6 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
      </svg>
      <span>首页</span>
    </div>
  )
}
UnoCSS 图标方案
// 使用 @unocss/preset-icons(推荐)

// 基础用法
function IconDemo() {
  return (
    <div flex items-center gap-2>
      <div className="i-mdi-home w-6 h-6 text-blue-500" />
      <span>首页</span>
    </div>
  )
}

// Attributify 写法
function AttributifyIconDemo() {
  return (
    <div flex items-center gap-2>
      <div i-mdi-home w-6 h-6 text-blue-500 />
      <span>首页</span>
    </div>
  )
}

// 使用不同图标集
function MultiIconDemo() {
  return (
    <div flex gap-4>
      {/* Material Design */}
      <div i-mdi-home w-6 h-6 />
      
      {/* Phosphor Icons */}
      <div i-ph-house w-6 h-6 />
      
      {/* Heroicons */}
      <div i-heroicons-home w-6 h-6 />
      
      {/* Lucide */}
      <div i-lucide-home w-6 h-6 />
      
      {/* Tabler */}
      <div i-tabler-home w-6 h-6 />
    </div>
  )
}

// 动态图标
function DynamicIcon({ name, iconSet = 'mdi' }) {
  return (
    <div className={`i-${iconSet}-${name} w-6 h-6`} />
  )
}

// 使用自定义图标
function CustomIconDemo() {
  return (
    <div i-custom-logo w-8 h-8 />
  )
}

UnoCSS 图标配置:

// uno.config.ts
import { defineConfig, presetUno, presetIcons } from 'unocss'
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'

export default defineConfig({
  presets: [
    presetUno(),
    presetIcons({
      // 缩放比例
      scale: 1.2,
      
      // 额外 CSS 属性
      extraProperties: {
        'display': 'inline-block',
        'vertical-align': 'middle'
      },
      
      // 自定义图标集
      collections: {
        // 从文件系统加载
        custom: FileSystemIconLoader('./assets/icons'),
        
        // 内联 SVG
        inline: {
          'logo': '<svg viewBox="0 0 24 24">...',
          'arrow': '<svg viewBox="0 0 24 24">...'
        }
      },
      
      // 自动安装图标集(开发模式)
      autoInstall: true,
      
      // 警告未找到的图标
      warn: true
    })
  ]
})

支持的图标集(100+):

图标集 前缀 数量
Material Design Icons i-mdi-* 7000+
Phosphor Icons i-ph-* 7000+
Heroicons i-heroicons-* 300+
Lucide i-lucide-* 800+
Tabler Icons i-tabler-* 4000+
Carbon Icons i-carbon-* 2000+
Simple Icons i-simple-icons-* 2500+
Flag Icons i-flag-* 250+


6. 配置系统深度对比

6.1 Tailwind CSS 4.0 配置详解

Tailwind CSS 4.0 引入了 CSS 优先的配置方式,这是与 UnoCSS 最大的区别之一。

CSS 配置文件结构
/* styles.css */
@import "tailwindcss";

/* 主题配置 */
@theme {
  /* 颜色 */
  --color-brand-50: #f0f9ff;
  --color-brand-100: #e0f2fe;
  --color-brand-500: #0ea5e9;
  --color-brand-900: #0c4a6e;
  
  /* 字体 */
  --font-display: "Inter", sans-serif;
  --font-mono: "Fira Code", monospace;
  
  /* 间距 */
  --spacing-18: 4.5rem;
  --spacing-88: 22rem;
  
  /* 断点 */
  --breakpoint-3xl: 1920px;
  
  /* 动画 */
  --animate-fade-up: fade-up 0.5s ease-out;
  
  @keyframes fade-up {
    0% { opacity: 0; transform: translateY(10px); }
    100% { opacity: 1; transform: translateY(0); }
  }
}

/* 基础层 */
@layer base {
  html {
    @apply antialiased;
  }
  
  body {
    @apply bg-gray-50 text-gray-900;
  }
}

/* 组件层 */
@layer components {
  .btn {
    @apply px-4 py-2 rounded-md font-medium transition-colors;
  }
  
  .btn-primary {
    @apply btn bg-brand-500 text-white hover:bg-brand-600;
  }
}

/* 工具层 */
@layer utilities {
  .text-shadow {
    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
  
  .scrollbar-hide {
    -ms-overflow-style: none;
    scrollbar-width: none;
  }
  
  .scrollbar-hide::-webkit-scrollbar {
    display: none;
  }
}
与 JavaScript 配置的对比
特性 CSS 配置 (v4) JS 配置 (v3) 说明
配置位置 @theme 指令 tailwind.config.js v4 更直观
主题继承 自动继承默认主题 需手动 extend v4 更智能
变量类型 CSS 自定义属性 JS 对象 v4 原生支持
运行时修改 支持 不支持 v4 可动态调整
构建工具 更轻量 需要 PostCSS v4 更快速

6.2 UnoCSS 配置系统

UnoCSS 使用 TypeScript/JavaScript 配置,提供了极高的灵活性。

配置文件结构
// uno.config.ts
import { 
  defineConfig, 
  presetUno, 
  presetAttributify, 
  presetIcons,
  presetTypography,
  presetWebFonts,
  transformerDirectives,
  transformerVariantGroup,
  extractorSplit
} from 'unocss'
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'

export default defineConfig({
  // 内容扫描配置
  content: {
    filesystem: [
      'src/**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}',
      // 排除某些文件
      '!src/**/*.test.{js,ts}'
    ],
    // 内联内容
    inline: [
      '<div class="p-4 m-2">',
    ]
  },
  
  // 预设列表
  presets: [
    // 核心预设
    presetUno({
      dark: 'class',  // 或 'media'
      attributifyPseudo: true,
    }),
    
    // 属性化模式
    presetAttributify({
      prefix: 'uno-',
      prefixedOnly: false,
    }),
    
    // 图标预设
    presetIcons({
      scale: 1.2,
      extraProperties: {
        'display': 'inline-block',
        'vertical-align': 'middle',
      },
      collections: {
        custom: FileSystemIconLoader('./assets/icons'),
        // 内联图标
        inline: {
          logo: '<svg viewBox="0 0 24 24">...</svg>',
        }
      },
      autoInstall: true,
    }),
    
    // 排版预设
    presetTypography({
      cssExtend: {
        'code': {
          color: '#476582',
          backgroundColor: '#f3f4f6',
        }
      }
    }),
    
    // Web 字体
    presetWebFonts({
      provider: 'google',  // 或 'bunny'
      fonts: {
        sans: 'Inter:400,600,800',
        mono: 'Fira Code:400,600',
      }
    }),
  ],
  
  // 自定义规则
  rules: [
    // 静态规则
    ['m-1', { margin: '0.25rem' }],
    ['m-2', { margin: '0.5rem' }],
    
    // 动态规则
    [/^m-(\d+)$/, ([, d]) => ({ margin: `${d / 4}rem` })],
    [/^p-(\d+)$/, ([, d]) => ({ padding: `${d / 4}rem` })],
    
    // 复杂规则 - 圆角
    [/^rounded-([\w-]+)$/, ([, s]) => {
      const map: Record<string, string> = {
        'sm': '0.125rem',
        'md': '0.375rem',
        'lg': '0.5rem',
        'xl': '0.75rem',
        '2xl': '1rem',
        '3xl': '1.5rem',
        'full': '9999px',
      }
      if (map[s]) {
        return { 'border-radius': map[s] }
      }
    }],
    
    // 使用主题
    [/^text-brand-(\d+)$/, ([, d], { theme }) => {
      const color = theme.colors?.brand?.[d]
      if (color) {
        return { color }
      }
    }],
  ],
  
  // 快捷方式
  shortcuts: {
    // 基础组件
    'btn': 'px-4 py-2 rounded font-medium transition-colors inline-flex items-center justify-center gap-2',
    'btn-primary': 'btn bg-blue-600 text-white hover:bg-blue-700 focus:ring-2 focus:ring-blue-500',
    'btn-secondary': 'btn bg-gray-200 text-gray-800 hover:bg-gray-300',
    'btn-ghost': 'btn hover:bg-gray-100',
    'btn-danger': 'btn bg-red-600 text-white hover:bg-red-700',
    
    // 布局
    'flex-center': 'flex items-center justify-center',
    'flex-between': 'flex items-center justify-between',
    'flex-col-center': 'flex flex-col items-center justify-center',
    
    // 卡片
    'card': 'bg-white rounded-lg shadow-md overflow-hidden',
    'card-hover': 'card hover:shadow-lg transition-shadow',
    
    // 表单
    'input': 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500',
    'input-error': 'input border-red-500 focus:ring-red-500',
    
    // 响应式容器
    'container-fluid': 'w-full px-4 sm:px-6 lg:px-8',
    'container-prose': 'max-w-prose mx-auto px-4',
  },
  
  // 主题配置
  theme: {
    colors: {
      brand: {
        50: '#f0f9ff',
        100: '#e0f2fe',
        200: '#bae6fd',
        300: '#7dd3fc',
        400: '#38bdf8',
        500: '#0ea5e9',
        600: '#0284c7',
        700: '#0369a1',
        800: '#075985',
        900: '#0c4a6e',
        950: '#082f49',
      },
      // 语义化颜色
      primary: 'var(--color-primary)',
      secondary: 'var(--color-secondary)',
      success: '#10b981',
      warning: '#f59e0b',
      error: '#ef4444',
    },
    spacing: {
      '18': '4.5rem',
      '88': '22rem',
      '128': '32rem',
    },
    breakpoints: {
      'xs': '480px',
      '3xl': '1920px',
      '4xl': '2560px',
    },
    animation: {
      'fade-up': 'fade-up 0.5s ease-out',
      'fade-in': 'fade-in 0.3s ease-out',
      'slide-in': 'slide-in 0.3s ease-out',
    },
    keyframes: {
      'fade-up': {
        '0%': { opacity: '0', transform: 'translateY(10px)' },
        '100%': { opacity: '1', transform: 'translateY(0)' },
      },
      'fade-in': {
        '0%': { opacity: '0' },
        '100%': { opacity: '1' },
      },
      'slide-in': {
        '0%': { transform: 'translateX(-100%)' },
        '100%': { transform: 'translateX(0)' },
      },
    },
  },
  
  // 变体(类似 Tailwind 的 modifiers)
  variants: [
    // 自定义变体
    (matcher) => {
      if (!matcher.startsWith('hover:')) return matcher
      return {
        matcher: matcher.slice(6),
        selector: s => `${s}:hover`,
      }
    },
  ],
  
  // 提取器
  extractors: [
    extractorSplit,
    // 自定义提取器
    {
      name: 'custom',
      extract({ code }) {
        // 自定义类名提取逻辑
        return [...code.matchAll(/class\(['"`]([^'"`]+)['"`]\)/g)]
          .map(m => m[1].split(/\s+/))
          .flat()
      }
    }
  ],
  
  // 安全列表
  safelist: [
    'bg-red-500',
    'text-3xl',
    'lg:text-4xl',
    'animate-fade-up',
    // 动态安全列表
    ...Array.from({ length: 10 }, (_, i) => `p-${i}`),
  ],
  
  // 预检(CSS Reset)
  preflights: [
    {
      getCSS: () => `
        *, *::before, *::after {
          box-sizing: border-box;
          margin: 0;
          padding: 0;
        }
        
        html {
          -webkit-text-size-adjust: 100%;
          -moz-tab-size: 4;
          tab-size: 4;
        }
        
        body {
          line-height: inherit;
        }
      `
    }
  ],
  
  // 后处理
  postprocess: [
    // 自定义后处理器
    (util) => {
      // 修改生成的 CSS
      if (util.selector.includes('important')) {
        util.entries.forEach((entry) => {
          entry[1] = `${entry[1]} !important`
        })
      }
      return util
    }
  ],
  
  // 转换器(Transformers)
  transformers: [
    transformerDirectives(),      // @apply 等指令
    transformerVariantGroup(),    // 变体组 (hover:(bg-red text-white))
  ],
  
  // 配置合并策略
  configDeps: [
    './config/colors.ts',
    './config/spacing.ts',
  ],
})

6.3 配置系统能力对比

动态规则对比

Tailwind CSS 4.0(有限)

/* 使用任意值 */
<div class="w-[123px] h-[calc(100vh-4rem)]">

/* 但无法自定义规则逻辑 */

UnoCSS(完全灵活)

// 完全自定义规则逻辑
rules: [
  // 动态间距
  [/^gap-(\d+)-(\d+)$/, ([, x, y]) => ({
    gap: `${x}px ${y}px`
  })],
  
  // 复杂计算
  [/^grid-cols-fit-(\d+)$/, ([, min]) => ({
    'grid-template-columns': `repeat(auto-fit, minmax(${min}px, 1fr))`
  })],
  
  // 条件规则
  [/^if-(\w+):(.*)$/, ([, condition, className], { theme }) => {
    if (theme.conditions?.[condition]) {
      return { [className]: theme.conditions[condition] }
    }
  }],
]
快捷方式对比
特性 Tailwind v4 UnoCSS
定义位置 @layer components shortcuts 配置
参数支持 有限(@apply) 完整(函数支持)
嵌套能力 一层 无限嵌套
动态生成 不支持 支持

UnoCSS 高级快捷方式

shortcuts: [
  // 静态快捷方式
  ['btn', 'px-4 py-2 rounded font-medium'],
  
  // 动态快捷方式
  [/^btn-(.*)$/, ([, c], { theme }) => {
    if (theme.colors[c]) {
      return `bg-${c}-500 text-white hover:bg-${c}-600`
    }
  }],
  
  // 嵌套快捷方式
  {
    'card': 'bg-white rounded-lg shadow-md',
    'card-interactive': 'card hover:shadow-lg transition-shadow cursor-pointer',
    'card-interactive-primary': 'card-interactive border-2 border-blue-500',
  },
]

7. 实战案例

7.1 企业级设计系统构建

使用 Tailwind CSS 4.0 构建
// design-system/index.ts
// 基于 Tailwind CSS 4.0 的设计系统

export const designTokens = {
  colors: {
    brand: {
      50: '#f0f9ff',
      500: '#0ea5e9',
      900: '#0c4a6e',
    },
    semantic: {
      success: '#10b981',
      warning: '#f59e0b',
      error: '#ef4444',
      info: '#3b82f6',
    }
  },
  spacing: {
    '4.5': '1.125rem',
    '18': '4.5rem',
  },
  borderRadius: {
    '4xl': '2rem',
  }
} as const

// components/Button.tsx
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
  size?: 'sm' | 'md' | 'lg' | 'xl'
  loading?: boolean
  disabled?: boolean
  children: React.ReactNode
}

export function Button({
  variant = 'primary',
  size = 'md',
  loading,
  disabled,
  children,
  ...props
}: ButtonProps) {
  return (
    <button
      className={cn(
        // 基础样式
        'inline-flex items-center justify-center gap-2',
        'font-medium transition-all duration-200',
        'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
        'disabled:opacity-50 disabled:cursor-not-allowed',
        
        // 尺寸
        size === 'sm' && 'h-8 px-3 text-sm rounded-md',
        size === 'md' && 'h-10 px-4 text-base rounded-lg',
        size === 'lg' && 'h-12 px-6 text-lg rounded-lg',
        size === 'xl' && 'h-14 px-8 text-xl rounded-xl',
        
        // 变体
        variant === 'primary' && [
          'bg-brand-500 text-white',
          'hover:bg-brand-600',
          'focus-visible:ring-brand-500',
          'active:scale-[0.98]',
        ],
        variant === 'secondary' && [
          'bg-gray-100 text-gray-900',
          'hover:bg-gray-200',
          'focus-visible:ring-gray-500',
        ],
        variant === 'ghost' && [
          'text-gray-700',
          'hover:bg-gray-100',
          'focus-visible:ring-gray-500',
        ],
        variant === 'danger' && [
          'bg-red-500 text-white',
          'hover:bg-red-600',
          'focus-visible:ring-red-500',
        ],
        
        // 加载状态
        loading && 'opacity-70 cursor-wait',
      )}
      disabled={disabled || loading}
      {...props}
    >
      {loading && <Spinner className="w-4 h-4 animate-spin" />}
      {children}
    </button>
  )
}
使用 UnoCSS 构建
// uno.config.ts
// 企业级设计系统配置

import { defineConfig, presetUno, presetAttributify, presetIcons } from 'unocss'

const buttonShortcuts = {
  // 基础按钮
  'btn': 'inline-flex items-center justify-center gap-2 font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed',
  
  // 尺寸变体
  'btn-sm': 'btn h-8 px-3 text-sm rounded-md',
  'btn-md': 'btn h-10 px-4 text-base rounded-lg',
  'btn-lg': 'btn h-12 px-6 text-lg rounded-lg',
  'btn-xl': 'btn h-14 px-8 text-xl rounded-xl',
  
  // 颜色变体
  'btn-primary': 'btn-md bg-brand-500 text-white hover:bg-brand-600 focus-visible:ring-brand-500 active:scale-[0.98]',
  'btn-secondary': 'btn-md bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-500',
  'btn-ghost': 'btn-md text-gray-700 hover:bg-gray-100 focus-visible:ring-gray-500',
  'btn-danger': 'btn-md bg-red-500 text-white hover:bg-red-600 focus-visible:ring-red-500',
  
  // 状态变体
  'btn-loading': 'opacity-70 cursor-wait',
}

export default defineConfig({
  presets: [
    presetUno({
      dark: 'class',
    }),
    presetAttributify(),
    presetIcons(),
  ],
  
  shortcuts: {
    ...buttonShortcuts,
    
    // 输入框
    'input': 'w-full h-10 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent transition-all',
    'input-error': 'input border-red-500 focus:ring-red-500',
    'input-success': 'input border-green-500 focus:ring-green-500',
    
    // 卡片
    'card': 'bg-white rounded-lg shadow-md overflow-hidden',
    'card-bordered': 'card border border-gray-200',
    'card-hoverable': 'card hover:shadow-lg transition-shadow cursor-pointer',
    
    // 布局
    'page-container': 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8',
    'section': 'py-12 md:py-16 lg:py-20',
    
    // 排版
    'heading-1': 'text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight',
    'heading-2': 'text-3xl md:text-4xl font-bold tracking-tight',
    'heading-3': 'text-2xl md:text-3xl font-semibold',
    'text-body': 'text-base text-gray-600 leading-relaxed',
    'text-small': 'text-sm text-gray-500',
  },
  
  theme: {
    colors: {
      brand: {
        50: '#f0f9ff',
        100: '#e0f2fe',
        200: '#bae6fd',
        300: '#7dd3fc',
        400: '#38bdf8',
        500: '#0ea5e9',
        600: '#0284c7',
        700: '#0369a1',
        800: '#075985',
        900: '#0c4a6e',
        950: '#082f49',
      },
    },
    
    animation: {
      'spin-slow': 'spin 3s linear infinite',
      'bounce-slow': 'bounce 2s infinite',
      'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
    },
  },
})

// components/Button.tsx - 使用 Attributify
export function Button({ variant = 'primary', size = 'md', loading, children, ...props }) {
  const variantClass = `btn-${variant}`
  const sizeClass = size !== 'md' ? `btn-${size}` : ''
  
  return (
    <button
      class={[variantClass, sizeClass, loading && 'btn-loading'].filter(Boolean).join(' ')}
      disabled={loading}
      {...props}
    >
      {loading && <div i-svg-spinners-90-ring-with-bg text-lg animate-spin />}
      {children}
    </button>
  )
}

7.2 性能优化实战

Tailwind CSS 优化策略
/* 1. 使用 CSS 层控制优先级 */
@layer utilities {
  /* 高性能动画 */
  .gpu-accelerated {
    transform: translateZ(0);
    will-change: transform;
  }
  
  /* 减少重绘 */
  .content-visibility {
    content-visibility: auto;
    contain-intrinsic-size: 0 500px;
  }
}

/* 2. 容器查询优化 */
@layer components {
  .card-grid {
    @apply grid gap-4;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  }
  
  @container (min-width: 768px) {
    .card-grid {
      grid-template-columns: repeat(3, 1fr);
    }
  }
}
// tailwind.config.js - 优化配置
module.exports = {
  // 精确控制扫描范围
  content: [
    './src/**/*.{js,ts,jsx,tsx}',
    // 明确排除测试文件
    '!./src/**/*.test.{js,ts}',
    '!./src/**/*.spec.{js,ts}',
    '!./src/**/__tests__/**',
  ],
  
  // 仅启用需要的核心插件
  corePlugins: {
    container: false,  // 使用自定义容器
    float: false,      // 使用 flex/grid
    clear: false,
    objectFit: true,
    objectPosition: true,
    // ... 按需启用
  },
  
  // 自定义提取器
  content: {
    files: ['./src/**/*.{js,ts,jsx,tsx}'],
    extract: {
      tsx: (content) => {
        // 更精确的类名提取
        return [...content.matchAll(/className=(?:["']([^"']+)["']|\{`([^`]+)`\})/g)]
          .flatMap(match => (match[1] || match[2]).split(/\s+/))
          .filter(Boolean)
      }
    }
  },
}
UnoCSS 优化策略
// uno.config.ts - 性能优化配置

export default defineConfig({
  // 1. 精确的内容匹配
  content: {
    filesystem: [
      'src/**/*.{html,js,ts,jsx,tsx,vue,svelte}',
    ],
    // 自定义提取逻辑
    pipeline: {
      include: [/\.vue$/, /\.tsx?$/],
      exclude: [/node_modules/, /\.git/, /test/],
    }
  },
  
  // 2. 选择器合并优化
  mergeSelectors: true,
  
  // 3. 最小化输出
  minify: process.env.NODE_ENV === 'production',
  
  // 4. 安全列表优化 - 仅保留必要的
  safelist: [
    // 动态类名
    ...Array.from({ length: 5 }, (_, i) => `col-span-${i + 1}`),
    // 主题切换
    'dark',
    'light',
  ],
  
  // 5. 后处理优化
  postprocess: [
    // 移除无用的前缀
    (util) => {
      util.entries = util.entries.filter(([key]) => 
        !key.startsWith('-webkit-') || key === '-webkit-appearance'
      )
      return util
    }
  ],
  
  // 6. 提取器优化
  extractors: [
    {
      name: 'optimized',
      order: 0,
      extract({ code }) {
        // 预过滤,减少正则匹配次数
        if (!code.includes('class') && !code.includes('className')) {
          return []
        }
        // 高效的提取逻辑
        return [...code.matchAll(/(?:class|className)=(?:["']([^"']+)["']|\{`([^`]+)`\})/g)]
          .flatMap(m => (m[1] || m[2]).split(/\s+/))
          .filter(c => c.length > 0 && !c.includes('${'))
      }
    }
  ],
})

7.3 大型项目架构对比

Tailwind CSS 项目结构

project-tailwind/
├── src/
│   ├── components/
│   │   ├── Button.tsx
│   │   ├── Card.tsx
│   │   └── index.ts
│   ├── styles/
│   │   ├── globals.css       # @import "tailwindcss"
│   │   ├── components.css    # @layer components
│   │   └── utilities.css     # @layer utilities
│   ├── app/
│   │   ├── layout.tsx
│   │   └── page.tsx
│   └── lib/
│       └── utils.ts          # cn() 函数
├── tailwind.config.ts        # 主题配置
└── package.json

UnoCSS 项目结构

project-unocss/
├── src/
│   ├── components/
│   │   ├── Button.tsx
│   │   ├── Card.tsx
│   │   └── index.ts
│   ├── app/
│   │   ├── layout.tsx
│   │   └── page.tsx
│   └── lib/
│       └── utils.ts
├── uno.config.ts             # 核心配置(包含主题、规则、快捷方式)
├── presets/
│   ├── shortcuts.ts          # 快捷方式定义
│   ├── rules.ts              # 自定义规则
│   └── theme.ts              # 主题配置
└── package.json

8. 最佳实践

8.1 代码组织

Tailwind CSS 推荐模式

// components/ui/Button.tsx
import { cn } from '@/lib/utils'
import { cva, type VariantProps } from 'class-variance-authority'

const buttonVariants = cva(
  'inline-flex items-center justify-center gap-2 whitespace-nowrap',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        link: 'text-primary underline-offset-4 hover:underline',
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm: 'h-9 px-3',
        lg: 'h-11 px-8',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {}

export function Button({ className, variant, size, ...props }: ButtonProps) {
  return (
    <button
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}

UnoCSS 推荐模式

// 1. 配置集中管理
// uno.config.ts
shortcuts: {
  // 使用语义化命名
  'btn': 'inline-flex items-center justify-center gap-2',
  'btn-primary': 'btn bg-blue-600 text-white hover:bg-blue-700',
  'btn-secondary': 'btn bg-gray-200 text-gray-800 hover:bg-gray-300',
}

// 2. 组件中使用
// components/Button.tsx
export function Button({ variant = 'primary', size = 'md', children }) {
  return (
    <button class={`btn-${variant} btn-${size}`}>
      {children}
    </button>
  )
}

// 3. Attributify 模式(可选)
// components/Card.tsx
export function Card({ title, children }) {
  return (
    <div 
      bg="white dark:gray-800"
      rounded="lg"
      shadow="md hover:lg"
      p="6"
      transition="shadow"
    >
      <h3 text-xl font-bold mb-4>{title}</h3>
      {children}
    </div>
  )
}

8.2 团队协作规范

Tailwind CSS 团队规范

// .prettierrc
{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindFunctions": ["cn", "cva"]
}

// .eslintrc
{
  "plugins": ["tailwindcss"],
  "rules": {
    "tailwindcss/classnames-order": "error",
    "tailwindcss/enforces-negative-arbitrary-values": "error",
    "tailwindcss/enforces-shorthand": "error",
    "tailwindcss/migration-from-tailwind-2": "error",
    "tailwindcss/no-arbitrary-value": "off",
    "tailwindcss/no-custom-classname": "off"
  }
}

UnoCSS 团队规范

// uno.config.ts - 团队共享配置
import { defineConfig } from 'unocss'

export default defineConfig({
  // 使用预设确保一致性
  presets: [
    presetUno(),
    presetAttributify({
      prefix: 'uno-',  // 避免冲突
    }),
  ],
  
  // 团队约定的快捷方式
  shortcuts: {
    // 命名规范
    // 1. 组件:单数名词
    'btn': '...',
    'card': '...',
    'input': '...',
    
    // 2. 变体:[组件]-[变体名]
    'btn-primary': '...',
    'btn-danger': '...',
    'card-hover': '...',
    
    // 3. 工具:动词或形容词
    'flex-center': '...',
    'text-truncate': '...',
    'visually-hidden': '...',
  },
  
  // 主题锁定
  theme: {
    colors: {
      // 只允许使用这些颜色
      brand: {
        50: '#f0f9ff',
        500: '#0ea5e9',
        900: '#0c4a6e',
      },
      // 禁止直接使用 tailwind 颜色
      // red: null,
      // blue: null,
    },
  },
})

8.3 深色模式最佳实践

Tailwind CSS v4 实现

/* styles.css */
@import "tailwindcss";

@theme {
  --color-bg-primary: var(--bg-primary);
  --color-text-primary: var(--text-primary);
}

@layer base {
  :root {
    --bg-primary: #ffffff;
    --text-primary: #1f2937;
  }
  
  .dark {
    --bg-primary: #111827;
    --text-primary: #f9fafb;
  }
}

/* 使用 */
<div class="bg-bg-primary text-text-primary">

UnoCSS 实现

// uno.config.ts
export default defineConfig({
  presets: [
    presetUno({
      dark: 'class',  // 或 'media'
    }),
  ],
  
  shortcuts: {
    'bg-primary': 'bg-white dark:bg-gray-900',
    'text-primary': 'text-gray-900 dark:text-gray-100',
    'border-primary': 'border-gray-200 dark:border-gray-800',
  },
})

// 组件中使用
<div class="bg-primary text-primary border-primary">

// 或 Attributify 模式
<div 
  bg="white dark:gray-900"
  text="gray-900 dark:gray-100"
  border="gray-200 dark:gray-800"
>

9. 常见问题与解决方案

9.1 类名冲突问题

问题: Tailwind 和 UnoCSS 类名冲突

解决方案:

// uno.config.ts
export default defineConfig({
  presets: [
    presetUno({
      // 添加前缀避免冲突
      prefix: 'u-',
    }),
  ],
})

// 使用
<div class="u-flex u-p-4 tailwind-class">

9.2 动态类名问题

Tailwind CSS(需要配置)

// tailwind.config.js
module.exports = {
  safelist: [
    // 明确列出动态类名
    'bg-red-500',
    'bg-blue-500',
    'text-lg',
    'text-xl',
    // 使用模式
    { pattern: /bg-(red|blue|green)-(100|500|900)/ },
  ],
}

// 使用
function getColorClass(color) {
  return `bg-${color}-500`  // 可能被 tree-shake
}

UnoCSS(自动处理)

// uno.config.ts
export default defineConfig({
  // 提取器会自动处理
  // 只需确保内容扫描包含动态类名
  content: {
    filesystem: ['src/**/*.{js,ts,jsx,tsx}'],
    // 如果需要,添加安全列表
    safelist: [
      ...['red', 'blue', 'green'].flatMap(c => 
        [100, 500, 900].map(n => `bg-${c}-${n}`)
      ),
    ],
  },
})

// 使用
function getColorClass(color) {
  return `bg-${color}-500`  // 会被自动检测
}

9.3 VS Code 智能提示失效

Tailwind CSS

// .vscode/settings.json
{
  "tailwindCSS.includeLanguages": {
    "plaintext": "html",
    "vue": "html",
    "svelte": "html"
  },
  "tailwindCSS.experimental.classRegex": [
    ["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
    ["cva\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)", "(?:'|\"|`)([^']*)(?:'|\"|`)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
  ]
}

UnoCSS

// .vscode/settings.json
{
  "unocss.root": "./uno.config.ts",
  "unocss.include": [
    "src/**/*.{html,js,ts,jsx,tsx,vue,svelte}"
  ]
}

9.4 构建失败问题

Tailwind CSS 常见问题

# 错误:Content 路径配置错误
# 解决:检查 tailwind.config.js 中的 content 配置

# 错误:PostCSS 配置问题
# 解决:确保 postcss.config.js 正确配置
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

# 错误:找不到 CSS 文件
# 解决:确保在入口文件导入 CSS
import './styles/globals.css'

UnoCSS 常见问题

# 错误:虚拟模块未找到
# 解决:确保导入虚拟模块
import 'virtual:uno.css'

# 错误:Vite 配置问题
# 解决:确保插件顺序正确
import { defineConfig } from 'vite'
import UnoCSS from 'unocss/vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue(),
    UnoCSS(),  // 放在框架插件之后
  ],
})

10. 迁移指南

10.1 从 Tailwind CSS v3 迁移到 v4

# 1. 升级依赖
npm install tailwindcss@latest

# 2. 更新配置文件
# v3: tailwind.config.js
# v4: styles.css (CSS 优先)

# 3. 迁移步骤
/* v3: tailwind.config.js */
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          500: '#0ea5e9',
        }
      }
    }
  }
}

/* v4: styles.css */
@import "tailwindcss";

@theme {
  --color-brand-500: #0ea5e9;
}

10.2 从 Tailwind CSS 迁移到 UnoCSS

迁移检查清单:

- [ ] 安装 UnoCSS 依赖
- [ ] 配置 UnoCSS(使用 preset-wind 保持兼容)
- [ ] 迁移自定义配置到 uno.config.ts
- [ ] 检查第三方插件兼容性
- [ ] 测试所有组件
- [ ] 优化性能

详细步骤:

// 1. 安装依赖
// npm install -D unocss @unocss/preset-wind

// 2. 配置兼容模式
// uno.config.ts
import { defineConfig, presetWind, presetAttributify } from 'unocss'

export default defineConfig({
  presets: [
    // 使用 Wind 预设保持 100% 兼容
    presetWind(),
    // 可选:启用 Attributify
    presetAttributify(),
  ],
  
  // 3. 迁移主题配置
  theme: {
    // 从 tailwind.config.js 复制
    colors: {
      brand: {
        50: '#f0f9ff',
        500: '#0ea5e9',
      }
    },
    extend: {
      spacing: {
        '18': '4.5rem',
      }
    }
  },
  
  // 4. 迁移自定义类
  shortcuts: {
    // 从 @layer components
    'btn-primary': 'bg-brand-500 text-white hover:bg-brand-600',
  },
})

// 3. 更新构建配置
// vite.config.ts
import UnoCSS from 'unocss/vite'

export default {
  plugins: [
    UnoCSS(),
  ]
}

// 4. 更新入口文件
// main.ts
import 'virtual:uno.css'  // 替换掉 tailwind.css

10.3 从 UnoCSS 迁移到 Tailwind CSS

# 这种情况较少见,通常是团队要求统一技术栈

# 1. 安装 Tailwind
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# 2. 配置 Tailwind
# tailwind.config.js
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {
      // 从 uno.config.ts 迁移
    },
  },
}

# 3. 创建 CSS 文件
# src/styles/tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;

# 4. 更新所有组件
# 将 UnoCSS 快捷方式转换为 Tailwind 类名

11. 未来发展趋势

11.1 Tailwind CSS 路线图

v4.1+ 预期功能:

  • 更完善的 CSS 原生配置支持
  • 更好的容器查询集成
  • 增强的动画工具
  • 改进的深色模式切换

长期方向:

  • 与原生 CSS 标准更深度的集成
  • 零 JavaScript 运行时依赖
  • 更好的性能优化

11.2 UnoCSS 路线图

近期功能:

  • 更多官方预设
  • 改进的 VS Code 体验
  • 更强的类型安全

长期方向:

  • 成为构建工具的默认选择
  • 更广泛的框架集成
  • 社区预设生态扩张

11.3 技术趋势预测

2024-2025 年趋势:
├── CSS 原生能力提升
│   ├── @property 更广泛支持
│   ├── color-mix() 普及
│   └── 容器查询标准化
│
├── 构建工具演进
│   ├── Rspack/SWC 更普及
│   ├── 更快的构建速度
│   └── 更智能的 tree-shaking
│
└── 原子化 CSS 主流化
    ├── 更多框架采用
    ├── 标准化工具链
    └── 更好的开发体验

12. 总结与建议

12.1 决策矩阵

项目特征 推荐选择 理由
企业级大型项目 Tailwind CSS 生态完善、团队熟悉度高
初创/小型项目 UnoCSS 启动快速、配置简单
追求极致性能 UnoCSS 构建速度快、运行时高效
需要丰富 UI 组件 Tailwind CSS shadcn/ui 等生态成熟
高度定制化需求 UnoCSS 动态规则、灵活配置
团队技术栈现代 UnoCSS 与现代工具链集成好
长期维护考虑 Tailwind CSS 稳定性高、社区活跃
快速原型开发 两者皆可 都支持快速开发

12.2 混合使用策略

渐进式迁移方案:

阶段 1:新项目直接使用 UnoCSS
阶段 2:旧项目逐步引入 UnoCSS
阶段 3:统一技术栈

具体做法:
1. 使用 preset-wind 保持兼容
2. 逐步迁移组件
3. 统一配置管理

12.3 最终建议

选择 Tailwind CSS 4.0,如果你:

  • 需要稳定、成熟的解决方案
  • 团队对 Tailwind 已有经验
  • 依赖丰富的 UI 组件生态
  • 需要长期的项目维护支持

选择 UnoCSS,如果你:

  • 追求极致的开发体验
  • 需要高度定制化的配置
  • 使用现代构建工具(Vite 等)
  • 愿意尝试新技术

无论选择哪个,都要:

  • 建立团队规范
  • 使用类型安全工具
  • 关注性能优化
  • 保持配置的一致性

TCP 与 UDP 核心差异及面试高分指南

作者 NEXT06
2026年2月19日 17:13

在计算机网络传输层(Transport Layer),TCP 与 UDP 是两大基石协议。理解二者的底层差异,不仅是网络编程的基础,更是后端架构选型的关键依据。本文剥离冗余表述,直击技术核心。

第一部分:协议本质

  • TCP (Transmission Control Protocol) :一种面向连接的、可靠的、基于字节流的传输层通信协议。
  • UDP (User Datagram Protocol) :一种无连接的、不可靠的、基于数据报的传输层通信协议。

第二部分:核心差异拆解

1. 连接机制

  • TCP面向连接。通信前必须通过三次握手建立连接,结束时需通过四次挥手释放连接。这种机制确保了通信双方的状态同步。
  • UDP无连接。发送数据前不需要建立连接,发送结束也无需关闭。发送端想发就发,接收端有数据就收。

2. 传输模式(核心底层差异)

  • TCP面向字节流 (Byte Stream)

    • TCP 将应用层数据看作一连串无结构的字节流。
    • 无边界:TCP 不保留应用层数据的边界。发送方连续发送两次数据,接收方可能一次收到(粘包),也可能分多次收到(拆包)。因此,应用层必须自行处理粘包/拆包问题(如定义消息长度或分隔符)。
  • UDP面向数据报 (Datagram)

    • UDP 对应用层交下来的报文,不合并、不拆分,保留这些报文的边界
    • 有边界:发送方发一次,接收方就收一次。只要数据包大小不超过 MTU(最大传输单元),UDP 就能保证应用层数据的完整性。

3. 可靠性保障

  • TCP强可靠性。通过以下机制确保数据无差错、不丢失、不重复、按序到达:

    • 序列号 (Sequence Number)  与 确认应答 (ACK)
    • 超时重传机制。
    • 流量控制 (滑动窗口) 与 拥塞控制 (慢启动、拥塞避免、快重传、快恢复)。
  • UDP不可靠性

    • 只负责尽最大努力交付 (Best Effort Delivery)。
    • 不保证数据包顺序,不保证不丢包。
    • 无拥塞控制,网络拥堵时也不会降低发送速率(这对实时应用是优势也是风险)。

4. 头部开销

  • TCP开销大

    • 头部最小长度为 20 字节(不含选项字段),最大可达 60 字节。包含源/目的端口、序列号、确认号、窗口大小、校验和等复杂信息。
  • UDP开销极小

    • 头部固定仅 8 字节。仅包含源端口、目的端口、长度、校验和。这使得 UDP 在网络带宽受限或对传输效率要求极高的场景下更具优势。

5. 传输效率与并发

  • TCP:仅支持点对点 (Unicast) 通信。每条 TCP 连接只能有两个端点。
  • UDP:支持一对一一对多多对一多对多交互通信。原生支持广播 (Broadcast) 和多播 (Multicast)。

第三部分:场景选择

TCP 典型场景

适用于对数据准确性要求高、不能容忍丢包、对速度相对不敏感的场景:

  • HTTP/HTTPS (网页浏览)
  • FTP (文件传输)
  • SMTP/POP3 (邮件传输)
  • SSH (远程登录)

UDP 典型场景

适用于对实时性要求高、能容忍少量丢包、网络开销要求低的场景:

  • DNS (域名解析,要求快速)
  • 直播/视频会议/VoIP (RTP/RTCP,实时性优先)
  • DHCP/SNMP (局域网服务)
  • QUIC/HTTP3 (基于 UDP 实现可靠传输的下一代 Web 协议)

第四部分:面试回答范式

当面试官问到“TCP 和 UDP 的区别”时,建议采用结构化具备演进思维的回答策略。

回答模板:

  1. 先下定义(定基调)
    “TCP 是面向连接的、可靠的字节流协议;而 UDP 是无连接的、不可靠的数据报协议。”

  2. 细述差异(展示底层功底)
    “具体区别主要体现在三个维度:

    • 连接与开销:TCP 需要三次握手,头部最小 20 字节;UDP 无需连接,头部仅 8 字节
    • 数据模式:TCP 是字节流,没有边界,应用层需要处理粘包问题;UDP 是报文,保留边界。
    • 可靠性机制:TCP 有序列号、ACK、拥塞控制来保证有序传输;UDP 则是尽最大努力交付,不保证顺序和完整性。”
  3. 升华主题(架构师视角 - 加分项)
    “值得注意的是,虽然 TCP 可靠,但在弱网环境下存在TCP 队头阻塞(Head-of-Line Blocking)问题(即一个包丢失导致后续所有包等待)。
    这也是为什么最新的 HTTP/3 (QUIC)  协议选择基于 UDP 来构建。QUIC 在应用层实现了可靠性和拥塞控制,既利用了 UDP 的低延迟和无队头阻塞优势,又保证了数据的可靠传输。这是当前传输层协议演进的一个重要趋势。”

第五部分:总结对比表

维度 TCP UDP
连接性 面向连接 (三次握手/四次挥手) 无连接
可靠性 高 (无差错、不丢失、不重复、有序) 低 (尽最大努力交付)
传输模式 字节流 (无边界,需处理粘包) 数据报 (有边界)
头部开销 20 ~ 60 字节 固定 8 字节
传输效率 较低 (需维护连接状态、拥塞控制) 很高 (无连接、无控制)
并发支持 仅点对点 支持广播、多播、单播
拥塞控制 有 (慢启动、拥塞避免等)

HTTP 协议演进史:从 1.0 到 2.0

作者 NEXT06
2026年2月19日 16:59

HTTP 协议的演进本质是追求传输效率与资源利用率的平衡。本文剖析从 1.0 到 2.0 的技术迭代逻辑。

第一部分:HTTP 1.0 —— 基础与瓶颈

HTTP 1.0 确立了请求-响应模型,但其设计初衷仅为传输简单的超文本内容。

核心机制

  • 短连接(Short Connection) :默认采用“一求一连”模式。浏览器每次请求资源,都需要与服务器建立一个 TCP 连接,传输完成后立即断开。
  • 无状态(Stateless) :服务器不跟踪客户端状态,每次请求都是独立的。

致命缺陷

  1. TCP 连接成本极高
    每个请求都需要经历 三次握手 和 四次挥手。在加载包含数十个资源(图片、CSS、JS)的现代网页时,连接建立的耗时甚至超过数据传输本身。
  2. 严重的队头阻塞(Head-of-Line Blocking)
    由于无法复用连接,前一个请求未处理完成前,后续请求无法发送(虽然可以通过浏览器开启多个并行连接缓解,但数量有限)。
  3. 缓存控制简陋
    主要依赖 Expires 和 Last-Modified,缺乏精细的控制策略。

第二部分:HTTP 1.1 —— 性能优化标准

HTTP 1.1 旨在解决 1.0 的连接效率问题,是当前互联网使用最广泛的协议版本。

核心改进

  1. 持久连接(Persistent Connection)

    • 引入 Keep-Alive 机制,且默认开启。
    • 允许多个 HTTP 请求复用同一个 TCP 连接,显著减少了 TCP 握手开销和慢启动(Slow Start)的影响。
  2. 管道化(Pipelining)

    • 允许客户端在收到上一个响应前发送下一个请求。
    • 痛点现状:服务器必须按请求顺序返回响应。若第一个请求处理阻塞,后续响应都会被拖延。因此,主流浏览器默认禁用此功能。
  3. 虚拟主机(Virtual Host)

    • 引入 Host 头部字段。
    • 允许在同一台物理服务器(同一 IP)上托管多个域名,是现代云主机和负载均衡的基础。
  4. 功能增强

    • 断点续传:引入 Range 头,支持只请求资源的某一部分(如 206 Partial Content)。
    • 缓存增强:引入 Cache-Control、ETag 等机制,提供更复杂的缓存策略。

遗留问题

  • 应用层队头阻塞:虽然 TCP 连接复用了,但 HTTP 请求依然是串行的。一旦某个请求发生阻塞,整个管道停滞。
  • 头部冗余:Cookie 和 User-Agent 等头部信息在每次请求中重复传输,且未经压缩,浪费带宽。
  • 文本协议解析低效:基于文本的解析容易出错且效率低于二进制解析。

第三部分:HTTP 2.0 —— 架构级变革

HTTP 2.0 并非简单的功能修补,而是对传输层的重新设计,旨在突破 HTTP 1.x 的性能天花板。

核心技术

  1. 二进制分帧(Binary Framing)

    • 机制:抛弃 ASCII 文本,将所有传输信息分割为更小的消息和帧,并采用二进制编码。
    • 价值:计算机解析二进制数据的效率远高于文本,且容错率更高。
  2. 多路复用(Multiplexing)

    • 机制:基于二进制分帧,允许在同一个 TCP 连接中同时发送多个请求和响应。数据流(Stream)被打散为帧(Frame)乱序发送,接收端根据帧首部的流标识(Stream ID)进行重组。
    • 价值:彻底解决了 应用层的队头阻塞 问题,实现了真正的并发传输。
  3. 头部压缩(HPACK)

    • 机制:通信双方维护一张静态字典和动态字典。
    • 价值:传输时仅发送索引号或差异数据,极大减少了 Header 的传输体积(尤其是 Cookie 较大的场景)。
  4. 服务端推送(Server Push)

    • 服务器可在客户端请求 HTML 时,主动推送后续可能需要的 CSS 或 JS 资源,减少往返延迟(RTT)。

第四部分:总结对比

维度 HTTP 1.0 HTTP 1.1 HTTP 2.0
连接管理 短连接(每请求新建 TCP) 长连接(Keep-Alive 复用) 多路复用(单 TCP 连接并发)
数据格式 文本 文本 二进制(帧)
并发机制 管道化(常被禁用,存在阻塞) 多路复用(真正并发)
头部处理 原文传输 原文传输 HPACK 算法压缩
主机支持 单一主机 虚拟主机(Host 头) 虚拟主机
内容获取 完整获取 断点续传(Range) 断点续传

构建无障碍组件之Accordion Pattern

作者 anOnion
2026年2月19日 16:38

Accordion Pattern 详解:构建垂直堆叠的展开收起组件

Accordion(手风琴)是一种常见的交互组件,由垂直堆叠的可交互标题组成,每个标题包含一个内容部分的标题、摘要或缩略图。本文基于 W3C WAI-ARIA Accordion Pattern 规范,详解如何构建无障碍的 Accordion 组件。

一、Accordion 的定义与核心概念

Accordion 是一组垂直堆叠的交互式标题,每个标题都包含一个内容部分的标题、摘要或缩略图。标题作为控件,允许用户显示或隐藏其关联的内容部分。

Accordion 常用于在单个页面上呈现多个内容部分时减少滚动需求。

1.1 核心术语

  • Accordion Header(手风琴标题):内容部分的标签或缩略图,同时作为显示(在某些实现中也包括隐藏)内容部分的控件
  • Accordion Panel(手风琴面板):与手风琴标题关联的内容部分

在某些 Accordion 中,手风琴标题旁边始终可见额外的元素。例如,每个手风琴标题可能伴随一个菜单按钮,用于提供适用于该部分的操作访问。

二、WAI-ARIA 角色与属性

2.1 基本角色

每个手风琴标题的内容包含在具有 role="button" 的元素中。

2.2 标题层级

每个手风琴标题按钮包装在具有 role="heading" 的元素中,并设置适合页面信息架构的 aria-level 值:

  • 如果原生宿主语言具有隐式标题和 aria-level 的元素(如 HTML 标题标签),可以使用原生宿主语言元素
  • 按钮元素是标题元素内部的唯一元素
<!-- 手风琴标题 -->
<h3>
  <button aria-expanded="true" aria-controls="panel-1" id="accordion-header-1">
    第一部分标题
  </button>
</h3>

<!-- 手风琴面板 -->
<div id="panel-1" role="region" aria-labelledby="accordion-header-1">
  <p>第一部分的内容...</p>
</div>

2.3 状态属性

  • aria-expanded:如果与手风琴标题关联的面板可见,设置为 true;如果面板不可见,设置为 false
  • aria-controls:设置为包含手风琴面板内容的元素的 ID
  • aria-disabled:如果与手风琴标题关联的面板可见,且手风琴不允许折叠该面板,则设置为 true

2.4 区域角色(可选)

每个作为面板内容容器的元素可以具有 role="region"aria-labelledby,其值引用控制面板显示的按钮:

  • 避免在会创建过多地标区域的情况下使用 region 角色,例如在可以同时展开超过约 6 个面板的手风琴中
  • 当面板包含标题元素或嵌套手风琴时,region 角色对屏幕阅读器用户感知结构特别有帮助
<!-- 手风琴标题按钮 -->
<h3>
  <button aria-expanded="true" aria-controls="panel-1" id="header-1">
    面板标题
  </button>
</h3>

<!-- 手风琴面板内容 -->
<div role="region" aria-labelledby="header-1" id="panel-1">
  <p>面板内容...</p>
</div>

三、键盘交互规范

3.1 基本键盘操作

按键 功能
Enter 或 Space 当焦点位于折叠面板的手风琴标题上时,展开关联面板。如果实现只允许一个面板展开,且另一个面板已展开,则折叠该面板
Tab 将焦点移动到下一个可聚焦元素;手风琴中的所有可聚焦元素都包含在页面 Tab 序列中
Shift + Tab 将焦点移动到上一个可聚焦元素;手风琴中的所有可聚焦元素都包含在页面 Tab 序列中

3.2 可选键盘操作

按键 功能
Down Arrow 如果焦点在手风琴标题上,将焦点移动到下一个手风琴标题。如果焦点在最后一个手风琴标题上,要么不执行任何操作,要么将焦点移动到第一个手风琴标题
Up Arrow 如果焦点在手风琴标题上,将焦点移动到上一个手风琴标题。如果焦点在第一个手风琴标题上,要么不执行任何操作,要么将焦点移动到最后一个手风琴标题
Home 当焦点在手风琴标题上时,将焦点移动到第一个手风琴标题
End 当焦点在手风琴标题上时,将焦点移动到最后一个手风琴标题

四、实现方式

4.1 基础结构

<div class="accordion">
  <!-- 第一部分 -->
  <h3>
    <button 
      aria-expanded="true" 
      aria-controls="section1"
      id="accordion-header-1">
      第一部分标题
    </button>
  </h3>
  <div 
    id="section1" 
    role="region" 
    aria-labelledby="accordion-header-1">
    <p>第一部分的内容...</p>
  </div>

  <!-- 第二部分 -->
  <h3>
    <button 
      aria-expanded="false" 
      aria-controls="section2"
      id="accordion-header-2">
      第二部分标题
    </button>
  </h3>
  <div 
    id="section2" 
    role="region" 
    aria-labelledby="accordion-header-2"
    hidden>
    <p>第二部分的内容...</p>
  </div>
</div>

4.2 单展开模式

在单展开模式下,一次只能展开一个面板:

<div class="accordion" data-accordion-single>
  <h3>
    <button 
      aria-expanded="true" 
      aria-controls="panel-1"
      aria-disabled="true">
      始终展开的面板
    </button>
  </h3>
  <div id="panel-1" role="region">
    <p>此面板无法折叠...</p>
  </div>
  
  <h3>
    <button 
      aria-expanded="false" 
      aria-controls="panel-2">
      可切换的面板
    </button>
  </h3>
  <div id="panel-2" role="region" hidden>
    <p>点击上方标题可展开此面板...</p>
  </div>
</div>

4.3 多展开模式

在多展开模式下,可以同时展开多个面板:

<div class="accordion" data-accordion-multiple>
  <h3>
    <button aria-expanded="true" aria-controls="multi-1">
      第一个面板
    </button>
  </h3>
  <div id="multi-1" role="region">
    <p>第一个面板内容...</p>
  </div>
  
  <h3>
    <button aria-expanded="true" aria-controls="multi-2">
      第二个面板(也可同时展开)
    </button>
  </h3>
  <div id="multi-2" role="region">
    <p>第二个面板内容...</p>
  </div>
</div>

4.4 使用原生 HTML <details> + name 实现

HTML5.2 起,<details> 元素支持 name 属性,可以实现原生的单展开模式(Accordion 效果),无需 JavaScript:

<details name="accordion-group" open>
  <summary>第一部分标题</summary>
  <p>第一部分的内容...</p>
</details>

<details name="accordion-group">
  <summary>第二部分标题</summary>
  <p>第二部分的内容...</p>
</details>

<details name="accordion-group">
  <summary>第三部分标题</summary>
  <p>第三部分的内容...</p>
</details>
关键点说明
特性 说明
name 属性 相同 name 值的 <details> 元素会互斥,实现单展开
open 属性 指定默认展开的面板
浏览器支持 Chrome 120+, Firefox, Safari 17.1+
增强版实现(添加 heading 结构)

⚠️ 注意<details> 元素的实现方式与 W3C Accordion Pattern 的 DOM 结构要求不完全一致。W3C 标准要求按钮元素必须是 heading 元素内部的唯一子元素(<h3><button>...</button></h3>),而 <details> 使用 <summary> 作为交互元素。

如果需要更好的无障碍支持,可以在 <summary> 内添加标题:

<details name="accordion-group" open>
  <summary>
    <h3 style="display: inline; font-size: inherit;">第一部分标题</h3>
  </summary>
  <p>第一部分的内容...</p>
</details>

重要提示:这种结构虽然添加了 heading,但仍然是 heading 在 summary 内部,与 W3C 要求的 button 在 heading 内部 的结构相反。因此,这种方式:

  • ✅ 提供了基本的标题层级信息
  • ❌ 不完全符合 W3C Accordion Pattern 的 DOM 结构规范
  • ❌ 可能不被某些屏幕阅读器正确识别为手风琴组件
适用场景

推荐使用 <details name>

  • 简单的 FAQ 页面
  • 不需要复杂样式的场景
  • 追求原生、轻量实现
  • 现代浏览器环境

推荐使用 W3C 模式:

  • 需要多展开模式
  • 需要箭头键导航
  • 需要精确的标题层级(SEO/屏幕阅读器)
  • 需要复杂的自定义样式

五、常见应用场景

5.1 表单分步填写

将长表单分成多个部分,用户逐步填写:

<div class="accordion">
  <h3>
    <button aria-expanded="true" aria-controls="step-1">
      步骤 1:个人信息
    </button>
  </h3>
  <div id="step-1" role="region">
    <label>姓名 <input type="text" /></label>
    <label>邮箱 <input type="email" /></label>
  </div>
  
  <h3>
    <button aria-expanded="false" aria-controls="step-2">
      步骤 2:地址信息
    </button>
  </h3>
  <div id="step-2" role="region" hidden>
    <label>城市 <input type="text" /></label>
    <label>邮编 <input type="text" /></label>
  </div>
</div>

5.2 FAQ 页面

常见问题解答页面,每个问题作为一个可展开的部分:

<div class="accordion">
  <h3>
    <button aria-expanded="false" aria-controls="faq-1">
      如何注册账户?
    </button>
  </h3>
  <div id="faq-1" role="region" hidden>
    <p>点击页面右上角的"注册"按钮,填写必要信息...</p>
  </div>
  
  <h3>
    <button aria-expanded="false" aria-controls="faq-2">
      如何重置密码?
    </button>
  </h3>
  <div id="faq-2" role="region" hidden>
    <p>点击登录页面的"忘记密码"链接...</p>
  </div>
</div>

5.3 设置面板

应用程序的设置页面,将相关设置分组:

<div class="accordion">
  <h3>
    <button aria-expanded="true" aria-controls="settings-general">
      通用设置
    </button>
  </h3>
  <div id="settings-general" role="region">
    <label><input type="checkbox" /> 启用通知</label>
    <label><input type="checkbox" /> 自动保存</label>
  </div>
  
  <h3>
    <button aria-expanded="false" aria-controls="settings-privacy">
      隐私设置
    </button>
  </h3>
  <div id="settings-privacy" role="region" hidden>
    <label><input type="checkbox" /> 公开个人资料</label>
    <label><input type="checkbox" /> 允许搜索</label>
  </div>
</div>

六、最佳实践

6.1 语义化标记

  • 使用适当的标题层级(h1-h6)包装手风琴标题按钮
  • 为每个面板添加 role="region" 以增强结构感知(面板数量较少时)
  • 确保按钮元素是标题元素内部的唯一元素

6.2 键盘导航

  • 实现基本的 Enter/Space 和 Tab 导航
  • 可选实现箭头键导航以提升用户体验
  • 确保所有手风琴标题都包含在 Tab 序列中

6.3 视觉指示

  • 使用清晰的视觉指示器表示展开/折叠状态
  • 为当前聚焦的标题提供明显的焦点样式
  • 考虑使用动画过渡提升用户体验

6.4 状态管理

  • 明确区分单展开和多展开模式
  • 在单展开模式中,考虑是否允许所有面板同时折叠
  • 使用 aria-disabled 表示不允许折叠的面板

6.5 嵌套考虑

  • 避免过深的嵌套层级
  • 嵌套手风琴时,确保每个层级有清晰的视觉区分
  • 考虑使用不同的标题层级表示嵌套关系

七、Accordion 与 Disclosure 的区别

特性 Accordion Disclosure
内容组织 多个垂直堆叠的面板 单个内容块
展开模式 支持单展开或多展开 独立控制
标题结构 使用 heading + button 结构 简单按钮或 summary
导航支持 支持箭头键导航 基本 Tab 导航
用途 表单分步、设置面板、FAQ 详细信息展示

八、总结

构建无障碍的 Accordion 组件需要关注三个核心:正确的语义化标记(heading + button 结构)、完整的键盘交互支持(包括可选的箭头键导航)、清晰的状态管理(aria-expanded、aria-controls、aria-disabled)。与简单的 Disclosure 不同,Accordion 强调多个面板的组织和管理,适用于更复杂的内容展示场景。

遵循 W3C Accordion Pattern 规范,我们能够创建既美观又包容的手风琴组件,为不同能力的用户提供一致的体验。

文章同步于 an-Onion 的 Github。码字不易,欢迎点赞。

DC-SDK 实战指南:基于 Cesium 的三维数字孪生大屏开发 前言 在当今数字孪生、智慧城市等领域的开发中,三维地图可视化已经成为核心需求。

作者 Marshall151
2026年2月19日 12:53

DC-SDK 实战指南:基于 Cesium 的三维数字孪生大屏开发

前言

在当今数字孪生、智慧城市等领域的开发中,三维地图可视化已经成为核心需求。本文将结合一个实际的政务大屏项目,分享如何使用 DC-SDK(@dvgis/dc-sdk)快速构建高性能的三维可视化应用。

本文基于实际生产项目 databoard-ui,涉及人口、房屋、场所、事件等多维度数据的三维可视化展示。


一、DC-SDK 是什么

DC-SDK 是一款基于 Cesium 的 WebGL 三维地图可视化开发框架,它在 Cesium 原生 API 的基础上进行了封装和增强,提供了:

  • 更简洁的 API 设计
  • 丰富的图层类型(矢量图层、聚合图层、GeoJSON图层、3DTiles图层等)
  • 完善的事件交互体系
  • Vue/React 友好的集成方式

安装方式

npm install @dvgis/dc-sdk
// main.js 中引入
import * as DC from '@dvgis/dc-sdk'
import '@dvgis/dc-sdk/dist/dc.min.css'

// 挂载到全局,方便使用
window.DC = DC

二、初始化三维地球

基础配置

在 Vue 项目中,我们通常将地图实例管理放在 Vuex 中:

// store/modules/map.js
initMapInstance({ commit, state }) {
  return new Promise((resolve, reject) => {
    DC.ready().then(async () => {
      const viewer = new DC.Viewer('viewer-container', {
        sceneMode: DC.SceneMode.SCENE3D,      // 3D模式
        baseLayer: true,                       // 启用基础图层
        enableCursorStyle: false,              // 自定义鼠标样式
        scene3DOnly: true,                     // 仅3D场景
        requestRenderMode: true,               // 请求渲染模式(性能优化)
      })
      
      // 配置地球外观和相机控制
      viewer.setOptions({
        showAtmosphere: false,                 // 隐藏大气层
        showSun: false,                        // 隐藏太阳
        showMoon: false,                       // 隐藏月亮
        cameraController: {
          minimumZoomDistance: 280,            // 最小缩放距离
          maximumZoomDistance: 500000,         // 最大缩放距离
        },
        globe: {
          show: true,
          showGroundAtmosphere: false,         // 隐藏地面大气层
          enableLighting: false,               // 禁用光照
          baseColor: DC.Color.fromCssColorString("rgba(0, 23, 75, 1)"),
        },
        skyBox: { show: false }                // 隐藏天空盒
      })
      
      commit('SET_MAP_INSTANCE', viewer)
      resolve(viewer)
    })
  })
}

添加影像底图

// 创建 XYZ 瓦片底图
const baseLayer = DC.ImageryLayerFactory.createXYZImageryLayer({
  url: process.env.VUE_APP_IMAGERYLAYER_URL_XYZ,
  style: "elec",
  tilingScheme: new DC.WebMercatorTilingScheme(),
  crs: "WGS84",
  rectangle: {
    west: -180 * Math.PI / 180,
    south: -85.05 * Math.PI / 180,
    east: 180 * Math.PI / 180,
    north: 85.05 * Math.PI / 180
  }
})

await viewer.addBaseLayer([baseLayer], { brightness: 0.1 })

三、核心图层实战

1. 聚合图层 (ClusterLayer) - 海量点位展示

聚合图层是处理海量点位数据的利器,自动根据缩放级别聚合显示:

// 创建聚合图层
this.layers.clusterLayer = new DC.ClusterLayer('clusterLayer', {
  radius: 40,                                    // 聚合像素范围
  maxZoom: 25,                                   // 最大聚合缩放级别
  image: require('@/assets/img/icon-cs.png'),   // 单点图标
  gradientColors: {                              // 聚合颜色渐变
    "0.0001": DC.Color.BLUEVIOLET,
    "0.001": DC.Color.BLUEVIOLET,
    "0.01": DC.Color.BLUEVIOLET,
    "0.1": DC.Color.BLUEVIOLET
  },
  fontColor: DC.Color.WHITE,                     // 数字颜色
  style: 'circle',                               // 样式:circle/clustering/custom
  clusterSize: 16,                              // 聚合图标尺寸
})

// 设置点位数据
const positions = dataList.map(item => ({
  attr: item,           // 自定义属性,点击时可获取
  lng: item.lng,
  lat: item.lat
}))

this.layers.clusterLayer.setPoints(positions)
this.viewer.addLayer(this.layers.clusterLayer)

聚合点点击事件处理

this.layers.clusterLayer.on(DC.MouseEventType.CLICK, (movement) => {
  const { attr } = movement.overlay
  
  // 判断是聚合点还是单个点
  if (Object.keys(attr).includes('count')) {
    // 聚合点:获取聚合内的点列表
    const leaves = this.layers.clusterLayer.getLeaves(attr.properties.cluster_id)
    
    // 或者放大到展开级别
    const expansionZoom = this.layers.clusterLayer.getClusterExpansionZoom(attr.properties.cluster_id)
    this.viewer.camera.flyTo({ destination: ... })
  } else {
    // 单个点:显示详情信息
    this.showDetailPopup(attr)
  }
})

2. GeoJSON 图层 - 区域边界可视化

加载 GeoJSON 数据展示行政区划:

async renderGeoJson(geoJson) {
  // 创建 GeoJsonLayer
  this.layers.geoJsonLayer = new DC.GeoJsonLayer('geoJsonLayer', geoJson, {
    clampToGround: true,        // 贴地
    pickable: true,             // 可点击(重要!)
    fill: DC.Color.fromCssColorString("rgba(0, 0, 0, 0)"),
    stroke: DC.Color.fromCssColorString("rgba(0, 171, 255, 1)"),
    strokeWidth: 10,
  })
  
  this.viewer.addLayer(this.layers.geoJsonLayer)
  
  // 遍历每个多边形,添加拉伸效果
  const delegate = await this.layers.geoJsonLayer.delegate
  this.layers.geoJsonLayer.eachOverlay((item) => {
    if (item.polygon) {
      let polygon = DC.Polygon.fromEntity(item)
      polygon.setStyle({
        height: 198,
        material: DC.Color.fromCssColorString("rgba(0, 171, 255, 0.2)"),
        extrudedHeight: 200,    // 拉伸高度,形成3D效果
      })
      this.vectorLayer.addOverlay(polygon)
    }
  })
}

3. 3DTiles 图层 - 城市建筑白模

加载城市级 3DTiles 数据,实现建筑白模渲染:

loadWhiteTileset() {
  const tileset = new DC.Tileset(tilesetUrl, {
    // ⭐ 核心性能优化参数
    maximumScreenSpaceError: 16,           // 降低值更流畅
    maximumNumberOfLoadedTiles: 1000,      // 限制同时加载瓦片数
    skipLevelOfDetail: true,               // 跳过中间LOD
    maximumMemoryUsage: 512,               // 内存限制(MB)
    
    // 视锥体优化
    cullRequestsWhileMoving: true,         // 移动时暂停新请求
    cullRequestsWhileMovingMultiplier: 60,
    
    // 动态调整精度
    dynamicScreenSpaceError: true,
    dynamicScreenSpaceErrorDensity: 0.00278,
    dynamicScreenSpaceErrorFactor: 4.0,
  })
  
  // 自定义着色器,统一建筑颜色
  tileset.ready((tileset) => {
    tileset.customShader = new Cesium.CustomShader({
      fragmentShaderText: `
        void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
          material.diffuse = vec3(0.0/255.0, 191.0/255.0, 255.0/255.0);
        }
      `
    })
  })
  
  this.tilesetLayer.addOverlay(tileset)
  this.viewer.addLayer(this.tilesetLayer)
}

4. HTML 图层 - 自定义弹窗

使用 HtmlLayer 实现 Vue 组件弹窗:

// 创建 HTML 图层
this.layers.htmlLayer = new DC.HtmlLayer('htmlLayer')
this.viewer.addLayer(this.layers.htmlLayer)

// 在指定位置创建 HTML 图标
createHtmlIcon(options) {
  const { position, htmlLayer, props, component } = options
  const id_html = "id_" + Date.now()
  
  // 创建 DivIcon
  let htmlIcon = new DC.DivIcon(
    new DC.Position(position.lng, position.lat),
    `<div id="${id_html}"></div>`
  )
  htmlIcon.setStyle({
    className: 'custom-popup',
    zIndex: '2200'
  })
  
  htmlLayer.addOverlay(htmlIcon)
  
  // 将 Vue 组件挂载到 DOM
  const vm = mountRemote(id_html, props, component)
  return vm
}

// 使用示例
const vm = this.createHtmlIcon({
  position: { lng: 118.18, lat: 28.42 },
  htmlLayer: this.layers.htmlLayer,
  props: { title: '详情信息' },
  component: PlaceDetails,  // Vue 组件
})

vm.$on('close', () => {
  this.layers.htmlLayer.clear()
})

四、相机控制与视角管理

常用相机操作

// 1. 设置初始视角
viewer.camera.setView({
  destination: Cesium.Cartesian3.fromDegrees(118.18, 28.42, 1500),
  orientation: {
    heading: Cesium.Math.toRadians(0),     // 正北
    pitch: Cesium.Math.toRadians(-45),     // 俯角45度
    roll: 0
  }
})

// 2. 飞行到指定位置(带动画)
viewer.camera.flyTo({
  destination: Cesium.Cartesian3.fromDegrees(lng, lat, height),
  orientation: { heading: 0, pitch: -45, roll: 0 },
  duration: 2,                              // 飞行时间
  complete: () => console.log('到达目标')
})

// 3. 飞行到边界范围(适配数据)
viewer.flyToBounds([minLon, minLat, maxLon, maxLat], {
  heading: 0,
  pitch: -90,
  roll: 0
})

// 4. 计算边界球飞行(适配大量点位)
const positions = points.map(p => 
  Cesium.Cartesian3.fromDegrees(p.lng, p.lat)
)
const boundingSphere = Cesium.BoundingSphere.fromPoints(positions)
viewer.camera.flyToBoundingSphere(boundingSphere, {
  duration: 2,
  padding: new Cesium.HeadingPitchRange(0, -0.5, boundingSphere.radius * 1.5)
})

监听相机高度变化

viewer.camera.changed.addEventListener(() => {
  const zoom = viewer.zoom
  const currentHeight = viewer.camera.positionCartographic.height
  console.log('当前相机高度: ' + currentHeight.toFixed(2) + ' 米')
})

五、性能优化技巧

1. 渲染模式优化

const viewer = new DC.Viewer('viewer-container', {
  requestRenderMode: true,        // 请求渲染模式,只在需要时渲染
  maximumRenderTimeChange: 0.5,   // 最大渲染间隔
})

2. 3DTiles 性能调优

const tileset = new DC.Tileset(url, {
  // 随相机移动动态调整精度
  maximumScreenSpaceError: 16,
  
  // 移动时降低精度
  cullRequestsWhileMoving: true,
  cullRequestsWhileMovingMultiplier: 60.0,
  
  // 相机停止后恢复精度
})

viewer.camera.moveStart.addEventListener(() => {
  tileset.maximumScreenSpaceError = 32
})
viewer.camera.moveEnd.addEventListener(() => {
  setTimeout(() => {
    tileset.maximumScreenSpaceError = 8
  }, 500)
})

3. 图层生命周期管理

// 页面离开时清理图层
beforeDestroy() {
  for (let key in this.layers) {
    this.viewer.removeLayer(this.layers[key])
  }
}

六、完整业务封装示例

将地图操作封装为业务类,便于各页面复用:

// layout/dcMap/common.js
export default class Common {
  layers = {}
  viewer = null
  vueInstance = null
  mapUtils = null

  constructor(viewer, vueInstance) {
    this.viewer = viewer
    this.vueInstance = vueInstance
    this.mapUtils = new MapUtils(this.viewer)
  }

  initCommon() {
    // 原型方法扩展
    for (const key in viewerRewrite) {
      DC.Viewer.prototype[key] = viewerRewrite[key]
    }
  }

  // 工具方法
  zoomIn() {
    this.mapUtils._zoomIn()
  }

  zoomOut() {
    this.mapUtils._zoomOut()
  }

  homeLayer() {
    const homePosition = store.state.map.currentView.homePosition
    this.viewer.camera.flyTo({
      destination: Cesium.Cartesian3.fromDegrees(
        homePosition.longitude, 
        homePosition.latitude, 
        homePosition.height
      ),
      orientation: { ... }
    })
  }
}

业务类继承

// layout/dcMap/dashboard.js
import Common from './common.js'

export default class Dashboard extends Common {
  constructor(viewer, vueInstance) {
    super(viewer, vueInstance)
    this.initMap()
  }
  
  initMap() {
    this.initCommon()
    // 初始化业务图层
    this.layers.vectorLayer = new DC.VectorLayer('vectorLayer')
    this.viewer.addLayer(this.layers.vectorLayer)
  }
  
  // 业务方法...
  renderPopulation(data) { }
  renderHouse(data) { }
}

七、项目结构参考

src/
├── layout/
│   ├── DcMap.vue           # 地图容器组件
│   └── dcMap/
│       ├── common.js       # 基础地图操作类
│       ├── dashboard.js    # 首页业务操作类
│       ├── place.js        # 场所业务操作类
│       └── event.js        # 事件业务操作类
├── store/
│   └── modules/
│       └── map.js          # 地图状态管理
├── utils/
│   └── mapUtils.js         # 地图工具函数
└── views/
    └── Dashboard.vue       # 首页大屏

八、常见问题与解决方案

Q1: 弹窗被地球遮挡?

// 设置 disableDepthTestDistance 禁用深度测试
billboard.disableDepthTestDistance = Number.POSITIVE_INFINITY

Q2: 图层点击事件不响应?

// 创建图层时必须开启 pickable
const layer = new DC.GeoJsonLayer(id, geoJson, {
  pickable: true  // 必须开启!
})

Q3: 3DTiles 加载太慢?

// 调整 maximumScreenSpaceError
const tileset = new DC.Tileset(url, {
  maximumScreenSpaceError: 32,  // 值越大越模糊但越快
  skipLevelOfDetail: true
})

Q4: 内存持续增长?

// 及时清理图层和实体
this.viewer.entities.removeAll()
this.viewer.dataSources.removeAll()
this.viewer.scene.primitives.removeAll()

总结

DC-SDK 为 Cesium 开发提供了更友好的开发体验,通过本文介绍的:

  1. 基础初始化 - 地球创建与底图配置
  2. 核心图层 - ClusterLayer、GeoJsonLayer、Tileset、HtmlLayer
  3. 相机控制 - 视角管理与飞行控制
  4. 性能优化 - 渲染模式与资源管理
  5. 工程封装 - 面向业务的类封装

希望这篇实战指南能帮助你快速上手 DC-SDK,构建出高性能的三维数字孪生应用。


参考资源

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

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]
}
昨天 — 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 黑名单 的组合拳,以在安全性、性能和扩展性之间取得最佳平衡。

对象数组的排序与分组: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,你的电子学友。

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

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主页,欢迎访问

我的知乎主页,欢迎访问

【节点】[MainLightDirection节点]原理解析与实际应用

作者 SmalBox
2026年2月18日 09:17

【Unity Shader Graph 使用与特效实现】专栏-直达

在Unity URP(Universal Render Pipeline)着色器图形(Shader Graph)中,Main Light Direction节点是一个功能强大且常用的工具节点,它为着色器开发者提供了访问场景中主要光源方向的能力。这个节点在创建各种光照效果、阴影计算和视觉渲染方面发挥着至关重要的作用。通过准确获取主光源的方向信息,开发者能够实现更加真实和动态的光照交互效果,提升项目的视觉质量和用户体验。

Main Light Direction节点的核心价值在于它能够智能地识别场景中的主要光源,无论是用于阴影投射的主方向光,还是作为备用的第一个非阴影投射方向光。这种智能回退机制确保了在各种光照配置下都能获得可用的光源方向数据,使得着色器开发更加灵活和可靠。在URP渲染管线中,正确理解和应用Main Light Direction节点对于创建高质量、性能优化的实时渲染效果至关重要。

随着现代游戏和实时应用对视觉效果要求的不断提高,对光照系统的精细控制变得愈发重要。Main Light Direction节点作为URP着色器图形中光照系统的关键组成部分,为开发者提供了直接访问引擎底层光照数据的接口。通过掌握这个节点的使用方法和应用场景,开发者能够创建出更加生动、响应迅速的光照效果,从而提升整体项目的视觉表现力。

描述

Main Light Direction节点是URP着色器图形中专门用于获取场景中主方向光方向信息的核心节点。在实时渲染中,光源方向是计算光照、阴影和各种光学效果的基础参数,而Main Light Direction节点正是提供这一关键数据的桥梁。该节点设计精巧,能够适应不同的光照场景配置,确保在各种情况下都能返回有意义的光源方向值。

主方向光的定义与识别机制

在URP渲染管线中,主方向光通常指的是场景中最主要的方向光源,这个光源负责提供场景的基础照明和投射主要阴影。Main Light Direction节点通过一套智能的识别机制来确定哪个光源应该被视为"主方向光":

  • 首先,节点会搜索场景中所有设置了投射阴影(Cast Shadows)属性的方向光
  • 如果存在多个投射阴影的方向光,节点会选择其中强度最高或者被认为是最主要的那一个
  • 如果场景中没有任何方向光设置了投射阴影属性,节点会回退到选择第一个不投射阴影的方向光
  • 这种回退机制确保了即使在没有阴影投射光源的情况下,节点仍然能够提供可用的方向数据

光源方向的计算与标准化

Main Light Direction节点输出的方向向量是经过归一化处理的,这意味着向量的长度始终为1。归一化处理在光照计算中非常重要,因为它确保了方向向量只表示方向信息而不包含强度或距离因素。这种标准化输出使得该节点可以直接用于点积计算、反射计算和其他需要纯方向数据的着色器操作。

光源方向的计算基于世界空间坐标系,这意味着无论相机如何移动或旋转,返回的方向向量都始终保持在世界空间中的一致性。这种世界空间的表示方式使得光照计算更加直观和一致,开发者不需要担心相机变换对光照方向的影响。

节点在渲染管线中的角色

在URP渲染管线的光照处理流程中,Main Light Direction节点扮演着信息传递的角色。它从URP的光照系统中获取当前帧的主光源方向数据,并将其提供给着色器图形使用。这个过程发生在每一帧的渲染过程中,因此即使光源在运行时发生移动或变化,节点也能实时更新方向信息。

该节点的设计考虑了性能优化因素,它通过URP的内部接口直接访问已经计算好的光源数据,避免了在着色器中重复计算光源方向的性能开销。这种高效的数据访问方式使得即使在性能受限的平台上,使用Main Light Direction节点也不会对渲染性能造成显著影响。

与其他光照节点的协同工作

Main Light Direction节点通常不单独使用,而是与其他光照相关的节点配合工作,共同构建完整的光照解决方案:

  • 与Main Light Color节点配合,可以同时获取光源的方向和颜色信息
  • 与光照计算节点(如Dot Product、Reflection等)结合,实现复杂的光照效果
  • 在自定义光照模型中作为关键输入参数,替代标准的URP光照计算

这种协同工作的能力使得Main Light Direction节点成为构建高级自定义着色效果的基础构建块。通过将其与其他节点组合,开发者可以创建出从简单的朗伯反射到复杂的各向异性高光等各种光照效果。

端口

Main Light Direction节点的端口设计简洁而高效,只包含一个输出端口,这反映了其功能的专一性——专注于提供主光源的方向信息。这种简洁的设计使得节点易于理解和使用,同时也保证了其在着色器图中的高效执行。

Direction输出端口

Direction端口是Main Light Direction节点唯一的输出接口,它负责提供世界空间中主方向光的归一化方向向量。理解这个端口的特性和正确使用其输出数据对于实现准确的光照效果至关重要。

端口数据类型与特性

Direction端口输出的是Vector 3类型的数据,包含三个浮点数值,分别表示在世界空间坐标系中X、Y、Z轴方向上的分量:

  • X分量:表示光源方向在世界空间X轴上的投影
  • Y分量:表示光源方向在世界空间Y轴上的投影
  • Z分量:表示光源方向在世界空间Z轴上的投影

向量的归一化特性意味着无论实际光源的强度或距离如何,这个方向向量的长度(模)始终为1。数学上表示为:√(X² + Y² + Z²) = 1。这种特性简化了后续的光照计算,因为开发者不需要手动对向量进行归一化处理。

方向向量的几何意义

从几何角度理解,Direction端口输出的向量表示从场景中的表面点指向光源的方向。这一点在光照计算中非常重要,因为标准的光照模型(如Phong或Blinn-Phon模型)通常要求光向量指向光源而非从光源发出。

在实际使用时需要注意,某些光照计算(特别是基于物理的渲染PBR)可能需要不同定义的光向量。在这种情况下,可能需要对Direction端口的输出取反,以获得从光源发出的方向向量。

世界空间坐标系的重要性

Direction端口输出的是世界空间中的方向向量,这一特性具有重要优势:

  • 一致性:世界空间坐标与场景的全局坐标系一致,不受相机或物体变换的影响
  • 预测性:向量的值在场景布局不变的情况下是稳定的,便于调试和效果预测
  • 通用性:世界空间是大多数光照计算和物理模拟的自然选择

当需要在其他坐标系(如视图空间或切线空间)中进行计算时,开发者可以使用相应的变换节点将世界空间的方向向量转换到目标空间。

端口数据的实时性

Direction端口输出的数据是实时更新的,这意味着当场景中的主光源发生移动、旋转或被替换时,端口的输出值会立即反映这些变化。这种实时性使得基于Main Light Direction节点的着色器效果能够动态响应光照环境的变化,创造出更加生动和沉浸式的视觉体验。

在动画或游戏场景中,这种实时更新特性特别有价值。例如,当实现日夜循环系统时,Main Light Direction节点可以自动提供不断变化的太阳方向,而不需要额外的脚本或手动调整。

与其他节点的连接方式

Direction输出端口可以连接到任何接受Vector 3类型数据的输入端口,这种灵活性使得Main Light Direction节点能够与着色器图中的多种节点配合使用:

  • 直接连接到光照计算节点的向量输入
  • 作为参数传递给自定义函数节点
  • 与其他向量运算节点结合,构建复杂的光照模型

在实际连接时,通常需要使用适当的向量运算节点(如Negate、Transform或Normalize)来调整方向向量,使其符合特定光照计算的要求。

使用场景与示例

Main Light Direction节点在URP着色器开发中有着广泛的应用场景,从基础的光照计算到高级的渲染效果都能见到它的身影。理解这些应用场景并通过实际示例学习其使用方法,对于掌握该节点的全面应用至关重要。

基础光照计算

在实现自定义光照模型时,Main Light Direction节点是最基础的构建块之一。通过将其与简单的数学运算节点结合,可以创建各种基本的光照效果。

朗伯反射(漫反射)计算

朗伯反射是模拟粗糙表面光照的最基本模型,它计算光线方向与表面法线之间的夹角:

  • 将Main Light Direction的Direction输出与表面法线向量进行点积计算
  • 使用Dot Product节点计算两个向量的点积结果
  • 使用Saturate节点将结果限制在0-1范围内,避免负值
  • 将结果与主光源颜色相乘,得到最终的漫反射光照

这种简单的漫反射计算能够为物体提供基础的立体感和形状定义,是大多数着色器的起点。

镜面高光计算

基于主光源方向的镜面高光计算可以增加表面的光泽感和材质感:

  • 使用Main Light Direction和相机方向计算半角向量(Half Vector)
  • 将半角向量与表面法线进行点积计算
  • 使用Power节点对结果进行指数运算,控制高光的锐利度
  • 结合光源颜色和强度参数,输出镜面高光分量

通过调整高光的强度和范围,可以模拟从塑料到金属等各种不同材质的表面特性。

高级渲染效果

除了基础光照,Main Light Direction节点在实现各种高级渲染效果中也发挥着关键作用。

动态阴影效果

虽然URP提供了内置的阴影映射系统,但有时需要实现自定义的阴影效果:

  • 使用Main Light Direction确定阴影投射的方向
  • 基于光源方向计算虚拟的阴影投影矩阵
  • 实现屏幕空间或物体空间的阴影映射
  • 创建软阴影或特殊风格的阴影效果

这种自定义阴影系统可以用于实现风格化渲染或特殊视觉效果。

环境光遮蔽与全局光照

在实现简化的环境光遮蔽或全局光照效果时,主光源方向可以作为重要的参考:

  • 基于主光源方向调整环境光遮蔽的强度和分布
  • 实现方向性的环境光遮蔽,增强场景的立体感
  • 结合主光源方向模拟简单的全局光照效果
  • 创建基于光源方向的环境光反射和折射

这些效果可以显著提升场景的真实感和视觉质量。

风格化与非真实感渲染

在风格化渲染中,Main Light Direction节点可以用于创建各种艺术化的光照效果:

卡通着色(Cel Shading)

实现卡通渲染中的硬边缘光照效果:

  • 使用Main Light Direction计算基础的光照强度
  • 通过Step或SmoothStep节点将连续的光照强度量化为离散的色阶
  • 基于光源方向添加轮廓线或边缘高光
  • 创建方向性的色调分离效果

这种技术常用于动漫风格或低多边形风格的游戏中。

水墨与绘画风格

模拟传统艺术媒介的渲染效果:

  • 基于主光源方向控制笔触的方向和密度
  • 实现方向性的纹理化或噪波效果
  • 创建光源方向影响的色彩扩散或混合
  • 模拟光线在特定方向上的散射效果

这些效果可以创造出独特的视觉风格和艺术表达。

性能优化实践

在使用Main Light Direction节点时,合理的性能优化策略非常重要:

计算复杂度管理

  • 避免在片段着色器中进行复杂的光照计算,尽可能在顶点着色器阶段处理
  • 使用适当的精度修饰符(如half或fixed)减少计算开销
  • 将复杂的光照计算预处理为查找表或简化公式

分支优化策略

  • 尽量减少基于光源方向的条件分支
  • 使用数学技巧替代条件判断,如使用max、saturate等函数
  • 将光源方向相关的计算分组,提高缓存效率

通过这些优化实践,可以在保持视觉效果的同时确保渲染性能。

常见问题与解决方案

在使用Main Light Direction节点的过程中,开发者可能会遇到各种问题和技术挑战。了解这些常见问题及其解决方案有助于提高开发效率和代码质量。

光源方向不正确

有时可能会发现Main Light Direction节点返回的方向与预期不符,这通常由以下原因引起:

坐标系理解错误

  • 问题描述:开发者可能误解了方向向量的几何意义,错误地认为向量是从光源发出而非指向光源
  • 解决方案:在使用方向向量前,明确其几何定义。如需从光源发出的方向,对向量取反即可
  • 验证方法:在简单场景中测试,确认光照效果与场景中实际的光源方向一致

空间变换问题

  • 问题描述:在世界空间中进行计算时,忽略了物体的变换关系,导致光照方向不正确
  • 解决方案:确保所有参与计算的向量都在同一坐标系中,必要时使用Transform节点进行空间转换
  • 调试技巧:使用可视化节点将方向向量显示为颜色,直观检查向量的正确性

性能相关问题

在复杂场景或低性能平台上,基于Main Light Direction节点的着色器可能会遇到性能瓶颈。

计算开销过大

  • 问题描述:在片段着色器中进行基于光源方向的复杂计算,导致填充率受限
  • 解决方案:将计算上移到顶点着色器,或使用简化计算模型
  • 优化策略:使用插值方式在顶点和片段间传递光照计算结果,减少每像素计算量

频繁的向量运算

  • 问题描述:不必要的向量归一化、变换或其他运算重复执行
  • 解决方案:缓存常用计算结果,避免重复运算
  • 最佳实践:在着色器图的子图中封装常用的光照计算,确保计算的一致性

平台兼容性问题

不同平台对着色器的支持和优化程度不同,可能会导致Main Light Direction节点在不同设备上表现不一致。

移动平台限制

  • 问题描述:在移动设备上,复杂的光照计算可能导致性能下降或精度问题
  • 解决方案:使用简化光照模型,减少基于光源方向的复杂运算
  • 适配策略:为移动平台创建专门简化版本的着色器,保持核心视觉效果的同时优化性能

图形API差异

  • 问题描述:不同图形API对向量运算的精度和处理方式可能存在细微差异
  • 解决方案:使用URP提供的跨平台兼容函数和数据类型
  • 测试建议:在目标平台上进行全面测试,确保光照效果的一致性

调试与验证技巧

有效的调试方法对于解决Main Light Direction节点相关的问题至关重要。

方向向量可视化

  • 将Direction输出直接连接到基础色,通过颜色直观判断方向向量的值和变化
  • 使用不同的颜色映射方案表示向量的不同分量或方向
  • 创建调试视图,同时显示光源方向和其他相关参数

数值验证方法

  • 在简单测试场景中验证方向向量的准确性
  • 使用脚本输出光源方向的实际值,与着色器中的计算结果对比
  • 创建单元测试场景,自动化验证光照计算的正确性

最佳实践与高级技巧

掌握Main Light Direction节点的高级使用技巧和最佳实践,可以帮助开发者创建出更加高效、美观的视觉效果。

高效的光照模型设计

设计基于Main Light Direction节点的光照模型时,应考虑计算效率和视觉质量的平衡。

多光源支持策略

虽然Main Light Direction节点只提供主光源方向,但可以通过特定技术模拟多光源效果:

  • 使用光照贴图或光照探针提供额外的静态光照信息
  • 实现简化的多光源累积模型,将次要光源作为环境光处理
  • 结合屏幕空间光照信息,增强场景的光照丰富度

实时全局光照技巧

利用主光源方向实现近似的实时全局光照效果:

  • 基于光源方向预计算环境光的分布
  • 使用球谐函数或其它基函数表示方向性的环境光照
  • 实现简化的光线追踪或光线步进效果,增强场景的真实感

艺术导向的视觉效果

将技术实现与艺术表达相结合,创建具有独特视觉风格的效果。

风格化光照控制

通过参数化控制实现灵活的艺术化光照:

  • 创建可调节的光照方向偏移,用于艺术夸张或风格化表达
  • 实现非真实的光照衰减模型,增强视觉冲击力
  • 基于光源方向控制特效的生成和表现

动态效果集成

将Main Light Direction节点与各种动态效果系统集成:

  • 与天气系统结合,实现基于光源方向的风、雨、雪等效果
  • 集成到材质系统中,实现光源方向敏感的动态材质变化
  • 与后期处理效果配合,创建方向性的色彩分级或光晕效果

性能与质量平衡

在保持高质量视觉效果的同时,确保渲染性能的优化。

多层次细节策略

实现基于距离或重要性的多层次光照计算:

  • 在远距离使用简化的光照模型,减少计算开销
  • 根据表面特性动态调整光照计算的复杂度
  • 使用计算着色器或GPU实例化优化批量对象的光照计算

自适应质量调整

根据运行时的性能指标动态调整光照质量:

  • 监控帧率并相应调整光照计算的采样率或精度
  • 在性能受限时使用预计算的光照数据替代实时计算
  • 实现可伸缩的光照系统,适应不同的硬件能力

【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

《吃透防抖与节流:从原理到实战,彻底解决高频事件性能问题》

作者 随逸177
2026年2月17日 21:53

吃透防抖与节流:从原理到实战,彻底解决高频事件性能问题

在前端开发中,我们经常会遇到高频触发的事件——比如搜索框输入、页面滚动、按钮连续点击、窗口缩放等。如果不对这些事件进行处理,频繁执行回调函数(尤其是复杂任务如AJAX请求),会导致页面卡顿、请求开销激增,严重影响用户体验和系统性能。

而防抖(Debounce)和节流(Throttle),就是解决这类高频事件性能问题的两大“神器”。它们基于闭包原理实现,用法相似但场景不同,很多新手容易混淆。今天就结合实战代码,从原理、区别、场景到实战,彻底吃透这两个知识点,帮你在项目中精准落地性能优化。

一、先搞懂核心痛点:为什么需要防抖节流?

我们先看一个真实场景:百度搜索建议(baidu ajax suggest)。当你在搜索框输入关键词时,每输入一个字符,浏览器都会触发一次keyup事件,若直接绑定AJAX请求,就会出现高频请求的问题。

如果不做任何处理,会出现两个核心问题:

  • 执行太密集:用户输入速度快(比如每秒输入3个字符),会在1秒内触发3次keyup事件、发送3次AJAX请求,不仅服务器压力大,也会浪费前端性能;
  • 用户体验失衡:请求太快,频繁发送请求可能导致响应混乱、页面卡顿;请求太慢,又会让联想建议延迟,影响使用体验。

类似的场景还有很多,比如代码编辑器的代码提示(code suggest)、页面滚动加载、按钮重复提交、窗口resize等——这些高频触发的事件,都需要通过防抖或节流来优化,避免“性能浪费”。

而这一切的实现,都离不开 闭包 的支持:利用闭包保留定时器ID、上一次执行时间等状态,让函数能够“记住”之前的执行情况,从而实现精准的触发控制,这也是防抖节流的核心底层逻辑。

二、防抖(Debounce):管你触发多少次,我只执行最后一次

1. 防抖核心定义

防抖的核心逻辑:在规定时间内,无论事件触发多少次,都只执行最后一次回调。就像你反复按电梯按钮,电梯只会在你停止按按钮后的一定时间内关门,不会因为你按了多次就多次关门。

对应到前端场景:搜索框keyup事件太频繁,没必要每次触发都执行AJAX请求,我们用防抖控制——无论用户快速输入多少字符,都只在用户停止输入500ms(可自定义)后,发送一次AJAX请求,既节约请求资源,又保证用户体验。

2. 防抖的关键实现(基于闭包+定时器)

以下是防抖的实战实现代码,逐行解析核心逻辑,可直接复制到HTML中运行:

// 模拟AJAX请求(复杂任务,频繁执行会消耗性能)
function ajax(content) {
  console.log('ajax request', content);
}

// 防抖函数(高阶函数:参数或返回值是函数,依托闭包实现)
function debounce(fn, delay) {
  var id; // 自由变量(闭包核心):保存定时器ID,方便后续清除
  return function(args) {
    if(id) clearTimeout(id); // 每次触发事件,先清除之前的定时器,重置倒计时
    var that = this; // 保存当前this指向,避免定时器内this丢失
    id = setTimeout(function(){
      fn.call(that, args); // 推迟执行:延迟delay毫秒后,执行目标函数(最后一次触发的回调)
    }, delay);
  }
}

// 生成防抖后的AJAX函数(延迟500ms执行)
let debounceAjax = debounce(ajax, 500);

// 给防抖输入框绑定keyup事件(高频触发)
const inputb = document.getElementById('debounce');
inputb.addEventListener('keyup', function(e) {
  debounceAjax(e.target.value); // 触发防抖后的函数,而非直接执行ajax
});

3. 防抖核心逻辑拆解(新手必看)

  • 闭包的作用:变量id是定义在debounce函数内部的自由变量,被返回的匿名函数引用。因此即使debounce执行完毕,id也不会被垃圾回收,能持续保存定时器ID,实现“记住”上一次定时器的效果——这是防抖能“重置倒计时”的关键。
  • 定时器的作用:通过setTimeout推迟目标函数(ajax)的执行,每次触发keyup事件时,先清除上一次的定时器(clearTimeout(id)),再重新设置新的定时器。这样无论触发多少次,只有最后一次的定时器会生效,实现“只执行最后一次”。
  • this指向问题:定时器内部的this默认指向window,因此用var that = this保存当前事件触发的上下文(比如input元素),再通过fn.call(that, args)绑定this,确保目标函数(ajax)内的this指向正确,避免出现bug。

4. 防抖的典型应用场景

  • 搜索框输入联想(百度搜索、谷歌搜索):用户不断输入值时,用防抖节约请求资源;
  • 代码编辑器的代码提示(code suggest):避免输入时频繁触发提示逻辑;
  • 按钮防重复提交:比如表单提交按钮,避免用户连续点击发送多次请求;
  • 窗口resize事件:调整窗口大小时,避免频繁执行布局调整逻辑。

三、节流(Throttle):每隔一定时间,只执行一次

1. 节流核心定义

节流的核心逻辑:在规定时间内,无论事件触发多少次,都只执行一次回调。它和防抖的区别在于:防抖是“最后一次触发后延迟执行”,节流是“间隔固定时间执行一次”。

用一个形象的比喻:函数节流就像是FPS游戏的射速,就算你一直按着鼠标射击,也只会在规定射速内射出子弹(比如每秒3发),不会无限制触发——无论触发多频繁,都严格按照固定间隔执行。

对应到前端场景:页面滚动加载数据时,用户可能会一直滚动页面,若每次滚动都触发AJAX请求,会导致请求密集。用节流控制后,每隔500ms只执行一次请求,既保证数据及时加载,又避免性能浪费。

2. 节流的关键实现(基于闭包+时间戳+定时器)

以下是节流的实战实现代码,可直接和防抖代码配合运行,拆解核心逻辑:

// 节流函数(依托闭包,保留上一次执行时间和定时器状态)
function throttle(fn, delay) {
  let last, // 闭包变量:记录上一次执行目标函数的时间戳(毫秒数)
      deferTimer; // 闭包变量:保存尾部执行的定时器ID
  return function() {
    let that = this; // 保存当前this指向,避免this丢失
    let _args = arguments; // 保存事件参数(类数组对象),方便传递给目标函数
    let now = + new Date(); // 类型转换:获取当前时间戳(毫秒数),等价于Date.now()
    
    // 核心判断:上次执行过,且当前时间还没到“上一次执行时间+节流间隔”
    if(last && now < last + delay) {
      clearTimeout(deferTimer); // 清除之前的尾部定时器,避免重复执行
      // 重新设置定时器,延迟执行(尾部补执行,避免最后一次触发被忽略)
      deferTimer = setTimeout(function(){
        last = now; // 更新上一次执行时间为当前时间
        fn.apply(that, _args); // 执行目标函数,绑定this和参数
      }, delay);
    } else {
      // 否则:第一次执行,或已过节流间隔,立即执行目标函数
      last = now; // 更新上一次执行时间为当前时间
      fn.apply(that, _args); // 立即执行目标函数
    }
  }
}

// 生成节流后的AJAX函数(每隔500ms执行一次)
let throttleAjax = throttle(ajax, 500);

// 给节流输入框绑定keyup事件(高频触发)
const inputc = document.getElementById('throttle');
inputc.addEventListener('keyup', function(e) {
  throttleAjax(e.target.value); // 触发节流后的函数
});

3. 节流核心逻辑拆解(新手必看)

  • 闭包的作用:变量last(上一次执行时间)和deferTimer(定时器ID)都是闭包变量,被返回的匿名函数引用,持续保留状态——即使节流函数执行完毕,这两个变量也不会被销毁,确保每次触发都能判断“是否到了执行时间”。
  • 时间戳的作用+ new Date() 将日期对象转为毫秒级时间戳,通过now < last + delay 判断当前时间是否在节流间隔内,决定是否立即执行目标函数。
  • 尾部补执行逻辑:当触发时间在节流间隔内时,通过定时器实现“尾部补执行”——避免最后一次触发被忽略(比如用户滚动页面停止后,确保最后一次滚动能触发数据加载)。
  • 参数和this处理_args = arguments 保存事件参数(比如keyup事件的e对象),that = this 保存当前上下文,确保目标函数(ajax)能正确接收参数、this指向正确。

4. 节流的典型应用场景

  • 页面滚动加载:用户不断滚动页面时,用节流节约请求资源,固定间隔加载数据;
  • 鼠标移动事件:比如拖拽元素时,避免频繁触发位置更新逻辑;
  • 高频点击按钮:比如游戏中的攻击按钮,限制每秒点击次数;
  • 窗口scroll事件:监听页面滚动位置,固定间隔执行导航栏样式切换逻辑。

四、防抖与节流的核心区别(必记,避免混淆)

很多新手会把防抖和节流搞混,其实两者的核心区别很简单,用一句话就能分清,整理如下:

1. 核心逻辑区别

  • 防抖(Debounce) :在一定时间内,只执行最后一次触发的回调(依托setTimeout实现);
  • 节流(Throttle) :每隔一定时间,只执行一次回调(依托时间戳+setTimeout实现,类似setInterval,但更灵活)。

2. 形象对比

  • 防抖:像按电梯,反复按,只在最后一次按完后延迟关门;
  • 节流:像FPS游戏射速,一直按鼠标,只按固定间隔射出子弹。

3. 场景对比(精准落地,避免用错)

特性 防抖(Debounce) 节流(Throttle)
核心逻辑 最后一次触发后延迟执行 固定间隔执行一次
依托技术 闭包 + setTimeout 闭包 + 时间戳 + setTimeout
典型场景 搜索建议、按钮防重复提交 滚动加载、鼠标拖拽
核心目的 避免“无效触发”(比如输入时的中间字符) 避免“密集触发”(比如滚动时的连续触发)

五、实战演示:三者对比(无处理、防抖、节流)

为了让你更直观看到效果,以下是“无处理、防抖、节流”三种效果的完整对比代码,复制到本地即可运行,清晰感受三者差异:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>防抖与节流实战对比</title>
  <style>
    input { margin: 10px 0; padding: 8px; width: 300px; }
    div { font-size: 14px; color: #666; }
  </style>
</head>
<body>
  <div>无处理(高频触发):</div>
  <input type="text" id="undebounce" />
  <br>
  <div>防抖(500ms,只执行最后一次):</div>
  <input type="text" id="debounce" />
  <br>
  <div>节流(500ms,每隔500ms执行一次):</div>
  <input type="text" id="throttle" />

  <script>
  // 模拟AJAX请求(复杂任务)
  function ajax(content) {
    console.log('ajax request', content);
  }

  // 防抖函数
  function debounce(fn, delay) {
    var id;
    return function(args) {
      if(id) clearTimeout(id);
      var that = this;
      id = setTimeout(function(){
        fn.call(that, args)
      }, delay);
    }
  }

  // 节流函数
  function throttle(fn, delay) {
    let last, deferTimer;
    return function() {
      let that = this;
      let _args = arguments;
      let now = + new Date();
      if(last && now < last + delay) {
        clearTimeout(deferTimer);
        deferTimer = setTimeout(function(){
          last = now;
          fn.apply(that, _args);
        }, delay);
      } else {
        last = now;
        fn.apply(that, _args);
      }
    }
  }
  
  // 获取三个输入框元素
  const inputa = document.getElementById('undebounce');
  const inputb = document.getElementById('debounce');
  const inputc = document.getElementById('throttle');

  // 生成防抖、节流函数
  let debounceAjax = debounce(ajax, 500);
  let throttleAjax = throttle(ajax, 500);

  // 1. 无处理:keyup每次触发都执行ajax(高频触发)
  inputa.addEventListener('keyup', function(e) {
    ajax(e.target.value); // 频繁触发,控制台会疯狂打印
  })

  // 2. 防抖处理:keyup触发后,500ms内无新触发才执行ajax
  inputb.addEventListener('keyup', function(e) {
    debounceAjax(e.target.value);
  })

  // 3. 节流处理:keyup触发后,每隔500ms只执行一次ajax
  inputc.addEventListener('keyup', function(e) {
    throttleAjax(e.target.value);
  })
  </script>
</body>
</html>

运行效果说明

  • 无处理输入框:快速输入字符,控制台会疯狂打印“ajax request”,触发频率和keyup一致;
  • 防抖输入框:快速输入字符,控制台只在停止输入500ms后,打印最后一次输入的内容;
  • 节流输入框:快速输入字符,控制台每隔500ms打印一次当前输入内容,严格按照固定间隔执行。

六、总结与注意事项(新手避坑)

1. 核心总结

  • 防抖和节流的核心目的一致:优化高频事件的性能,避免频繁执行复杂任务(如AJAX请求、DOM操作);
  • 两者的核心区别:防抖“只执行最后一次”,节流“间隔固定时间执行一次”;
  • 底层依赖:两者都基于闭包实现,通过闭包保留状态(定时器ID、上一次执行时间),实现精准控制;
  • 场景选择:需要“最后一次触发生效”用防抖,需要“固定间隔生效”用节流。

2. 新手避坑点

  • 不要混淆防抖和节流的场景:比如搜索建议用防抖(避免中间输入触发请求),滚动加载用节流(保证固定间隔加载),用反会影响用户体验;
  • 注意this指向:定时器内部this默认指向window,一定要提前保存this(如var that = this),避免出现this丢失问题;
  • 参数传递:若目标函数需要接收参数(如ajax的content),要保存事件参数(如_args = arguments),并通过call/apply传递;
  • 延迟时间选择:根据场景调整delay(如搜索建议500ms,滚动加载1000ms),太快达不到优化效果,太慢影响用户体验。

防抖和节流是前端性能优化的基础知识点,也是面试高频考点。掌握它们的原理和场景,能帮你在实际项目中解决很多性能问题,提升页面体验。建议把上面的实战代码复制到本地运行,亲手感受三者的区别,加深理解~

❌
❌