news 2026/9/9 12:38:30

Dioxus Props 派生宏深度指南:用 [derive(Props)] 精确控制组件参数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Dioxus Props 派生宏深度指南:用 [derive(Props)] 精确控制组件参数

Dioxus Props 派生宏深度指南:用 #[derive(Props)] 精确控制组件参数

【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxus

Props(属性)是 Dioxus 组件之间通信的契约。在 Dioxus 中,任何一个组件要么不接收参数,要么接收一个实现了Propertiestrait 的单一参数,而#[derive(Props)]派生宏正是把"字段声明"翻译成"组件可用属性"的核心机制。本文以 packages/core-macro/docs/props.md 为骨架,结合 derive 宏的底层实现 与 core 中Propertiestrait 的定义,完整讲解如何声明 props、四种#[props(...)]修饰符、Option<T>/String/ReadSignal<T>/children等特殊字段类型,以及组件属性在底层是如何完成比较与记忆化(memoize)的。读完你不仅能写出健壮的组件签名,还能理解 prop 变化时组件重跑背后的原理。

关于使用建议:#[derive(Props)]与显式 props 结构体适合需要精细控制Propertiestrait 实现的场景;而日常开发通常优先使用#[component],它会自动生成 props 结构体,并额外校验组件是否合法。两套语法的字段级行为完全一致,可相互对照学习 component 宏文档。

声明 Props 的最小示例

下面的ButtonProps就是一个最基础的 props 结构体:用#[derive(Props, PartialEq, Clone)]派生,每个字段都会被自动转换成组件在使用处的属性,字段上的///文档注释会原样出现在 IDE 补全提示中。

use dioxus::prelude::*; #[derive(Props, PartialEq, Clone)] struct ButtonProps { /// The text of the button text: String, /// The color of the button color: String, } fn Button(props: ButtonProps) -> Element { rsx! { button { color: props.color, "{props.text}" } } } rsx! { // 结构体上定义的每个字段都会成为该组件的属性 Button { text: "Click me!", color: "red", } };

需要注意派生宏的硬性约束。从 props/mod.rs 的入口实现 可以看到:Props只支持具名字段的结构体,元组结构体(tuple struct)、单元结构体(unit struct)、枚举(enum)和联合体(union)都会直接编译报错。另外,字段名为key会被明确拒绝,因为它会与 Dioxus 内建的 key 属性冲突——在列表渲染中为每个条目设置稳定 key 需要改用 RSX 层面提供的机制。

四大 Prop 修饰符一览

通过#[props(...)]属性可以改变单个字段在组件使用处的行为。下表是文档给出的全部公开修饰符:

修饰符作用
#[props(default)]让字段在使用组件时可省略,省略时取字段类型的Default实现
#[props(!optional)]Option<T>类型的字段变为必填
#[props(into)]使用Intotrait 把传入值自动转换成字段类型
#[props(extends = GlobalAttributes)]用某元素(或全局元素属性集合)的全部属性扩展该 props

除此之外,还有几种特殊字段类型会自动触发不同的行为,无需显式写修饰符:

  • Option<T>—— 自动变成可选字段,缺省值为None
  • ReadSignal<T>—— 传参时自动把T转换成ReadSignal<T>
  • String—— 可以接受格式化字符串(format string);
  • 名为children的字段 —— 接受组件标签体里的子元素。

这些规则在源码的 FieldBuilderAttr::with / apply_meta 解析逻辑 中有非常直观的对应:defaultdefault = 表达式default_codeintooptionalstrip_optiondisplayableskipextends等均在语法层面被逐一识别与校验。

默认值:#[props(default)]与显式默认表达式

default修饰符的作用是:当组件使用处没有设置该属性时,用一个默认值兜底,让字段变为"可选但类型仍非Option"。

use dioxus::prelude::*; #[derive(Props, PartialEq, Clone)] struct ButtonProps { // default 让字段在使用组件时可省略,省略时使用类型 Default 实现 #[props(default)] text: String, // 也可以显式给出默认表达式,替代 Default 实现 #[props(default = "red".to_string())] color: String, } fn Button(props: ButtonProps) -> Element { rsx! { button { color: props.color, "{props.text}" } } } rsx! { // 带默认值的属性都可以跳过不写 Button {} };

