Provider vs Contract:Balance 查询对比

一句话结论

  • Provider 的 getBalance 查的是地址的原生币余额。
  • Contract(如 ERC20)里的 balanceOf 查的是地址在该合约中的 Token 余额。

1) Provider:getBalance(address)

含义

查询某个地址当前持有的原生代币数量。

典型场景

  • 查询 EOA 钱包地址的 ETH 余额
  • 查询合约地址持有的 ETH(比如某个合约金库里的 ETH)

关键点

  • 不关心该地址是否是 EOA/Contract,统一按“地址的原生币”查询
  • 不能直接查 ERC20(USDT/WETH/DAI)余额

示例

1
2
const eth1 = await provider.getBalance("0xEOA...");
const eth2 = await provider.getBalance("0xContract...");

2) Contract:balanceOf(address)(以 ERC20 为例)

含义

查询某地址在某个 Token 合约账本中的余额。

典型场景

  • 查询用户的 USDT/WETH/DAI 余额
  • 查询某地址在指定 ERC20 里的持仓

关键点

  • 必须先确定“哪一个 token 合约”
  • 同一个地址在不同 token 合约中的余额互不影响

示例

1
2
3
4
5
const usdt = new ethers.Contract(USDT_ADDR, erc20Abi, provider);
const weth = new ethers.Contract(WETH_ADDR, erc20Abi, provider);

const usdtBalance = await usdt.balanceOf("0xUser...");
const wethBalance = await weth.balanceOf("0xUser...");

3) 对比表

对比项 Provider getBalance Contract balanceOf
查询对象 地址的原生币余额 地址在某合约中的 token 余额
资产类型 ETH/BNB/MATIC 等原生币 ERC20/ERC721/ERC1155 等合约资产(方法不同)
是否需要合约 ABI 否 是
是否需要合约地址 否(只要目标地址) 是(必须指定 token 合约)
输入地址角色 EOA 或 Contract 都可 EOA 或 Contract 都可
常见误区 以为可直接查 ERC20 忘记切换到正确 token 合约

4) 常见误区澄清

  • 误区:给 provider.getBalance 传 token 合约地址,就能得到该 token 总余额。
    实际:得到的是“这个合约地址持有的原生币余额”,不是 token 余额。
  • 误区:provider 不能查合约地址余额。
    实际:可以查,但查的是该合约地址的原生币余额。

5) 记忆口诀

  • 原生币 -> provider.getBalance
  • 合约资产 -> contract.xxx(...)(ERC20 用 balanceOf)

Flutter 到区块链:分层架构与交互全流程

本文从移动端工程视角,说明 Flutter + Trust Wallet Core(多为 Dart FFI)+ 节点 RPC + 区块链 在用户完成一笔链上交易时的典型分工与数据流。适用于自托管钱包、DEX 集成等场景(具体产品可能叠加网关、风控、生物识别等)。

1. 先厘清三件事

  1. 区块链节点不会直接和你的手机 App「长连接握手」完成业务;App 通常通过 HTTPS 上的 JSON-RPC(或 REST) 与远程节点通信。
  2. Trust Wallet Core 一类库,主体多为 C++,负责 地址派生、交易构造、数字签名、按链协议序列化;不负责替你选公共节点或发起 HTTP(由宿主 App 实现)。
  3. Flutter 负责 UI 与业务状态。与 TWC 的主交互在 Gate 类集成里常见为 Dart dart:ffi 加载 libTrustWalletCore.so(iOS 为进程内 WalletCore);MethodChannel 更常用于 生物识别、Keychain/Keystore 解锁、Host API 等,与 FFI 可并存,但不替代「Dart 直接调 TWC」。

2. 分层总览:谁做什么

层级 典型技术 职责
表现层 Flutter Widget 表单、确认弹窗、加载态、结果页
业务 / 状态 Bloc / Riverpod / Provider 等 参数校验、组装交易模型、编排读链 / 签名 / 广播
TWC 绑定 dart:ffi(DynamicLibrary)+ 社区/自研 Dart API Dart 进程内调用 C API:HDWallet、AnySigner 等
可选原生协作 MethodChannel / Pigeon 生物识别、安全存储解锁、壳层 Host;不是 TWC 的必经主路径
密码学 / 链协议 Trust Wallet Core(C++ .so / xcframework) 未签名交易 → 签名 → 原始交易字节 / hex
网络 Dio / web3dart Provider / OkHttp 等 调 RPC:读 nonce、余额、estimateGas;写 eth_sendRawTransaction 等
链 公链节点集群 验证交易、广播、mempool、出块

核心结论:「上链」在工程上 = 库内完成签名 + 网络层把已签名交易发给节点;在 Flutter 内置钱包场景下,签名一步多为 FFI → TWC。


3. 架构图:从界面到链(FFI 主路径)

