Rust in Everything
基于 dioxus 0.7.10

#[component] 与 Props

主线位置:更新主线里,父组件重跑之后 diff 走到一个组件节点,渲染函数相同时就问这个组件 props 的 memoize:返回 true 就跳过子组件,返回 false 才 run_scope 重跑它。首屏主线里每个组件只跑一次,不涉及 memoize。本页讲 props 这一侧:宏生成了什么,memoize 怎么判断,哪些写法会让它永远判「变了」。diff 怎么调用它在 diff,run_scope 在 Scope 与 hooks。

flowchart TB
  fnrow["fn Row 带参数"] --> attr["component 属性宏"]
  attr --> props["RowProps 结构体"]
  attr --> impls["手写 Clone 和 PartialEq"]
  props --> derive["Props derive"]
  derive --> builder["RowPropsBuilder"]
  derive --> memo["Properties memoize"]
  builder --> vcomp["into_vcomponent"]
  vcomp --> diffc["diff 组件节点"]
  memo --> diffc
  diffc -->|true| skip[跳过子组件]
  diffc -->|false| rerun["run_scope 重跑"]

源码在 packages/core-macro/src/component.rs(#[component])、packages/core-macro/src/props/mod.rs(Props derive,约 1.8k 行,从 typed-builder 改来)、packages/core/src/properties.rs 和 events.rs(Callback)。

#[component] 生成了什么

下面这个组件,cargo expand --offline --release 展开(只留结构体、两个 impl 和函数本身,builder 后面讲):

#[component]
fn Row(label: String, count: i32) -> Element {
  rsx! { "{label}: {count}" }
}
///Properties for the [`Row`] component.
#[allow(missing_docs)]
#[allow(non_camel_case_types)]
struct RowProps {
    label: String,
    count: i32,
}
// ... RowProps::builder()、RowPropsBuilder、impl Properties(见下文)
impl ::core::clone::Clone for RowProps {
    #[inline]
    fn clone(&self) -> Self {
        Self {
            label: ::core::clone::Clone::clone(&self.label),
            count: ::core::clone::Clone::clone(&self.count),
        }
    }
}
impl ::core::cmp::PartialEq for RowProps {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        self.label == other.label && self.count == other.count && true
    }
}
#[allow(non_snake_case)]
fn Row(RowProps { mut label, mut count }: RowProps) -> Element {
    {
        {
            dioxus_core::Element::Ok({
                // ... rsx! 的展开
            })
        }
    }
}
// ... mod Row_completions { pub enum Component { Row {} } },只为编辑器补全

ComponentBody 的 ToTokens 做三件事:

  • 参数收成结构体。 props_struct 生成 {函数名}Props,每个参数一个字段,加 #[derive(Props)],再手写 Clone 和 PartialEq 两个 impl。
  • 函数改成只收一个参数。 comp_fn 把参数列表换成结构体解构模式 RowProps { mut label, mut count }: RowProps,函数体原样放进去。所以组件函数在类型上就是 fn(RowProps) -> Element,满足 ComponentFunction。
  • 三种情况不生成结构体。 没有参数时什么也不生成,函数保持 fn() -> Element;第一个参数名叫 props,或者参数本身就是结构体模式(fn Nav(NavProps { title }: NavProps))时,认为用户自己写了 props 类型,只给函数加一个 #[allow(non_snake_case)]。

validate_component_fn 还拒绝几种函数:没写返回类型(「Must return a <dioxus_core::Element>」)、带生命周期参数、async fn、const fn、带 self。props 不能借用,因为它要被存进 scope、跨多次渲染活着,见下文。

PartialEq 是逐字段 == 连起来,所以字段类型必须实现 PartialEq。少了它,rustc 的报错会指在 #[component] 那一行(实验 crate rsx-fail,Config 只 derive 了 Clone):

#[derive(Clone)]
struct Config {
  depth: u32,
}

#[component]
fn Panel(config: Config) -> Element {
  rsx! { "{config.depth}" }
}
error[E0369]: binary operation `==` cannot be applied to type `Config`
 --> src/main.rs:8:1
  |
8 | #[component]
  | ^^^^^^^^^^^^
  |
note: an implementation of `PartialEq` might be missing for `Config`
 --> src/main.rs:4:1
  |