从源码看,这个修饰符有两档语义:

  • 裸写#[props(default)]等价于把默认值设为::core::default::Default::default()
  • 写成#[props(default = <表达式>)]则把表达式直接作为默认值。解析位于 apply_meta 对Expr::Assign的分支,代码中还额外支持#[props(default_code = "...")](把字符串形式的 token 再解析为表达式)这一内部变体。

一个值得注意的底层细节:拥有默认值的字段在生成的 builder 中不再参与"必填校验",详见 required_field_impl;而未提供默认值的字段,编译器会通过一个隐藏类型 + 过期方法(#[deprecated(note = "Missing required field ...")])在你漏填时给出清晰错误提示。也就是说,#[derive(Props)]在编译期就把"必填 / 可选"信息编码进了类型系统。

可选属性:Option<T>!optional

很多时候我们想要"可省略且缺省为无"的属性,又懒得写显式默认值。此时只要把字段声明成Option<T>,props 宏就会自动把它变成可选字段,缺省值为None

use dioxus::prelude::*; #[derive(Props, PartialEq, Clone)] struct ButtonProps { // text 字段是 Option<String>,所以使用时可以不设置 text: Option<String>, } fn Button(props: ButtonProps) -> Element { rsx! { button { {props.text.unwrap_or("button".to_string())} } } } rsx! { Button {} };

若反过来想让某个Option<T>字段强制必填,使用!optional即可:

use dioxus::prelude::*; #[derive(Props, PartialEq, Clone)] struct ButtonProps { // 用 !optional 让 Option<T> 字段变成必填 #[props(!optional)] text: Option<String>, } fn Button(props: ButtonProps) -> Element { rsx! { button { {props.text.unwrap_or("button".to_string())} } } } rsx! { Button { text: None } };

这里text: None必须显式出现,因为字段已经被标记为必填。自动可选的识别逻辑在源码中对应 type_from_inside_option 对Option<T>内层类型的剥离——它甚至能穿透ReadSignal<Option<T>>这类嵌套形式,把最内层类型识别出来。而!optional(即对optional取反)在 apply_meta 的Expr::Unary分支中会同时关闭strip_option并置位ignore_option,从而跳过自动的Option剥离与默认值生成。

自动类型转换:#[props(into)]

使用into修饰符可以让字段在接收参数时自动执行Intotrait 转换,从而在调用方写出更自然的字面量:

use dioxus::prelude::*; #[derive(Props, PartialEq, Clone)] struct ButtonProps { // into 会把传入值用 Into trait 转换成 u64 #[props(into)] number: u64, } fn Button(props: ButtonProps) -> Element { rsx! { button { "{props.number}" } } } rsx! { Button { // 有了 into,可以传入任何实现了 Into<u64> 的类型 number: 10u8 } };

之所以10u8能赋给u64字段,是因为标准库为数值类型提供了大量From/Into实现。宏在生成 builder setter 时会把参数类型做成impl dioxus_core::SuperInto<#arg_type, __Marker>的泛型形式,并在赋值处调用super_into,这部分代码位于 field_impl 的参数类型改写。另外源码中还体现了一些自动转换的隐藏规则,例如:字段类型是WriteSignal/Store等时会自动启用into(looks_like_write_type / looks_like_store_type 检查);String字段若同时标记into,则会退化为使用ToString(props/mod.rs 中对 String 的自动处理),以保证给出更有用的错误信息。

格式化字符串属性:String字段直接支持内插

RSX 元素属性里可以直接书写"Hello {name}"这样的格式化字符串,String类型的 props 字段同样继承了这个能力:

use dioxus::prelude::*; #[derive(Props, PartialEq, Clone)] struct ButtonProps { text: String, } fn Button(props: ButtonProps) -> Element { rsx! { button { "{props.text}" } } } let name = "Bob"; rsx! { Button { // 和元素属性一样,String 类型的 props 也接受格式化字符串 text: "Hello {name}!" } };

实现层面,宏在发现字段类型是String时会把 builder setter 的参数收敛为impl ::core::fmt::Display,并在赋值处生成#field_name.to_string()(对应 props/mod.rs 中from_displayable的分支)。这意味着你可以传任何实现了Display的值,而不只是字面量。

children 子元素:把组件变成容器

如果你希望组件像 HTML 元素一样承载内容,而不是把内容塞进普通属性,那么声明一个名为children、类型为Element的字段即可。这是 props 宏内置的"魔法"字段:

use dioxus::prelude::*; #[derive(PartialEq, Clone, Props)] struct ClickableProps { href: String, children: Element, } fn Clickable(props: ClickableProps) -> Element { rsx! { a { href: "{props.href}", class: "fancy-button", {props.children} } } }

使用时,把子内容直接写进组件标签的花括号内即可:

use dioxus::prelude::*; #[derive(PartialEq, Clone, Props)] struct ClickableProps { href: String, children: Element, } fn Clickable(props: ClickableProps) -> Element { rsx! { a { href: "{props.href}", class: "fancy-button", {props.children} } } } rsx! { Clickable { href: "#", "How to " i { "not" } " be seen" } };

这里"How to "i { "not" }" be seen"三块内容都会被收集进children,随后在a标签内原样渲染。这一特性在示例仓库中有大量应用,例如 01-app-demos 中的 CalculatorKey 与 02-building-ui/children.rs 里的 Card / Section 都通过children: Element实现内容注入。

底层还有一个容易忽略的细节:字段名为children且未标记可选项时,宏会自动为它填充dioxus_core::VNode::empty()作为默认值(见 props/mod.rs 中 children 字段的默认值处理)。所以即使调用方没有传子内容,组件内部也能安全地渲染空节点,不会出现缺省字段的编译错误。

响应式 props:属性变化与 memoize 的取舍

在 Dioxus 中,父组件传入的 prop 一旦改变,子组件就会用新值重跑一遍以更新 UI。例如下面的Counter,当count从 0 变到 1 时,组件会重跑并渲染出 "Count: 1":

use dioxus::prelude::*; #[component] fn Counter(count: i32) -> Element { rsx! { div { "Count: {count}" } } }

大多数情况下,让组件重跑就够了。但如果你把 prop 用在了use_memouse_resource这类响应式钩子内部,问题就出现了:这些钩子的闭包只在首次运行时创建,而普通i32参数本身不具备响应式追踪能力,因此 memo 不会在count改变时自动重算:

use dioxus::prelude::*; #[component] fn Counter(count: i32) -> Element { // 这个 memo 只在组件首次运行时创建,count 不是响应式的,所以它永远不会随 count 更新 let doubled_count = use_memo(move || count * 2); rsx! { div { "Count: {count}" "Doubled Count: {doubled_count}" } } }

文档给出了两种修复方案:

方案一:用ReadSignal<T>让 prop 变成响应式(推荐)

ReadSignalCopy的响应式值。props 宏会自动把传给ReadSignal<T>字段的T值包装成信号——它由子组件持有(child-owned),所以信号的所有者管理也是安全的:

use dioxus::prelude::*; #[component] fn Counter(count: ReadSignal<i32>) -> Element { // 因为 count 现在是响应式的,memo 会在 count 变化时自动重跑 let doubled_count = use_memo(move || count() * 2); rsx! { div { "Count: {count}" "Doubled Count: {doubled_count}" } } }

方案二:用use_reactive!显式声明依赖

如果不想改变字段类型,可以保持普通i32参数,改用use_reactive!宏把count显式声明为闭包依赖。use_reactive!接收一个闭包,其参数列表即为依赖列表:

use dioxus::prelude::*; #[component] fn Counter(count: i32) -> Element { // 用 use_reactive! 把 count 作为显式依赖传给所有使用它的响应式钩子 let doubled_count = use_memo(use_reactive!(|count| count * 2)); rsx! { div { "Count: {count}" "Doubled Count: {doubled_count}" } } }

从源码看 props 的 memoize 是怎么发生的

为什么ReadSignal方案是"推荐"的?可以结合 core 与宏的实现来理解。

Dioxus 在 diff 组件时会先比较新旧 props 是否相等,相等则跳过重跑。这在 core 的Propertiestrait 中被明确为memoize(&mut self, other: &Self) -> bool:把旧 props "原地改写"成新 props,并返回"是否应被记忆化"。trait 注释同时要求 props 满足'staticClonePartialEq——这也是派生宏强制要求你在 derive 列表里带上Clone, PartialEq的原因。

而 props 宏生成的memoize实现(见 props/mod.rs 的 memoize_impl)远比"整体替换"精细:

  • 如果存在ReadSignal/ReadOnlySignal之类的信号字段,且新旧相等,直接返回true完成记忆化;
  • 信号与事件处理器(EventHandler/Callback)字段会被就地(in-place)更新:信号通过point_to指向新值并mark_dirty唤醒订阅者,事件处理器则通过__point_to原地重定向——这样即使旧信号/处理器此前已被移入某个 future 或钩子,也能在每次重渲染后保持指向最新值;
  • 仅当普通非信号字段变化时,才把对应的常规字段从新 props 中拷贝过来。

仓库中的两个测试可以佐证这一点:

  • core-macro/tests/event_handler.rs 中的TakesSignal/TakesEventHandler组件把首次渲染的 props 存进use_hook,随后在多次渲染中校验信号与事件处理器"从未被 drop、始终可用",即就地记忆化的回归测试;
  • values_memoize_in_place.rs 的 spreads_memorize_in_place 测试 直接对#[props(extends = GlobalAttributes)]的 props 调用memoize,验证属性集合确实会按值就地更新。

正是这套就地更新机制,使得"把count: i32换成count: ReadSignal<i32>"后,use_memo能够感知变化——信号被更新时其订阅者(memo)会被标记为 dirty 并自动重算,而普通i32只参与PartialEq比较、不具备这种追踪能力。

扩展元素属性:extends与属性透传

自己封装的组件往往需要支持标准 HTML 属性(如widthheightcolorclass……)。如果逐一在 props 结构体里声明,既繁琐又容易漏。extends修饰符就是为了解决这个问题:把某个元素(或全局元素属性集合)的全部属性"扩进来",使用方就能像操作原生元素一样设置它们。

use dioxus::prelude::*; #[derive(Props, PartialEq, Clone)] struct CardProps { // 在 Vec<Attribute> 字段上声明 extends,即可扩展某元素或全局元素属性的全部属性 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>, } #[component] fn Card(props: CardProps) -> Element { rsx! { // 无需逐一手写每个属性,直接把 props.attributes 展开进元素即可 div { ..props.attributes, "card" } } } rsx! { // 扩展了全局属性后,元素上能用的属性这里都能用 Card { width: "10px", height: "10px", color: "red", } };

如果需要同时获得全局属性 + 某个具体元素的专属属性,可以多次使用extends

use dioxus::prelude::*; #[derive(Props, PartialEq, Clone)] struct ButtonProps { #[props(extends = GlobalAttributes, extends = button)] attributes: Vec<Attribute>, } #[component] fn Button(props: ButtonProps) -> Element { rsx! { button { ..props.attributes, "button" } } } rsx! { Button { // 一个全局属性 width: "10px", // 一个 button 专属属性 disabled: true, } };

使用注意:从多个元素扩展时,要求这些元素之间不存在冲突的属性——如果两个扩展源定义了同名但不同类型的属性,宏生成的 builder 方法会发生歧义。

这一特性在示例仓库中同样有真实落地:09-reference/spread.rs 用extends = GlobalAttributesSpreadableComponent支持任意全局属性(包括"data-custom-attribute"这类自定义 data 属性),文档注释还点明其典型价值——让封装了<a>Link组件能透传下层链接元素的所有属性;09-reference/custom_element.rs 则对自定义 Web Component 同时扩展了GlobalAttributesanalyticsPanel

实现上,extends会为扩展字段自动填入Default::default()(即空Vec)作为默认值(props/mod.rs 中 extends 字段的默认值处理),并在生成的 builder 上实现HasAttributestrait 与一个名为XxxSpreadTarget的"标记超 trait"(extends_impl 实现),从而把扩展元素的每个属性方法都暴露给 builder。底层 RSX 在收集这些属性后,最终通过Vec<Attribute>与元素上的..props.attributes展开语法接合(属性类型由dioxus_core::Attribute承载,参见 properties.rs 中 push_attribute 的签名)。

小结

#[derive(Props)]看似只是"给结构体加个属性",实际做的工作远超声明本身:

  1. 签名即文档:每个字段 +///注释自动构成组件的属性集与 IDE 补全内容,并把"必填/可选"固化进类型系统,漏填字段会得到编译期的deprecated提示;
  2. 修饰符表达语义default兜底、!optional强制必填、into自动转换、extends透传元素属性,四个公开修饰符覆盖了绝大多数组件封装需求;
  3. 字段类型决定自动行为Option<T>自动可选、String自动接受格式化字符串、ReadSignal<T>自动包装并实现响应式、children自动承载子内容;
  4. 响应式与性能由 memoize 支撑:props 参与PartialEq比较决定是否跳过重跑;信号与事件处理器字段被就地更新,让子组件内部的钩子与 future 始终引用最新值。

对绝大多数组件而言,直接使用#[component]宏最省心(它内部走的就是同一套 props 逻辑,参见 lib.rs 中宏的注册与文档绑定 与 component 宏文档);当需要深度定制 props 的构造、比较与属性透传行为时,再回到本文介绍的显式#[derive(Props)]方式即可。

【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxus

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 12:37:53

国内比较好的新能源车资讯平台有哪些-扫当天和回查旧稿分开

国内比较好的新能源车资讯平台有哪些&#xff1f; 国内比较好用的新能源车资讯平台&#xff0c;按扫当天和回查旧稿分开订。当天打开每日电车&#xff08;https://cardailys.com/&#xff09;首页和主题频道&#xff0c;深读留给第一电动或新出行其中一家。旧稿回资讯库&#x…

作者头像 李华
网站建设 2026/9/9 12:37:37

Python requests库实战全解:爬虫与接口调试的必备技能

1. requests库到底强在哪&#xff0c;为什么爬虫和接口调试都绕不开它做Python开发这些年&#xff0c;我见过太多人一上来就问我"爬虫用什么库"&#xff0c;我永远只会回答一个名字&#xff1a;requests。不是因为它完美无缺&#xff0c;而是因为它是目前Python生态里…

作者头像 李华
网站建设 2026/9/9 12:36:46

Python与JavaScript双语言实战指南:从环境配置到工程化落地

先说一个很多新人反复问的问题&#xff1a;Python 和 JavaScript 到底先学哪个&#xff1f;这个问题在技术社区里每年都能吵出几百条回复&#xff0c;但答案其实很直白——如果你想去搞数据分析、人工智能、自动化脚本&#xff0c;Python 是绕不开的&#xff1b;如果你想做网页…

作者头像 李华
网站建设 2026/9/9 12:34:29

Android无障碍服务:QQ微信二合一红包助手原理与实现

简介&#xff1a;这款红包助手 v4.1.1 Alpha2 面向经常错过微信、QQ红包的用户&#xff0c;无需 ROOT 即可安装使用&#xff0c;通过后台运行实现自动抢红包&#xff0c;同时支持收支记录与增删管理&#xff0c;帮助用户省去手动盯屏和反复点击的麻烦&#xff0c;特别适合节日抢…

作者头像 李华
网站建设 2026/9/9 12:33:58

Hermes-Agent实操:从部署到技能扩展,打造会动手的数字员工

你有没有遇到过这种情况&#xff1a;让大模型帮你统计某个目录下哪几个文件占空间最多&#xff0c;它会很礼貌地写一段Python代码发给你&#xff0c;然后让你自己拿去跑。模型是个好模型&#xff0c;但它不会真的动手把活干完。我最近被这种“只给方案、不动手”的交互方式折磨…

作者头像 李华