普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月1日首页

用 Tauri 2.0 + React + Rust 打造跨平台文件工具箱

作者 借个火er
2026年1月1日 11:46

用 Tauri 2.0 + React + Rust 打造跨平台文件工具箱

前言

最近在整理电脑文件时,发现了大量重复的照片和视频,占用了几十 GB 的空间。市面上的去重工具要么收费,要么功能臃肿,于是萌生了自己造一个的想法。

正好 Tauri 2.0 正式发布,相比 Electron,它有着更小的包体积和更好的性能。于是决定用 Tauri 2.0 + React + Rust 来实现这个工具。

最终成品:File Toolkit —— 一个跨平台的文件工具箱,支持文件统计、文件去重、视频截取。

GitHub: github.com/220529/file…

功能展示

📊 文件统计

递归扫描文件夹,按类型统计文件数量和大小:

  • 支持拖拽选择文件夹
  • 按文件类型分组
  • 显示占比和总大小

🔍 文件去重

这是核心功能,支持:

  • 两阶段扫描:先按文件大小筛选,再计算哈希
  • xxHash3 快速哈希:比 MD5 快 5-10 倍
  • 并行计算:充分利用多核 CPU
  • 大文件采样:只读头部 + 中间 + 尾部,避免全量读取
  • 缩略图预览:图片直接显示,视频用 FFmpeg 截帧
  • 智能选择:自动选中较新的文件,保留最早的

✂️ 视频截取

  • 快速模式:无损截取(-c copy),秒级完成
  • 精确模式:重新编码,时间精确到毫秒
  • 时间轴预览:8 帧缩略图,快速定位
  • 实时进度:精确模式显示编码进度

技术选型

为什么选 Tauri 而不是 Electron?

对比项 Electron Tauri
包体积 150MB+ 10MB+
内存占用 高(Chromium) 低(系统 WebView)
后端语言 Node.js Rust
性能 一般 优秀

对于文件处理这种 CPU 密集型任务,Rust 的性能优势非常明显。

技术栈

┌─────────────────────────────────────────────────────┐
│                    Frontend                          │
│         React 19 + TypeScript + Tailwind CSS        │
├─────────────────────────────────────────────────────┤
│                   Tauri IPC                          │
├─────────────────────────────────────────────────────┤
│                    Backend                           │
│                  Rust + Tauri 2.0                   │
│  ┌─────────────┬─────────────┬─────────────────┐   │
│  │ file_stats  │    dedup    │     video       │   │
│  │  walkdir    │  xxHash3    │    FFmpeg       │   │
│  │             │  rayon      │                 │   │
│  │             │  memmap2    │                 │   │
│  └─────────────┴─────────────┴─────────────────┘   │
└─────────────────────────────────────────────────────┘

核心实现

1. 文件去重算法

去重的核心是计算文件哈希,但如果对每个文件都完整计算哈希,效率会很低。我采用了两阶段策略:

第一阶段:按文件大小筛选

let mut size_map: HashMap<u64, Vec<String>> = HashMap::new();

for entry in WalkDir::new(&path)
    .into_iter()
    .filter_map(|e| e.ok())
    .filter(|e| e.file_type().is_file())
{
    if let Ok(meta) = entry.metadata() {
        let size = meta.len();
        if size > 0 {
            size_map.entry(size).or_default().push(entry.path().to_string_lossy().to_string());
        }
    }
}

// 只对大小相同的文件计算哈希
let files_to_hash: Vec<_> = size_map
    .iter()
    .filter(|(_, files)| files.len() >= 2)
    .flat_map(|(size, files)| files.iter().map(move |f| (*size, f.clone())))
    .collect();

这一步可以过滤掉大部分文件,因为大小不同的文件肯定不重复。

第二阶段:并行计算哈希

use rayon::prelude::*;

let results: Vec<(String, FileInfo)> = files_to_hash
    .par_iter()  // 并行迭代
    .filter_map(|(size, file_path)| {
        let hash = calculate_fast_hash(Path::new(file_path), *size).ok()?;
        // ...
        Some((hash, file_info))
    })
    .collect();

使用 rayon 实现并行计算,充分利用多核 CPU。

2. 快速哈希算法

传统的 MD5 哈希速度较慢,我选择了 xxHash3,它是目前最快的非加密哈希算法之一:

fn calculate_fast_hash(path: &Path, size: u64) -> Result<String, String> {
    use memmap2::Mmap;
    use xxhash_rust::xxh3::Xxh3;

    const SMALL_FILE: u64 = 1024 * 1024;      // 1MB
    const THRESHOLD: u64 = 10 * 1024 * 1024;  // 10MB
    const SAMPLE_SIZE: usize = 1024 * 1024;   // 采样 1MB

    let file = File::open(path).map_err(|e| e.to_string())?;

    if size <= SMALL_FILE {
        // 小文件:内存映射,零拷贝
        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
        let hash = xxhash_rust::xxh3::xxh3_64(&mmap);
        return Ok(format!("{:016x}", hash));
    }

    let mut hasher = Xxh3::new();

    if size <= THRESHOLD {
        // 中等文件:完整读取
        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
        hasher.update(&mmap);
    } else {
        // 大文件:只读头部 + 中间 + 尾部
        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
        let len = mmap.len();

        hasher.update(&mmap[..SAMPLE_SIZE]);                    // 头部
        hasher.update(&mmap[len/2 - SAMPLE_SIZE/2..][..SAMPLE_SIZE]); // 中间
        hasher.update(&mmap[len - SAMPLE_SIZE..]);              // 尾部
        hasher.update(&size.to_le_bytes());                     // 文件大小
    }

    Ok(format!("{:016x}", hasher.digest()))
}

优化点

  • xxHash3:比 MD5 快 5-10 倍
  • memmap2:内存映射,零拷贝读取
  • 大文件采样:只读头中尾各 1MB,避免全量读取

3. 前后端通信

Tauri 使用 IPC 进行前后端通信。后端定义命令:

#[tauri::command]
pub async fn find_duplicates(app: AppHandle, path: String) -> Result<DedupResult, String> {
    // ...
}

前端调用:

import { invoke } from "@tauri-apps/api/core";

const result = await invoke<DedupResult>("find_duplicates", { path });

4. 进度反馈

长时间任务需要显示进度,Tauri 支持事件机制:

后端发送进度

use tauri::{AppHandle, Emitter};

let _ = app.emit("dedup-progress", DedupProgress {
    stage: "计算文件指纹".into(),
    current,
    total: total_to_hash,
    percent,
});

前端监听

import { listen } from "@tauri-apps/api/event";

useEffect(() => {
  const unlisten = listen<DedupProgress>("dedup-progress", (event) => {
    setProgress(event.payload);
  });
  return () => { unlisten.then((fn) => fn()); };
}, []);

5. 内嵌 FFmpeg

视频功能依赖 FFmpeg,为了让用户开箱即用,我把 FFmpeg 打包进了应用:

配置 tauri.conf.json

{
  "bundle": {
    "externalBin": [
      "binaries/ffmpeg",
      "binaries/ffprobe"
    ]
  }
}

Rust 中获取路径

fn get_ffmpeg_path(app: &AppHandle) -> PathBuf {
    app.path()
        .resource_dir()
        .ok()
        .map(|p| p.join("binaries").join("ffmpeg"))
        .filter(|p| p.exists())
        .unwrap_or_else(|| PathBuf::from("ffmpeg"))  // 回退到系统 PATH
}