4 | struct Config {
  | ^^^^^^^^^^^^^ must implement `PartialEq`
  = note: this error originates in the attribute macro `component` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Config` with `#[derive(PartialEq)]`
  |
4 + #[derive(PartialEq)]
5 | struct Config {
  |

反过来只 derive PartialEq 不 derive Clone,报的是 error[E0277]: the trait bound Config: Clone is not satisfied,同样指在 #[component]。

📝NOTE
冷知识:PartialEq 和 Clone 以前是 derive 出来的

0.6 的 #[component] 给 props 结构体写的是 #[derive(Props, Clone, PartialEq)]。标准库的 derive 会给每个泛型参数加上 T: PartialEq、T: Clone 约束,不管字段用没用到 T。#3963 报告了这个问题:「When have generics in Props, it would be required to be Clone and PartialEq even not needed」,例子是 fn C2<T: 'static>(a: Box<T>),Box<T> 自己对任意 T 实现了 PartialEq,却还是报「binary operation == cannot be applied to type &mut C2Props<T>」。#3968「fix: more accurate derive Clone, PartialEq for generic components」改成上面那样逐字段手写(提交说明「convert to manual clone implements」),约束只来自字段本身。首次发布于 0.7.0-alpha.0,正式版是 0.7.0。这也是现在缺 PartialEq 时报错是 E0369「binary operation ==」的原因:报错来自手写的那个 self.config == other.config。

Props derive:类型状态的 builder

rsx! 里写 Row { label: "a", count: 1 },展开成(rsx 的 Component::create_props):

            dioxus_core::DynamicNode::Component({
                use dioxus_core::Properties;
                let __comp = ({
                    dioxus_core::fc_to_builder(Row)
                        .label({ { "a" } })
                        .count({ { 1 } })
                        .build()
                })
                    .into_vcomponent(Row);
                __comp
            }),

fc_to_builder 不调用组件,只借组件函数的类型推出 P,返回 P::builder()。Props derive 生成的 builder 用一个元组类型参数记录「哪些字段已经设过」:

    fn builder() -> RowPropsBuilder<((), ())> {
        RowPropsBuilder {
            fields: ((), ()),
            _phantom: ::core::default::Default::default(),
        }
    }
// ...
impl<__count> RowPropsBuilder<((), __count)> {
    #[allow(clippy::type_complexity)]
    pub fn label(
        self,
        label: impl ::core::fmt::Display,
    ) -> RowPropsBuilder<((String,), __count)> {
        let label = (label.to_string(),);
        let (_, count) = self.fields;
        RowPropsBuilder {
            fields: (label, count),
            _phantom: self._phantom,
        }
    }
}
// ...
impl RowPropsBuilder<((String,), (i32,))> {
    pub fn build(self) -> RowProps {
        let (label, count) = self.fields;
        let label = label.0;
        let count = count.0;
        RowProps { label, count }
    }
}

() 表示没设,(T,) 表示设过。label 只在第一个槽是 () 时有这个方法,build 只在两个槽都是 (T,) 时可用。少设一个必填字段时,能匹配上的是另一个 build 重载:它被标成 #[deprecated(note = "Missing required field count")],还要求一个无法构造的枚举参数,于是先有一条警告,再有一条错误(实验 rsx-fail 的 missing_field):

warning: use of deprecated method `RowPropsBuilder::<((std::string::String,), ())>::build`: Missing required field count
 --> src/bin/missing_field.rs:9:14
  |
9 |   rsx! { Row { label: "a" } }
  |              ^^^^^^^^^^^^^^
  |
  = note: `#[warn(deprecated)]` on by default

error[E0061]: this method takes 1 argument but 0 arguments were supplied
 --> src/bin/missing_field.rs:9:14
  |
9 |   rsx! { Row { label: "a" } }
  |              ^^^^^^^^^^^^^^ argument #1 of type `RowPropsBuilder_Error_Missing_required_field_count` is missing
...

缺字段的名字藏在枚举名 RowPropsBuilder_Error_Missing_required_field_count 和 deprecated 说明里。重复设同一个字段用的是同样的办法(RowPropsBuilder_Error_Repeated_field_label)。这些检查全在类型层面,运行期的 builder 只是一个元组。

几种字段有默认行为,都在 FieldInfo::new 一带:

字段默认行为
Stringsetter 收 impl Display,内部 to_string(),所以 label: 42 能编译
Option<T>可选,默认 None;setter 收 impl SuperInto<Option<T>>,可以直接写 T
名叫 children默认 VNode::empty()(字段是 Option 时默认 None)
ReadSignal<T> / EventHandler / Callbacksetter 收 impl SuperInto<..>,并在 props 自己的 Owner 下转换,见后文

#[props(...)] 能写的项在 apply_meta 里:

写法作用
default / default = 表达式 / default_code = "..."可以不传,用 Default::default() 或给定的表达式
optional等于 default 加 strip_option;!optional 关掉 Option<T> 的自动可选
intosetter 收 impl SuperInto<T>,任何 From 都行;对 String 字段改成 to_string
strip_optionsetter 收 T,存成 Some(T)
displayablesetter 收 impl Display
skipbuilder 上没有这个 setter,必须同时给 default
extends = GlobalAttributes(或 extend)字段类型是 Vec<Attribute>,rsx! 里写的元素属性收进来,可以再用 ..attributes 展开到元素上

实验 props_attrs 把这几种放在一起,用 dioxus_ssr::render 看结果:

#[component]
fn Badge(
  // String fields take `impl Display`
  label: String,
  // Option<T> fields are optional and accept a bare T
  title: Option<String>,
  #[props(default = 3)] size: u8,
  #[props(into)] width: f64,
  #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
  children: Element,
) -> Element {
  rsx! {
    span { title, "data-size": "{size}", "data-width": "{width}", ..attributes,
      "{label}"
      {children}
    }
  }
}

fn app() -> Element {
  rsx! {
    Badge { label: 42, width: 2u8 }
    Badge { label: "new", title: "hi", size: 1, width: 1.5f32, id: "b2", class: "x", em { "!" } }
  }
}
<span data-size="3" data-width="2">42</span><span title="hi" data-size="1" data-width="1.5" id="b2" class="x">new<em>!</em></span>

第一个 Badge 没传 title,None 变成 AttributeValue::None,属性不出现;size 取默认值 3;width: 2u8 经 into 变成 2.0;label: 42 经 Display。第二个的 id、class 不是 Badge 的字段,被 extends = GlobalAttributes 收进 attributes,再由 ..attributes 放到 span 上;em { "!" } 成了 children。

Properties 与 memoize

builder 的产物要实现 Properties:

pub trait Properties: Clone + Sized + 'static {
    /// The type of the builder for this component.
    /// Used to create "in-progress" versions of the props.
    type Builder;

    /// Create a builder for this component.
    fn builder() -> Self::Builder;

    /// Make the old props equal to the new props. Return if the props were equal and should be memoized.
    fn memoize(&mut self, other: &Self) -> bool;

    /// Create a component from the props.
    fn into_vcomponent<M: 'static>(self, render_fn: impl ComponentFunction<Self, M>) -> VComponent {
        let type_name = std::any::type_name_of_val(&render_fn);
        VComponent::new(render_fn, self, type_name)
    }
}

