Rust in Everything
基于 dioxus 0.7.10

diff

主线位置:更新主线里 run_scope 拿到新 VNode 之后、WriteMutations 之前的那一步。点击让 count 变了,组件重跑,新旧 VNode 用的是同一个模板,diff 只看到一段动态文本不同,发出一条 SetText。

flowchart TB
  start["old.diff_node new"] --> same{"模板相同"}
  same -->|"否"| repl["建新节点 ReplaceWith 旧根"]
  same -->|"是"| mv["挂载信息移给新 VNode"]
  mv --> attrs["逐个比动态属性"]
  attrs --> dyn["逐个比动态节点"]
  dyn --> txt["Text 与 Text 值不同发 SetText"]
  dyn --> frag["Fragment 与 Fragment 比列表"]
  dyn --> comp["Component 与 Component 比 props"]
  dyn --> other["种类不同 建新的 替换旧的"]
  comp --> memo{"memoize 为真"}
  memo -->|"是"| skip[不重跑]
  memo -->|"否"| rerun["run_and_diff_scope 子组件"]

这一页讲新旧两个 VNode 怎么比、比出什么指令。VNode、Template、DynamicNode 的结构在 Template 与 VNode;列表(DynamicNode::Fragment)的比较在 带 key 的列表;每条指令的含义和第一次渲染的创建路径在 Mutations。代码在 diff/node.rs、diff/component.rs、arena.rs。

入口:diff_scope

render_immediate 每取出一个脏 scope,就调 run_and_diff_scope:先 run_scope 得到新的 Element,再进 diff_scope:

    fn diff_scope<M: WriteMutations>(
        &mut self,
        to: Option<&mut M>,
        scope: ScopeId,
        new_nodes: Element,
    ) {
        self.runtime.clone().with_scope_on_stack(scope, || {
            // We don't diff the nodes if the scope is suspended or has an error
            let Ok(new_real_nodes) = &new_nodes else {
                return;
            };
            let scope_state = &mut self.scopes[scope.0];
            // Load the old and new rendered nodes
            let old = scope_state.last_rendered_node.take().unwrap();

            // If there are suspended scopes, we need to check if the scope is suspended before we diff it
            // If it is suspended, we need to diff it but write the mutations nothing
            // Note: It is important that we still diff the scope even if it is suspended, because the scope may render other child components which may change between renders
            let mut render_to = to.filter(|_| self.runtime.scope_should_render(scope));
            old.diff_node(new_real_nodes, self, render_to.as_deref_mut());

            self.scopes[scope.0].last_rendered_node = Some(LastRenderedNode::new(new_nodes));

            if render_to.is_some() {
                self.runtime.get_state(scope).mount(&self.runtime);
            }
        })
    }

两件事值得记住。第一,组件返回 Err(出错或挂起)时根本不 diff,旧节点原样留着,错误交给边界去处理(Scope 与 hooks 里 hook 出错的实验看到的就是这个:出错那一轮页面没变)。第二,to 是个 Option:在挂起的 suspense 边界下面,diff 照样走一遍(子组件该建的建、该删的删),只是 to 变成 None,一条指令都不写。下面所有 diff 函数都收 Option<&mut impl WriteMutations>,凡是「只是副作用」的地方(文本、属性)在 to 为 None 时直接跳过。

挂载信息:VNode 之外的那张表

VNode 是组件返回的不可变数据,Rc<VNodeInner> 加一个 Cell<MountId>。它被挂到 DOM 上之后,「这个模板的第 i 个根是哪个 ElementId」「第 j 个动态节点挂在哪」这些信息不放在 VNode 里,而是放在 Runtime.mounts 这个 slab 里的 VNodeMount:

pub(crate) struct VNodeMount {
    /// The parent of this node
    pub parent: Option<ElementRef>,

    /// A back link to the original node
    pub node: VNode,