踩坑记录

1. Tauri 拖拽事件是全局的

最初使用 CSS hidden 隐藏非活动 Tab 来保持状态,但发现拖拽文件时所有 Tab 都会响应。

解决方案:给每个组件传递 active 属性,只有激活的组件才监听拖拽事件。

useEffect(() => {
  if (!active) return;  // 非激活状态不监听
  
  const unlisten = listen("tauri://drag-drop", (event) => {
    if (!active) return;
    // 处理拖拽
  });
  // ...
}, [active]);

2. 并行计算进度跳动

使用 rayon 并行计算时,多线程同时更新计数器,导致进度显示不连续。

解决方案:使用原子变量 + compare_exchange 确保进度单调递增:

let progress_counter = Arc::new(AtomicUsize::new(0));
let last_reported = Arc::new(AtomicUsize::new(0));

// 在并行迭代中
let current = progress_counter.fetch_add(1, Ordering::Relaxed) + 1;
let last = last_reported.load(Ordering::Relaxed);

if current > last && (current - last >= 20 || current == total) {
    if last_reported.compare_exchange(last, current, Ordering::SeqCst, Ordering::Relaxed).is_ok() {
        // 发送进度
    }
}

3. Release 比 Debug 快很多

开发时觉得去重速度一般,打包后发现快了好几倍。

模式 说明
Debug 无优化,保留调试信息
Release LTO、内联、循环展开等优化

对于 CPU 密集型任务,Release 版本可能快 3-5 倍。

打包发布

本地打包

# 1. 下载 FFmpeg 静态版本(macOS 示例)
curl -L "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip" -o /tmp/ffmpeg.zip
unzip /tmp/ffmpeg.zip -d src-tauri/binaries/
mv src-tauri/binaries/ffmpeg src-tauri/binaries/ffmpeg-x86_64-apple-darwin

# 2. 执行打包
pnpm tauri build

产物:src-tauri/target/release/bundle/macos/File Toolkit.app(约 62MB,含 FFmpeg)

GitHub Actions 多平台自动打包

本地打包只能生成当前平台的安装包。要支持 Windows、Linux,需要在对应平台编译。

解决方案:用 GitHub Actions,它提供 macOS、Windows、Linux 虚拟机,可以并行打包。

git push tag v0.2.0
        ↓
GitHub Actions 触发
        ↓
┌─────────────────────────────────────────────────────┐
│  同时启动 4 台虚拟机(并行执行)                       │
├─────────────┬─────────────┬────────────┬────────────┤
│  macOS VM   │  macOS VM   │ Windows VM │  Linux VM  │
│  (Intel)    │  (ARM)      │            │            │
├─────────────┼─────────────┼────────────┼────────────┤
│ 装环境      │ 装环境       │ 装环境     │ 装环境      │
│ 下载 FFmpeg │ 下载 FFmpeg │ 下载FFmpeg │ 下载FFmpeg │
│ tauri build │ tauri build │ tauri build│ tauri build│
├─────────────┼─────────────┼────────────┼────────────┤
│   .dmg.dmg.msi.deb     │
│  (x86_64)   │  (aarch64)  │   .exe.AppImage │
└─────────────┴─────────────┴────────────┴────────────┘
        ↓
   全部上传到 GitHub Release

核心配置 .github/workflows/release.yml

name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    strategy:
      matrix:
        include:
          - platform: macos-latest
            target: x86_64-apple-darwin
          - platform: macos-latest
            target: aarch64-apple-darwin
          - platform: windows-latest
            target: x86_64-pc-windows-msvc
          - platform: ubuntu-22.04
            target: x86_64-unknown-linux-gnu

    runs-on: ${{ matrix.platform }}

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - uses: pnpm/action-setup@v4
      - uses: dtolnay/rust-toolchain@stable

      # 各平台下载对应的 FFmpeg
      - name: Download FFmpeg (macOS)
        if: matrix.platform == 'macos-latest'
        run: |
          curl -L "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip" -o /tmp/ffmpeg.zip
          # ...

      - name: Download FFmpeg (Windows)
        if: matrix.platform == 'windows-latest'
        shell: pwsh
        run: |
          Invoke-WebRequest -Uri "https://www.gyan.dev/ffmpeg/builds/..." -OutFile ...
          # ...

      # 打包并上传到 Release
      - uses: tauri-apps/tauri-action@v0
        with:
          tagName: ${{ github.ref_name }}
          releaseDraft: true
          args: --target ${{ matrix.target }}

使用方式

git tag v0.2.0
git push origin v0.2.0
# 自动触发,完成后去 Releases 页面发布

为什么能跨平台?

  • 前端:React 编译成 HTML/CSS/JS,哪都能跑
  • 后端:Rust 在目标平台的虚拟机上编译,生成原生二进制
  • FFmpeg:每个平台下载对应的静态版本

本质是在真实目标平台上编译,GitHub 免费提供这些虚拟机。

总结

这个项目让我对 Tauri 2.0 有了更深的理解:

  1. Rust 性能确实强:文件哈希、并行计算等场景优势明显
  2. Tauri 开发体验不错:前后端分离,IPC 通信简单
  3. 包体积小:不含 FFmpeg 只有 10MB 左右
  4. 跨平台:一套代码,多端运行

如果你也想尝试 Tauri,这个项目可以作为参考。

GitHub: github.com/220529/file…

欢迎 Star ⭐️


相关文章

昨天以前首页

npm/yarn/pnpm 原理与选型指南

作者 借个火er
2025年12月30日 17:27

npm/yarn/pnpm 深度对比:包管理工具的底层原理与选型

从 node_modules 的黑洞说起,剖析 npm、yarn、pnpm 的依赖解析算法、安装策略、锁文件机制,搞懂为什么 pnpm 能省 50% 磁盘空间。

一、包管理器演进史

2010 ────────────────────────────────────────────────► 2025
  
  ├─ 2010: npm 诞生(随 Node.js 一起发布)
           └─ 嵌套依赖,node_modules 黑洞的开始
  
  ├─ 2016: yarn 发布(Facebook)
           └─ 扁平化 + lockfile,解决依赖地狱
  
  ├─ 2017: npm 5.0
           └─ 引入 package-lock.json,追赶 yarn
  
  ├─ 2017: pnpm 发布
           └─ 硬链接 + 符号链接,革命性架构
  
  ├─ 2020: yarn 2 (Berry)
           └─ Plug'n'Play,零 node_modules
  
  └─ 2024: npm/yarn/pnpm 三足鼎立
            └─ pnpm 市场份额快速增长

二、node_modules 结构演进

2.1 npm v2:嵌套地狱

node_modules/
├── A@1.0.0/
│   └── node_modules/
│       └── B@1.0.0/
│           └── node_modules/
│               └── C@1.0.0/
├── D@1.0.0/
│   └── node_modules/
│       └── B@1.0.0/        ← 重复!
│           └── node_modules/
│               └── C@1.0.0/  ← 重复!
└── E@1.0.0/
    └── node_modules/
        └── B@2.0.0/        ← 不同版本
            └── node_modules/
                └── C@1.0.0/  ← 又重复!

问题

  • 🔴 路径过长(Windows 260 字符限制)
  • 🔴 大量重复依赖,磁盘爆炸
  • 🔴 安装速度慢