into_vcomponent 把渲染函数、props 和 P::memoize 这个函数指针一起装进 VProps,再类型擦除成 Box<dyn AnyProps> 放进 VComponent(Template 与 VNode 里的 DynamicNode::Component)。三个约束各有来处:

  • 'static:props 存在 scope 里,跨很多次渲染活着,不能借用任何东西。这也是 #[component] 拒绝生命周期参数的原因。
  • Clone:VProps::render 每次渲染都是 self.render_fn.rebuild(self.props.clone()),scope 手里留着一份,组件函数按值拿走一份。所以 props 里放大块数据时,Clone 的代价每次渲染都要付一遍;放 Rc、signal 句柄这种廉价克隆的东西更合适。
  • PartialEq:给 memoize 用。

diff 遇到新旧两个组件节点时(diff_vcomponent):渲染函数地址不同就整个替换;相同就调 old_props.memoize(new_props),true 就直接返回,false 才 run_and_diff_scope 重跑子组件。memoize 拿的是 &mut self:scope 里存的是旧 props,新 props 在 diff 完就丢掉,所以 memoize 要负责在不相等时把新值搬进旧 props,子组件重跑时读到的才是新值。Row 生成的版本:

    fn memoize(&mut self, new: &Self) -> bool {
        let equal = self == new;
        if !equal {
            let new_clone = new.clone();
            self.label = new_clone.label;
            self.count = new_clone.count;
        }
        equal
    }

没有参数的组件用的是 impl Properties for (),memoize 恒为 true:父组件怎么重跑都不会带着它重跑,它只会因为自己读的 signal 变了而重跑。

实验:子组件什么时候重跑

实验 memo_props:一个父组件下面挂七个子组件,每个子组件每跑一次就给自己的计数加一。父组件读一个线程局部的 TICK;用 dom.mark_dirty(ScopeId::APP) 让父组件重跑三轮:第一次构建、TICK 不变重跑、TICK 从 0 变成 1 再重跑。