flowchart TB
  subgraph UI["表现层"]
    F["Flutter Widget\n按钮、金额、链选择、确认弹窗"]
  end

  subgraph VM["业务层"]
    B["Flutter 业务逻辑\n校验、状态机、组装交易参数"]
  end

  subgraph Opt["可选:原生协作"]
    CH["MethodChannel / Pigeon\n生物识别、解锁密钥材料"]
    N1["Kotlin / Swift\nKeychain / Keystore"]
  end

  subgraph Crypto["密码学(同进程)"]
    FFI["dart:ffi\nDynamicLibrary.open"]
    N2["Trust Wallet Core\n(C++ 动态库)\n交易体 + 签名"]
  end

  subgraph Net["网络层"]
    H["HTTP / web3dart"]
    RPC["节点 RPC\nJSON-RPC / REST"]
  end

  subgraph Chain["区块链"]
    BC["共识与出块"]
  end

  F --> B
  B -.->|若产品要求| CH --> N1
  B --> FFI --> N2
  N2 -->|"signed tx"| B
  B --> H --> RPC --> BC

4. 主流程时序图:用户点击「确认」之后

以下为 Dart FFI 调 TWC 签名,RPC 由 Flutter / web3dart 发起 的常见形态(与 gate_global.apk 中 flutter_trust_wallet_core / ffi_impl 证据一致)。

sequenceDiagram
  autonumber
  participant U as 用户
  participant W as Flutter UI
  participant B as Flutter 业务层
  participant N as 原生宿主(可选)
  participant TWC as Trust Wallet Core(FFI)
  participant RPC as 节点 RPC
  participant BC as 区块链

  U->>W: 点击确认交易
  W->>B: 提交 to / amount / chainId 等
  B->>B: 校验与组装参数

  opt 生物识别 / 解锁安全存储
    B->>N: MethodChannel(一次性)
    N-->>B: 通过 / 取消
  end

  B->>RPC: 读链:nonce、fee、余额等
  RPC-->>B: 返回值

  B->>TWC: FFI:构造未签名交易并签名
  TWC-->>B: 已签名交易 hex / bytes

  B->>RPC: 写链:eth_sendRawTransaction 等
  RPC->>BC: 广播
  BC-->>RPC: 返回 tx hash
  RPC-->>B: tx hash

  B-->>W: 更新 UI
  W-->>U: 展示哈希 / 区块浏览器

步骤解读

  1. 用户操作:仅在 UI 层。
  2. 业务层:产品规则校验(限额、格式),编排后续步骤。
  3. 可选 MethodChannel:仅当策略要求签名前走原生认证 / 解锁;不是「每笔签名都经 Channel 调 TWC」。
  4. 读链 RPC:nonce、gas 等与签名无关的只读,走 HTTP / JSON-RPC。
  5. TWC(FFI):输入链类型、账户、交易内容,输出 签名后的原始交易。
  6. 写链 RPC:将已签名交易发给节点。
  7. 链:打包与确认。

5. 变体:若团队把签名完全下沉到原生

少数架构会让 原生 Kotlin/Swift 再调 TWC(JNI / Swift 绑定),Dart 只经 MethodChannel 拿 signedHex。这与 Dart FFI 直调 .so 是两条不同集成路径;Gate 类 Flutter 包证据指向后者为主。

sequenceDiagram
  participant B as Flutter
  participant CH as MethodChannel
  participant N as 原生
  participant TWC as Trust Wallet Core
  participant RPC as 节点

  Note over B,TWC: 变体:签名在原生侧完成(非 Gate FFI 主路径)
  B->>CH: signOnly(unsignedTxParams)
  CH->>N: 
  N->>TWC: 签名(JNI / 绑定)
  TWC-->>N: signed hex
  N-->>CH: 
  CH-->>B: signed hex

  B->>RPC: eth_sendRawTransaction(signedHex)
  RPC-->>B: tx hash

6. 数据流简图:字节往哪走

flowchart LR
  subgraph in["输入"]
    A1["用户输入\n地址、金额"]
    A2["链与合约参数\nchainId、ABI、nonce"]
  end

  subgraph twc["Trust Wallet Core(经 FFI)"]
    S["编码 + 签名\n→ raw transaction"]
  end

  subgraph out["对外"]
    H["0x… 已签名交易"]
    TX["交易哈希"]
  end

  A1 --> S
  A2 --> S
  S --> H
  H -->|"HTTPS JSON-RPC"| RPC["节点"]
  RPC --> TX

7. Trust Wallet Core 与「和链交互」的关系

  • 开发语言:核心为 C++;Android 为 libTrustWalletCore.so,iOS 为 xcframework。
  • Flutter 绑定:常见为 Dart FFI(DynamicLibrary.open / DynamicLibrary.process)+ Dart API;插件注册(如 TrustWalletCorePlugin)多用于打包与初始化,日常 HDWallet / 签名不走 MethodChannel 转发 TWC。
  • 和链的交互方式:
    • 库内部:密码学、交易结构、签名算法、各链差异。
    • 库外部:App 用 RPC 把 signed transaction 和只读请求发到节点;协议多为 JSON-RPC。