2.2 npm v3+ / yarn:扁平化

node_modules/
├── A@1.0.0/
├── B@1.0.0/          ← 提升到顶层
├── C@1.0.0/          ← 提升到顶层
├── D@1.0.0/
├── E@1.0.0/
│   └── node_modules/
│       └── B@2.0.0/  ← 版本冲突,保留嵌套
└── ...

解决了:路径过长、部分重复

新问题

  • 🔴 幽灵依赖:可以 require 未声明的包
  • 🔴 依赖分身:同一个包可能有多个副本
  • 🔴 不确定性:安装顺序影响结构

2.3 pnpm:内容寻址 + 符号链接

~/.pnpm-store/                    ← 全局存储(硬链接源)
└── v3/
    └── files/
        ├── 00/
        │   └── abc123...         ← 按内容哈希存储
        ├── 01/
        └── ...

node_modules/
├── .pnpm/                        ← 真实依赖(硬链接)
│   ├── A@1.0.0/
│   │   └── node_modules/
│   │       ├── A → <store>/A     ← 硬链接到 store
│   │       └── B → ../../B@1.0.0/node_modules/B  ← 符号链接
│   ├── B@1.0.0/
│   │   └── node_modules/
│   │       └── B → <store>/B
│   └── B@2.0.0/
│       └── node_modules/
│           └── B → <store>/B
├── A.pnpm/A@1.0.0/node_modules/A    ← 符号链接
├── D.pnpm/D@1.0.0/node_modules/D
└── E.pnpm/E@1.0.0/node_modules/E

核心原理

┌─────────────────────────────────────────────────────────────────┐
│                    pnpm 的三层结构                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   项目 node_modules/          只有直接依赖的符号链接             │
│            │                                                     │
│            ▼                                                     │
│   .pnpm/ 虚拟存储             所有依赖的扁平结构(符号链接)     │
│            │                                                     │
│            ▼                                                     │
│   ~/.pnpm-store/              全局存储(硬链接,真实文件)       │
│                                                                  │
│   💡 同一个包版本,全局只存一份                                  │
│   💡 不同项目通过硬链接共享                                      │
│   💡 项目只能访问声明的依赖(解决幽灵依赖)                      │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

三、幽灵依赖问题详解

3.1 什么是幽灵依赖?

// package.json 只声明了 express
{
  "dependencies": {
    "express": "^4.18.0"
  }
}

// 但你可以这样写(npm/yarn 扁平化后)
const debug = require('debug');  // 😱 未声明,但能用!
const qs = require('qs');        // 😱 express 的依赖

// 问题:
// 1. express 升级后可能不再依赖 debug → 你的代码挂了
// 2. 换台机器安装顺序不同 → 可能找不到
// 3. 代码审查看不出真实依赖

3.2 pnpm 如何解决?

node_modules/
├── express → .pnpm/express@4.18.0/...  ← 只有 express
└── .pnpm/
    └── express@4.18.0/
        └── node_modules/
            ├── express/
            ├── debug/      ← debug 在这里,外面访问不到
            └── qs/
// pnpm 项目中
const debug = require('debug');  
// ❌ Error: Cannot find module 'debug'

// 必须显式声明
// package.json: "debug": "^4.0.0"
// 然后才能用

四、依赖解析算法

4.1 npm/yarn 的依赖提升

// 依赖关系
A@1.0 → B@1.0
C@1.0 → B@2.0

// npm/yarn 解析结果(取决于安装顺序)
// 情况1:先安装 A
node_modules/
├── A@1.0/
├── B@1.0/          ← B@1.0 被提升
└── C@1.0/
    └── node_modules/
        └── B@2.0/  ← B@2.0 嵌套

// 情况2:先安装 C
node_modules/
├── A@1.0/
│   └── node_modules/
│       └── B@1.0/  ← B@1.0 嵌套
├── B@2.0/          ← B@2.0 被提升
└── C@1.0/

这就是为什么需要 lockfile!

4.2 pnpm 的确定性解析

// pnpm 不做提升,结构永远确定
node_modules/
├── A → .pnpm/A@1.0.0/node_modules/A
├── C → .pnpm/C@1.0.0/node_modules/C
└── .pnpm/
    ├── A@1.0.0/node_modules/
    │   ├── A/
    │   └── B → ../../B@1.0.0/node_modules/B
    ├── B@1.0.0/node_modules/B/
    ├── B@2.0.0/node_modules/B/
    └── C@1.0.0/node_modules/
        ├── C/
        └── B → ../../B@2.0.0/node_modules/B

// 💡 每个包都能找到正确版本的依赖
// 💡 不受安装顺序影响

五、Lockfile 机制对比

5.1 三种 lockfile 格式

# ==================== package-lock.json (npm) ====================
{
  "name": "my-app",
  "lockfileVersion": 3,
  "packages": {
    "node_modules/lodash": {
      "version": "4.17.21",
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
      "integrity": "sha512-v2kDE..."
    }
  }
}

# ==================== yarn.lock (yarn) ====================
lodash@^4.17.0:
  version "4.17.21"
  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz"
  integrity sha512-v2kDE...

# ==================== pnpm-lock.yaml (pnpm) ====================
lockfileVersion: '9.0'
packages:
  lodash@4.17.21:
    resolution: {integrity: sha512-v2kDE...}
    engines: {node: '>=0.10.0'}

5.2 Lockfile 作用

┌─────────────────────────────────────────────────────────────────┐
│                    Lockfile 解决的问题                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   package.json: "lodash": "^4.17.0"                             │
│                                                                  │
│   没有 lockfile:                                                │
│   • 今天安装:lodash@4.17.20                                    │
│   • 明天安装:lodash@4.17.21(新版本发布了)                    │
│   • 😱 不同时间/机器安装结果不同                                │
│                                                                  │
│   有 lockfile:                                                  │
│   • 锁定 lodash@4.17.20                                         │
│   • 任何时间/机器安装结果相同                                   │
│   • ✅ 可复现的构建                                             │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

5.3 Lockfile 对比

特性 package-lock.json yarn.lock pnpm-lock.yaml
格式 JSON 自定义 YAML
可读性 差(嵌套深)
合并冲突 难解决 较易 较易
包含信息 完整树结构 扁平列表 扁平 + 依赖关系
文件大小

六、性能对比实测

6.1 安装速度

┌─────────────────────────────────────────────────────────────────┐
                    安装速度对比(中型项目,约 500 依赖)          
├─────────────────────────────────────────────────────────────────┤
                                                                  
   首次安装(无缓存)                                             
   npm:     ████████████████████████████████  65s                
   yarn:    ██████████████████████████        52s                
   pnpm:    ████████████████████              40s   🔥 最快     
                                                                  
   重复安装(有缓存)                                             
   npm:     ████████████████████              38s                
   yarn:    ██████████████                    28s                
   pnpm:    ████████                          15s   🔥  2.5x  
                                                                  
   CI 环境(有 lockfile,无 node_modules)                        
   npm ci:  ████████████████████████          48s                
   yarn:    ██████████████████                35s                
   pnpm:    ████████████                      22s   🔥  2x    
                                                                  
└─────────────────────────────────────────────────────────────────┘

6.2 磁盘占用

┌─────────────────────────────────────────────────────────────────┐
                    磁盘占用对比                                  