#[component]
fn Fixed(n: i32) -> Element {
  ran("Fixed");
  rsx! { "{n}" }
}

#[component]
fn Varying(n: i32) -> Element {
  ran("Varying");
  rsx! { "{n}" }
}

#[component]
fn WithHandler(onclick: EventHandler<()>) -> Element {
  ran("WithHandler");
  rsx! { button { onclick: move |_| onclick.call(()), "x" } }
}

#[component]
fn WithChildren(children: Element) -> Element {
  ran("WithChildren");
  rsx! { div { {children} } }
}

#[component]
fn SigRead(n: ReadSignal<i32>) -> Element {
  ran("SigRead");
  rsx! { "{n}" }
}

#[component]
fn SigNoRead(n: ReadSignal<i32>) -> Element {
  ran("SigNoRead");
  rsx! { "static" }
}

#[component]
fn NoProps() -> Element {
  ran("NoProps");
  rsx! { "none" }
}

fn app() -> Element {
  ran("app");
  let tick = TICK.with(|t| t.get());
  rsx! {
    Fixed { n: 1 }
    Varying { n: tick }
    WithHandler { onclick: move |_| {} }
    WithChildren { span { "hi" } }
    SigRead { n: tick }
    SigNoRead { n: tick }
    NoProps {}
  }
}

async fn settle(dom: &mut VirtualDom) {
  dom.render_immediate_to_vec();
  report("  after 1st render_immediate");
  // pick up scopes that were marked dirty by signals during the diff
  while tokio::time::timeout(Duration::from_millis(20), dom.wait_for_work()).await.is_ok() {
    dom.render_immediate_to_vec();
  }
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let mut dom = VirtualDom::new(app);
  dom.rebuild_to_vec();
  report("rebuild");

  dom.mark_dirty(ScopeId::APP);
  settle(&mut dom).await;
  report("app rerun, tick unchanged");

  TICK.with(|t| t.set(1));
  dom.mark_dirty(ScopeId::APP);
  settle(&mut dom).await;
  report("app rerun, tick 0 -> 1");
}

(ran 给一个 BTreeMap 计数,report 按名字顺序打印累计次数,代码略。)debug 构建输出:

rebuild                      Fixed=1 NoProps=1 SigNoRead=1 SigRead=1 Varying=1 WithChildren=1 WithHandler=1 app=1
  after 1st render_immediate Fixed=1 NoProps=1 SigNoRead=1 SigRead=1 Varying=1 WithChildren=2 WithHandler=2 app=2
app rerun, tick unchanged    Fixed=1 NoProps=1 SigNoRead=1 SigRead=1 Varying=1 WithChildren=2 WithHandler=2 app=2
  after 1st render_immediate Fixed=1 NoProps=1 SigNoRead=1 SigRead=1 Varying=2 WithChildren=3 WithHandler=3 app=3
app rerun, tick 0 -> 1       Fixed=1 NoProps=1 SigNoRead=1 SigRead=2 Varying=2 WithChildren=3 WithHandler=3 app=3

父组件跑了 3 次,各子组件:

子组件次数原因
Fixed { n: 1 }11 == 1,memoize 一直是 true
NoProps1() 的 memoize 恒为 true
Varying { n: tick }2第三轮 0 != 1,重跑一次
WithHandler3每轮都重跑,值没变也一样
WithChildren3每轮都重跑
SigRead2第三轮重跑,但不是 diff 让它跑的,见下文
SigNoRead1值变了也没跑

Fixed、Varying、NoProps 是预期中的样子。剩下四个要分开讲。

EventHandler 与 Callback:闭包每次都是新的

EventHandler<T> 就是 Callback<T>。Callback 是两个字段:创建它的 scope origin,和一个 GenerationalBox 里的闭包,所以它是 Copy 的,可以随便移进别的闭包和 future。它的相等是比指针:

impl<Args, Ret> PartialEq for Callback<Args, Ret> {
    fn eq(&self, other: &Self) -> bool {
        self.callback.ptr_eq(&other.callback) && self.origin == other.origin
    }
}

rsx! 里写 onclick: move |_| {},setter 每次都 Callback::new 一个新的 GenerationalBox,指针每次都不同,于是 props 的 == 每次都是 false,子组件跟着父组件每次重跑。这就是 WithHandler 跑了 3 次的原因。

