ethers.js vs web3.js vs viem vs wagmi

1. 核心定位与层级关系

四者分属不同层级,服务于以太坊 Web3 前端开发的不同需求:

定位 层级 核心价值
ethers.js 以太坊 JavaScript 库 底层 提供与以太坊交互的核心功能
web3.js 以太坊 JavaScript 库 底层 以太坊基金会官方维护的全功能库
viem 以太坊 JavaScript 库 底层 现代、轻量的以太坊交互库
wagmi React Hooks 工具库 上层 基于底层库封装的前端开发工具

2. 详细功能对比

(1)ethers.js

  • 核心功能
    • 钱包管理(生成、导入、签名交易)
    • 智能合约交互(部署、调用、监听事件)
    • 网络连接(与以太坊节点通信)
    • 以太坊数据解析(地址、交易、区块等)
  • 特点
    • 轻量级(体积小,按需引入)
    • API 现代(使用 Promise,支持 async/await)
    • TypeScript 友好
    • 文档清晰,社区活跃
  • 使用场景
    • 需要直接与以太坊区块链交互的项目
    • 对包体积敏感的应用
    • 追求现代 API 设计的开发者

(2)web3.js

  • 核心功能
    • 与 ethers.js 类似,提供全面的以太坊交互功能
    • 支持更多底层 RPC 方法
    • 内置更多工具函数
  • 特点
    • 功能全面(官方维护,覆盖所有以太坊功能)
    • 相对较重(包体积大)
    • API 设计较传统(早期版本使用回调,新版支持 Promise)
    • 历史悠久,生态成熟
  • 使用场景
    • 需要使用官方全功能实现的项目
    • 传统 Web3 项目迁移
    • 对官方支持有强依赖的场景

(3)viem

  • 核心功能
    • 与 ethers.js 类似,提供以太坊交互的核心功能
    • 支持 EIP-1193 钱包标准
    • 内置链数据和地址工具
  • 特点
    • 极致轻量(体积比 ethers.js 更小)
    • 性能优化(更快的交易签名和网络请求)
    • 现代 API 设计(链式调用,类型安全)
    • 专注于前端使用场景
  • 使用场景
    • 对包体积和性能要求极高的项目
    • 现代前端框架(如 React 18+)
    • 追求最新技术栈的开发者

(4)wagmi

  • 核心功能
    • 基于 React Hooks 的以太坊交互封装
    • 钱包连接与管理(支持多种钱包)
    • 智能合约调用的 Hook 化(如 useContractRead
    • 交易发送与状态管理
    • @tanstack/react-query 集成,提供数据缓存
  • 特点
    • 前端开发友好(Hooks 化 API)
    • 支持多底层库(默认支持 viem,可选 ethers.js)
    • 与现代前端生态集成(React、TypeScript)
    • 配置灵活,扩展性强
  • 使用场景
    • React 前端 Web3 项目
    • 需要快速实现钱包连接、合约交互的场景
    • 追求开发效率和代码简洁的项目

3. 联系与依赖关系

  • 底层库之间的关系
    • ethers.jsweb3.jsviem 均为底层以太坊交互库,功能重叠但设计理念和性能不同
    • viem 是较新的库,针对前端场景优化,体积和性能优于传统库
  • wagmi 与底层库的关系
    • wagmi v1+ 默认使用 viem 作为底层库(替代之前的 ethers.js)
    • 仍支持通过配置使用 ethers.js(兼容性考虑)
    • wagmi 将底层库的复杂逻辑封装为简洁的 React Hooks
  • 功能互补
    • 底层库(ethers.js/web3.js/viem)负责与区块链交互的核心逻辑
    • 上层库(wagmi)简化前端开发,提供 Hooks 化 API 和状态管理
  • 生态组合
    • 现代 Web3 前端开发的主流组合:viem + wagmi + RainbowKit(或其他钱包连接库)
    • 传统组合:ethers.js/web3.js + 自定义钱包连接逻辑

4. 技术演进与选择建议

  • 技术趋势
    • web3.jsethers.js:追求更现代、轻量的 API
    • ethers.jsviem:进一步优化性能和体积
    • 从直接使用底层库到 wagmi:提升前端开发效率
  • 选择建议
    • 现代前端项目:推荐使用 viem + wagmi 组合
      • 优势:极致轻量、性能优异、开发效率高
    • 需要兼容性:选择 ethers.js + wagmi
      • 优势:生态成熟、文档丰富、社区支持广
    • 传统项目:继续使用 web3.js
      • 优势:官方维护、功能全面、历史兼容性好
    • 底层定制需求:直接使用 viemethers.js
      • 优势:灵活性高,可按需实现特定功能

5. 总结

  • 底层库(ethers.js/web3.js/viem):负责与以太坊区块链交互的核心逻辑,选择依据为性能、体积和 API 偏好
  • 上层库(wagmi):简化前端开发,提供 Hooks 化 API 和状态管理,大幅提升开发效率
  • 最佳实践:现代项目优先选择 viem + wagmi 组合,兼顾性能和开发体验

这种分层设计反映了 Web3 前端开发的演进趋势:从复杂的底层操作到简洁的上层抽象,从单一功能库到系统化的生态工具链。

Solidity 入门:变量、函数与回调机制

这篇把 Solidity 初学最容易混的几块放在一起:变量类型、函数可见性、数据位置(memory/storage/calldata)、以及 receive / fallback 回调。

你可以把它当作一份“先跑通认知,再写代码”的速查稿。

一、Solidity 是什么

Solidity 是面向 EVM 的静态编译型高级语言,语法受 C++、JavaScript 影响明显。
如果你有前端或后端基础,上手门槛不高,但它和普通服务端语言的最大区别在于:每一步状态写入都和 Gas 成本直接挂钩


二、合约最小结构

一个最简单的合约通常包含四件事:

  1. 编译器版本声明(pragma solidity
  2. 合约定义(contract
  3. 状态变量(state variable)
  4. 函数(function)
1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.8.0;

contract Counter {
uint public counter;

constructor() {
counter = 0;
}

function count() public {
counter = counter + 1;
}
}

三、函数:可见性与状态可变性

1) 可见性(visibility)

  • public:内外都可调
  • external:通常给外部调用
  • internal:仅当前合约及继承合约可调
  • private:仅当前合约可调

2) 状态可变性(mutability)

  • view:只读状态,不写
  • pure:既不读状态也不写状态
  • payable:允许接收 ETH
1
2
3
4
5
6
7
8
9
10
11
function add(uint i, uint j) public pure returns (uint) {
return i + j;
}

function addTo(uint y) public view returns (uint) {
return counter + y;
}

function deposit() public payable {
deposited += msg.value;
}

四、变量与类型:先记住这一层

1) 状态变量 vs 本地变量

  • 状态变量:存在链上 storage,写入贵
  • 本地变量:函数执行期的临时数据,多在 memory

2) 常量相关

  • constant:编译期常量,写死
  • immutable:部署时赋值,之后不可改

3) 常见类型

  • 值类型:boolint/uintaddressbytes1~bytes32enum
  • 引用类型:arraystructmapping

五、引用类型与数据位置

Solidity 里引用类型的成本差异,核心就看数据位置:

  • storage:链上持久化,最贵
  • memory:函数调用期间存在
  • calldata:外部函数参数区,只读,通常更省 Gas
1
2
3
4
function copy(uint[] calldata arrs) public returns (uint len) {
numbers = arrs; // calldata -> storage(发生拷贝)
return numbers.length;
}

一个常见优化点:参数只读时优先 calldata,别先拷贝到 memory 再用。


六、数组、结构体、映射:三个高频容器

1) 数组(Array)

  • 定长:uint[10]
  • 动态:uint[]
  • 常用成员:lengthpushpop

注意:在链上循环数组,长度不可控时很容易把 Gas 顶上去。

2) 结构体(Struct)

适合把多个相关字段打包成一个业务对象,例如用户资料、订单记录。

3) 映射(Mapping)

1
mapping(address => uint) public balances;

映射读取不存在的 key 会返回默认值(如 0)。
它没有“长度”概念,也不能直接遍历全部 key。


七、地址与转账:address vs address payable

  • address:20 字节地址
  • address payable:可转账地址,可调用 transfer / send