├─────────────────────────────────────────────────────────────────┤
                                                                  
   单项目 node_modules                                            
   npm:     ████████████████████████████████  850MB              
   yarn:    ████████████████████████████████  850MB              
   pnpm:    ████████████████████████████████  850MB(首次)      
                                                                  
   10 个相似项目(共享依赖 80%)                                  
   npm:     ████████████████████████████████  8.5GB              
   yarn:    ████████████████████████████████  8.5GB              
   pnpm:    ████████████                      2.1GB   🔥  75% 
                                                                  
   💡 pnpm 通过硬链接共享,相同文件只存一份                      
                                                                  
└─────────────────────────────────────────────────────────────────┘

6.3 性能对比表

指标 npm yarn pnpm
首次安装
重复安装 最快
磁盘占用 低(共享)
内存占用
并行下载
离线模式

七、Monorepo 支持对比

7.1 Workspace 配置

# ==================== npm (v7+) ====================
# package.json
{
  "workspaces": ["packages/*"]
}

# ==================== yarn ====================
# package.json
{
  "workspaces": ["packages/*"]
}

# ==================== pnpm ====================
# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'apps/*'
  - '!**/test/**'  # 排除

7.2 Monorepo 命令对比

# 在所有包中执行命令
npm exec --workspaces -- npm run build
yarn workspaces run build
pnpm -r run build                    # 🔥 最简洁

# 在指定包中执行
npm exec --workspace=@my/pkg -- npm run build
yarn workspace @my/pkg run build
pnpm --filter @my/pkg run build      # 🔥 filter 更强大

# 添加依赖到指定包
npm install lodash --workspace=@my/pkg
yarn workspace @my/pkg add lodash
pnpm add lodash --filter @my/pkg

# pnpm filter 高级用法
pnpm --filter "@my/*" run build           # 匹配模式
pnpm --filter "...@my/app" run build      # 包含依赖
pnpm --filter "@my/app..." run build      # 包含被依赖
pnpm --filter "...[origin/main]" run build # Git 变更的包

7.3 Monorepo 对比表

特性 npm yarn pnpm
Workspace 支持 v7+ v1+
依赖提升 默认提升 默认提升 不提升(严格)
Filter 语法 基础 基础 强大
并行执行
拓扑排序
变更检测 ✅ (--filter)

八、特殊场景处理

8.1 Peer Dependencies

// 包 A 声明
{
  "peerDependencies": {
    "react": "^17.0.0 || ^18.0.0"
  }
}

// npm 7+:自动安装 peer deps(可能导致冲突)
// yarn:警告但不自动安装
// pnpm:严格模式,必须显式安装
# pnpm 处理 peer deps 警告
pnpm install --strict-peer-dependencies=false

# 或在 .npmrc 配置
strict-peer-dependencies=false
auto-install-peers=true

8.2 可选依赖失败

// package.json
{
  "optionalDependencies": {
    "fsevents": "^2.3.0"  // macOS only
  }
}

// npm/yarn:失败时静默跳过
// pnpm:同样静默跳过,但日志更清晰

8.3 私有仓库配置

# .npmrc(三者通用)

# 指定 registry
registry=https://registry.npmmirror.com

# 私有包使用私有仓库
@mycompany:registry=https://npm.mycompany.com

# 认证
//npm.mycompany.com/:_authToken=${NPM_TOKEN}

九、安全性对比

9.1 安全审计

# npm
npm audit
npm audit fix
npm audit fix --force  # 强制升级(可能破坏性)

# yarn
yarn audit
yarn audit --json

# pnpm
pnpm audit
pnpm audit --fix

9.2 安全特性对比

特性 npm yarn pnpm
安全审计
自动修复
幽灵依赖防护
完整性校验
签名验证 ✅ (v8.6+)

9.3 pnpm 的安全优势

┌─────────────────────────────────────────────────────────────────┐
│                    pnpm 安全优势                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   1. 防止幽灵依赖攻击                                            │
│      • 恶意包无法被意外引入                                     │
│      • 只能访问显式声明的依赖                                   │
│                                                                  │
│   2. 内容寻址存储                                                │
│      • 文件按哈希存储                                           │
│      • 篡改会导致哈希不匹配                                     │
│                                                                  │
│   3. 严格的依赖解析                                              │
│      • 不会意外使用错误版本                                     │
│      • 依赖关系更清晰                                           │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

十、迁移指南

10.1 从 npm 迁移到 pnpm

# 1. 安装 pnpm
npm install -g pnpm

# 2. 删除 node_modules 和 lockfile
rm -rf node_modules package-lock.json

# 3. 导入(自动生成 pnpm-lock.yaml)
pnpm import  # 可以从 package-lock.json 导入

# 4. 安装
pnpm install

# 5. 更新 CI 脚本
# npm ci → pnpm install --frozen-lockfile
# npm install → pnpm install
# npm run → pnpm run

10.2 从 yarn 迁移到 pnpm

# 1. 删除 yarn 相关文件
rm -rf node_modules yarn.lock .yarnrc.yml

# 2. 导入
pnpm import  # 可以从 yarn.lock 导入

# 3. 安装
pnpm install

# 4. 处理可能的幽灵依赖问题
# pnpm 会报错,按提示添加缺失的依赖
pnpm add <missing-package>

10.3 常见迁移问题

# 问题1:幽灵依赖报错
# Error: Cannot find module 'xxx'
# 解决:显式添加依赖
pnpm add xxx

# 问题2:peer deps 警告
# 解决:配置 .npmrc
echo "auto-install-peers=true" >> .npmrc

# 问题3:某些包不兼容符号链接
# 解决:配置 shamefully-hoist
echo "shamefully-hoist=true" >> .npmrc  # 不推荐,最后手段

# 问题4:postinstall 脚本路径问题
# 解决:使用相对路径或 pnpm 的 hooks

十一、最佳实践

11.1 项目配置建议

# .npmrc(推荐配置)

# 使用国内镜像
registry=https://registry.npmmirror.com

# 自动安装 peer deps
auto-install-peers=true

# 严格模式(推荐)
strict-peer-dependencies=false

# 提升特定包(兼容性问题时使用)
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*

11.2 CI/CD 配置

# GitHub Actions 示例
- name: Setup pnpm
  uses: pnpm/action-setup@v2
  with:
    version: 8

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'pnpm'  # 🔥 缓存 pnpm store

- name: Install dependencies
  run: pnpm install --frozen-lockfile  # 🔥 CI 必须用 frozen

- name: Build
  run: pnpm run build

11.3 团队协作规范

// package.json
{
  "packageManager": "pnpm@8.15.0",  // 🔥 锁定包管理器版本
  "engines": {
    "node": ">=18",
    "pnpm": ">=8"
  },
  "scripts": {
    "preinstall": "npx only-allow pnpm"  // 🔥 强制使用 pnpm
  }
}

十二、选型建议

12.1 决策树

┌─────────────────────────────────────────────────────────────────┐
│                    包管理器选型决策                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   你的场景是?                                                   │
│        │                                                         │
│        ├─ 新项目 ──────────────────────► pnpm(推荐)           │
│        │                                                         │
│        ├─ 老项目迁移成本高 ────────────► 保持现状               │
│        │                                                         │
│        ├─ Monorepo ────────────────────► pnpm(filter 强大)    │
│        │                                                         │
│        ├─ 磁盘空间紧张 ────────────────► pnpm(省 50%+)        │
│        │                                                         │
│        ├─ CI 速度敏感 ─────────────────► pnpm(快 2x)          │
│        │                                                         │
│        └─ 团队不想学新工具 ────────────► npm(零学习成本)      │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