宏为回调字段生成的 memoize 还多做一件事:先算 let equal = self == new;,再对每个回调字段调 self.onclick.__point_to(&new.onclick),让旧的 Callback 句柄指向新闭包。这样子组件即使被跳过,它之前移进 future 或子闭包里的那个旧句柄,调用时跑的也是最新的闭包。它保证的是「不用旧闭包」,不是「不重跑」。

📝NOTE
没有蠢问题

问: Callback 的 callback 字段上有一段注释,说「During diffing components with EventHandler, we move the EventHandler over in place instead of rerunning the child component」,例子里还写着「Diffing Child will not rerun this component」。为什么实验里它每次都重跑?

答: 那段注释写的是旧行为。0.5 的 EventHandler 的 PartialEq 是 fn eq(&self, _: &Self) -> bool { true },回调字段永远「相等」,所以那时确实只换闭包、不重跑。0.6.0 起(#3271 的改动里)Callback 的相等改成了上面的指针比较,注释没跟着改。目前上游还开着一个 PR #5685,打算让 Element props 按值比较,但对闭包仍然保持现状:「event listeners and EventHandler/Callback props pointing at fresh closures compare unequal, so those subtrees re-render exactly as before」。所以在 0.7.10 里,传闭包的子组件就当它每次都重跑。

想让它被跳过,就每次传同一个 Callback。use_callback 在 use_hook 里只 Callback::new 一次,之后每次渲染只把里面的闭包换掉(inner.replace(..)),句柄不变。实验 use_callback_memo:

#[component]
fn WithHandler(onclick: EventHandler<()>) -> Element {
  RUNS.with(|r| r.set(r.get() + 1));
  rsx! { button { onclick: move |_| onclick.call(()), "x" } }
}

fn app() -> Element {
  let cb = use_callback(move |_: ()| {});
  rsx! { WithHandler { onclick: cb } }
}

fn main() {
  let mut dom = VirtualDom::new(app);
  dom.rebuild_to_vec();
  for _ in 0..3 {
    dom.mark_dirty(ScopeId::APP);
    dom.render_immediate_to_vec();
  }
  println!("app rendered 4 times, WithHandler ran {} times", RUNS.with(|r| r.get()));
}
app rendered 4 times, WithHandler ran 1 times

children: Element 也是每次都变

children 字段的类型是 Element,rsx! 里写在组件花括号里的子节点展开成 .children(dioxus_core::Element::Ok({ ... })),这是一个新的 VNode。Element 的 == 落到 VNode 的 ==,即 Rc::ptr_eq(Template 与 VNode),父组件每跑一次就是一个新的 Rc,于是 WithChildren 也每轮都跑,哪怕 span { "hi" } 一个动态部分都没有。

这是一个已知的问题,#1929「Implement memoization by value of Elements passed as props」2024 年 2 月开出来,到现在还开着,上面那个 #5685 的说明里写得很直接:「VNode's PartialEq compared by Rc pointer, so any component receiving an Element prop (most commonly children) re-rendered every time its parent re-rendered, even when the passed nodes were identical」。子组件重跑并不意味着 DOM 被改:它返回的 VNode 和上一次模板相同、动态部分相同,diff 不发任何指令。代价是组件函数本身和它的 hooks 多跑一遍。

ReadSignal props:跳过子组件,但通知订阅者

ReadSignal<T>(旧名 ReadOnlySignal,现在是它的类型别名)字段走另一条路。looks_like_signal_type 只看类型路径最后一段叫不叫 ReadSignal / ReadOnlySignal;Option<ReadSignal<T>> 不算。

传值时,setter 在 props 自己的 Owner 下做 SuperInto(setter 的生成代码里是 dioxus_core::with_owner(self.owner.clone(), move || dioxus_core::SuperInto::super_into(..))),而 SuperFrom<T> for ReadSignal<O> 就是 ReadSignal::new(Signal::new(O::super_from(input)))。所以 SigRead { n: tick } 每次都新建一个 signal,它属于这次的 props;props 里有 signal 或回调字段时,宏额外生成一个 {名字}PropsWithOwner 包装(这里是 SigReadPropsWithOwner),把 Owner 和 props 绑在一起,build() 返回的是这个包装。

关键在 memoize_impl 里 signal 字段那一支(这是宏里 quote! 的模板,#signal_fields 会对每个 signal 字段展开一遍):

                Ok(quote! {
                    // First check if the fields are equal. This will compare the signal fields by pointer
                    let exactly_equal = self == new;
                    if exactly_equal {
                        // If they are return early, they can be memoized without any changes
                        return true;
                    }

                    // If they are not, move the signal fields into self and check if they are equal now that the signal fields are equal
                    #(
                        let mut #signal_fields = self.#signal_fields;
                        self.#signal_fields = new.#signal_fields;
                    )*

                    // Then check if the fields are equal now that we know the signal fields are equal
                    // NOTE: we don't compare other fields individually because we want to let users opt-out of memoization for certain fields by implementing PartialEq themselves
                    let non_signal_fields_equal = self == new;

                    // If they are not equal, we need to move over all the fields that are not event handlers or signals to self
                    if !non_signal_fields_equal {
                        let new_clone = new.clone();
                        #(
                            self.#regular_fields = new_clone.#regular_fields;
                        )*
                    }
                    // Move any signal and event fields into their old container.
                    // We update signals and event handlers in place so that they are always up to date even if they were moved into a future in a previous render
                    #move_signal_fields
                    #move_event_handlers

                    non_signal_fields_equal
                })

