首页
/ 深入 Dioxus 核心:VirtualDOM 架构、渲染管线与响应式运行时全解析

深入 Dioxus 核心:VirtualDOM 架构、渲染管线与响应式运行时全解析

2026-09-08 16:33:54作者:史锋燃Gardner

本文基于仓库 notes/architecture/01-CORE.md 架构笔记,结合 dioxus-core crate 的真实源码(位于 packages/core/src)整理而成。文中所有结构体、方法与字段均有源码或文档依据,可作为理解 Dioxus 渲染引擎与进行二次开发的参考资料。

写在前面:本篇文章讲什么

Dioxus 是一个面向 Web、桌面与移动端的全栈应用框架,而 dioxus-core 是整个框架的心脏。它承担了虚拟 DOM 的构建与差分(diffing)、组件生命周期管理、渲染管线的驱动以及响应式运行时的调度。本篇将沿着 01-CORE.md 的骨架,逐步拆解 VirtualDom 的内部结构与关键方法、初始/更新两套渲染流程、组件与 Props 的类型擦除机制、Scope 作用域系统、事件冒泡、Diffing 算法、Mutations 通信协议、调度器优先级、异步任务、Effects、Suspense、错误边界等核心机制,并给出对应的源码入口,帮助读者建立起对整个响应式渲染引擎的完整心智模型。

1. VirtualDom:组件树的唯一总控

VirtualDom 是架构文档定义的核心枢纽,它"orchestrates the entire reactive component tree"(编排整个响应式组件树)。在源码 virtual_dom.rs 中,该结构体真实字段如下:

pub struct VirtualDom {
    scopes: Slab<ScopeState>,                 // 所有组件作用域的对象池分配器
    dirty_scopes: BTreeSet<ScopeOrder>,       // 被标记需要重渲染的作用域,按 (height, id) 排序
    runtime: Rc<Runtime>,                     // 共享的运行时
    resolved_scopes: Vec<ScopeId>,            // 在 suspense 阶段已解析的作用域
    rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>, // 来自调度器的消息通道
}

对应到文档,各字段职责如下:

  • scopes: Slab<ScopeState>:使用 Slab 这种"稀疏向量"做 arena 分配器,所有组件作用域以 ScopeId 为索引存放。它的特性是插入/删除 O(1) 且内存紧凑,代价是 ID 在时间上可能被复用(详见后文 ScopeId)。
  • dirty_scopes: BTreeSet<ScopeOrder>:被标记需要重渲染的作用域集合。使用 B 树集合并以 ScopeOrder 作为键,天然保证按高度(距根距离)有序,从而确保父作用域先于子作用域被处理。
  • runtime: Rc<Runtime>Runtime 承载全部异步任务、事件、Effects 与元素挂载表,VirtualDom 通过共享指针持有它。
  • rx:调度器到 VirtualDom 的单向消息通道(unbounded),例如任务被唤醒、作用域被标脏都会通过它送达。

1.1 关键方法一览

文档列出了 VirtualDom 的核心 API,源码 virtual_dom.rs 均能逐一对应:

方法 源码位置 行为说明
VirtualDom::new(app) virtual_dom.rs#L242 用无 Props 的根组件创建实例;内部转调 new_with_props(app, ())
new_with_props<P,M>(root, props) virtual_dom.rs#L287 为根组件携带初始 Props 创建;根组件被包装为名为 "root"VComponent
prebuilt(app) virtual_dom.rs#L302 创建后立即 rebuild_in_place(),一步到位
rebuild() / rebuild_in_place() virtual_dom.rs#L562 / virtual_dom.rs#L535 全量重建组件树,将所有编辑写入 MultiWriter_in_place 变体丢弃 mutations(常用于测试)
render_immediate(to) virtual_dom.rs#L579 不等待 suspense、尽快渲染所有脏作用域,差分结果写入 writer
wait_for_work() virtual_dom.rs#L446 异步等待任务/future/调度消息就绪,cancel-safe,可在 select! 中使用
wait_for_suspense() virtual_dom.rs#L670 阻塞直至所有挂起的 future 完成(SSR/SSG 场景使用)
mark_dirty(id) / mark_all_dirty() virtual_dom.rs#L397 / virtual_dom.rs#L382 把单个 / 全部作用域加入重渲染队列
with_root_context<T>(ctx) virtual_dom.rs#L364 向根作用域注入全局上下文(依赖注入)
process_events() virtual_dom.rs#L490 排空调度消息:先把通道积压全部入队,若没有脏作用域再轮询任务
runtime() virtual_dom.rs#L786 取出共享的 Rc<Runtime>