    /// The IDs for the roots of this template - to be used when moving the template around and removing it from
    /// the actual Dom
    pub root_ids: Box<[ElementId]>,

    /// The element in the DOM that each attribute is mounted to
    pub(crate) mounted_attributes: Box<[ElementId]>,

    /// For components: This is the ScopeId the component is mounted to
    /// For other dynamic nodes: This is element in the DOM that each dynamic node is mounted to
    pub(crate) mounted_dynamic_nodes: Box<[usize]>,
}

mounted_dynamic_nodes 一格两用:动态文本和占位符存的是 ElementId,组件存的是 ScopeId,还没建 scope 时是 usize::MAX(ScopeId::PLACEHOLDER)。diff 里频繁出现的 get_mounted_dyn_node(mount, idx) 读的就是这一格。node 是一条反向链接,事件冒泡时由 ElementId 找到挂载、再找到模板里的监听器属性,见 事件。

diff_node:同一个模板只比动态部分

VNode::diff_node:

    pub(crate) fn diff_node(
        &self,
        new: &VNode,
        dom: &mut VirtualDom,
        mut to: Option<&mut impl WriteMutations>,
    ) {
        // ...
        // If the templates are different, we need to replace the entire template
        if self.template != new.template {
            let mount_id = self.mount.get();
            let parent = dom.get_mounted_parent(mount_id);
            return self.replace(std::slice::from_ref(new), parent, dom, to);
        }

        self.move_mount_to(new, dom);

        // If the templates are the same, we don't need to do anything, except copy over the mount information
        if self == new {
            return;
        }

        // If the templates are the same, we can diff the attributes and children
        // Start with the attributes
        // Since the attributes are only side effects, we can skip diffing them entirely if the node is suspended and we aren't outputting mutations
        if let Some(to) = to.as_deref_mut() {
            self.diff_attributes(new, dom, to);
        }

        // Now diff the dynamic nodes
        let mount_id = new.mount.get();
        for (dyn_node_idx, (old, new)) in self
            .dynamic_nodes
            .iter()
            .zip(new.dynamic_nodes.iter())
            .enumerate()
        {
            self.diff_dynamic_node(mount_id, dyn_node_idx, old, new, dom, to.as_deref_mut())
        }
    }
  • 先比模板。 Template 的 PartialEq 在「相同的静态项会被合并」的构建里比较三个 &'static 切片的指针,否则按值比较(Template 与 VNode 讲为什么)。同一处 rsx! 每次渲染产出的模板是同一个 static,所以相等。
  • 模板相同就把挂载信息移给新节点。 move_mount_to 把旧 VNode 的 MountId 交给新 VNode,并把 VNodeMount.node 换成新节点。从这一刻起,事件冒泡时找到的监听器就是新闭包。
  • self == new 是 Rc 指针相等。 同一个 VNode 再次出现(比如所有占位符共用的线程局部 VNode::placeholder())时,挂载信息搬完就结束。
  • 然后只看动态部分。 静态元素、静态文本、静态属性根本不在比较范围内:模板里有多少静态节点都不影响 diff 的成本。动态属性和动态节点都按下标一一配对,因为模板相同,两边的槽位数一定相同。

实验:同一个模板

本卷 03-core 的实验都用一个自己实现 WriteMutations 的记录器 Log 打印指令(每个方法一行,名字和 Mutation 的分支一致,LoadTemplate 多写出模板根的标签),见 VirtualDom 与 Runtime 的实验说明。组件读线程局部的 STEP,每一步改 STEP、mark_dirty(ScopeId::APP)、render_immediate(&mut log):

// 1. one template, a dynamic attribute and a dynamic text
fn same_template() -> Element {
  let n = step();
  let cls = if n >= 2 { "big" } else { "small" };
  rsx! {
    div { class: "{cls}", id: "static-id",
      "count {n}"
      span { "static text" }
    }
  }
}
===== 1 same template
--- rebuild (step 0): 5 edits
  LoadTemplate <div> index=0 id=1
  SetAttribute class=Text("small") id=1
  CreateTextNode "count 0" id=2
  ReplacePlaceholder path=[0] m=1
  AppendChildren id=0 m=1