1
2
3
4
5
function testTransfer(address payable to) public {
if (address(this).balance >= 10) {
to.transfer(10);
}
}

transfer/send 都有 2300 gas stipend 的历史限制语境。实际开发中,很多团队更倾向 call + 显式检查返回值,配合重入保护一起做。


八、特殊函数:constructorreceivefallback

1) constructor

部署时执行一次,用于初始化。
链上运行时字节码里不再包含构造逻辑本体,而是构造执行后的结果。

2) receive() external payable

合约接收纯 ETH(空 calldata)时触发。

3) fallback() external [payable]

调用了不存在的函数,或某些不匹配场景时触发。
如果没有 receive,转账时也可能落到 fallback(取决于调用方式与 calldata)。


九、全局变量里最常用的几个

  • block.number:当前区块号
  • block.timestamp:当前区块时间戳
  • msg.sender:当前调用者
  • msg.value:本次调用携带的 wei
  • tx.origin:整条调用链最初发起者(权限判断一般不建议依赖)

十、初学阶段最容易踩的坑

  1. 循环不设边界:数组过大时函数可能直接超 Gas。
  2. 数据位置乱用:能 calldata 的参数别无脑拷贝。
  3. 权限控制粗糙:管理员操作要配合 modifier 和事件。
  4. 回调函数理解不清:收款路径、调用路径没分开。
  5. 版本差异忽略:例如 0.8+ 默认带溢出检查,和 0.8 前行为不同。

十一、练手建议:做一个简版 Bank 合约

可以用下面这组要求自测:

  • 用户可向合约存款(payable
  • 记录每个地址余额(mapping(address => uint)
  • 维护存款 Top N(可先做 Top 3)
  • 仅管理员可提取全部 ETH(onlyOwner

先把功能跑通,再做两件事:

  • 补事件(Deposit / Withdraw
  • 补安全细节(重入防护、错误处理)

如果你刚开始学 Solidity,别急着追“大全”。先把这篇里的函数、类型和回调路径吃透,再进到 token、DEX、治理类合约,速度会快很多。

EIP-1559:为什么需要它,以及 Gas 怎么算

EIP-1559 是以太坊手续费模型的一次关键改造。它没有把 Gas 费“变便宜”,但把原来难以预估的纯竞价模式,换成了更容易理解的“基础费 + 小费”。

这篇就讲三件事:

  1. 为什么会有 EIP-1559
  2. 它具体改了什么
  3. Gas 费用到底怎么计算(含具体数字例子)

一、为什么会有 EIP-1559

在 EIP-1559 之前,以太坊主要用 First-Price Auction(第一价格拍卖)

  • 你给出一个 gasPrice
  • 矿工优先打包 gasPrice 更高的交易
  • 你实际支付的单价就是你自己出的那个价

这个模型在高峰期会很难受:

  • 费用很难预估:用户往往只能“多给一点”,否则交易容易卡住。
  • 过度竞价:很多人同时抬价,结果整体支付高于真实所需。
  • 体验不稳定:同样一笔转账,不同时间价格差异非常大。
  • 钱包策略复杂:钱包要反复估算“现在到底该出多少”。

所以 EIP-1559 不是来承诺“长期低价”的,它主要解决的是定价不透明和体验抖动


二、EIP-1559 具体内容

EIP-1559 引入了几个关键概念。

1) Base Fee(基础费)

  • 每个区块都有一个基础费 baseFeePerGas
  • 这个值由协议自动调整,不是矿工随意定价
  • 基础费会被销毁(burn),不会给打包者

这点很关键:基础费不是“给矿工/验证者”的收入,而是直接销毁。

2) Priority Fee(小费)

  • 也叫 tip,参数通常是 maxPriorityFeePerGas
  • 这部分给打包者(PoW 时期矿工 / PoS 下验证者)
  • 本质上是你给打包者的激励

3) Fee Cap(最高可接受总单价)

  • 参数名是 maxFeePerGas
  • 表示你愿意支付的“每单位 gas 总上限”

交易最终每单位 gas 实际支付单价遵循:

effectiveGasPrice = min(maxFeePerGas, baseFeePerGas + maxPriorityFeePerGas)

并且要满足交易可被执行的基本条件:

maxFeePerGas >= baseFeePerGas

4) 弹性区块(Elastic Block Size)

  • 每个区块有一个“目标 gas 使用量”(target gas)
  • 实际区块可在目标附近弹性波动(上限通常是目标的 2 倍)
  • 如果区块持续高于目标,下一块的 baseFee 上升
  • 如果持续低于目标,下一块的 baseFee 下降

这样做的效果是:短期拥堵不会把手续费一下子拉爆,而是分摊到后续几个区块里。


三、Gas 计算:先看总公式

先看一条最常用的公式:

交易总花费 = gasUsed * effectiveGasPrice

其中(单位常用 gwei):

  • effectiveGasPrice = min(maxFeePerGas, baseFeePerGas + maxPriorityFeePerGas)
  • 用户支付会拆成两部分:
    • 销毁gasUsed * baseFeePerGas
    • 小费给验证者gasUsed * priorityFeePaid

这里:

priorityFeePaid = effectiveGasPrice - baseFeePerGas

你可以把它理解成“愿意付到这里为止”,不是“按这个值强制扣费”。


四、具体算例(带数字)

下面都用 gwei 做单价单位,1 ETH = 1,000,000,000 gwei

例子 1:普通转账

假设:

  • gasUsed = 21,000
  • baseFee = 30 gwei
  • maxPriorityFee = 2 gwei
  • maxFee = 100 gwei

步骤:

  1. 先算 baseFee + priority = 32 gwei
  2. effectiveGasPrice = min(100, 32) = 32 gwei
  3. 总费用:
    21,000 * 32 = 672,000 gwei = 0.000672 ETH

拆分:

  • 销毁:21,000 * 30 = 630,000 gwei = 0.00063 ETH
  • 小费:21,000 * 2 = 42,000 gwei = 0.000042 ETH

例子 2:你把 maxFee 设得很高,但不会按高价全扣

假设:

  • gasUsed = 100,000
  • baseFee = 40 gwei
  • maxPriorityFee = 3 gwei
  • maxFee = 200 gwei

计算:

  • baseFee + priority = 43 gwei
  • effectiveGasPrice = min(200, 43) = 43 gwei
  • 总费用:100,000 * 43 = 4,300,000 gwei = 0.0043 ETH

这个例子想说明一件事:maxFee 设高是为了防止交易卡住,不等于你会按高价成交。

例子 3:maxFee 过低导致交易不可打包

假设:

  • baseFee = 55 gwei
  • 你设置 maxFee = 50 gwei

因为 maxFee < baseFee,交易直接不满足基本条件,节点通常会拒绝或一直 pending。

例子 4:合约调用(更大 gasUsed)

假设一次 DEX 交互:

  • gasUsed = 180,000
  • baseFee = 25 gwei
  • maxPriorityFee = 1.5 gwei
  • maxFee = 35 gwei

先算:

  • baseFee + priority = 26.5 gwei
  • effectiveGasPrice = min(35, 26.5) = 26.5 gwei

总费用:

  • 180,000 * 26.5 = 4,770,000 gwei = 0.00477 ETH

拆分:

  • 销毁:180,000 * 25 = 4,500,000 gwei = 0.0045 ETH
  • 小费:180,000 * 1.5 = 270,000 gwei = 0.00027 ETH

五、设置建议(钱包/脚本)

如果你自己写交易脚本(ethers.js/web3.js),可以按这个思路:

  • maxPriorityFeePerGas:给一个适中小费(例如 1-3 gwei,拥堵时上调)
  • maxFeePerGas:至少覆盖“当前 base fee + 小费”,并留一定缓冲

常见经验做法是:

maxFeePerGas ≈ 2 * 当前baseFee + priority

这样做的好处是,后面 1-2 个区块就算变贵,交易也不至于马上失效。


六、常见误区

误区 1:EIP-1559 之后手续费就会越来越低

不一定。它主要改善的是定价机制和可预测性,不是保证低价。

误区 2:maxFeePerGas 就是实际支付单价

不是。它只是上限,实际支付看当时的 baseFee + priority

误区 3:全部手续费都给验证者

不是。基础费被销毁,验证者拿的是小费部分。