#move_signal_fields 对每个 signal 字段做的是:用 peek() 比较新旧两个 signal 里的值(不订阅),然后 point_to 让旧 signal 指向新 signal 的存储,值不相等时再 mark_dirty() 通知旧 signal 的订阅者。返回值只看非 signal 字段是否相等。

于是:

  • SigNoRead 只有一个 signal 字段,非 signal 字段「全相等」,memoize 返回 true,diff 不重跑它。它的函数体没读 n,没有订阅,mark_dirty 通知不到它,所以一直是 1 次。
  • SigRead 同样被 diff 跳过,但它在渲染时读了 n("{n}"),是 n 的订阅者。第三轮值从 0 变成 1,mark_dirty 把它标脏,它因为自己的订阅而重跑。输出里 after 1st render_immediate 那一行它还是 1,等 wait_for_work 返回、再跑一次 render_immediate 才变成 2:标脏发的是调度消息,不在当前这次 diff 里处理,见 调度。
  • 第二轮值没变(都是 0),peek 比较相等,不 mark_dirty,谁都不跑。

这正是 ReadSignal props 的用途:子组件里的 use_memo、use_effect 读这个 signal 就能跟着 props 变,不需要整个子组件重跑;普通值 props 做不到这一点,因为 hook 里的闭包只在第一次渲染时捕获了旧值,见 Memo 与 effect。

📝NOTE
冷知识:ReadSignal props 以前是搬值,克隆一下就会 panic
早先的 memoize 在值不相等时把新 signal 里的值取出来塞进旧 signal((#signal_fields).__set(new.#signal_fields.__take())),回调字段也一样(self.#name.__set(new.#name.__take()))。#2331「Diffing cloned Components with readonlysignal panics on double drop」报告:「Any props with the T -> ReadOnlySignal<T> logic fail to diff if you try to render it multiple times」,例子是把同一段 rsx! 克隆一份、两处都渲染,第一处 diff 已经把值搬走,第二处再 diff 就 panic。#2839「Fix cloned rsx double drop」先给 generational-box 加了引用计数的指针(提交说明「Implement Rc generational pointers」),然后「Move over the pointer instead of the data in ReadOnlySignal props」「use rc generational boxes for event handlers as well」,即现在的 point_to 加 mark_dirty 和回调的 __point_to。首次出现在 0.6.0-alpha.3,正式版 0.6.0。

小结

  • #[component] 把参数收成 {名字}Props,派生 Props、逐字段手写 Clone 和 PartialEq,函数改成解构这个结构体;没参数、参数叫 props 或是结构体模式时不生成。
  • Props derive 生成类型状态 builder:缺必填字段、重复设字段都是编译错误,错误信息藏在 ..._Error_Missing_required_field_x 这样的类型名里。String 收 Display,Option<T> 自动可选,children 默认空,#[props(default, into, optional, extends = GlobalAttributes, ...)] 调整这些行为。
  • Properties::memoize 决定子组件重不重跑:相等返回 true 跳过;不等时把新值搬进 scope 里的旧 props 再返回 false。'static 因为 props 存在 scope 里,Clone 因为每次渲染都克隆一份交给组件函数。
  • 闭包(EventHandler / Callback)和 children: Element 按指针比,父组件每次重跑它们都判「变了」,子组件跟着重跑;Callback 字段上的注释说不会重跑,那是 0.5 的行为。
  • ReadSignal 字段不参与这个判断:memoize 让旧 signal 指向新存储,值变了就 mark_dirty,子组件只有读过它才会重跑,而且是在下一轮调度里。

💬 关联讨论

发起讨论
加载中...