--- step 1: 1 edits
  SetText "count 1" id=2
--- step 2: 2 edits
  SetAttribute class=Text("big") id=1
  SetText "count 2" id=2

M2 Max、macOS 27、rustc 1.99.0,debug。id: "static-id" 和 span { "static text" } 是静态的,从来不出现在指令里:它们在 LoadTemplate 克隆的模板里就已经有了。第 1 步 class 没变,没有 SetAttribute;只有文本变了,一条 SetText。第 2 步两者都变,各一条。

模板不同:先建新的,再替换旧的

模板不同时 replace 走到 replace_inner:

    pub(crate) fn replace_inner(
        &self,
        right: &[VNode],
        parent: Option<ElementRef>,
        dom: &mut VirtualDom,
        mut to: Option<&mut impl WriteMutations>,
        destroy_component_state: bool,
    ) {
        let m = dom.create_children(to.as_deref_mut(), right, parent);

        // Instead of *just* removing it, we can use the replace mutation
        self.remove_node_inner(dom, to, destroy_component_state, Some(m))
    }

先把新节点整棵创建出来(它们留在渲染器的栈上,共 m 个),再移除旧节点,移除到最后一个根时不发 Remove,而是发 ReplaceWith id=旧根 m,让栈上的新节点顶替它的位置。旧模板里的组件 scope 全部销毁(destroy_component_state = true),状态不保留。

rsx! 里的 if / else 就是这种情况:两个分支是两个不同的模板,放在外层模板的同一个动态槽位里。

// 2. if / else: two different templates in one dynamic slot
fn if_else() -> Element {
  let n = step();
  rsx! {
    if n % 2 == 0 {
      div { "even {n}" }
    } else {
      p { "odd {n}" }
    }
  }
}
===== 2 if/else
--- rebuild (step 0): 4 edits
  LoadTemplate <div> index=0 id=1
  CreateTextNode "even 0" id=2
  ReplacePlaceholder path=[0] m=1
  AppendChildren id=0 m=1
--- step 1: 4 edits
  LoadTemplate <p> index=0 id=3
  CreateTextNode "odd 1" id=4
  ReplacePlaceholder path=[0] m=1
  ReplaceWith id=1 m=1
--- step 2: 4 edits
  LoadTemplate <div> index=0 id=1
  CreateTextNode "even 2" id=2
  ReplacePlaceholder path=[0] m=1
  ReplaceWith id=3 m=1
--- step 3: 4 edits
  LoadTemplate <p> index=0 id=3
  CreateTextNode "odd 3" id=4
  ReplacePlaceholder path=[0] m=1
  ReplaceWith id=1 m=1

每一步都是「建 <p> 或 <div> 整块 + 填文本 + ReplaceWith 旧根」,哪怕两个分支都只差一个标签名。if 分支的 VNode 经 IntoDynNode for VNode 变成 DynamicNode::Fragment(vec![node]),外层模板的这个槽位是 Fragment 对 Fragment,进 列表的 diff,长度都是 1,再对这一对调 diff_node,在这里因为模板不同而替换。注意 id 的变化:第 1 步新节点拿到 3、4(1、2 还没释放),旧节点删掉后 1、2 被回收;第 2 步新节点又拿到 1、2。下面讲 ElementId 时再说。

动态节点的五种配对