七、小结

EIP-1559 可以记成三句话:

  • 把“纯拍卖”变成“协议定基础价 + 用户给小费”
  • 让费用估算更稳定,减少盲目竞价
  • 通过 burn 机制把基础费从流通里移除

只要把 baseFee / maxPriorityFee / maxFee 这三个参数分清楚,Gas 计算基本就通了。更重要的是根据拥堵情况动态调整,而不是记一套固定数字。

主流公链与测试链对比

主流公链对比(生产环境)

类型 EVM 兼容 Gas 成本 速度/吞吐(体感) 生态成熟度 典型场景
Ethereum Mainnet L1 非常高 高价值 DeFi、核心资产结算
BNB Smart Chain (BSC) L1 交易频繁、成本敏感应用
Polygon PoS 侧链/L2风格网络 游戏、社交、低费应用
Arbitrum One Ethereum L2 (Optimistic) 低-中 很高 DeFi、交易类 dApp
Optimism Ethereum L2 (Optimistic) 低-中 公共品生态、通用 dApp
Base Ethereum L2 (OP Stack) 高(增长快) 消费级应用、社交应用
Avalanche C-Chain L1 子网架构 低-中 中-高 DeFi、企业/子网场景
zkSync Era Ethereum L2 (ZK) 是(有差异) 中-高 低费支付、ZK 叙事应用
Linea Ethereum L2 (ZK) EVM 迁移、低费交互
Solana L1 否(非 EVM) 很低 很快 高频交易、消费级应用

主流测试链对比(开发/联调)

主网 推荐测试链 是否常用水龙头 与主网一致性(开发体感) 备注
Ethereum Sepolia 当前最常用以太坊测试网
BSC BSC Testnet 中-高 需注意节点稳定性差异
Polygon PoS Polygon Amoy 中-高 Mumbai 已逐步被替代
Arbitrum One Arbitrum Sepolia L2 手续费模型更接近主网
Optimism OP Sepolia OP Stack 生态通用性好
Base Base Sepolia Base 开发首选测试网
Avalanche C-Chain Fuji 中-高 AVAX 测试币相对容易获取
zkSync Era zkSync Sepolia 注意部分工具链兼容细节
Linea Linea Sepolia 新生态,文档更新较快
Solana Devnet / Testnet Solana 习惯用 Devnet 做开发

选链建议(简版)

  • 想要最稳和最大生态:Ethereum + Arbitrum/Optimism/Base
  • 想要低费和高频交互:BSC / Polygon / Solana
  • 想做 EVM 且方便迁移:优先 Arbitrum / Base / OP
  • 想提前布局 ZK:zkSync / Linea

React / Flutter 状态管理

由浅入深,从基本概念到原理与源码,再到示例与实际项目应用案例,系统梳理两大主流框架中的状态管理方案

一、状态管理基础概念

1.1 什么是状态(State)?

状态是驱动 UI 变化的数据。当状态改变时,界面随之更新,形成「数据驱动视图」的声明式模式。

1
2
3
4
状态 ──► 视图
▲ │
│ ▼
└── 用户交互 / 网络请求 / 定时器等

1.2 状态的分类

类型 作用域 典型场景 生命周期
本地状态 单组件 输入框内容、展开/折叠、选中项 跟随组件
共享状态 多组件 用户信息、主题、购物车 需要提升或全局管理
服务端状态 与后端同步 API 数据、缓存 异步、需缓存策略

1.3 为什么需要状态管理?

随着应用复杂度上升,会出现:

  • 状态提升导致 props 层层传递(prop drilling)
  • 状态分散导致难以追踪和调试
  • 重复请求缓存失效等数据一致性问题

状态管理方案的目标:集中、可预测、易维护


二、React 状态管理

2.1 内置方案概览

方案 适用场景 特点
useState 本地状态 简单、轻量
useReducer 复杂本地状态 可预测、易测试
Context API 跨层级共享 官方内置、易造成不必要的重渲染

2.2 useState:最简单的本地状态

1
2
3
4
5
6
7
8
9
10
11
12
function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* 函数式更新,避免闭包陷阱 */}
<button onClick={() => setCount(prev => prev + 1)}>+1 (安全)</button>
</div>
);
}

惰性初始化:初始值可以是函数,仅在首次渲染执行。

1
const [state, setState] = useState(() => expensiveComputation());

2.3 useReducer:复杂状态的 reducer 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });

return (
<div>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}

2.4 Context API:跨层级共享状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const ThemeContext = createContext('light');

function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Page />
</ThemeContext.Provider>
);
}

function Page() {
const { theme } = useContext(ThemeContext);
return <div className={theme}>...</div>;
}

注意:Provider 的 value 变化会导致所有 useContext 的消费者重渲染,需配合 useMemo 或拆分 Context 优化。

2.5 Redux / Redux Toolkit:全局状态管理

Redux 采用单向数据流View → Action → Reducer → Store → View

1
2
3
4
5
┌─────────┐   dispatch    ┌─────────┐   reduce    ┌────────┐
│ View │ ───────────► │ Action │ ─────────► │ Store │
└─────────┘ └─────────┘ └────────┘
▲ │
└──────────────── subscribe ─────────────────────┘

Redux Toolkit 示例(官方推荐写法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => { state.value += 1; },
decrement: state => { state.value -= 1; },
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
1
2
3
4
5
6
7
8
9
10
// 组件中使用
import { useDispatch, useSelector } from 'react-redux';
import { increment } from './store/counterSlice';

function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();

return <button onClick={() => dispatch(increment())}>{count}</button>;
}

2.6 Zustand:轻量级全局状态

Zustand 基于 Hooks,API 简洁,无 Provider 包裹。

1
2
3
4
5
6
7
8
9
10
11
12
import { create } from 'zustand';

const useStore = create((set) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
}));

function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}

选择器优化:只订阅需要的字段,避免无关更新。

1
const count = useStore(state => state.count); // 仅 count 变化时重渲染

2.7 React 状态管理原理浅析

useState 的链表结构

React 内部用链表存储 Hooks。每个 Hook 对应链表中的一个节点,通过 FibermemoizedState 串联。

1
2
3
Fiber.memoizedState → Hook1 → Hook2 → Hook3 → ...

[state, setState]

这就是为什么 Hooks 必须在顶层调用、不能放在条件/循环中:链表顺序必须稳定。

setState 的批处理(Batching)

React 18 默认对所有更新进行自动批处理,多次 setState 会合并为一次渲染。

1
2
3
4
5
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// 仅触发一次重渲染
}

三、Flutter 状态管理

3.1 方案概览

方案 官方/社区 适用场景 特点
setState 内置 本地状态 简单,整组件重建
InheritedWidget 内置 跨层级共享 底层基础,一般不直接使用
Provider 官方推荐 中小型应用 基于 InheritedWidget,易上手
Riverpod 社区主流 中大型应用 编译期安全、可测试、无 context
Bloc 社区 复杂业务逻辑 事件驱动、可预测、适合团队
GetX 社区 快速开发 全能型,状态+路由+依赖注入

3.2 setState:最简单的本地状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Counter extends StatefulWidget {
@override
_CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
int _count = 0;

void _increment() {
setState(() {
_count++;
});
}

@override
Widget build(BuildContext context) {
return Column(
children: [
Text('$_count'),
ElevatedButton(onPressed: _increment, child: Text('+1')),
],
);
}
}

原理setState 会标记当前 Element 为脏,在下一帧触发 build 重建子树。

3.3 Provider:官方推荐方案

Provider 基于 InheritedWidget,通过 context.watch<T>() 监听变化并重建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1. 定义 Model(继承 ChangeNotifier)
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;

void increment() {
_count++;
notifyListeners(); // 通知监听者
}
}

// 2. 在根节点提供
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
),
);
}

// 3. 在子组件使用
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterModel>();
return Text('${counter.count}');
}
}

多种 Provider 类型

类型 用途
Provider 不可变值
ChangeNotifierProvider 可变、需 notifyListeners
FutureProvider 异步数据
StreamProvider 流数据
MultiProvider 组合多个 Provider

3.4 Riverpod:下一代状态管理

Riverpod 无 BuildContext 依赖,支持编译期类型安全、易于测试和复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 1. 定义 Provider
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});

