dioxus-core 深度解析:Rust 生态中最完整的高性能 VirtualDom 实现
dioxus-core 是 Dioxus 全栈应用框架的渲染内核,为 Web、桌面、移动端、SSR、LiveView、TUI 等各类渲染器提供一套统一的 VirtualDom(虚拟 DOM)实现。 本文以 packages/core/README.md 为主体骨架,结合 packages/core/src 下的真实源码,讲解 dioxus-core 的设计理念、核心抽象(VirtualDom、Component/Properties、事件、异步、Suspense)以及从零驱动一个虚拟 DOM 渲染循环的完整姿势。读完本文,你将理解 Dioxus 为什么能让"组件只是一个函数",并能独立实现(或深度定制)一个对接 dioxus-core 的自定义渲染器。
dioxus-core 在 Dioxus 体系中的位置
Dioxus 是一个面向 Web、Desktop、Mobile 的全栈应用框架(见 根目录 Cargo.toml 工作区结构与项目描述)。而 dioxus-core 是其中最底层、最核心的组件——它不关心状态管理怎么实现、UI 长什么样,只提供"一棵高效、灵活的树状数据结构"以及围绕它的一套可复用的渲染原语。
README 中给出了它的一句话定义:
dioxus-coreprovides a fast and featureful VirtualDom implementation for Rust.
这句话包含两个关键承诺:fast(快) 与 featureful(功能全)。README 进一步指出,Dioxus VirtualDom "或许是 Rust 中功能最完备的 virtualdom 实现",它支撑了跨 Web、Desktop、Mobile、SSR、TUI、LiveView 等多个平台运行的渲染器。当你使用 Dioxus VirtualDom 时,你的渲染器用户就能立刻使用 Dioxus 庞大的组件、hooks 与配套工具生态。
从包定义(packages/core/Cargo.toml)看,dioxus-core 的定位是轻量内核:
- 版本为 workspace 统一管理,
edition = "2024",rust-version = "1.85.0"; - 依赖经过精心裁剪,包括
generational-box(代际盒)、slab/slotmap(arena 存储)、rustc-hash(快速哈希)、longest-increasing-subsequence(最长递增子序列,用于 keyed diff 优化)、subsecond(毫秒级计时/热替换支持)等基础设施; - 提供可选的
serializefeature(开启后引入 serde 与dioxus-core-template/serialize),用于跨端序列化场景; - 内建两个基准程序
jsframework与keyed_diff(packages/core/benches),说明其性能取向从设计之初就对标 JS 框架基准与 keyed 列表 diff。
六大核心特性
README 将 dioxus-core 的特性归纳为六点,这既是它的能力清单,也是理解其架构的索引:
- UI 组件只是普通函数(UI components are just functions)——组件模型无需继承、无需 class,函数签名即接口;
- 状态由 hooks 提供(State is provided by hooks)——内核只保留渲染与调度能力,状态管理沉淀在
dioxus-hooks、dioxus-signals等上层 crate; - 与 async 深度集成(Deep integration with async)——任务队列、waker、Suspense 内建在调度器里;
- 对性能的强烈关注(Strong focus on performance)——模板静态化、arena 存储、keyed diff、增量 mutation 流;
- 内建热重载支持(Integrated hotreloading support)——通过
hotreload_utils、subsecond等实现组件/rsx 的热替换; - UI 元素与属性的可扩展系统(Extensible system for UI elements and their attributes)——通过
dioxus-core-types与#[component]/rsx宏体系把标签、属性做成可扩展的开放集。
这些能力的底层形态可以直接在 packages/core/src/lib.rs 的模块清单中看到:diff(diff 引擎)、suspense、scope_arena/arena(组件作用域存储)、tasks(任务调度)、effects、mounts(挂载树)与 view(VNode 视图类型)等。
理解实现:五个核心抽象
README 强调:dioxus-core 被刻意设计成一个轻量 crate,它暴露若干灵活的底层原语,但并不深陷于状态管理的具体细节;基于这些原语构建的高层抽象(hooks、signals)在 dioxus-hooks 与 dioxus-signals 中提供。
需要理解的最重要的抽象有五个:
1. VirtualDom——并发虚拟 DOM 主循环
VirtualDom 是 dioxus-core 的心脏。从源码看(packages/core/src/virtual_dom.rs),它的结构清晰而克制:
pub struct VirtualDom {
scopes: Slab<ScopeState>, // 以 arena 形式存储所有组件作用域
dirty_scopes: BTreeSet<ScopeOrder>, // 需要重渲染的作用域集合(按高度+ID 排序)
runtime: Rc<Runtime>, // 运行时中枢,持有任务、挂载、事件目标表
resolved_scopes: Vec<ScopeId>, // 本次渲染中已 resolve 的作用域
rx: UnboundedReceiver<SchedulerMsg>,// 与 runtime 之间的调度消息通道
}
其核心方法组成了渲染器可直接调用的公共 API:
VirtualDom::new(app)/VirtualDom::new_with_props(root, props):以根组件(及根 props)创建实例。注意创建后并不会自动渲染,README 与源码 doc 注释都明确提醒:"you must either run_with_deadline or use rebuild to progress it"(virtual_dom.rs#L241-L242)。VirtualDom::rebuild/rebuild_to_vec/rebuild_in_place:执行一次全量重建,把所有编辑指令写入MultiWriter/Mutations。此过程不轮询任务、不处理事件队列,组件内状态会被完全清除,但已注册的 template 会保留(virtual_dom.rs#L546-L568)。render_immediate/render_immediate_to_vec:先process_events()冲刷调度消息,再计算新旧两棵 UI 树的差异并把编辑写入 writer。这是一次"增量渲染"。wait_for_work():一个 cancel-safe 的异步方法,当内部没有待处理工作(dirty scope / 任务)时挂起,一旦调度器有任何工作就返回,适合放在tokio::select!里与外部事件源竞争。wait_for_suspense():等待所有 suspense 子树完成,用于 SSR/SSG 等必须等整棵树渲染完成的场景。
值得注意的实现细节:根作用域(ScopeId::ROOT,即 ID 为 0 的 base scope)始终存在,可通过 base_scope() 获取;根 props 用 RootProps 包装并且永不 memoize(见 properties.rs#L81-L108 中的注释 "Root properties never need to be memoized")。在 debug 构建下,VirtualDom::new 还会注册一个 subsecond handler(virtual_dom.rs#L329-L330),用于框架自身计时/调度相关诊断。
2. Component 与 Properties——组件即函数
在 dioxus-core 中,组件是一个接受 props、返回 Element 的普通函数。核心类型别名定义在 lib.rs#L112-L115:
/// An Element is a possibly-none VNode created by calling render on a scope.
pub type Element = std::result::Result<VNode, RenderError>;
/// A Component is a function that takes Properties and returns an Element.
pub type Component<P = ()> = fn(P) -> Element;
也就是说,Element 本质上是 Result<VNode, RenderError>:正常返回就渲染节点;返回 Err(RenderError) 则把错误传播到最近的错误边界(ErrorBoundary)。这正是 Dioxus 优雅错误处理(? 运算符直接在组件函数体内可用)的根基,仓库中 error_boundary 模块与 throw_error、ErrorBoundary 等导出(见 lib.rs#L118-L135)共同支撑这套机制。
props 由 Properties trait 约束(properties.rs#L48-L65)。它要求 props 满足 Clone + 'static,并通过 PartialEq(在 memoize 方法中体现)来判断"props 是否变化",从而决定组件是否可以跳过重渲染:
pub trait Properties: Clone + Sized + 'static {
type ComponentBuilder<RenderFn, Marker>;
fn component_builder<RenderFn, Marker>(render_fn: RenderFn) -> Self::ComponentBuilder<RenderFn, Marker>;
fn memoize(&mut self, other: &Self) -> bool; // 判断 props 是否相等以决定是否 memoize
fn into_vcomponent<M: 'static>(self, render_fn: impl ComponentFunction<Self, M>) -> VComponent;
}
开发者通常不需要手写该 trait,两种方式都可:
- 手写 props 结构体并派生:
#[derive(Props, PartialEq, Clone)]; - 更推荐用
#[component]宏直接把函数参数变成 props(见 properties.rs#L31-L38 的 doc 示例与diagnostic::on_unimplemented提示——如果忘记派生Props或添加#[component],编译器会给出相当友好的错误引导)。
同时 ComponentFunction trait 对满足 Fn(P) -> Element 形式的函数自动实现(properties.rs#L299-L326),其 fn_ptr() 通过 subsecond::HotFn 取得函数指针地址——这是"函数式组件 + 热重载"能够稳定工作的机制之一。
3. 事件处理
渲染器把平台事件(如鼠标点击)交给 VirtualDom 内部触发对应的监听器。旧式 API 是 VirtualDom::handle_event,当前源码已将其标记为 #[deprecated],并明确提示改用 runtime().handle_event(virtual_dom.rs#L790-L795)。
事件真正被分发的入口在 Runtime 上(runtime.rs#L446-L484):
Runtime::handle_event(name, event, element):默认把事件路由到根渲染目标(RenderTargetId::ROOT);Runtime::handle_event_for_target(target_id, name, event, element):显式指定渲染目标。由于ElementId是渲染器本地的编号,多目标渲染器(如多窗口、Web+Desktop 混合场景)必须使用后者。
分发过程可以概括为:先根据 ElementId 找到所在 mount 与静态模板锚点(event_target_path),再根据 Event::propagates() 决定走冒泡路径还是非冒泡路径(runtime.rs#L475-L483)。事件的最终效果由监听器自己负责把节点标记为 dirty——README 及源码注释都强调 "It is up to the listeners themselves to mark nodes as dirty"。
事件模型的基础设施(Event、EventHandler、Callback、ListenerCallback)由 packages/core/src/events.rs 定义并从 lib.rs#L118-L135 导出。仓库测试 packages/core/tests/event_propagation.rs 完整覆盖了事件在元素树与组件树中的传播行为。
4. 与异步的深度集成
异步是 dioxus-core 的内建能力而非外挂。调度器通过 SchedulerMsg 在 runtime 与 VirtualDom 之间传递三类消息:Immediate(id)(某 scope 立即可渲染)、TaskNotified(id)(某个任务被唤醒)、AllDirty(全部标脏)。VirtualDom::wait_for_work/process_events/poll_tasks 负责消费这些消息并在空闲时轮询任务队列(virtual_dom.rs#L446-L526)。
在组件作用域内,开发者可以直接使用一系列 spawn/suspend 工具(导出见 lib.rs#L118-L135):
spawn(global_context.rs#L137):把 future 变成与作用域生命周期绑定的任务,作用域销毁时任务自动取消,这是组件内"副作用清理"的关键;spawn_forever(global_context.rs#L199):让任务逃逸作用域生命周期,适合后台长任务;spawn_isomorphic(global_context.rs#L111):客户端/服务端一致语义的派发;use_hook/use_hook_with_cleanup、use_drop:把资源生命周期接入渲染周期。
任务本体的实现位于 tasks.rs#L153(spawn 方法)与 packages/core/src/tasks.rs。由于内核要求"UI 事件无锁、主线程不被异步阻塞",VirtualDom 的 waker 设计保证了在 select! 中空闲等待时不占 CPU。
5. Suspense
Suspense 是 dioxus-core 面向 SSR/SSG 等"必须等完整棵树渲染"场景的关键能力。组件内部可通过 suspend() 挂起一个 future,让所在子树进入"等待"状态;整棵 VirtualDom 则用 wait_for_suspense 驱动这些挂起子树完成:
pub async fn wait_for_suspense(&mut self) {
loop {
self.queue_events();
if !self.suspended_tasks_remaining() && !self.has_dirty_scopes() { break; }
self.wait_for_suspense_work().await;
self.render_suspense_immediate().await;
}
}
实现上有两个精妙之处:
render_suspense_immediate渲染不写 mutation(run_and_diff_scope(None, scope_id)),即纯为推进 future 而渲染,避免把中间态刷给真实 DOM;且只运行位于 suspense 边界之下、标记为 "runs during suspense" 的 scope(virtual_dom.rs#L729-L783);- 每次处理超过 32 个工作单元后主动
yield_now()让出调度器,防止饿死其他异步任务(virtual_dom.rs#L772-L777)。
补充抽象:Mutations 与 WriteMutations——VirtualDom 与真实 DOM 之间唯一的桥梁
README 的事件循环示例中,SomeRenderer::apply() 返回一个 Mutations。要真正理解这段代码,需要认识 WriteMutations trait(packages/core/src/mutations.rs):
Mutations are the only link between the RealDOM and the VirtualDOM.
diff 过程产生的不是整棵 DOM 树,而是一份栈式、可增量应用的编辑流。WriteMutations 定义了渲染器需要实现的最小指令集:
- 结构指令:
create_element(tag, ns)、create_text、append_children(m)、insert_before/after、replace_with、remove; - 属性与内容:
set_attribute(name, ns, value)、set_text、add_event_listener/remove_event_listener; - 栈导航:
push_id、child(index)、pop、clone、set_id(给节点登记渲染器本地的ElementId)。
这样一套指令流意味着:每次更新只需传输"差异",静态部分可以做成模板原型缓存(trait 中的 can_cache_template_roots 与 renderer-local template 缓存机制,mutations.rs#L20-L23),从而让 Web/Desktop 等渲染器获得接近手写 DOM 操作的高效补丁。作为骨架,VirtualDom::rebuild_to_vec 正是把指令收集到 Vec 形式的标准 Mutations 中,便于测试与纯逻辑调试。
快速上手:从零驱动一个 VirtualDom
README 的 Usage 小节给出了最简可运行路径。核心流程只有三步:声明根组件 → 创建 VirtualDom → 执行 rebuild 得到编辑流。
第一步:用 rsx 声明组件
Dioxus 通过 rsx 宏把类 JSX 的语法转换成 Rust 原生代码(rsx 由 dioxus crate 导出,宏展开后生成的正是 dioxus-core 的 VNode 结构)。先声明根组件:
use dioxus::prelude::*;
// 声明一个根组件:组件就是一个返回 Element 的普通函数
fn app() -> Element {
rsx! {
div { "hello world" }
}
}
第二步:创建 VirtualDom 并完成首次渲染
fn main() {
// 以 app 为根组件创建 VirtualDom
let mut dom = VirtualDom::new(app);
// 初始渲染会生成一串“编辑指令”(mutations),供真实 DOM 应用
let mutations = dom.rebuild_to_vec();
}
此处 rebuild_to_vec 就是 README 强调的"initial render … generate a stream of edits"。对于只想在测试中拿到初始渲染结果、或把 VirtualDom 当纯逻辑树使用的场景,这一步已经完全足够。若需要把结果应用给真实渲染器,则应调用 rebuild(&mut writer),其中 writer 是实现 MultiWriter/WriteMutations 的对象(多渲染目标宿主传 BTreeMap<RenderTargetId, W>,单目标宿主直接传单个 writer,virtual_dom.rs#L546-L568)。
实战:构建渲染器的标准事件循环
README 开头展示了 dioxus-core 面向渲染器作者的核心用法:一个把"平台事件"与"VirtualDom 内部工作"统一驱动的 tokio::select! 循环。它是所有渲染器(Web/Desktop/移动端/LiveView……)共用的骨架范式:
use dioxus_core::{VirtualDom, Event, Element, Mutations, VNode, ElementId};
let mut vdom = VirtualDom::new(app);
let real_dom = SomeRenderer::new();
loop {
tokio::select! {
// 分支一:平台产生了一个事件(如用户点击)
evt = real_dom.event() => {
let evt = Event::new(evt, true); // 第二个参数控制事件是否冒泡
vdom.runtime().handle_event("onclick", evt, ElementId::from_raw(0))
}
// 分支二:VirtualDom 内部有任务/脏作用域需要推进
_ = vdom.wait_for_work() => {}
}
// 把差异以编辑流形式写回真实 DOM
vdom.render_immediate(&mut real_dom.apply())
}
(示例来源:README 首屏代码块,其中 SomeRenderer 等为示意类型。更完整的对照实现可看 virtual_dom.rs#L139-L185 中带真实类型签名的 "Building an event loop around Dioxus" 文档示例。)
把这段骨架映射到上面介绍过的抽象,可以得到完整的闭环:
| 循环要素 | 对应的 dioxus-core 抽象 | 源码位置 |
|---|---|---|
| 平台事件进入 | Event::new(data, bubbles) + runtime().handle_event / handle_event_for_target |
events.rs、runtime.rs#L446-L484 |
| 等待内部工作 | vdom.wait_for_work()(cancel-safe,适合 select!) |
virtual_dom.rs#L446-L460 |
| 增量 diff | vdom.render_immediate(&mut writer) |
virtual_dom.rs#L579-L590 |
| 编辑流应用 | 实现 WriteMutations(写 mutation)或使用现成 Mutations |
mutations.rs#L12-L89 |
| 需要等待整棵树完成(SSR/SSG) | vdom.wait_for_suspense().await 之后再渲染 |
virtual_dom.rs#L670-L682 |
三种渲染时机的取舍
- 首次/全量:
rebuild系列——不做事件与任务推进,一次跑完根组件并产出全部编辑,且组件内状态会被清空(见 virtual_dom.rs#L556-L560 的 doc 注释)。适用于冷启动与 SSR 输出完整 HTML。 - 增量:
render_immediate——先process_events()消费调度消息,再 diff。事件循环里每次select!返回后调用它,保证界面收敛。 - 等 suspense:
render_immediate不等待 suspense,因此需要完整渲染(如 SSR 需输出异步数据)时用wait_for_suspense先把挂起子树推进完(virtual_dom.rs#L663-L682)。
如何验证与深入学习
dioxus-core 的测试与基准是学习其行为的第二手资料(均位于 packages/core 下):
- diff 行为测试:diff_keyed_list.rs、diff_unkeyed_list.rs、diff_element.rs 等验证列表复用、属性清理、节点移动等核心 diff 语义;
- 生命周期与错误:lifecycle.rs、error_boundary.rs、use_drop.rs 覆盖组件挂载/卸载与错误传播;
- 事件与热重载:event_propagation.rs、hotreloading.rs;
- 内存与调度:memory_leak.rs、runtime_effects.rs、miri_stress.rs(miri 严格模式验证无 UB)与 cycle.rs;
- 性能基准:
cargo bench对应 jsframework.rs 与 keyed_diff.rs。
在 Dioxus 应用中实际使用
对绝大多数应用开发者而言,并不需要直接触碰 dioxus-core——日常使用的 dioxus::prelude::* 已经为你封装好 rsx!、组件宏与 VirtualDom 启动。但要理解上层 API 的行为边界,内核知识至关重要:
- 想要底层 diff 直觉,可阅读仓库中基于同一套
rsx!/Element 模型的示例,例如 counter.rs、signals.rs; - 需要等待异步再输出完整内容(SSR 语义)时,对应 suspense.rs 示例;
- 若你想为 Dioxus 接入全新平台(如自定义原生渲染、游戏引擎 UI 等),那么 dioxus-core 的这一整套原语就是你的接入层——只需要实现事件采集与
WriteMutations,即可复用整个 Dioxus 组件生态。
无论哪种用途,理解"组件即函数、状态靠 hooks、diff 产出指令流、调度器驱动异步"这四句话,就等于掌握了 dioxus-core 的骨架,剩下的细节都可以在 packages/core/src 与 packages/core/tests 中按图索骥。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00