12.2 场景推荐

场景 推荐 原因
新项目 pnpm 性能好、安全、现代
Monorepo pnpm filter 语法强大
老项目维护 保持现状 迁移有成本
开源项目 npm/pnpm npm 兼容性最好
企业项目 pnpm 磁盘省、速度快
学习/教程 npm 文档最多

12.3 总结对比

维度 npm yarn pnpm
安装速度 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
磁盘占用 ⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐
安全性 ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
Monorepo ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
兼容性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
学习成本 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
社区生态 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐

最终建议

┌─────────────────────────────────────────────────────────────────┐
│                    2025 年推荐                                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   🥇 pnpm:新项目首选                                           │
│      • 性能最好,磁盘最省                                       │
│      • 解决幽灵依赖,更安全                                     │
│      • Monorepo 支持最强                                        │
│      • Vue、Vite 等主流项目都在用                               │
│                                                                  │
│   🥈 npm:兼容性优先                                            │
│      • Node.js 自带,零配置                                     │
│      • 文档最全,问题最好搜                                     │
│      • 开源项目贡献者友好                                       │
│                                                                  │
│   🥉 yarn:特定场景                                             │
│      • 已有 yarn 的老项目                                       │
│      • 需要 Plug'n'Play 的场景                                  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

如果这篇文章对你有帮助,欢迎点赞收藏!有问题评论区见 🎉

Vue.js 源码揭秘(一):Vue3 架构总览

作者 借个火er
2025年12月27日 13:51

Vue.js 源码揭秘(一):Vue3 架构总览

本文从全局视角解析 Vue3 的核心架构,建立源码阅读的整体认知。

一、整体架构

┌─────────────────────────────────────────────────────────────┐
│                      Vue Application                        │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                    Compiler (编译时)                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         │
│  │   Parse     │─►│  Transform  │─►│   Codegen   │         │
│  │  (解析)     │  │   (转换)    │  │  (代码生成)  │         │
│  └─────────────┘  └─────────────┘  └─────────────┘         │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼ render function
┌─────────────────────────────────────────────────────────────┐
│                    Runtime (运行时)                          │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         │
│  │ Reactivity  │  │   Renderer  │  │  Scheduler  │         │
│  │  (响应式)   │  │   (渲染器)   │  │   (调度器)   │         │
│  └─────────────┘  └─────────────┘  └─────────────┘         │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                      DOM / Platform                         │
└─────────────────────────────────────────────────────────────┘

二、编译时 vs 运行时

2.1 编译时(Compile Time)

// 模板
<template>
  <div>{{ msg }}</div>
</template>

// 编译后的 render 函数
function render(_ctx) {
  return _createElementVNode("div", null, _toDisplayString(_ctx.msg))
}

2.2 运行时(Runtime)

// 运行时执行 render 函数
const vnode = render(ctx)

// patch 到 DOM
patch(null, vnode, container)

三、响应式系统

3.1 核心 API

// reactive - 对象响应式
const state = reactive({ count: 0 })

// ref - 基本类型响应式
const count = ref(0)

// computed - 计算属性
const double = computed(() => count.value * 2)

// effect - 副作用
effect(() => {
  console.log(count.value)
})

3.2 依赖收集与触发

┌─────────────────────────────────────────────────────────────┐
│                    响应式流程                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────┐    get     ┌─────────┐                       │
│   │  Proxy  │ ─────────► │  track  │ ──► 收集当前 effect    │
│   └─────────┘            └─────────┘                       │
│        │                                                    │
│        │ set                                                │
│        ▼                                                    │
│   ┌─────────┐            ┌─────────┐                       │
│   │ trigger │ ─────────► │ effects │ ──► 执行所有 effect    │
│   └─────────┘            └─────────┘                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3.3 Dep 与 Effect

// Dep - 依赖容器
class Dep {
  subs: Set<Subscriber>  // 订阅者集合
  
  track() {
    if (activeSub) {
      this.subs.add(activeSub)
    }
  }
  
  trigger() {
    this.subs.forEach(sub => sub.notify())
  }
}

// ReactiveEffect - 副作用
class ReactiveEffect {
  deps: Link[]  // 依赖链表
  
  run() {
    activeSub = this
    return this.fn()
  }
  
  notify() {
    this.scheduler ? this.scheduler() : this.run()
  }
}

四、虚拟 DOM

4.1 VNode 结构

interface VNode {
  type: string | Component    // 节点类型
  props: object | null        // 属性
  children: VNode[] | string  // 子节点
  el: Element | null          // 真实 DOM
  key: string | number        // diff key
  shapeFlag: number           // 节点类型标记
  patchFlag: number           // 优化标记
}

4.2 ShapeFlags

enum ShapeFlags {
  ELEMENT = 1,                    // 普通元素
  FUNCTIONAL_COMPONENT = 1 << 1,  // 函数组件
  STATEFUL_COMPONENT = 1 << 2,    // 有状态组件
  TEXT_CHILDREN = 1 << 3,         // 文本子节点
  ARRAY_CHILDREN = 1 << 4,        // 数组子节点
  SLOTS_CHILDREN = 1 << 5,        // 插槽子节点
  TELEPORT = 1 << 6,              // Teleport
  SUSPENSE = 1 << 7,              // Suspense
  COMPONENT = STATEFUL_COMPONENT | FUNCTIONAL_COMPONENT
}

五、渲染器

5.1 patch 函数

const patch = (n1, n2, container) => {
  if (n1 === n2) return
  
  // 类型不同,卸载旧节点
  if (n1 && !isSameVNodeType(n1, n2)) {
    unmount(n1)
    n1 = null
  }
  
  const { type, shapeFlag } = n2
  
  switch (type) {
    case Text:
      processText(n1, n2, container)
      break
    case Fragment:
      processFragment(n1, n2, container)
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(n1, n2, container)
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(n1, n2, container)
      }
  }
}

5.2 组件挂载

const mountComponent = (vnode, container) => {
  // 1. 创建组件实例
  const instance = createComponentInstance(vnode)
  
  // 2. 设置组件(执行 setup)
  setupComponent(instance)
  
  // 3. 设置渲染副作用
  setupRenderEffect(instance, vnode, container)
}

const setupRenderEffect = (instance, vnode, container) => {
  const effect = new ReactiveEffect(() => {
    if (!instance.isMounted) {
      // 首次挂载
      const subTree = instance.render()
      patch(null, subTree, container)
      instance.subTree = subTree
      instance.isMounted = true
    } else {
      // 更新
      const nextTree = instance.render()
      patch(instance.subTree, nextTree, container)
      instance.subTree = nextTree
    }
  })
  
  effect.run()
}

六、调度器

6.1 任务队列

const queue: SchedulerJob[] = []
let isFlushing = false

function queueJob(job) {
  if (!queue.includes(job)) {
    queue.push(job)
    queueFlush()
  }
}

function queueFlush() {
  if (!isFlushing) {
    isFlushing = true
    Promise.resolve().then(flushJobs)
  }
}

function flushJobs() {
  queue.sort((a, b) => getId(a) - getId(b))
  
  for (const job of queue) {
    job()
  }
  
  queue.length = 0
  isFlushing = false
}

6.2 nextTick

const resolvedPromise = Promise.resolve()