class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}

// 2. 在 runApp 外包一层 ProviderScope
void main() {
runApp(ProviderScope(child: MyApp()));
}

// 3. 在组件中使用(无需 context)
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Text('$count'),
);
}
}

ref 的三大方法

方法 作用
ref.watch() 监听变化,值变化时重建
ref.read() 一次性读取,不监听
ref.listen() 监听变化并执行副作用,不重建

3.5 Bloc:事件驱动架构

Bloc 将 UI 与业务逻辑解耦,通过 Event → Bloc → State 的流程管理状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. 定义 Event 和 State
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}

class CounterState {
final int count;
CounterState(this.count);
}

// 2. 实现 Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0)) {
on<Increment>((event, emit) => emit(CounterState(state.count + 1)));
on<Decrement>((event, emit) => emit(CounterState(state.count - 1)));
}
}

// 3. 在 UI 中使用
BlocProvider(
create: (_) => CounterBloc(),
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text('${state.count}');
},
),
)

3.6 Flutter 状态管理原理浅析

setState 与 Element 树

1
2
3
4
5
6
7
8
9
10
setState() 被调用


标记 Element 为 dirty


下一帧 SchedulerBinding 触发 build


Element.rebuild() → State.build()

InheritedWidget 与依赖收集

InheritedWidget 通过 context.dependOnInheritedWidgetOfExactType<T>() 建立「依赖关系」。当 InheritedWidget 更新时,依赖它的 Element 会被标记为脏并重建。

Provider 的 notifyListeners() 会触发 InheritedWidget 的更新,从而通知所有 context.watch 的消费者。


四、React vs Flutter 状态管理对比

4.1 概念映射

概念 React Flutter
本地状态 useState setState
复杂本地状态 useReducer 自建 StatefulWidget + 内部逻辑
跨层级共享 Context InheritedWidget / Provider
全局 Store Redux / Zustand Provider / Riverpod / Bloc
选择器/按需订阅 useSelector / useStore(selector) context.select / ref.watch(provider.select())

4.2 设计哲学差异

维度 React Flutter
更新粒度 组件级,虚拟 DOM diff Widget 树重建,Element 复用
数据流 单向(Redux)或自由(Zustand) 多为单向,Bloc 强调事件流
依赖注入 通过 props / Context 通过 context / ref(Riverpod)
服务端状态 React Query / SWR 等 Riverpod 的 FutureProvider、flutter_bloc 等

五、源码层面的理解

5.1 React useState 的调度

React 的 setState 会调用 dispatchSetState,将更新放入 updateQueue,由调度器(Scheduler)在合适的时机批量处理,触发 rendercommit

1
2
3
4
5
6
// 简化流程
setState(newState)
enqueueUpdate(fiber, update)
scheduleUpdateOnFiber(fiber)
→ performConcurrentWorkOnRoot / performSyncWorkOnRoot
→ commitRoot

5.2 Flutter ChangeNotifier 与 Listenable

ChangeNotifier 继承 Listenable,内部维护 _listeners 列表。notifyListeners() 遍历并调用所有监听者。

1
2
3
4
5
6
// 简化逻辑
void notifyListeners() {
for (final listener in _listeners) {
listener(); // 触发 Consumer 等重建
}
}

ProviderInheritedProvideraddListenerChangeNotifier,当 notifyListeners 被调用时,触发自身 updateShouldNotify 并重建子树。


六、实际项目应用案例

6.1 案例一:电商购物车(React + Zustand)

需求:跨页面购物车数量、增删改、持久化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// store/cartStore.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useCartStore = create(
persist(
(set) => ({
items: [],
addItem: (product, qty = 1) =>
set((state) => ({
items: state.items.some((i) => i.id === product.id)
? state.items.map((i) =>
i.id === product.id ? { ...i, qty: i.qty + qty } : i
)
: [...state.items, { ...product, qty }],
})),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
totalCount: (state) => state.items.reduce((sum, i) => sum + i.qty, 0),
}),
{ name: 'cart-storage' }
)
);

// Header 中只订阅 totalCount,避免整 store 变化导致重渲染
const totalCount = useCartStore((s) =>
s.items.reduce((sum, i) => sum + i.qty, 0)
);

6.2 案例二:用户认证流(Flutter + Riverpod)

需求:登录态、token 刷新、路由守卫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// providers/auth_provider.dart
final authStateProvider = StateNotifierProvider<AuthNotifier, AsyncValue<User?>>((ref) {
return AuthNotifier(ref);
});

class AuthNotifier extends StateNotifier<AsyncValue<User?>> {
AuthNotifier(this.ref) : super(const AsyncValue.loading()) {
_init();
}
final Ref ref;

Future<void> _init() async {
final token = await storage.getToken();
if (token == null) {
state = const AsyncValue.data(null);
return;
}
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => api.getCurrentUser());
}

Future<void> login(String email, String pwd) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => api.login(email, pwd));
}

void logout() {
storage.clearToken();
state = const AsyncValue.data(null);
}
}

// 路由守卫:根据 authState 跳转登录页或首页
ref.listen(authStateProvider, (prev, next) {
next.whenData((user) {
if (user == null) navigator.pushReplacement(LoginRoute());
});
});

6.3 案例三:列表筛选与分页(React + Redux Toolkit + RTK Query)

需求:筛选条件、分页、缓存、乐观更新。

1
2
3
4
5
6
7
8
// 使用 RTK Query 管理服务端状态
const { data, isLoading, refetch } = useGetProductsQuery({
page: currentPage,
category: selectedCategory,
});

// 本地筛选状态用 Redux 或 useState 均可
const [filters, setFilters] = useState({ category: '', sort: 'default' });

6.4 案例四:主题与多语言(Flutter + Provider)

需求:亮/暗主题、中英文切换,全局生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用 MultiProvider 组合
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ThemeModel()),
ChangeNotifierProvider(create: (_) => LocaleModel()),
],
child: MyApp(),
),
);

// 任意子组件
final theme = context.watch<ThemeModel>();
final locale = context.watch<LocaleModel>();

七、选型建议

场景 React 推荐 Flutter 推荐
小项目/原型 useState + Context setState + Provider
中大型项目 Redux Toolkit / Zustand Riverpod / Bloc
强类型、可测试 Redux + TypeScript / Zustand Riverpod
复杂业务流、事件驱动 Redux / XState Bloc
服务端状态 React Query / SWR Riverpod FutureProvider / dio + 自封装

八、总结

  • React:从 useState 起步,全局状态优先考虑 Redux ToolkitZustand,服务端状态用 React Query 等。
  • Flutter:从 setState 起步,共享状态用 Provider 入门,进阶用 RiverpodBloc
  • 选型时关注:团队熟悉度项目规模可测试性与框架生态的契合度

由浅入深掌握上述方案后,可以根据具体业务灵活组合,构建可维护、可扩展的状态管理体系。

实用小软件

  • Snipaste:截图

    • f1截图,f1+fn
    • f3定在屏幕上,f3+fn
    • 取色:shift切换
  • ZoomIt:画笔

    • ctrl+1放大
    • Ctrl+2画图
    • 画图时按住Ctrl是矩形
    • 画图时按住tab是椭圆
    • 画图时按字母切换画笔颜色。r红色、b蓝色、g绿色、p粉色、o橙色
  • XMind:思维导图

    • enter同级
    • tab下级
  • Typora:Markdown

    • 主题可下载

React 18:渲染入口主链路

React 18:渲染入口主链路(简化)

sequenceDiagram
  participant User as 用户(调用 API)
  participant DOM as ReactDOM
  participant Root as FiberRoot / HostRoot Fiber
  participant Queue as updateQueue / lanes
  participant Sched as Scheduler 调度入口

  User->>DOM: createRoot(container)
  DOM->>Root: createContainer(ConcurrentRoot)
  Root->>Root: createFiberRoot + initializeUpdateQueue
  DOM->>DOM: markContainerAsRoot + listenToAllSupportedEvents
  DOM-->>User: 返回 root(ReactDOMRoot)

  User->>DOM: root.render(element)
  DOM->>Root: updateContainer(element, root)
  DOM->>Queue: requestEventTime() + requestUpdateLane()
  DOM->>Queue: createUpdate(eventTime, lane) + update.payload={element}
  DOM->>Queue: enqueueUpdate(HostRootFiber, update, lane)
  DOM->>Sched: scheduleUpdateOnFiber(root, hostFiber, lane, eventTime)
  DOM->>Sched: ensureRootIsScheduled(root, eventTime)
  Sched-->>DOM: 进入并发/同步 render(Day1 Block2)

