首页
/ dioxus-core 深度解析:Rust 生态中最完整的高性能 VirtualDom 实现

dioxus-core 深度解析:Rust 生态中最完整的高性能 VirtualDom 实现

2026-09-08 17:11:49作者:何将鹤

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-core provides 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(毫秒级计时/热替换支持)等基础设施;
  • 提供可选的 serialize feature(开启后引入 serde 与 dioxus-core-template/serialize),用于跨端序列化场景;
  • 内建两个基准程序 jsframeworkkeyed_diffpackages/core/benches),说明其性能取向从设计之初就对标 JS 框架基准与 keyed 列表 diff。

六大核心特性

README 将 dioxus-core 的特性归纳为六点,这既是它的能力清单,也是理解其架构的索引:

  1. UI 组件只是普通函数(UI components are just functions)——组件模型无需继承、无需 class,函数签名即接口;
  2. 状态由 hooks 提供(State is provided by hooks)——内核只保留渲染与调度能力,状态管理沉淀在 dioxus-hooksdioxus-signals 等上层 crate;
  3. 与 async 深度集成(Deep integration with async)——任务队列、waker、Suspense 内建在调度器里;
  4. 对性能的强烈关注(Strong focus on performance)——模板静态化、arena 存储、keyed diff、增量 mutation 流;
  5. 内建热重载支持(Integrated hotreloading support)——通过 hotreload_utilssubsecond 等实现组件/rsx 的热替换;
  6. UI 元素与属性的可扩展系统(Extensible system for UI elements and their attributes)——通过 dioxus-core-types#[component]/rsx 宏体系把标签、属性做成可扩展的开放集。

这些能力的底层形态可以直接在 packages/core/src/lib.rs 的模块清单中看到:diff(diff 引擎)、suspensescope_arena/arena(组件作用域存储)、tasks(任务调度)、effectsmounts(挂载树)与 view(VNode 视图类型)等。

理解实现:五个核心抽象

README 强调:dioxus-core 被刻意设计成一个轻量 crate,它暴露若干灵活的底层原语,但并不深陷于状态管理的具体细节;基于这些原语构建的高层抽象(hooks、signals)在 dioxus-hooksdioxus-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_errorErrorBoundary 等导出(见 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_eventvirtual_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"。

事件模型的基础设施(EventEventHandlerCallbackListenerCallback)由 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):

  • spawnglobal_context.rs#L137):把 future 变成与作用域生命周期绑定的任务,作用域销毁时任务自动取消,这是组件内"副作用清理"的关键;
  • spawn_foreverglobal_context.rs#L199):让任务逃逸作用域生命周期,适合后台长任务;
  • spawn_isomorphicglobal_context.rs#L111):客户端/服务端一致语义的派发;
  • use_hook/use_hook_with_cleanupuse_drop:把资源生命周期接入渲染周期。

任务本体的实现位于 tasks.rs#L153spawn 方法)与 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;
    }
}

virtual_dom.rs#L670-L682

实现上有两个精妙之处:

  • 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_textappend_children(m)insert_before/afterreplace_withremove
  • 属性与内容:set_attribute(name, ns, value)set_textadd_event_listener/remove_event_listener
  • 栈导航:push_idchild(index)popcloneset_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 原生代码(rsxdioxus 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.rsruntime.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! 返回后调用它,保证界面收敛。
  • 等 suspenserender_immediate 不等待 suspense,因此需要完整渲染(如 SSR 需输出异步数据)时用 wait_for_suspense 先把挂起子树推进完(virtual_dom.rs#L663-L682)。

如何验证与深入学习

dioxus-core 的测试与基准是学习其行为的第二手资料(均位于 packages/core 下):

在 Dioxus 应用中实际使用

对绝大多数应用开发者而言,并不需要直接触碰 dioxus-core——日常使用的 dioxus::prelude::* 已经为你封装好 rsx!、组件宏与 VirtualDom 启动。但要理解上层 API 的行为边界,内核知识至关重要:

  • 想要底层 diff 直觉,可阅读仓库中基于同一套 rsx!/Element 模型的示例,例如 counter.rssignals.rs
  • 需要等待异步再输出完整内容(SSR 语义)时,对应 suspense.rs 示例;
  • 若你想为 Dioxus 接入全新平台(如自定义原生渲染、游戏引擎 UI 等),那么 dioxus-core 的这一整套原语就是你的接入层——只需要实现事件采集与 WriteMutations,即可复用整个 Dioxus 组件生态。

无论哪种用途,理解"组件即函数、状态靠 hooks、diff 产出指令流、调度器驱动异步"这四句话,就等于掌握了 dioxus-core 的骨架,剩下的细节都可以在 packages/core/srcpackages/core/tests 中按图索骥。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395