值得注意的是 new_with_component 内部 virtual_dom.rs#L310-L333:它会创建 (tx, rx) 通道、构造 Runtime、以 RootScopeWrapper 作为根 Props 并调用 new_scope 建立最外层作用域;在 debug 构建下还会注册 subsecond 热补丁处理器。

1.2 驱动 VirtualDom 的标准事件循环

把上述方法串联起来,就是文档中"Building an event loop around Dioxus"给出的典型模式(源码 doc 注释见 virtual_dom.rs#L139-L185):

let mut dom = VirtualDom::new(app);
dom.rebuild(&mut real_dom.apply());          // 1. 全量构建

loop {
    tokio::select! {
        _   = dom.wait_for_work() => {}      // 2. 等待任务/事件
        evt = real_dom.wait_for_event() => { // 3. 等待渲染器侧事件
            dom.runtime().handle_event_for_target(
                RenderTargetId::ROOT, "onclick",
                Event::new(evt, true),
                ElementId::from_raw(0),
            )
        }
    }
    dom.render_immediate(&mut real_dom.apply()); // 4. 执行差分并应用编辑
}

这正是各种渲染器(Web、Desktop、SSR)驱动 Dioxus 的通用协议:任何宿主都只需要实现一个能消费 WriteMutations 的 writer,并按时回调事件即可。

2. 渲染管线:从根组件到 DOM Mutations

文档将渲染明确划分为"初始渲染"与"更新渲染"两条路径。

2.1 初始渲染流程(rebuild)

  1. rebuild() 调用 run_scope(ScopeId::ROOT),运行根作用域;
  2. 结果包装进 LastRenderedNode 并调用 create_scope()
  3. create_scope() 递归创建全部子作用域与 DOM 元素;
  4. 所有 mutations 写入 WriteMutations 汇(sink)。

源码层面的真实链路稍有抽象:rebuild 先把 writer 包装成 TargetRouter(负责把编辑按渲染目标路由,见 virtual_dom.rs#L562-L568),随后在 rebuild_with_writer 中通过根作用域的 render driver 的 create(...) 方法在 while_rendering 保护下构建整棵树(virtual_dom.rs#L592-L604)。

文档特别提示了 rebuild 的三条语义(源码注释在 virtual_dom.rs#L546-L560):

  • 不轮询任务、不消费事件队列,仅把根组件跑一次再整体 diff;
  • 会彻底清空组件内所有状态
  • 已注册的 template(模板缓存)会保留。

2.2 更新渲染流程(render_immediate)

  1. wait_for_work() 轮询调度器与 pending futures;
  2. process_events() 消费 SchedulerMsg::Immediate(scope_id) 消息(在源码中 SchedulerMsg 位于 tasks.rs#L380,共有 Immediate(id)TaskNotified(id)AllDirty 三种变体,分别对应标脏、任务唤醒与全量标脏);
  3. render_immediate() 按高度顺序(自顶向下)弹出脏作用域;
  4. 对每个作用域执行 run_and_diff_scope()(先运行组件)再 diff_scope()
  5. diff 产生的编辑通过 WriteMutations trait 写出。

源码 render_immediate_with_writervirtual_dom.rs#L606-L626)通过 pop_work() 循环取出工作项,Work 分为两类:

match work {
    Work::PollTask(task) => self.runtime.handle_task_wakeup(task),
    Work::RerunScope(scope) => {
        self.runtime.clone().while_rendering(|| {
            self.run_and_diff_scope(Some(to), scope.id);
        });
    }
}

run_and_diff_scopediff_scope 的实现位于 packages/core/src/diff/component.rs:前者获取 scope 的 render driver 并调用 driver.diff(...),后者在旧输出与新输出都存在时进入节点差分。每次处理完一个工作项后还会调用 queue_events(),以收集本轮工作新产生的脏标记(例如子任务取消使 suspense 边界变脏),保证 DOM 在单次渲染内收敛。

2.3 Effects 的收尾时机

poll_tasksvirtual_dom.rs#L504-L526)中有一个关键细节:任务轮询若只是排队了 effect 而没有使任何 scope 变脏,wait_for_work 就不会返回去做渲染、effect 也就不会被冲刷。因此 poll_tasks 结束时通过 drain_remaining_effects() 兜底执行这些 effect(virtual_dom.rs#L628-L654)。这印证了文档中"Effects run AFTER mutations are applied to DOM"的次序保证。

3. 组件与 Props 系统

3.1 组件的定义

  • Component<P> 类型别名:fn(P) -> Element,即组件就是"Props 到 Element"的纯函数映射;
  • ComponentFunction<P, M> trait 描述组件函数,核心成员(文档原述):
    • fn_ptr(&self) -> usize:取原始函数指针,用于**函数身份(identity)**比较——diff 时判断"还是不是同一个组件";
    • rebuild(&self, props: P) -> Element:实际执行组件以得到新的 Element。

3.2 Properties trait

所有 Props 都实现 Properties trait(源码见 packages/core/src/properties.rs),其关键成员:

  • 关联类型 ComponentBuilder:用于面向渲染的 DSL Props 构造;
  • component_builder(render_fn):把某个组件的渲染函数与 Props 构建器绑定;
  • memoize(&mut self, other: &Self) -> bool:判断 Props 是否发生变化,返回 true 表示需要重渲染、可以跳过子树的昂贵 diff;
  • into_vcomponent():把自身包装成 VComponent

实际创建 VirtualDom 时(virtual_dom.rs#L292),源码会把组件与 Props 包进 VProps::new(root, |_, _| true, root_props, "Root")——注意这里 memoize 闭包直接返回 true,即根组件默认每次都重新执行。

3.3 类型擦除(Type Erasure)

由于组件树上不同节点的 Props 类型各不相同,Dioxus 需要把它们统一存储。链路为:

  1. VProps<F, P, M> 持有强类型 Props;
  2. 通过 AnyProps trait 擦除为 BoxedAnyProps(源码见 packages/core/src/any_props.rs);
  3. 擦除后仍保留以下能力:
    • render(&self) -> Element
    • memoize(&mut self, other: &dyn Any) -> bool
    • props() / props_mut():以 dyn Any 访问原始数据;
    • duplicate():克隆到新 box。

这套"先构造 VProps → 装箱擦除 → 以 Any 访问"的设计,让虚拟 DOM 节点可以在完全不知道 Props 具体类型的情况下完成记忆化、执行与销毁。

4. Scope 系统:组件作用域

4.1 ScopeId 语义

ScopeId 本质是 usize,作为内部 Slab<Scope> 的索引键(scopes.rs#L9-L16)。源码注释明确:该 ID 在时间上不唯一(slot 可能被复用),但框架保证在两次 wait_for_work 之间不会回收 ID,给依赖 ID 的逻辑留出更新窗口。

文档强调几个预定义 ID(源码常量位于 scopes.rs#L48-L63):

  • ScopeId::ROOT = 0:根包装作用域(root wrapper),常驻整个应用生命周期,便于放置长生命周期状态;
  • ScopeId::ROOT_SUSPENSE_BOUNDARY = 1:默认的顶级 Suspense 边界;
  • ScopeId::ROOT_ERROR_BOUNDARY = 2:默认的错误边界;
  • ScopeId::APP = 3:用户真正的根组件所在作用域。

这也解释了为什么用户根组件的 ID 是 3 而不是 0——框架在用户代码之上自动插入了一层包装器(详见 第 15 节)。

4.2 ScopeState(对外 API)与 Scope(内部状态)

文档区分了两层结构。面向公共 API 的 ScopeState 承载:

ScopeState
├── context_id: ScopeId
├── last_rendered_node: Option<LastRenderedNode>
├── props: BoxedAnyProps
└── reactive_context: ReactiveContext

而内部 Scope 则记录引擎所需的全部簿记数据(文档原述,对应源码 packages/core/src/scope_context.rspackages/core/src/scope_arena.rs):

Scope
├── name: &'static str            // 组件名(调试用)
├── id: ScopeId
├── parent_id: Option<ScopeId>    // 父作用域,context 查找与树清理都靠它
├── height: u32                   // 距根的距离
├── hooks: RefCell<Vec<Box<dyn Any>>>  // Hook 状态存储
├── hook_index: Cell<usize>       // 当前正被访问的 hook 下标
├── shared_contexts: RefCell<Vec<Box<dyn Any>>>
├── spawned_tasks: RefCell<FxHashSet<Task>>
├── before_render / after_render  // 渲染前后回调
├── status: RefCell<ScopeStatus>  // Mounted / Unmounted
└── suspense_boundary: SuspenseLocation

new_scopescope_arena.rs#L17-L46)中可以看到作用域创建的关键逻辑:高度取 parent.height() + 1,suspense 位置继承当前 runtime 的 suspense location(也可被 render driver 改写),同时为作用域绑定独立的 ReactiveContext

4.3 上下文传播

  • provide_context<T: Clone + 'static>(context):把值存进当前作用域的 shared_contexts
  • consume_context<T>():从当前作用域开始,沿 parent_id 一路向祖先查找;
  • 因此 Context 查找的时间复杂度与组件树深度成正比,且天然具有"就近遮蔽"的语义。

这也是 Dioxus 中所有隐式依赖注入(主题、路由、配置等)的底层机制。

5. 事件系统

5.1 事件结构

文档给出的 Event 定义(源码对应 packages/core/src/events.rs):

pub struct Event<T: ?Sized> {
    pub data: Rc<T>,
    pub(crate) metadata: Rc<RefCell<EventMetadata>>,
}

pub struct EventMetadata {
    pub propagates: bool,
    pub prevent_default: bool,
}

事件负载用 Rc 共享,跨作用域传播零拷贝;metadata 携带"是否冒泡""是否阻止默认行为"两个可被监听器就地修改的标志。

5.2 事件分发与冒泡流程

  1. Runtime::handle_event() 依据 ElementId 找到目标元素;
  2. 从元素 slab 取得其父级 ElementRef
  3. 若事件可冒泡,调用 handle_bubbling_event() 沿父链向上遍历;
  4. 按路径顺序收集监听器,然后逆序调用(父先子后,符合 DOM 语义)
  5. 一旦监听器调用 stop_propagation() 立即中断。

这套设计与浏览器 DOM 事件模型对齐,使 Web 端可以直接复用语义,而桌面/原生端通过同一套 ElementId → 父链 逻辑得到等价行为。

6. Diffing 算法:如何最小化真实 DOM 更新

6.1 入口

diff_scope()(见 diff/component.rs)取得作用域的旧、新 VNode,随后调用 old.diff_node(new, dom, mutations) 递归推进。这里的"节点"都是 VNode(虚拟节点),diff 对象是两棵虚拟树而不是真实 DOM。

6.2 VNode 级差分(对应文档 Template Check / Identity Check 等)

  • Template Check(模板检查):如果新旧节点来自不同的静态模板(Template),说明静态骨架已变,直接整棵子树替换;
  • Identity Check(身份检查):若新旧是同一个指针(RC 指向同一对象),整体跳过,无需任何处理;
  • Attribute Diffing:仅对 dynamic_attrs(动态属性)调用 diff_attributes(),静态属性不存在于动态路径;
  • Dynamic Node Diffing:递归逐个 diff 动态节点。

这里的优化核心是:RSX 在编译期就分离了"静态模板 + 动态插槽",diff 只发生在动态插槽上。

6.3 动态节点(DynamicNode)的五种情形

  • Text → Text:diff_vtext() 直接更新文本内容;
  • Placeholder → Placeholder:无操作;
  • Fragment → Fragment:递归 diff 子节点;
  • Component → Component:先比较渲染函数(fn_ptr),相同再走 Props memoize 判断;
  • 类型改变:移除旧节点、创建新节点。

6.4 带 key 的列表 diff(Keyed List Diffing)

文档给出的三段式算法在真实 diff 引擎中对应:

  1. Prefix Pass(前缀轮):从头开始匹配相同 key;
  2. Suffix Pass(后缀轮):从尾开始匹配相同 key;
  3. Middle Pass(中间轮):处理剩余节点的插入 / 删除 / 移动;
  4. 全程使用 FxHashMap 维护"旧 key → 新 key"映射,保证 O(n) 级别匹配。

7. Mutations:VDOM 与真实 DOM 的通信协议

7.1 WriteMutations trait

WriteMutations 是虚拟 DOM 与真实 DOM(渲染器)之间的唯一接口。文档列出了它的核心方法语义:

  • append_children(id, count):向元素追加 N 个节点;
  • assign_node_id(path, id):把模板路径处的元素登记为 id;
  • create_placeholder(id) / create_text_node(value, id):创建占位 / 文本节点;
  • load_template(template, index, id):从模板缓存克隆(静态部分只需克隆一次);
  • replace_node_with(id, count) / remove_node(id):替换 / 删除;
  • set_attribute(name, ns, value, id):更新属性;
  • create_event_listener(name, id):注册监听器。

这是整个框架跨平台的关键抽象:只要实现这套 trait,同一份组件代码就能跑在 Web、桌面、移动端、LiveView 与 SSR 上(可对照 00-OVERVIEW.md 中"WriteMutations Trait"架构决策)。

7.2 当前仓库的 Mutation 编码

需要说明:随着版本演进,当前 packages/core/src/mutations.rsMutation 枚举已经从文档示例的参数式风格演进为基于栈的紧凑编码(更利于跨进程/跨语言传输,例如桌面端与 LiveView 使用的二进制格式)。当前变体包括:

PushId / SetId / Child / Pop / CreateElement { tag }
CreateText { value } / Clone / AppendChildren { m }
ReplaceWith { m } / InsertAfter { m } / InsertBefore { m }
SetAttribute { name, ns, value } / SetText { value }
NewEventListener { name } / RemoveEventListener { name } / Remove

观察可知:元素通过 CreateElement 入栈、Child { index } 下钻、Pop 出栈,构建出一棵"模板路径 + 栈"描述的真实 DOM 操作序列;LoadTemplate 则由 Clone(克隆模板缓存)承载。这套编码可直接序列化交给任意宿主执行,是 Dioxus 渲染器协议的核心。

7.3 Mutation 结构中的模板缓存思想

静态部分(元素骨架、静态属性、静态文本)不重复生成,而是作为 Template 注册进渲染器模板缓存,之后任何位置克隆即可;diff 后产生的动态编辑只是针对插槽的小幅补丁。这使初始渲染后的更新成本与动态内容量成正比,而不是与整棵 UI 树大小成正比。

8. Scheduler:响应式工作的优先级编排

8.1 工作优先级

文档定义了三档优先级:

  1. Dirty Scopes(最高):需要重渲染的作用域,按高度顺序执行;
  2. Tasks(次之):轮询被 spawn 的 future;
  3. Effects(最低):在 DOM 变更之后执行。

8.2 ScopeOrder:排序保证

ScopeOrder(height: u32, id: ScopeId) 的复合键(scheduler.rs#L86-L95),存于 BTreeSet<ScopeOrder> 中。由于父作用域的 height 恒小于子作用域,集合迭代顺序自然保证父先子后。这在 Dioxus 响应式模型中非常重要:父组件先重渲染,把新 Props 传给子组件;子组件随后重渲染时读取到的新 Props 才是正确的。若顺序颠倒,就会出现"读到过期 Props"的竞态。

8.3 调度器方法(对应文档)

  • queue_scope(order):把作用域加入 dirty_scopes
  • queue_task(task, order):把任务加入脏任务集;
  • pop_work():取下一个脏作用域或脏任务;
  • pop_effect():取下一个待执行 effect。

调度循环的主循环在 VirtualDom::wait_for_work / render_immediate 中完成:排空通道(queue_events)→ 有脏作用域则渲染;否则轮询任务 → 仍无工作则挂起等待通道消息。对通道消息的处理见 virtual_dom.rs#L464-L474SchedulerMsg::Immediate(id) 标脏作用域、TaskNotified(id) 把任务插入运行时任务队列、AllDirty 触发全量标脏。queue_eventsvirtual_dom.rs#L477-L486)还特意用 try_recv 防自循环任务反复入队导致死锁。

9. Runtime:异步、作用域与任务的协调者

Runtime 是横切所有机制的单例数据中枢,文档给出其字段(源码见 packages/core/src/runtime.rs):

Runtime
├── scope_states: RefCell<Vec<Option<Scope>>>    // 全部作用域(与 slab 对齐)
├── scope_stack: RefCell<Vec<ScopeId>>            // 当前执行栈(嵌套组件执行用)
├── suspense_stack: RefCell<Vec<SuspenseLocation>>
├── tasks: RefCell<SlotMap<DefaultKey, Rc<LocalTask>>>  // 任务表
├── current_task: Cell<Option<Task>>              // 正在执行的任务
├── dirty_tasks: RefCell<BTreeSet<DirtyTasks>>
├── pending_effects: RefCell<BTreeSet<Effect>>
├── rendering: Cell<bool>                         // 是否正处于渲染期
├── sender: UnboundedSender<SchedulerMsg>         // 向 VirtualDom 发送消息
├── elements: RefCell<Slab<Option<ElementRef>>>   // 元素挂载表
└── mounts: RefCell<Slab<VNodeMount>>             // VNode 挂载信息

作用域的 provide_context / hook 访问等操作都必须"在某个 scope 上下文中"执行,这正是 scope_stackRuntimeGuard(见 VirtualDom::in_runtimevirtual_dom.rs#L349-L354)存在的意义——所有用户代码都经由它设置当前执行环境,避免把 runtime 作为参数层层传递。

10. 任务与异步(Tasks & Async)

10.1 Task 结构(文档原述)

  • id: TaskId:slotmap 键;
  • !Send + !Sync 标记:Dioxus 任务单线程驱动,无需 Send 约束,降低用户代码负担;
  • 方法:Task::new(future) 创建、cancel() 移除、pause() / resume() 控制轮询、wake() 唤醒。

10.2 LocalTask(内部实现)

  • 包装 Pin<Box<dyn Future<Output = ()>>>
  • 使用自定义 waker:唤醒时向通道发送 SchedulerMsg::TaskNotified(见 tasks.rs#L380 附近)而非直接驱动,把调度权交还给事件循环;
  • 任务与所属作用域生命周期绑定:作用域被销毁时任务随之 drop,杜绝悬挂 future 泄漏用户状态。

从架构文档 00-OVERVIEW.md 的"Scope-Based Cleanup"设计决策可进一步确认:信号、任务、Context 等一切状态的生命周期都锚定在组件作用域上。

11. Effects:DOM 提交之后才运行

文档明确了 Effects 的运行契约(源码支撑见 virtual_dom.rs#L628-L654drain_remaining_effects):

  1. 组件内通过 use_effect() 创建 effect;
  2. effect 被放入 Runtime::pending_effects
  3. 渲染结束后 finish_render() 发送 SchedulerMsg::EffectQueued
  4. 下一次 poll_tasks / 渲染兜底时,pop_effect() 取出并调用 effect.run()

之所以"必须后置",是为了保证 effect 读取到的 DOM 状态已经是 diff 应用后的最新值——这正是类 React useEffect 语义的底层保证。

12. Suspense 挂起机制

12.1 SuspenseContext

文档定义的内部状态:

  • suspended_tasks: RefCell<Vec<SuspendedFuture>>:被挂起的 future 列表;
  • suspended_nodes: RefCell<Option<VNode>>:挂起期间的占位 UI;
  • frozen: Cell<bool>:服务端用于锁定(渲染期间冻结状态)。

12.2 一次挂起的完整旅程

  1. 组件调用 suspend(),返回 Err(SuspendedFuture)
  2. 错误沿 Element::Err(RenderError::Suspended) 向上传播(相关类型在 packages/core/src/render_error.rs);
  3. 最近的 SuspenseBoundary 捕获该错误;
  4. 边界渲染占位内容(placeholder);
  5. 被挂起的 future 挂到边界的任务列表上;
  6. future 完成时边界被标记为脏,待重渲染解析挂起子树。

在服务端场景,VirtualDom::wait_for_suspensevirtual_dom.rs#L670-L682)会循环:排队事件 → 若无剩余挂起任务且无脏作用域则退出 → 否则 wait_for_suspense_work() + render_suspense_immediate()(后者只渲染位于 suspense 边界之下、允许在挂起期重跑的作用域,且每处理 32 个工作单元 yield_now() 一次以防饿死其他异步任务,见 virtual_dom.rs#L729-L783)。这也使 Dioxus 能在同一棵树上同时服务 SSR 与客户端交互式渲染。

13. VNode 与 Template:静态与动态的编译期分离

13.1 VNode 结构

pub struct VNode {
    vnode: Rc<VNodeInner>,
}

pub struct VNodeInner {
    pub template: Template,      // 静态模板引用
    pub view: DynamicValues,     // 动态插槽
}

pub struct DynamicValues {
    pub(crate) key: Option<String>,          // 列表 key
    pub(crate) dynamic_nodes: Vec<DynamicNode>,  // 动态子节点
    pub(crate) dynamic_attrs: Vec<Box<[Attribute]>>, // 动态属性组
}

重要设计:挂载后的节点状态(mounted state)不存放在 VNode 上,而是存放在 VirtualDOM 的 mount arena(Runtime.mounts)中、通过 MountedVNode 访问。这意味着 VNode 本身是无状态、可廉价复用的"渲染结果快照",而真实 DOM 的归属信息被集中管理——这正是 load_template 克隆机制与多渲染目标共享同一 VNode 的前提。

13.2 Template(静态部分)

pub struct Template {
    pub roots: &'static [TemplateNode],        // 静态节点
    pub node_paths: &'static [&'static [u8]],  // 动态节点位置
    pub attr_paths: &'static [&'static [u8]],  // 动态属性位置
}

TemplateNode 的三种变体:

  • Element { tag, namespace, attrs, children }:静态元素;
  • Text { text }:静态文本;
  • Dynamic { id }:指向 dynamic_nodes 的动态槽位。

DynamicNode 四种变体:

  • Component(VComponent):子组件;
  • Text(VText):文本节点;
  • Placeholder(VPlaceholder):suspense / 空占位标记;
  • Fragment(Vec<VNode>):多个连续子节点。

可以看出,rsx! 宏在编译期(经 packages/core-macropackages/rsx 等 crate)就把 UI 拆成"静态模板表 + 动态值表",运行时几乎不做模板层面的解释,只 diff 动态槽——这是 00-OVERVIEW.md 中"Template-Based Rendering"模式的实现载体。

14. 错误边界(Error Boundaries)

ErrorContext 数据结构:

  • error: Rc<RefCell<Option<CapturedError>>>:被捕获的错误;
  • subscribers: Subscribers:监听该上下文的组件(用于触发重渲染)。

错误处理流程(对应源码 packages/core/src/error_boundary.rs):

  1. 组件返回 Element::Err(error)
  2. 最近的 ErrorBoundary 捕获之;
  3. ErrorContext 保存错误;
  4. 边界重渲染时,子树可通过 error_context.error() 读取错误并渲染兜底 UI。

与 Suspense 边界相同,错误边界也提供了一套"范围内兜底"能力,使 UI 某处 panic / 出错不至于击穿整个应用。

15. 根包装器与全局上下文

默认的层级(源码见 packages/core/src/root_wrapper.rs,ID 常量见 scopes.rs#L48-L63):

ScopeId(0): RootScopeWrapper(根包装器)
└── ScopeId(1): SuspenseBoundary(默认顶级 Suspense)
    └── ScopeId(2): ErrorBoundary(默认顶级错误边界)
        └── ScopeId(3): 用户根组件

这层自动包裹意味着:即使开发者从未显式声明 <Suspense><ErrorBoundary>,整个应用也已经默认拥有了顶级挂起兜底与错误兜底。文档同时提到,多窗口桌面应用也共享同一棵 VirtualDOM 树与同一套根包装器,简化跨窗口状态管理(对应 00-OVERVIEW.md 的"Single VirtualDOM"决策)。

全局依赖注入则通过 with_root_context<T>() / provide_root_context<T>() 写入 ScopeId::ROOT 作用域实现——任何后代组件都能 consume_context 到它,是 Dioxus 官方推荐的"全局单例状态"注入方式之一。

16. 小结与延伸阅读

dioxus-core 的本质可以概括为一句话:用一份编译期生成的静态模板表和一套 arena 化的作用域系统,把"声明式 UI 重渲染"压缩为"对动态槽位的增量 diff"。其背后每一处机制——BTreeSet<ScopeOrder> 的父先子后调度、自定义 waker 的消息化唤醒、WriteMutations 的栈式编辑协议、Effect 的提交后执行——都是为了让这套响应式模型在 Web / 桌面 / 移动 / SSR 多端保持一致的确定性与性能。

如果想继续深入,推荐按顺序阅读同一目录下的架构笔记:

  • 00-OVERVIEW.md:crate 依赖全景与关键架构决策;
  • 01-CORE.md:本篇主体(VirtualDOM / 渲染 / 组件 / diff);
  • 04-SIGNALS.md:基于 generational-box 的信号与状态管理;
  • 06-RENDERERS.md:Web / Desktop / LiveView / SSR 渲染器如何实现 WriteMutations

源码核心文件索引:

关注点 源码路径
VirtualDom 全生命周期 packages/core/src/virtual_dom.rs
作用域 ID 与预置边界 packages/core/src/scopes.rs
作用域创建(高度/父链/ReactiveContext) packages/core/src/scope_arena.rs
作用域内部状态 packages/core/src/scope_context.rs
差分执行(run_and_diff_scope / diff_scope) packages/core/src/diff/component.rs
调度排序键 ScopeOrder packages/core/src/scheduler.rs
任务、LocalTaskSchedulerMsg packages/core/src/tasks.rs
栈式 Mutation 编码 packages/core/src/mutations.rs
事件与事件元数据 packages/core/src/events.rs
错误边界 packages/core/src/error_boundary.rs
根包装器 packages/core/src/root_wrapper.rs
Props 类型擦除 packages/core/src/any_props.rs

说明:本文所涉 API 与数据结构以当前仓库代码为准,其中 Mutation 枚举等实现已相对 01-CORE.md 早期示例有所演进,读者对照源码阅读时请注意这一版本差异。

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

项目优选

收起
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