function nextTick(fn?) {
  return fn 
    ? resolvedPromise.then(fn) 
    : resolvedPromise
}

七、组件系统

7.1 组件实例

interface ComponentInternalInstance {
  uid: number                    // 唯一 ID
  type: Component                // 组件定义
  parent: ComponentInternalInstance | null
  
  // 状态
  data: object                   // data()
  props: object                  // props
  setupState: object             // setup() 返回值
  ctx: object                    // 渲染上下文
  
  // 渲染
  render: Function               // render 函数
  subTree: VNode                 // 渲染的 VNode 树
  effect: ReactiveEffect         // 渲染副作用
  
  // 生命周期
  isMounted: boolean
  isUnmounted: boolean
  
  // 生命周期钩子
  bc: Function[] | null          // beforeCreate
  c: Function[] | null           // created
  bm: Function[] | null          // beforeMount
  m: Function[] | null           // mounted
  bu: Function[] | null          // beforeUpdate
  u: Function[] | null           // updated
  bum: Function[] | null         // beforeUnmount
  um: Function[] | null          // unmounted
}

7.2 setup 执行

function setupComponent(instance) {
  const { props, children } = instance.vnode
  
  // 初始化 props
  initProps(instance, props)
  
  // 初始化 slots
  initSlots(instance, children)
  
  // 执行 setup
  const { setup } = instance.type
  if (setup) {
    const setupResult = setup(instance.props, {
      attrs: instance.attrs,
      slots: instance.slots,
      emit: instance.emit,
      expose: instance.expose
    })
    
    handleSetupResult(instance, setupResult)
  }
}

八、编译优化

8.1 PatchFlags

enum PatchFlags {
  TEXT = 1,              // 动态文本
  CLASS = 1 << 1,        // 动态 class
  STYLE = 1 << 2,        // 动态 style
  PROPS = 1 << 3,        // 动态 props
  FULL_PROPS = 1 << 4,   // 有动态 key
  NEED_HYDRATION = 1 << 5,
  STABLE_FRAGMENT = 1 << 6,
  KEYED_FRAGMENT = 1 << 7,
  UNKEYED_FRAGMENT = 1 << 8,
  NEED_PATCH = 1 << 9,
  DYNAMIC_SLOTS = 1 << 10,
  HOISTED = -1,          // 静态提升
  BAIL = -2              // 退出优化
}

8.2 Block Tree

// 编译优化:只追踪动态节点
const _hoisted_1 = createVNode("div", null, "static")

function render() {
  return (openBlock(), createBlock("div", null, [
    _hoisted_1,  // 静态提升
    createVNode("span", null, ctx.msg, PatchFlags.TEXT)  // 动态节点
  ]))
}

九、完整渲染流程

┌─────────────────────────────────────────────────────────────┐
│                    Vue 渲染流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. createApp(App).mount('#app')                            │
│        │                                                    │
│        ▼                                                    │
│  2. 创建 VNode                                              │
│        │                                                    │
│        ▼                                                    │
│  3. render(vnode, container)                                │
│        │                                                    │
│        ▼                                                    │
│  4. patch(null, vnode, container)                           │
│        │                                                    │
│        ▼                                                    │
│  5. processComponent → mountComponent                       │
│        │                                                    │
│        ├── createComponentInstance                          │
│        ├── setupComponent (执行 setup)                      │
│        └── setupRenderEffect                                │
│              │                                              │
│              ▼                                              │
│  6. ReactiveEffect.run()                                    │
│        │                                                    │
│        ▼                                                    │
│  7. instance.render() → subTree VNode                       │
│        │                                                    │
│        ▼                                                    │
│  8. patch(null, subTree, container)                         │
│        │                                                    │
│        ▼                                                    │
│  9. 递归处理子节点 → 挂载到 DOM                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

十、小结

Vue3 架构的核心:

  1. 响应式系统:基于 Proxy,依赖收集 + 触发更新
  2. 虚拟 DOM:VNode 描述 UI,patch 算法高效更新
  3. 编译优化:PatchFlags、Block Tree、静态提升
  4. 调度器:批量更新,nextTick 微任务队列
  5. 组件系统:setup + Composition API

📦 源码地址:github.com/vuejs/core

下一篇:响应式系统详解

如果觉得有帮助,欢迎点赞收藏 👍

React 19 源码揭秘(二):useState 的实现原理

作者 借个火er
2025年12月27日 10:42

React 19 源码揭秘(二):useState 的实现原理

本文深入 React 源码,带你彻底搞懂 useState 从调用到更新的完整流程。

前言

useState 可能是你用得最多的 Hook,但你知道它背后是怎么工作的吗?

const [count, setCount] = useState(0);
setCount(count + 1);  // 这行代码背后发生了什么?

本文将从源码角度,完整解析 useState 的实现原理。

一、Hook 的数据结构

首先,我们需要了解 Hook 在 React 内部是如何存储的。

Hook 节点

每个 Hook 调用都会创建一个 Hook 对象:

type Hook = {
  memoizedState: any,    // 存储的状态值
  baseState: any,        // 基础状态(用于更新计算)
  baseQueue: Update | null,  // 基础更新队列
  queue: UpdateQueue | null, // 更新队列
  next: Hook | null,     // 指向下一个 Hook
};

Hook 链表

多个 Hook 以链表形式存储在 Fiber 节点的 memoizedState 上:

Fiber.memoizedState
        │
        ▼
    ┌───────┐     ┌───────┐     ┌───────┐
    │ Hook1 │ ──► │ Hook2 │ ──► │ Hook3 │ ──► null
    │useState│     │useEffect│    │useMemo│
    └───────┘     └───────┘     └───────┘

这就是为什么 Hook 不能在条件语句中调用——React 依赖调用顺序来匹配 Hook。

二、首次渲染:mountState

当组件首次渲染时,useState 会调用 mountState

// 源码位置:react-reconciler/src/ReactFiberHooks.js

function mountState(initialState) {
  // 1. 创建 Hook 节点,加入链表
  const hook = mountWorkInProgressHook();
  
  // 2. 处理初始值(支持函数式初始化)
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  
  // 3. 保存初始状态
  hook.memoizedState = hook.baseState = initialState;
  
  // 4. 创建更新队列
  const queue = {
    pending: null,           // 待处理的更新
    lanes: NoLanes,
    dispatch: null,          // setState 函数
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  };
  hook.queue = queue;
  
  // 5. 绑定 dispatch 函数(就是 setState)
  const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
  queue.dispatch = dispatch;
  
  // 6. 返回 [state, setState]
  return [hook.memoizedState, dispatch];
}

mountWorkInProgressHook

这个函数负责创建 Hook 节点并维护链表:

function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // 第一个 Hook,挂载到 Fiber
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 追加到链表末尾
    workInProgressHook = workInProgressHook.next = hook;
  }
  
  return workInProgressHook;
}

三、触发更新:dispatchSetState

当你调用 setCount(1) 时,实际执行的是 dispatchSetState