模板相同时,每对动态节点交给 diff_dynamic_node:

        match (old_node, new_node) {
            (Text(old), Text(new)) => {
                // Diffing text is just a side effect, if we are diffing suspended nodes and are not outputting mutations, we can skip it
                if let Some(to) = to {
                    let id = ElementId(dom.get_mounted_dyn_node(mount, idx));
                    self.diff_vtext(to, id, old, new)
                }
            }
            (Placeholder(_), Placeholder(_)) => {}
            (Fragment(old), Fragment(new)) => dom.diff_non_empty_fragment(
                to,
                old,
                new,
                Some(self.reference_to_dynamic_node(mount, idx)),
            ),
            (Component(old), Component(new)) => {
                let scope_id = ScopeId(dom.get_mounted_dyn_node(mount, idx));
                self.diff_vcomponent(
                    mount,
                    idx,
                    new,
                    old,
                    scope_id,
                    Some(self.reference_to_dynamic_node(mount, idx)),
                    dom,
                    to,
                )
            }
            (old, new) => {
                // ...
                let old_mount = dom.get_mounted_dyn_node(mount, idx);
                dom.set_mounted_dyn_node(mount, idx, usize::MAX);

                let new_nodes_on_stack =
                    self.create_dynamic_node(new, mount, idx, dom, to.as_deref_mut());

                // Restore the mount for the scope we are removing
                let new_mount = dom.get_mounted_dyn_node(mount, idx);
                dom.set_mounted_dyn_node(mount, idx, old_mount);

                self.remove_dynamic_node(mount, dom, to, true, idx, old, Some(new_nodes_on_stack));

                // Restore the mount for the node we created
                dom.set_mounted_dyn_node(mount, idx, new_mount);
            }
        };
旧 → 新做什么指令
Text → Textdiff_vtext:字符串不等才写SetText,或什么都没有
Placeholder → Placeholder什么都不做无
Fragment → Fragment列表 diff:都带 key 走 keyed,否则按位置见 带 key 的列表
Component → Componentdiff_vcomponent:比函数指针和 props子组件重跑后的 diff 结果,或无
种类不同先建新的,再用 ReplaceWith 移除旧的新节点的创建指令 + ReplaceWith

最后一行里那几次 set_mounted_dyn_node 是在玩一个小把戏:这一格同一时刻只能存一个值,可创建新节点要读这一格(是 usize::MAX 才会新建 scope),移除旧节点也要读这一格(拿旧的 ElementId 或 ScopeId)。于是先填 usize::MAX 让创建走「新建」分支,创建完把旧值放回去供移除使用,移除完再写入新值。

Option<String> 在 None 和 Some 之间切换,就是占位符和文本互换:

// 3. Option<String>: placeholder <-> text
fn option_text() -> Element {
  let n = step();
  let label = if n % 2 == 1 { Some(format!("label {n}")) } else { None };
  rsx! {
    div { {label} }
  }
}
===== 3 Option text
--- rebuild (step 0): 4 edits
  LoadTemplate <div> index=0 id=1
  CreatePlaceholder id=2
  ReplacePlaceholder path=[0] m=1
  AppendChildren id=0 m=1
--- step 1: 2 edits
  CreateTextNode "label 1" id=3
  ReplaceWith id=2 m=1
--- step 2: 2 edits
  CreatePlaceholder id=2
  ReplaceWith id=3 m=1
--- step 3: 2 edits
  CreateTextNode "label 3" id=3
  ReplaceWith id=2 m=1

None 也要在 DOM 里占一个位置(CreatePlaceholder,web 上是一个注释节点),这样换回 Some 时才知道新文本插在哪。外层 div 的模板没变,所以只动这一个槽位。

属性:按名字归并