Life of frame

浏览器中一帧内的事件顺序示意(约 16.6ms,约 60fps)。

总览

一帧时间预算约 16.6ms。从左到右大致为:输入事件 → JavaScript → Begin Frame → requestAnimationFrame → Layout → Paint → 空闲

主流程图(一帧内,左 → 右)

下列阶段在同一段帧预算内顺序推进(与教学用示意图一致;不同浏览器实现细节会略有差异)。

flowchart LR
    A["① 输入事件\n阻塞 touch·wheel\n非阻塞 click·keypress"] --> B["② JavaScript\n定时器 + 任务"]
    B --> C["③ Begin Frame\nresize · scroll · media query"]
    C --> D["④ requestAnimationFrame"]
    D --> E["⑤ Layout"]
    E --> F["⑥ Paint"]
    F --> G["⑦ Idle\nidle 回调"]

Layout / Paint 子步骤(接续 ④ rAF 之后)

flowchart LR
    L1["Recalculate style"] --> L2["Update layout"]
    L2 --> P1["Compositing update"]
    P1 --> P2["Paint invalidation"]
    P2 --> P3["Record"]
    P3 --> IDLE["⑦ Idle · idle 回调"]

说明:若在支持 Mermaid 的编辑器中预览(如 VS Code + 插件、GitHub),上图会渲染为流程图;纯文本阅读时也可对照下文分节标题。


1. 输入事件

  • 阻塞型输入:如 touchwheel
  • 非阻塞型输入:如 clickkeypress

2. JavaScript 执行

  • 定时器setTimeoutsetInterval 等回调。
  • 通用 JavaScript:任务队列中的同步/异步脚本执行。

3. Begin Frame(开始帧)

每帧可能触发的典型事件:

  1. windowresize
  2. scroll
  3. media query 变化

4. requestAnimationFrame

  • 执行通过 requestAnimationFrame 注册的 Frame callbacks
  • 适合与下一屏绘制对齐的更新与测量。

5. Layout(布局)

  1. Recalculate style:计算样式,确定规则如何应用到元素。
  2. Update layout:更新布局,计算几何信息(位置与尺寸)。

6. Paint(绘制)

  1. Compositing update:合成层更新。
  2. Paint invalidation:标记需要重绘的区域。
  3. Record:记录绘制指令。

7. Idle(空闲)

若前述步骤在 16.6ms 内完成,剩余时间为空闲期,可执行 idle 回调(例如 requestIdleCallback),如 idle callback1idle callback2


小结

输入 →(定时器 + JS) Begin Frame 事件(resize / scroll / media)→ rAF → 样式与布局 → 合成 / 失效 / 记录绘制 → 空闲期执行 idle 回调。

React Hooks 与 Fiber:原理与应用

一、React Hooks 的底层实现

1.1 核心问题

函数组件每次渲染都会重新执行,本身不保留状态。Hooks 如何「记住」上一次的值?

1.2 本质:链表 + 调用顺序

React 为每个组件实例维护一个 Fiber 节点,Fiber 上有 memoizedState,是一条链表

1
2
3
4
useState(0)   → Node1 { value: 0 }
useState('') → Node2 { value: '' }
useEffect(fn) → Node3 { effect: fn }
useMemo(calc) → Node4 { value: result }

Hooks 按调用顺序依次读取/更新链表上的节点,不做「按名字查找」。

1.2.1 示例:状态如何挂在 Fiber 链表上

组件与 Fiber 一一对应:每个组件实例有一个 Fiber 节点,Fiber 的 memoizedState 指向 Hooks 链表。

Counter 组件为例:

1
2
3
4
5
6
function Counter() {
const [count, setCount] = useState(0); // Hook 1
const [name, setName] = useState(''); // Hook 2
const [flag, setFlag] = useState(false); // Hook 3
return <div>{count}</div>;
}

首次渲染时,React 在 Fiber 上建立如下链表结构:

1
2
3
4
5
6
7
8
9
Fiber (Counter 组件)
└── memoizedState (链表头)

▼ Node1: { memoizedState: 0 } ← useState(0)
│ next ──────────────────────────────────┐
▼ Node2: { memoizedState: '' } ← useState('')
│ next ──────────────────────────────────┐
▼ Node3: { memoizedState: false } ← useState(false)
next: null

简化版实现(核心逻辑):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const fiber = { type: Counter, memoizedState: null };
let currentHook = null;

function useState(initialValue) {
if (!currentHook) {
const hook = { memoizedState: initialValue, next: null };
// 挂到 fiber.memoizedState 链表...
currentHook = hook;
}
const hook = currentHook;
currentHook = hook.next;

const setState = (newValue) => {
hook.memoizedState = newValue; // 直接改链表节点
scheduleReRender();
};
return [hook.memoizedState, setState];
}

// 每次渲染前重置为链表头
function renderComponent(Component) {
currentHook = fiber.memoizedState;
return Component();
}

两次渲染的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
首次渲染:
useState(0) → 创建 Node1 { memoizedState: 0 } 返回 [0, setCount]
useState('') → 创建 Node2 { memoizedState: '' } 返回 ['', setName]
useState(false)→ 创建 Node3 { memoizedState: false } 返回 [false, setFlag]

用户点击,setCount(1):
Node1.memoizedState = 1
scheduleReRender()

第二次渲染:
currentHook = fiber.memoizedState (Node1)
useState(0) → 读 Node1,返回 [1, setCount] ✓
useState('') → 读 Node2,返回 ['', setName] ✓
useState(false)→ 读 Node3,返回 [false, setFlag] ✓
调用顺序 Hook 对应链表节点 存的值
1 useState(0) Node1 0 → 1
2 useState(‘’) Node2 ‘’
3 useState(false) Node3 false

结论:状态不在组件函数里,而在该组件对应 Fiber 的 memoizedState 链表上,每个 Hook 按顺序对应链表中的一个节点。

1.3 为什么顺序不能变?

  • 第 1 次调用 Hook → 用链表第 1 个节点
  • 第 2 次调用 Hook → 用链表第 2 个节点
  • 顺序变了,就错位了

因此 Rules of Hooks 要求:顶层调用、不在循环/条件中。

1.4 各 Hook 的简化实现

useState:

1
2
3
4
5
6
7
8
9
10
11
12
13
let hooks = [];
let currentHookIndex = 0;

function useState(initialValue) {
const index = currentHookIndex;
const state = hooks[index] !== undefined ? hooks[index] : initialValue;
const setState = (newValue) => {
hooks[index] = newValue;
scheduleReRender();
};
currentHookIndex++;
return [state, setState];
}

useEffect: 将 callback 和 deps 存入链表,在 commit 阶段执行;依赖变化时重新执行。

useCallback / useMemo: 缓存上一次的值/函数,依赖数组变化才重新计算/创建。

1.4.1 useEffect 的三种情况

写法 何时执行 常见用途
useEffect(fn) 每次渲染后 很少用
useEffect(fn, []) 仅首次挂载后 订阅、初始化、只跑一次
useEffect(fn, [a, b]) a 或 b 变化时 依赖变化时执行副作用

实现思路:React 在链表节点上存「上次的 callback」和「上次的 deps」,每次渲染时比较 deps(用 Object.is):

  • 无 deps → 每次都执行
  • 空数组 [] → 首次执行(prevDeps 为空)
  • 有 deps → 逐项比较,任一变则执行
1
2
3
4
5
// 简化实现
let shouldRun = false;
if (!deps) shouldRun = true; // 无 deps:每次都执行
else if (!prevDeps) shouldRun = true; // 首次挂载
else shouldRun = deps.some((d, i) => !Object.is(d, prevDeps[i]));

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 无 deps:每次渲染后执行
useEffect(() => { console.log('每次渲染都执行'); });

// 2. 空 deps:仅首次
useEffect(() => {
console.log('只执行一次');
return () => console.log('卸载时执行');
}, []);