function dispatchSetState(fiber, queue, action) {
  // 1. 获取更新优先级
  const lane = requestUpdateLane(fiber);
  
  // 2. 创建更新对象
  const update = {
    lane,
    action,              // 新值或更新函数
    hasEagerState: false,
    eagerState: null,
    next: null,
  };
  
  // 3. 性能优化:Eager State(提前计算)
  if (fiber.lanes === NoLanes) {
    const currentState = queue.lastRenderedState;
    const eagerState = basicStateReducer(currentState, action);
    update.hasEagerState = true;
    update.eagerState = eagerState;
    
    // 如果新旧状态相同,跳过更新!
    if (Object.is(eagerState, currentState)) {
      return;  // Bailout!
    }
  }
  
  // 4. 将更新加入队列
  enqueueConcurrentHookUpdate(fiber, queue, update, lane);
  
  // 5. 调度更新
  scheduleUpdateOnFiber(root, fiber, lane);
}

Eager State 优化

这是一个重要的性能优化:

const [count, setCount] = useState(0);

// 点击按钮
setCount(0);  // 状态没变,React 会跳过这次更新!

React 会在调度之前就计算新状态,如果和旧状态相同(通过 Object.is 比较),直接跳过整个更新流程。

四、更新渲染:updateState

当组件重新渲染时,useState 会调用 updateState

function updateState(initialState) {
  // useState 本质上是预设了 reducer 的 useReducer
  return updateReducer(basicStateReducer, initialState);
}

// 基础 reducer:支持值或函数
function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

updateReducer

这是处理更新的核心逻辑:

function updateReducer(reducer, initialArg) {
  // 1. 获取当前 Hook
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  
  // 2. 获取待处理的更新
  const pending = queue.pending;
  
  // 3. 计算新状态
  let newState = hook.baseState;
  if (pending !== null) {
    let update = pending.first;
    do {
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null);
  }
  
  // 4. 保存新状态
  hook.memoizedState = newState;
  
  // 5. 返回新状态和 dispatch
  return [hook.memoizedState, queue.dispatch];
}

updateWorkInProgressHook

更新时,需要从 current 树复制 Hook:

function updateWorkInProgressHook() {
  // 从 current Fiber 获取对应的 Hook
  let nextCurrentHook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    nextCurrentHook = current.memoizedState;
  } else {
    nextCurrentHook = currentHook.next;
  }
  
  currentHook = nextCurrentHook;
  
  // 复制 Hook 到 workInProgress
  const newHook = {
    memoizedState: currentHook.memoizedState,
    baseState: currentHook.baseState,
    baseQueue: currentHook.baseQueue,
    queue: currentHook.queue,
    next: null,
  };
  
  // 加入链表...
  return newHook;
}

五、完整流程图

┌─────────────────────────────────────────────────────────┐
│                    首次渲染 (Mount)                      │
├─────────────────────────────────────────────────────────┤
│  useState(0)                                            │
│      │                                                  │
│      ▼                                                  │
│  mountState(0)                                          │
│      │                                                  │
│      ├──► 创建 Hook 节点                                │
│      ├──► 初始化 memoizedState = 0                      │
│      ├──► 创建 UpdateQueue                              │
│      ├──► 绑定 dispatch = dispatchSetState              │
│      │                                                  │
│      ▼                                                  │
│  返回 [0, setCount]                                     │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                    触发更新                              │
├─────────────────────────────────────────────────────────┤
│  setCount(1)                                            │
│      │                                                  │
│      ▼                                                  │
│  dispatchSetState(fiber, queue, 1)                      │
│      │                                                  │
│      ├──► 获取优先级 lane                               │
│      ├──► 创建 Update 对象                              │
│      ├──► Eager State: 计算新状态                       │
│      ├──► 比较新旧状态,相同则 Bailout                   │
│      ├──► 入队更新                                      │
│      │                                                  │
│      ▼                                                  │
│  scheduleUpdateOnFiber() ──► 调度重新渲染               │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                    重新渲染 (Update)                     │
├─────────────────────────────────────────────────────────┤
│  useState(0)  // 初始值被忽略                           │
│      │                                                  │
│      ▼                                                  │
│  updateState(0)                                         │
│      │                                                  │
│      ▼                                                  │
│  updateReducer(basicStateReducer, 0)                    │
│      │                                                  │
│      ├──► 获取对应的 Hook                               │
│      ├──► 处理 UpdateQueue 中的更新                     │
│      ├──► 计算新状态 = 1                                │
│      │                                                  │
│      ▼                                                  │
│  返回 [1, setCount]                                     │
└─────────────────────────────────────────────────────────┘

六、Dispatcher 切换

React 如何区分 mount 和 update?答案是 Dispatcher 切换

// renderWithHooks 中
ReactSharedInternals.H = 
  current === null || current.memoizedState === null
    ? HooksDispatcherOnMount   // 首次渲染
    : HooksDispatcherOnUpdate; // 更新渲染

// 两个 Dispatcher 的 useState 指向不同函数
const HooksDispatcherOnMount = {
  useState: mountState,
  useEffect: mountEffect,
  // ...
};

const HooksDispatcherOnUpdate = {
  useState: updateState,
  useEffect: updateEffect,
  // ...
};

渲染完成后,切换到 ContextOnlyDispatcher,禁止在组件外调用 Hook:

// 渲染完成后
ReactSharedInternals.H = ContextOnlyDispatcher;

const ContextOnlyDispatcher = {
  useState: throwInvalidHookError,  // 抛出错误
  // ...
};

七、为什么 Hook 不能条件调用?

现在你应该明白了:

// ❌ 错误
if (condition) {
  const [a, setA] = useState(0);  // Hook 1
}
const [b, setB] = useState(0);    // Hook 2 或 Hook 1?

// ✅ 正确
const [a, setA] = useState(0);    // 始终是 Hook 1
const [b, setB] = useState(0);    // 始终是 Hook 2

React 通过遍历链表来匹配 Hook,如果顺序变了,状态就乱了。

八、调试技巧

想要亲自验证?在这些位置打断点:

// 首次渲染
mountState          // react-reconciler/src/ReactFiberHooks.js

// 触发更新
dispatchSetState    // react-reconciler/src/ReactFiberHooks.js

// 重新渲染
updateReducer       // react-reconciler/src/ReactFiberHooks.js

用 Counter 组件测试:

const Counter = () => {
  const [count, setCount] = useState(0);  // 断点这里
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
};

小结

本文深入分析了 useState 的实现原理:

  1. 数据结构:Hook 以链表形式存储在 Fiber.memoizedState
  2. 首次渲染:mountState 创建 Hook 和 UpdateQueue
  3. 触发更新:dispatchSetState 创建 Update,调度渲染
  4. Eager State:提前计算,相同状态跳过更新
  5. 重新渲染:updateReducer 处理更新队列,计算新状态
  6. Dispatcher:通过切换实现 mount/update 的区分

下一篇我们将分析 useEffect 的实现原理,看看副作用是如何被调度和执行的。


📦 配套源码:github.com/220529/reac…

上一篇:React 19 源码全景图

下一篇:useEffect 的实现原理

如果觉得有帮助,欢迎点赞收藏 👍

React 19 源码全景图:从宏观到微观

作者 借个火er
2025年12月27日 10:41

React 19 源码全景图:从宏观到微观

本文是 React 源码系列的总览篇,帮你建立完整的知识框架,后续文章将逐一深入。

一、React 是什么?

一句话:React 是一个将状态映射为 UI 的函数

UI = f(state)

当状态变化时,React 会:

  1. 计算新的 UI(Reconciler)
  2. 调度更新任务(Scheduler)
  3. 将变化应用到 DOM(Renderer)

二、三大核心模块