因此:「与区块链交互」在实现上 = RPC 上的读写;TWC 负责产生可被链接受的那串字节。


8. 安全与工程要点

  • 私钥与助记词:优先 Keychain / Keystore / 硬件模块;即便签名走 FFI,仍应控制明文在 Dart 中的暴露面,并按产品策略叠加生物识别。
  • Channel 传参:若用 MethodChannel,传 交易参数、链 ID、认证结果,不传私钥明文。
  • RPC 端点:生产环境常配合 证书钉扎、代理、自有网关。
  • 重放与链切换:正确设置 chainId(EIP-155)。

9. 与「交易所托管」模式的对比(扩展)

若业务是 CEX 托管钱包,时序里往往在 用户确认 之后增加 服务端:风控、限额、内部记账,再由服务端或专用签名机与链交互。此时与纯 自托管 + TWC(FFI) 的图不同。


10. 小结

问题 答案
Flutter 怎么「上链」? FFI(或少数架构下的原生)完成签名 + HTTP JSON-RPC 广播 / 查回执。
TWC 与 Dart 怎么交互? Gate 类证据:Dart FFI → libTrustWalletCore.so;MethodChannel 多为生物识别 / Host。
TWC 做什么? 交易构造与签名(C++ 库)。
链从哪里来? 远程节点;广播即节点接受 sendRawTransaction。

文档为通用架构说明;Gate App 逆向结论仅作绑定方式参考,具体产品以实际源码与策略为准。

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.js、web3.js、viem 均为底层以太坊交互库,功能重叠但设计理念和性能不同
    • 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.js 到 ethers.js:追求更现代、轻量的 API
    • 从 ethers.js 到 viem:进一步优化性能和体积
    • 从直接使用底层库到 wagmi:提升前端开发效率
  • 选择建议:
    • 现代前端项目:推荐使用 viem + wagmi 组合
      • 优势:极致轻量、性能优异、开发效率高
    • 需要兼容性:选择 ethers.js + wagmi
      • 优势:生态成熟、文档丰富、社区支持广
    • 传统项目:继续使用 web3.js
      • 优势:官方维护、功能全面、历史兼容性好
    • 底层定制需求:直接使用 viem 或 ethers.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) 常见类型

  • 值类型:bool、int/uint、address、bytes1~bytes32、enum
  • 引用类型:array、struct、mapping

五、引用类型与数据位置

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[]
  • 常用成员:length、push、pop

注意:在链上循环数组,长度不可控时很容易把 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 + 显式检查返回值,配合重入保护一起做。


八、特殊函数:constructor、receive、fallback

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 对应链表中的一个节点,通过 Fiber 的 memoizedState 串联。

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)在合适的时机批量处理,触发 render 和 commit。

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 等重建
}
}

Provider 的 InheritedProvider 会 addListener 到 ChangeNotifier,当 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 Toolkit 或 Zustand,服务端状态用 React Query 等。
  • Flutter:从 setState 起步,共享状态用 Provider 入门,进阶用 Riverpod 或 Bloc。
  • 选型时关注:团队熟悉度、项目规模、可测试性、与框架生态的契合度。

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

实用小软件

  • 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:渲染入口主链路(简化)

用户(调用 API) ReactDOM FiberRoot / HostRoot Fiber updateQueue / lanes Scheduler 调度入口 createRoot(container) createContainer(ConcurrentRoot) createFiberRoot + initializeUpdateQueue markContainerAsRoot + listenToAllSupportedEvents 返回 root(ReactDOMRoot) root.render(element) updateContainer(element, root) requestEventTime() + requestUpdateLane() createUpdate(eventTime, lane) + update.payload={element} enqueueUpdate(HostRootFiber, update, lane) scheduleUpdateOnFiber(root, hostFiber, lane, eventTime) ensureRootIsScheduled(root, eventTime) 进入并发/同步 render(Day1 Block2)

Life of frame

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

总览

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

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

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

① 输入事件 阻塞 touch·wheel 非阻塞 click·key ② JavaScript 定时器 + 任务 ③ Begin Frame resize · scroll media query ④ rAF requestAnimationFrame ⑤ Layout ⑥ Paint ⑦ Idle idle 回调

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

Recalculate style Update layout Compositing update Paint invalidation Record ⑦ Idle · idle 回调

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


1. 输入事件

  • 阻塞型输入:如 touch、wheel。
  • 非阻塞型输入:如 click、keypress。

2. JavaScript 执行

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

3. Begin Frame(开始帧)

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

  1. window 的 resize
  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 callback1、idle callback2。


小结

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