// 3. 有 deps:依赖变化时执行
useEffect(() => {
console.log('count 或 name 变化时执行');
}, [count, name]);
情况 依赖比较 是否执行
无 deps 不比较 每次
[] 首次 prevDeps 为空 只执行一次
[a,b] 用 Object.is 逐项比较 a 或 b 变化时才执行

1.5 小结

概念 本质
状态存储 存在 Fiber 上的链表,不在函数内部
Hook 识别 调用顺序对应到链表节点
Rules of Hooks 保证顺序和数量恒定,才能一一对应

1.6 Hooks 的实现本质

Hooks 的实现本质可归纳为三点:

  1. 外部存储:状态不存于组件函数内部,而是存在 Fiber 的 memoizedState 链表上。组件每次渲染都会重新执行,但链表在组件实例的生命周期内持续存在。

  2. 顺序索引:每次调用 Hook 时,React 通过一个「当前索引」决定读写哪个链表节点。索引随调用递增,因此必须保证每次渲染的调用顺序、数量完全一致。

  3. 调度更新setState 等更新函数会修改链表上的值,并触发 React 的调度器安排一次重渲染。下次渲染时,组件函数重新执行,Hooks 按相同顺序从链表中读出最新值。

三者结合:外部存储解决「函数无状态」问题,顺序索引解决「多个 Hook 如何区分」问题,调度更新解决「变化如何驱动重渲染」问题。

1.7 闭包机制与 Hooks 的关系

Hooks 与闭包紧密相关:

1. setState 依赖闭包捕获索引

1
2
3
4
const setState = (newValue) => {
hooks[index] = newValue; // index 来自闭包
scheduleReRender();
};

setState 在首次渲染时创建,通过闭包捕获了当时的 index。即使用户在 3 秒后点击按钮调用 setState,它仍然能正确写入对应节点,因为闭包保留了 index

2. stale closure(陈旧闭包)问题

事件回调、useEffect 中的函数会闭包捕获当次渲染时的 state。若在回调中直接使用 state,可能拿到旧值:

1
2
3
4
5
6
7
8
9
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count 被闭包捕获,一直是 0
}, 1000);
return () => clearInterval(id);
}, []);
}

3. 正确做法:函数式更新

1
setCount(prev => prev + 1);  // 传入函数,React 注入最新 state

React 会传入最新的 state,避免闭包捕获 stale 值。

4. useCallback / useMemo 与闭包

useCallback(fn, deps) 缓存的是函数本身;若 deps 不变,返回的是同一个函数引用,其闭包捕获的也是旧依赖。useMemo 同理。因此 deps 必须完整列出闭包中用到的所有变量。

1.8 详细分析:一次更新的完整流程

useState 为例,从点击按钮到界面更新:

1
2
3
4
5
6
7
1. 用户点击 → 触发 onClick → 调用 setState(newValue)
2. setState 通过闭包中的 index 找到链表节点,写入新值
3. 调用 scheduleReRender(),React 将本次更新加入调度队列
4. 调度器在合适时机执行 render 阶段
5. 组件函数重新执行,useState(initialValue) 再次被调用
6. currentHookIndex 从 0 开始,按顺序遍历:hooks[index] 已有新值,返回 [newValue, setState]
7. 组件用新 state 生成新 JSX,进入 commit 阶段,DOM 更新

闭包保证了:异步回调中的 setState,在任意时刻被调用时,仍能通过 index 找到正确的链表节点。 链表保证了:多次渲染之间,状态得以保留。 两者缺一不可。


二、Fiber 的核心原理

2.1 一句话

Fiber 把「一次性递归完整个树」的同步更新,拆成「按节点逐步执行、可中断」的增量更新,让渲染不卡顿主线程。

2.2 为什么需要 Fiber?

旧版 (Stack Reconciler) Fiber 之后
从根递归整棵树,一气做完 拆成一个个小单元
无法暂停 可随时暂停,让出主线程
大树会长时间占用主线程 支持时间分片、优先级调度

2.3 Fiber 是什么?

Fiber = 对应一个 React 元素的「工作单元」,携带该节点的类型、props、子节点引用等。

1
2
3
4
5
6
7
8
9
Fiber {
type, key, props
child: 第一个子 Fiber
sibling: 下一个兄弟 Fiber
return: 父 Fiber(用于回溯)
alternate: 另一棵树的对应 Fiber(双缓冲)
flags: 增/删/改等标记
lane: 优先级
}

2.4 链表式遍历(而非递归)

树被转成链表结构,遍历时可随时停下、下次从断点继续:

1
2
3
4
5
6
7
        App
/ \
Header Main
/ \ |
Nav Logo Content

遍历顺序:App → Header → Nav → Logo → Main → Content ...

2.5 双缓冲 (alternate)

  • current:当前屏幕上的树
  • workInProgress:正在构建的新树

更新时在 workInProgress 上增量构建,commit 阶段一次性切换,避免闪烁。

2.6 总结

问题 回答
Fiber 是什么? 一个工作单元,带 child/sibling/return 的链表节点
为什么用? 实现可中断、可恢复、可调度
如何可中断? 链表遍历 + 时间分片
双缓冲? 两棵树交替,保证更新可复用且一次性提交

三、Fiber 作为一种设计思想

3.1 Fiber 可抽象为通用思想

思想 含义
可中断的增量工作 大任务拆成小单元,做一点可停
时间分片 固定时间片内工作,到点让出主线程
优先级调度 高优任务(如用户输入)插队
链表式遍历 用 child/sibling/return 实现可暂停、可恢复

这些思想可迁移到 UI 渲染、动画、大数据处理等场景。

3.2 Flutter 能否引入?

可以。Flutter 已具备类似机制:

Flutter 机制 对应思想
SchedulerBinding 分阶段调度(microtask、frame、idle)
优先级队列 高优任务优先执行
Isolate 重计算放到后台,减轻 UI 线程压力

若要在 layout 等环节做「增量可中断」,可借鉴 Fiber 的拆解与调度方式。

3.3 通用模式

1
2
3
4
5
1. 把大任务拆成小工作单元
2. 每个单元可单独执行、可暂停
3. 用链表/树记录进度,便于恢复
4. 调度器按优先级和时间片决定执行哪些
5. 高优任务可插队

四、整体逻辑关系

1
2
3
4
5
6
7
8
9
Hooks 底层
→ 状态存在 Fiber 的链表上,靠调用顺序对应
→ 闭包捕获索引,保证 setState 等更新函数能正确写入;需注意 stale closure

Fiber 核心
→ 把递归更新改为可中断的链表遍历 + 时间分片

Fiber 思想
→ 可中断、可调度、增量工作,具有普适性,可迁移到其他框架(如 Flutter)与场景

iOS 开发中的 React Native

由浅入深,从基本概念到源码解析,带你全面掌握 React Native 在 iOS 平台的开发与应用


一、什么是 React Native?为什么选择它?

1.1 从 Hybrid 到 React Native

移动开发经历了从纯原生(Native)到 Hybrid(WebView)再到跨平台框架的演进:

方案 代表 优势 劣势
原生 Swift/ObjC 性能最佳、体验最好 双端重复开发
Hybrid Cordova、WebView 一套 HTML/JS 性能差、体验割裂
跨平台 React Native、Flutter 一套代码、接近原生 学习曲线、生态依赖

React Native (RN) 由 Meta 于 2015 年开源,核心理念是:用 JavaScript 编写逻辑,用原生组件渲染 UI,而不是在 WebView 中渲染。

1
2
传统 Hybrid:    JS/HTML → WebView 渲染 → 间接调用原生 API
React Native: JS/React → 虚拟 DOM → 原生组件(UILabel、UIView 等)直接渲染

1.2 为什么 iOS 开发者要学 React Native?

  • 业务需要:公司采用 RN 做跨端,需要维护/扩展原生能力
  • 原生桥接:RN 依赖大量原生模块(相机、蓝牙、支付等),需要 iOS 侧配合开发
  • 性能优化:理解 RN 与原生通信机制,才能做性能调优和问题排查
  • 新架构:新架构大量使用 C++、JSI,与 iOS 底层结合更紧密

1.3 RN 与 Flutter 的简要对比