┌─────────────────────────────────────────────────────────┐
│                        你的代码                          │
│            <App /> → useState → setState                │
└─────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────┐
│                   Scheduler 调度器                       │
│                                                         │
│  • 优先级管理(用户交互 > 动画 > 数据请求)               │
│  • 时间切片(5ms 一片,避免卡顿)                        │
│  • 任务队列(最小堆实现)                                │
└─────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────┐
│                  Reconciler 协调器                       │
│                                                         │
│  • Fiber 架构(可中断的链表结构)                        │
│  • Diff 算法(最小化 DOM 操作)                          │
│  • Hooks 系统(状态和副作用管理)                        │
└─────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────┐
│                   Renderer 渲染器                        │
│                                                         │
│  • ReactDOM(Web)                                      │
│  • React Native(移动端)                               │
│  • React Three Fiber3D)                              │
└─────────────────────────────────────────────────────────┘

三、核心概念速览

1. Fiber

Fiber 是 React 的核心数据结构,每个组件对应一个 Fiber 节点:

FiberNode {
  // 类型信息
  tag,              // 组件类型(函数组件=0,类组件=1,DOM=5)
  type,             // 组件函数或 DOM 标签
  
  // 树结构
  return,           // 父节点
  child,            // 第一个子节点
  sibling,          // 兄弟节点
  
  // 状态
  memoizedState,    // Hooks 链表
  memoizedProps,    // 上次的 props
  
  // 副作用
  flags,            // 标记(插入、更新、删除)
  
  // 双缓冲
  alternate,        // 另一棵树的对应节点
}

2. Lane(优先级)

React 19 使用 31 位二进制数表示优先级:

SyncLane           = 0b0000000000000000000000000000010  // 同步(最高)
InputContinuousLane = 0b0000000000000000000000000001000  // 连续输入
DefaultLane        = 0b0000000000000000000000000100000  // 默认
TransitionLane     = 0b0000000000000000000000010000000  // 过渡
IdleLane           = 0b0010000000000000000000000000000  // 空闲(最低)

3. 双缓冲

React 维护两棵 Fiber 树:

  • current:当前屏幕显示的
  • workInProgress:正在构建的

更新完成后一行代码切换:root.current = workInProgress

四、渲染流程

完整流程图

setState() 
    │
    ▼
scheduleUpdateOnFiber()     ← 标记更新
    │
    ▼
ensureRootIsScheduled()     ← 确保调度
    │
    ▼
scheduleCallback()          ← Scheduler 调度
    │
    ▼
performConcurrentWorkOnRoot() ← 开始渲染
    │
    ├─────────────────────────────────────┐
    │         Render 阶段(可中断)         │
    │                                     │
    │  workLoopConcurrent()               │
    │      │                              │
    │      ▼                              │
    │  performUnitOfWork() ←──┐           │
    │      │                  │           │
    │      ▼                  │           │
    │  beginWork()            │ 循环      │
    │      │                  │           │
    │      ▼                  │           │
    │  completeWork() ────────┘           │
    │                                     │
    └─────────────────────────────────────┘
    │
    ▼
    ├─────────────────────────────────────┐
    │        Commit 阶段(不可中断)        │
    │                                     │
    │  commitBeforeMutationEffects()      │
    │      │                              │
    │      ▼                              │
    │  commitMutationEffects()  ← DOM操作 │
    │      │                              │
    │      ▼                              │
    │  root.current = finishedWork        │
    │      │                              │
    │      ▼                              │
    │  commitLayoutEffects()              │
    │                                     │
    └─────────────────────────────────────┘
    │
    ▼
flushPassiveEffects()       ← useEffect(异步)

Render 阶段

beginWork(向下递归):

  • 根据组件类型处理(函数组件、类组件、DOM 元素)
  • 调用组件函数,执行 Hooks
  • Diff 子节点,创建/复用 Fiber

completeWork(向上回溯):

  • 创建 DOM 节点
  • 收集副作用标记
  • 冒泡 subtreeFlags

Commit 阶段

三个子阶段:

阶段 时机 主要工作
Before Mutation DOM 操作前 getSnapshotBeforeUpdate
Mutation DOM 操作 增删改 DOM
Layout DOM 操作后 useLayoutEffect、componentDidMount

五、Hooks 原理

Hooks 以链表形式存储在 Fiber 的 memoizedState 上:

Fiber.memoizedState → useState → useEffect → useMemo → null

useState 流程

Mount:  mountState() → 创建 Hook → 初始化状态 → 返回 [state, setState]
Update: updateState() → 获取 Hook → 处理更新队列 → 返回 [newState, setState]

useEffect 流程

Mount:  mountEffect() → 创建 Effect → 标记 Passive
Commit: flushPassiveEffects() → 执行销毁函数 → 执行创建函数

六、Diff 算法

React Diff 的三个策略:

  1. 同层比较:不跨层级移动节点
  2. 类型判断:类型不同直接替换
  3. key 标识:通过 key 识别节点

单节点 Diff

key 相同 && type 相同 → 复用
key 相同 && type 不同 → 删除重建
key 不同 → 删除,继续找

多节点 Diff(三轮遍历)

第一轮:从左到右,处理更新
第二轮:处理新增或删除
第三轮:处理移动(Map 查找)

七、Scheduler 调度

优先级

ImmediatePriority   // -1ms,立即执行
UserBlockingPriority // 250ms,用户交互
NormalPriority      // 5000ms,普通更新
LowPriority         // 10000ms,低优先级
IdlePriority        // 永不过期,空闲执行

时间切片

function workLoop() {
  while (task && !shouldYield()) {  // 5ms 检查一次
    task = performTask(task);
  }
  if (task) {
    scheduleCallback(task);  // 还有任务,继续调度
  }
}

八、源码目录

packages/
├── react/                    # React API
│   └── src/ReactHooks.js     # Hooks 入口
│
├── react-reconciler/         # 协调器(核心)
│   └── src/
│       ├── ReactFiber.js           # Fiber 定义
│       ├── ReactFiberWorkLoop.js   # 工作循环
│       ├── ReactFiberBeginWork.js  # 递阶段
│       ├── ReactFiberCompleteWork.js # 归阶段
│       ├── ReactFiberHooks.js      # Hooks 实现
│       ├── ReactFiberCommitWork.js # Commit
│       ├── ReactFiberLane.js       # 优先级
│       └── ReactChildFiber.js      # Diff 算法
│
├── react-dom/                # DOM 渲染器
│
└── scheduler/                # 调度器
    └── src/
        ├── Scheduler.js          # 调度逻辑
        └── SchedulerMinHeap.js   # 最小堆

九、系列文章导航

序号 主题 核心内容
00 调试环境搭建 项目介绍、快速开始
01 架构总览(本文) 三大模块、核心概念
02 useState 原理 Hook 链表、更新队列
03 useEffect 原理 Effect 链表、执行时机
04 Fiber 工作循环 beginWork、completeWork
05 Diff 算法 单节点、多节点 Diff
06 Scheduler 调度器 优先级、时间切片
07 Commit 阶段 三个子阶段、DOM 操作

十、学习建议

  1. 先跑起来:clone react-debug,打断点调试
  2. 从 useState 开始:最简单也最核心
  3. 画流程图:边看边画,加深理解
  4. 写测试组件:验证你的理解

📦 配套源码:github.com/220529/reac…

上一篇:React 源码调试环境搭建

下一篇:useState 的实现原理

❌
❌