一个动态属性槽位里可以有不止一个属性(比如 ..spread 展开出的一组),diff_attributes 假定两边都按属性名排好序,像归并排序那样同时走两个列表:名字相同就比值,旧的多一个就移除,新的多一个就写入。比值的那一段:

                                // Volatile attributes are attributes that the browser may override so we always update them
                                let volatile = old.volatile;
                                // We only need to write the attribute if the attribute is volatile or the value has changed
                                // and this is not an event listener.
                                // Interpreters reference event listeners by name and element id, so we don't need to write them
                                // even if the closure has changed.
                                let attribute_changed = match (&old.value, &new.value) {
                                    (AttributeValue::Text(l), AttributeValue::Text(r)) => l != r,
                                    (AttributeValue::Float(l), AttributeValue::Float(r)) => l != r,
                                    (AttributeValue::Int(l), AttributeValue::Int(r)) => l != r,
                                    (AttributeValue::Bool(l), AttributeValue::Bool(r)) => l != r,
                                    (AttributeValue::Any(l), AttributeValue::Any(r)) => {
                                        !l.as_ref().any_cmp(r.as_ref())
                                    }
                                    (AttributeValue::None, AttributeValue::None) => false,
                                    (AttributeValue::Listener(_), AttributeValue::Listener(_)) => {
                                        false
                                    }
                                    _ => true,
                                };
  • 事件监听器永远不算变化。 每次渲染 onclick: move |_| .. 都是一个新闭包,但渲染器那边只按「事件名 + ElementId」登记监听,闭包本身留在 Rust 侧;move_mount_to 已经把挂载信息里的 node 换成了新 VNode,事件到来时找到的就是新闭包。所以不发任何指令。
  • volatile 属性总是重写。 浏览器自己会改的属性(注释里说 "the browser may override"),不管新旧值是否相等都写一遍:用户在输入框里打了字,DOM 上的 value 已经不是上次写的那个,只比较新旧 VNode 会漏掉这次更新。dioxus-html 的元素定义里标了 volatile 的有四个:input 的 value、option 的 selected、select 的 value、textarea 的 value;rsx! 展开时把这个标记放进 Attribute.volatile。
  • 移除属性(remove_attribute):监听器发 RemoveEventListener,普通属性发一条值为 AttributeValue::None 的 SetAttribute,渲染器把它当作删除。

条件属性 title: if cond { "odd" } 不是「有时有、有时没有」,而是同一个槽位里的同一个属性,值在 Text 和 None 之间切换:

// 5. a conditional attribute and an event listener
fn attr_listener() -> Element {
  let n = step();
  rsx! {
    button {
      title: if n % 2 == 1 { "odd" },
      onclick: move |_| println!("clicked at step {n}"),
      "btn"
    }
  }
}
===== 5 conditional attribute + listener
...
--- step 1: 1 edits
  SetAttribute title=Text("odd") id=1
--- step 2: 1 edits
  SetAttribute title=None id=1

onclick 的闭包每一步都换了(它捕获了不同的 n),指令里却一次都没有出现。

组件:比函数指针和 props

diff_vcomponent:

    pub(crate) fn diff_vcomponent(
        &self,
        mount: MountId,
        idx: usize,
        new: &VComponent,
        old: &VComponent,
        scope_id: ScopeId,
        parent: Option<ElementRef>,
        dom: &mut VirtualDom,
        to: Option<&mut impl WriteMutations>,
    ) {
        // Replace components that have different render fns
        if old.render_fn != new.render_fn {
            return self.replace_vcomponent(mount, idx, new, parent, dom, to);
        }

        // copy out the box for both
        let old_scope = &mut dom.scopes[scope_id.0];
        let old_props: &mut dyn AnyProps = old_scope.props.deref_mut();
        let new_props: &dyn AnyProps = new.props.deref();

        // If the props are static, then we try to memoize by setting the new with the old
        // The target ScopeState still has the reference to the old props, so there's no need to update anything
        // This also implicitly drops the new props since they're not used
        if old_props.memoize(new_props.props()) {
            tracing::trace!("Memoized props for component {:#?}", scope_id,);
            return;
        }

        // Now diff the scope
        dom.run_and_diff_scope(to, scope_id);

        let height = dom.runtime.get_state(scope_id).height;
        dom.dirty_scopes.remove(&ScopeOrder::new(height, scope_id));
    }
  • 函数不同就替换。 同一个槽位换了一个组件函数(render_fn 是函数指针),旧 scope 整个销毁,新建一个 scope。
  • memoize 为真就什么都不做。 子组件不重跑,它上一次的 VNode 和挂载信息原样保留。memoize 是 #[derive(Props)] 生成的:没有 signal 类字段时就是 PartialEq;有 ReadSignal 之类字段时会把旧 props 里的 signal 指向新值再比较(见 #[component] 与 Props)。没有参数的组件,props 是 (),memoize 恒为 true:父组件怎么重跑,它都不跟着跑。
  • 否则就地重跑子组件,然后把它从 dirty_scopes 里删掉:如果它本来也被 signal 标脏了,这一次已经顺带跑过,调度器不会再跑一次。