维度 React Native Flutter
语言 JavaScript/TypeScript Dart
渲染 原生组件 自绘引擎(Skia)
包体积 相对较小 相对较大
生态 依赖 React、npm 独立生态
与原生交互 通过 Bridge/JSI 通过 Platform Channel

二、核心概念与架构

2.1 三层架构概览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────────────────────────────────────┐
│ JavaScript 层 │
│ React 组件、业务逻辑、状态管理、事件处理 │
└──────────────────────────┬──────────────────────────────┘
│ Bridge / JSI
┌──────────────────────────▼──────────────────────────────┐
│ C++ 层(新架构) │
│ JSI、Fabric 渲染、TurboModules 调度 │
└──────────────────────────┬──────────────────────────────┘
│ FFI / Objective-C++
┌──────────────────────────▼──────────────────────────────┐
│ Native 层(iOS) │
│ UIKit、系统 API、自定义原生模块 │
└─────────────────────────────────────────────────────────┘

2.2 关键概念

概念 说明
Bridge 旧架构中 JS 与 Native 的异步通信桥梁,数据需序列化
JSI JavaScript Interface,新架构中 JS 可直接持有 C++ 对象引用,同步调用
Fabric 新架构的渲染系统,将布局、绘制逻辑下沉到 C++
TurboModules 新架构的原生模块系统,懒加载、类型安全
Hermes 字节码引擎,替代 JavaScriptCore,提升启动与运行性能

2.3 旧架构 vs 新架构

维度 旧架构 新架构
通信 Bridge 异步、JSON 序列化 JSI 同步、直接引用
渲染 各平台各自实现 Fabric 统一 C++ 渲染管线
原生模块 Native Modules 启动时全量加载 TurboModules 按需懒加载
类型 无强类型约定 通过 Codegen 生成类型

三、环境搭建与项目创建

3.1 环境要求

  • Node.js:建议 LTS 版本(18+)
  • Xcode:最新稳定版
  • CocoaPodsgem install cocoapods
  • Watchman(可选):brew install watchman,用于文件监听

3.2 创建新项目

1
2
3
4
5
6
7
8
9
10
11
# 使用 React Native CLI
npx @react-native-community/cli init MyApp

# 进入 iOS 目录
cd MyApp/ios

# 安装 CocoaPods 依赖
pod install

# 返回根目录,启动 Metro
cd .. && npx react-native start

另开终端运行 iOS:

1
2
3
npx react-native run-ios
# 或指定模拟器
npx react-native run-ios --simulator="iPhone 15"

3.3 项目结构(iOS 侧)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MyApp/
├── ios/
│ ├── MyApp/ # 原生 iOS 工程
│ │ ├── AppDelegate.mm # 入口,加载 RN 根视图
│ │ ├── Info.plist
│ │ └── ...
│ ├── Podfile # CocoaPods 配置
│ ├── Podfile.lock
│ └── MyApp.xcworkspace # 用此打开工程
├── android/
├── src/ # JS 源码
├── node_modules/
├── package.json
└── metro.config.js

3.4 AppDelegate 与 RN 加载

典型的 AppDelegate.mm 中加载 RN 的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];

RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"MyApp"
initialProperties:nil
launchOptions:launchOptions];

self.window.rootViewController = [[UIViewController alloc] init];
self.window.rootViewController.view = rootView;
[self.window makeKeyAndVisible];
return YES;
}

RCTRootView 负责加载 JS Bundle、创建 Bridge、挂载 React 组件树。


四、JS 与 Native 通信原理

4.1 旧架构:Bridge 模型

旧架构下,JS 与 Native 通过 异步 Bridge 通信:

1
2
3
4
5
6
7
JS 层发起调用

├─ 将参数序列化为 JSON

├─ 通过 Bridge 发送到 Native 队列

└─ Native 解析 JSON,执行对应模块方法,再序列化结果回传 JS

特点

  • 异步:所有跨端调用都是异步的
  • 序列化:参数和返回值需要 JSON 序列化,有性能开销
  • 全量加载:所有 Native Modules 在启动时注册

4.2 旧架构模块注册流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 实现 RCTBridgeModule 协议
@interface MyNativeModule : NSObject <RCTBridgeModule>
@end

@implementation MyNativeModule

RCT_EXPORT_MODULE(); // 导出模块名,默认类名

RCT_EXPORT_METHOD(getDeviceId:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
NSString *id = [[UIDevice currentDevice] identifierForVendor].UUIDString;
resolve(id);
}

@end

JS 端调用:

1
2
3
4
import { NativeModules } from 'react-native';
const { MyNativeModule } = NativeModules;

const id = await MyNativeModule.getDeviceId();

4.3 新架构:JSI 直接调用

JSI 允许 JavaScript 直接持有 C++ 对象的引用,无需经过 Bridge 序列化:

1
2
3
4
5
6
7
8
// C++ 侧:通过 JSI 暴露方法
jsi::Function getDeviceId = jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "getDeviceId"),
0,
[](jsi::Runtime& rt, const jsi::Value&, const jsi::Value*, size_t) {
return jsi::String::createFromUtf8(rt, getNativeDeviceId());
});

JS 可直接同步调用,无需 Promise 包装。


五、新架构:JSI、Fabric、TurboModules

5.1 JSI(JavaScript Interface)

JSI 是 C++ 实现的薄封装层,让 JS 引擎(Hermes/JSC)能够:

  • 调用 C++ 函数
  • 读取/写入 C++ 对象属性
  • 在 C++ 中执行 JS 回调
1
2
3
4
5
6
7
8
9
// 简化示意:HostObject 暴露给 JS 的对象
class DeviceModule : public jsi::HostObject {
jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name) override {
if (name.utf8(rt) == "getDeviceId") {
return jsi::Function::createFromHostFunction(...);
}
return jsi::Value::undefined();
}
};

5.2 Fabric 渲染管线

Fabric 将 React 的渲染逻辑从各平台分别实现,统一到 C++:

1
2
3
4
5
6
7
8
9
10
11
12
13
React 组件树


Shadow Tree(C++ 中的布局树)


布局计算(Yoga)


提交到原生层(Mount)


iOS UIView 创建/更新

优势:减少跨 Bridge 的序列化、支持同步布局、更好的并发与优先级调度。

5.3 TurboModules

TurboModules 的特性:

  • 懒加载:只在首次被 JS 引用时初始化
  • 类型安全:通过 Codegen 从 TypeScript 定义生成 C++ 和 ObjC 代码
  • 同步能力:通过 JSI 可实现同步调用

定义原生模块的规范(新架构):

1
2
3
4
5
6
7
8
9
10
// NativeMyModule.ts (Codegen 规范)
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
getDeviceId(): Promise<string>;
multiply(a: number, b: number): number;
}

export default TurboModuleRegistry.getEnforcing<Spec>('MyNativeModule');

六、原生模块开发(Native Modules)

6.1 旧架构:RCT_EXPORT_MODULE

完整示例:实现一个获取设备信息的原生模块。

Objective-C:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// DeviceInfoModule.h
#import <React/RCTBridgeModule.h>

@interface DeviceInfoModule : NSObject <RCTBridgeModule>
@end

// DeviceInfoModule.m
#import "DeviceInfoModule.h"
#import <React/RCTLog.h>
#import <UIKit/UIKit.h>

@implementation DeviceInfoModule

RCT_EXPORT_MODULE(DeviceInfo)

RCT_EXPORT_METHOD(getDeviceInfo:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_main_queue(), ^{
NSDictionary *info = @{
@"model": [[UIDevice currentDevice] model],
@"systemVersion": [[UIDevice currentDevice] systemVersion],
@"name": [[UIDevice currentDevice] name],
};
resolve(info);
});
}

@end

JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
import { NativeModules } from 'react-native';

const { DeviceInfo } = NativeModules;

async function loadDeviceInfo() {
try {
const info = await DeviceInfo.getDeviceInfo();
console.log(info);
} catch (e) {
console.error(e);
}
}

6.2 新架构:TurboModule + Swift

新架构推荐用 Swift 实现业务逻辑,用 ObjC++ 做 JSI 胶水层。

Swift 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// DeviceInfoModule.swift
import Foundation

@objc(DeviceInfoModule)
class DeviceInfoModule: NSObject {
@objc
static func requiresMainQueueSetup() -> Bool {
return false
}

@objc
func getDeviceInfo(_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
let info: [String: Any] = [
"model": UIDevice.current.model,
"systemVersion": UIDevice.current.systemVersion
]
resolve(info)
}
}

通过 RCT_EXTERN_MODULE 导出给 ObjC:

1
2
3
4
5
6
7
8
9
// DeviceInfoModule.m(桥接)
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(DeviceInfoModule, NSObject)

RCT_EXTERN_METHOD(getDeviceInfo:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

@end

6.3 事件发送:从 Native 到 JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在原生模块中
#import <React/RCTEventEmitter.h>

@interface MyModule : RCTEventEmitter <RCTBridgeModule>
@end

@implementation MyModule

RCT_EXPORT_MODULE()

- (NSArray<NSString *> *)supportedEvents {
return @[@"onScanResult"];
}

- (void)sendScanResult:(NSString *)result {
[self sendEventWithName:@"onScanResult" body:@{@"result": result}];
}

@end
1
2
3
4
5
6
import { NativeEventEmitter, NativeModules } from 'react-native';

const emitter = new NativeEventEmitter(NativeModules.MyModule);
emitter.addListener('onScanResult', (event) => {
console.log(event.result);
});

七、原生 UI 组件(Native UI Components)

7.1 使用 ViewManager 封装 UIView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// MyCustomViewManager.h
#import <React/RCTViewManager.h>

@interface MyCustomViewManager : RCTViewManager
@end

// MyCustomViewManager.m
#import "MyCustomViewManager.h"
#import "MyCustomView.h"

@implementation MyCustomViewManager

RCT_EXPORT_MODULE(MyCustomView)

- (UIView *)view {
return [[MyCustomView alloc] init];
}

RCT_EXPORT_VIEW_PROPERTY(title, NSString)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)

@end
1
2
3
4
5
// MyCustomView.h
@interface MyCustomView : UIView
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@end

7.2 JS 侧使用

1
2
3
4
5
6
7
8
9
10
11
12
import { requireNativeComponent } from 'react-native';

const MyCustomView = requireNativeComponent('MyCustomView');

export default function Screen() {
return (
<MyCustomView
title="Hello"
onPress={(e) => console.log(e.nativeEvent)}
/>
);
}

7.3 新架构:Fabric 组件

新架构下,通过 Codegen 定义 Props 和事件,生成 C++ 与各平台代码,实现类型安全和更好的性能。


八、源码解析

8.1 初始化流程(iOS)

1
2
3
4
5
6
7
8
9
10
11
12
main()
└─ UIApplicationMain
└─ AppDelegate didFinishLaunchingWithOptions
└─ RCTRootView initWithBundleURL:moduleName:...
├─ 创建 RCTBridge
│ ├─ 加载 JavaScript Bundle
│ ├─ 初始化 JS 引擎(Hermes/JSC)
│ ├─ 注册所有 Native Modules
│ └─ 执行 JS 入口(AppRegistry.runApplication)

└─ 创建 RCTRootContentView
└─ 挂载 React 根组件,触发首次渲染

8.2 Bridge 核心结构(旧架构)

1
2
3
4
5
6
7
8
9
10
11
12
// 简化示意
@interface RCTBridge : NSObject
@property (nonatomic, strong) RCTBridge *batchedBridge; // 实际执行 Bridge
@end

// 模块调用流程
// JS: NativeModules.DeviceInfo.getDeviceInfo()
// -> __callFunction(moduleName, methodName, args)
// -> 序列化为 JSON,通过 RCTBridge 发送
// -> Native: RCTModuleData 根据 moduleName 找到模块实例
// -> 反序列化参数,invoke 对应方法
// -> 结果序列化回传 JS

8.3 新架构关键路径

  • JSIReactCommon/jsi/,提供 jsi::RuntimeHostObject
  • FabricReactCommon/react/renderer/,Shadow Tree、Mount、Component 定义
  • TurboModulesReactCommon/react/nativemodule/,模块注册与调用

可参考官方仓库:
https://github.com/facebook/react-native


九、实战示例

9.1 调用系统分享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ShareModule.m
RCT_EXPORT_METHOD(share:(NSDictionary *)options
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *title = options[@"title"] ?: @"";
NSString *url = options[@"url"] ?: @"";

UIActivityViewController *activityVC = [[UIActivityViewController alloc]
initWithActivityItems:@[title, [NSURL URLWithString:url]]
applicationActivities:nil];

UIViewController *rootVC = [UIApplication sharedApplication]
.keyWindow.rootViewController;
[rootVC presentViewController:activityVC animated:YES completion:nil];
resolve(@YES);
});
}

9.2 封装原生 TabBar

在 RN 中嵌入 UITabBarController 的容器,通过 Native Module 控制 Tab 切换,实现与原生 TabBar 一致的外观和动效。

9.3 列表性能优化:FlashList

1
2
3
4
5
6
7
8
9
10
11
12
13
import { FlashList } from '@shopify/flash-list';

function ProductList({ data }) {
const renderItem = ({ item }) => <ProductCard item={item} />;

return (
<FlashList
data={data}
renderItem={renderItem}
estimatedItemSize={100}
/>
);
}

FlashList 使用按需渲染和复用,比 FlatList 更适合长列表场景。


十、实际项目中的应用案例

10.1 电商 App:商品详情混合栈

  • Native:顶部 Banner 轮播、视频播放、复杂动效
  • RN:评价列表、推荐列表、加购/下单逻辑

通过 RCTRootView 嵌入到 UIViewController 的指定区域,实现「上原生、下 RN」的混合页面。

10.2 金融 App:安全键盘

输入密码时使用 Native 自定义键盘(避免 RN 侧键盘被截屏/录屏),通过 Native Module 将输入结果回传 JS:

1
2
3
4
5
6
7
8
RCT_EXPORT_METHOD(showSecureKeyboard:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
SecureKeyboardViewController *vc = [[SecureKeyboardViewController alloc]
initWithCompletion:^(NSString *pin) {
resolve(pin);
}];
[self presentVC:vc];
}

10.3 地图与 LBS

地图、路径规划、定位等使用 Native SDK,通过 Native UI Component 和 Native Module 暴露给 RN,兼顾性能与功能完整性。

10.4 OTA 热更新

将打包好的 JS Bundle 下发到本地,启动时优先加载本地 Bundle,实现不发版即可更新业务逻辑(需注意各应用市场的合规要求)。


十一、常见问题与最佳实践

11.1 主线程与 UI 更新

原生模块中涉及 UI 的操作必须回到主线程:

1
2
3
dispatch_async(dispatch_get_main_queue(), ^{
[self presentViewController:vc animated:YES completion:nil];
});

11.2 避免内存泄漏

  • 使用 RCTEventEmitter 时正确实现 invalidate
  • Block 中使用 weakSelf 避免循环引用
  • 大对象及时释放,避免长期持有

11.3 调试技巧

1
2
3
4
5
6
7
8
# 查看 Metro 日志
npx react-native start

# 真机调试
npx react-native run-ios --device

# 查看原生日志
# Xcode -> Debug -> Open System Log,或使用 Console.app

11.4 性能建议

  • 长列表使用 FlashList 或优化 FlatListgetItemLayout
  • 复杂动画考虑 react-native-reanimated
  • 新项目尽量启用新架构(Fabric + TurboModules)
  • 图片使用 FastImage 或自定义 Native 图片组件做缓存

11.5 启用新架构

ios/Podfile 中:

1
ENV['RCT_NEW_ARCH_ENABLED'] = '1'

执行 pod install 后重新编译。


十二、总结

场景 建议
新项目 启用新架构(Fabric + TurboModules + Hermes)
原生能力扩展 通过 Native Module 暴露,优先用 Swift + ObjC 桥接
复杂 UI 封装 Native UI Component,或使用成熟第三方组件
性能敏感 长列表用 FlashList,动画用 Reanimated
调试 Metro + Flipper + Xcode 结合使用

React Native 在 iOS 上的核心价值是:用 React 生态统一业务逻辑,用原生能力保证体验与性能。理解 Bridge/JSI、Fabric、TurboModules 的演进,有助于在混合栈项目中做出更合适的架构与实现选择。