// 4. a component's props
fn props_app() -> Element {
  let n = step();
  // label changes at step 2 only
  let label = if n >= 2 { "B" } else { "A" };
  rsx! {
    h1 { "parent {n}" }
    Child { label }
  }
}

#[component]
fn Child(label: String) -> Element {
  CHILD_RUNS.with(|c| c.set(c.get() + 1));
  rsx! { p { "child {label}" } }
}
===== 4 component props
--- rebuild (step 0): 7 edits
  LoadTemplate <h1> index=0 id=1
  CreateTextNode "parent 0" id=2
  ReplacePlaceholder path=[0] m=1
  LoadTemplate <p> index=0 id=3
  CreateTextNode "child A" id=4
  ReplacePlaceholder path=[0] m=1
  AppendChildren id=0 m=2
--- step 1: 1 edits
  SetText "parent 1" id=2
  Child has run 1 times
--- step 2: 2 edits
  SetText "parent 2" id=2
  SetText "child B" id=4
  Child has run 2 times

第 1 步父组件重跑,label 还是 "A",Child 没有跑(累计仍是 1 次);第 2 步 label 变了,Child 重跑一次,它自己的 diff 发出 SetText "child B"。子组件的指令和父组件的指令混在同一批里,顺序就是 diff 走到它的顺序。

ElementId:从 slab 里分配,删了就复用

渲染器那边每个需要被引用的 DOM 节点(模板的根、动态文本、占位符、带动态属性的元素)都有一个 ElementId,Rust 和 JS 两侧都用它当下标。分配和回收是 arena.rs 里的 next_element 和 reclaim:

    pub(crate) fn next_element(&mut self) -> ElementId {
        let mut elements = self.runtime.elements.borrow_mut();
        ElementId(elements.insert(None))
    }

    pub(crate) fn reclaim(&mut self, el: ElementId) {
        if !self.try_reclaim(el) {
            tracing::error!("cannot reclaim {:?}", el);
        }
    }

    pub(crate) fn try_reclaim(&mut self, el: ElementId) -> bool {
        // We never reclaim the unmounted elements or the root element
        if el.0 == 0 || el.0 == usize::MAX {
            return true;
        }

        let mut elements = self.runtime.elements.borrow_mut();
        elements.try_remove(el.0).is_some()
    }

elements 是一个 Slab,insert 优先复用最近释放的下标。0 号永远是根容器,不回收。上面 if/else 实验里 id 在 1、2 和 3、4 之间来回跳就是这个原因:替换时先建新节点再删旧节点,所以新节点总拿不到刚要释放的那两个 id,要等下一轮。

回收顺序也是有意安排的。remove_node_inner 依次:回收挂在模板内部元素上的属性 id(reclaim_attributes),递归移除路径长度 ≥ 2 的内部动态节点(remove_nested_dyn_nodes,这些不发指令,因为它们会随根一起从 DOM 上消失,但 id 要回收、组件要销毁),最后处理各个根(reclaim_roots,只有根才发 Remove / ReplaceWith)。注释说 "This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim)":创建时先给根分配、后给内部节点分配,回收时倒过来,slab 的空闲链表里 id 的顺序就和原来一致,下一次同样的结构会拿回同样的 id(实验第 2 步的 id=1、id=2)。

📝NOTE
冷知识:不发指令的节点,id 也要回收

remove_nested_dyn_nodes 移除内部动态节点时传的是 to = None,不发指令。在 #3782("Reclaim element id for all removed nodes")之前,remove_dynamic_node 里的 dom.reclaim(id) 写在 if let Some(to) = to { .. } 里面,to 为 None 时 id 就不回收;reclaim_roots 对不发指令的静态根也不回收。PR 说明:"element ids associated with nodes removed via fn remove_nested_dyn_nodes are never reclaimed. This causes references to them to hang around, leading to unbounded growth of detached DOM nodes for applications that dynamically add and remove nodes frequently",修的是 #3760(列表长度来回变化时浏览器里脱离文档的 DOM 节点不断增长)。JS 侧按 ElementId 存着节点引用,id 不回收,节点就永远不会被垃圾回收。改动是把 reclaim 挪到 if 外面、给静态根补上 else 分支。首次发布在 v0.7.0-alpha.2,正式版 v0.7.0。

ElementId 被复用意味着它不是稳定标识:渲染器不能假设「id 3 永远是那个 <p>」。web 渲染器每次收到 LoadTemplate、AssignId、CreateTextNode 等指令时都会把新节点写进 JS 侧数组的那个下标,覆盖旧引用,见 WebsysDom 与 interpreter。

❗IMPORTANT
动脑筋:第 5 个例子 rebuild 时,n = 0,title 的值是 None。rebuild 会为 title 发一条指令吗?
  rsx! {
    button {
      title: if n % 2 == 1 { "odd" },
      onclick: move |_| println!("clicked at step {n}"),
      "btn"
    }
  }

数一数 rebuild 一共发几条指令(Log 的记法:LoadTemplate、SetAttribute、NewEventListener、AppendChildren……)。

动脑筋答案

===== 5 conditional attribute + listener
--- rebuild (step 0): 4 edits
  LoadTemplate <button> index=0 id=1
  SetAttribute title=None id=1
  NewEventListener click id=1
  AppendChildren id=0 m=1

会,4 条里有一条 SetAttribute title=None。创建路径(write_attrs → write_attribute)对每个动态属性无条件写一次,不看值是不是 None;diff 路径才比较新旧值。所以第一次渲染时一个值为 None 的条件属性也会产生一条「删除属性」指令,对一个刚克隆出来、本来就没有这个属性的元素是空操作。NewEventListener click:监听器只在创建时登记一次,名字去掉了 on 前缀,之后的 diff 永远不再碰它。

小结

  • diff_scope:组件返回错误或挂起时不 diff;在挂起的边界下 diff 照走,但 to 是 None,不写指令。
  • 挂载信息(根的 ElementId、动态节点的 ElementId 或 ScopeId、父节点)不在 VNode 里,在 Runtime.mounts;模板相同时 move_mount_to 把它交给新 VNode,事件因此找到新闭包。
  • 模板相同只比动态属性和动态节点,静态部分零成本;模板不同就先建新节点、最后一个旧根用 ReplaceWith 顶替,旧模板里的组件状态全部销毁。if / else 的两个分支就是两个模板。
  • 动态节点:文本不等才 SetText,占位符对占位符不动,Fragment 交给列表 diff,组件比 props,种类不同就创建加替换(Option<String> 的 None/Some 切换即此)。
  • 属性按名字归并;监听器永远不重发,volatile 属性总是重写,删除就是写 None。
  • 组件:函数指针不同就替换;memoize 为真不重跑(无参数组件恒为真);否则就地重跑并移出脏队列。
  • ElementId 是 slab 下标,先进后出地复用;回收顺序和分配顺序相反,所以同样的结构会拿回同样的 id。

💬 关联讨论

发起讨论
加载中...