Rust in Everything
基于 dioxus 0.7.10

rsx! 展开成什么

主线位置:更新主线里 run_scope 重跑组件,组件函数返回的那个 VNode 就是本页这段宏展开在运行期的产物;首屏主线里服务端每渲染一个组件也是同一段代码。本页只讲宏这一侧:rsx! 在编译期把一棵树切成哪几块、给动态部分怎么编号。运行期这些数据结构长什么样在 Template 与 VNode,新旧两棵怎么比在 diff。

flowchart TB
  tokens["rsx 的 token"] --> callbody[CallBody 解析]
  callbody --> bodynode["BodyNode 六种节点"]
  bodynode --> visitor["DynIdVisitor 编号"]
  visitor --> idx["cascade 模板序号"]
  idx --> totokens["TemplateBody 生成代码"]
  totokens --> rel["release: static Template"]
  totokens --> dbg["debug: 热重载表"]
  rel --> vnode["VNode::new"]
  dbg --> vnode
💡TIP
打个比方:印好的表格和填空

rsx! 像是在编译期印一张表格。表格上印死的标题、栏目名(TemplateNode::Element、TemplateNode::Text、TemplateAttribute::Static)每次都一样;需要每次现填的格子印成带编号的空格(TemplateNode::Dynamic { id }、TemplateAttribute::Dynamic { id }),旁边另附一张「第几个空在第几页第几行」的索引(node_paths、attr_paths)。组件每跑一次,就拿同一张表、按编号填一遍空(dynamic_nodes、dynamic_attrs)。两次填的表要比较时,只看空格里写了什么。

比方到这里为止:真实的空格里不只能写字,还能贴进另一整张表(for、if 的结果是嵌套的 VNode),每张小表也是自己印好的。

源码在 packages/rsx/src(约 5.7k 行),入口在 packages/core-macro/src/lib.rs。dioxus::prelude::rsx 就是这个过程宏。

从 token 到 CallBody

过程宏本身只有几行,rsx:

#[proc_macro]
pub fn rsx(tokens: TokenStream) -> TokenStream {
    match syn::parse::<rsx::CallBody>(tokens) {
        Err(err) => err.to_compile_error().into(),
        Ok(body) => body.into_token_stream().into(),
    }
}

解析和生成都在 dioxus-rsx 里。CallBody 是整个宏调用的内容:一个根 TemplateBody,加一个给模板发序号的计数器 template_idx。TemplateBody 是「一组处在模板根位置的节点」,里面每个节点是一个 BodyNode:

pub enum BodyNode {
    /// div {}
    Element(Element),

    /// Component {}
    Component(Component),

    /// "text {formatted}"
    Text(TextNode),

    /// {expr}
    RawExpr(ExprNode),

    /// for item in items {}
    ForLoop(ForLoop),

    /// if cond {} else if cond {} (else {}?)
    IfChain(IfChain),
}

BodyNode::parse 只看开头一两个 token 就决定是哪一种:字符串字面量是 Text;for、if 各自对应;match 和花括号都当 RawExpr;带 - 的标识符(my-element)是 web component,按元素处理;单个标识符、首字母小写、不含下划线是元素;其余(大写开头、带下划线、带 :: 的路径)都是组件。所以 my_button {} 会被当成组件,div {} 不会。

一个 rsx! 里有几个模板

一个 rsx! 不一定只生成一个模板。CallBody::new 解析完之后,用 cascade_hotreload_info 走一遍整棵树,给每个嵌套的 TemplateBody 发一个序号:

    fn cascade_hotreload_info(&self, nodes: &[BodyNode]) {
        for node in nodes.iter() {
            match node {
                BodyNode::Element(el) => {
                    self.cascade_hotreload_info(&el.children);
                }

                BodyNode::Component(comp) => {
                    comp.children.template_idx.set(self.next_template_idx());
                    self.cascade_hotreload_info(&comp.children.roots);
                }

                BodyNode::ForLoop(floop) => {
                    floop.body.template_idx.set(self.next_template_idx());
                    self.cascade_hotreload_info(&floop.body.roots);
                }

                BodyNode::IfChain(chain) => chain.for_each_branch(&mut |body| {
                    body.template_idx.set(self.next_template_idx());
                    self.cascade_hotreload_info(&body.roots)
                }),

                _ => {}
            }
        }
    }

根是 0 号,之后按深度优先:组件的 children、for 的循环体、if 链的每一个分支(包括 else)各占一个序号,各自生成一个独立的模板。元素的子节点不另起模板,它们是父模板静态树的一部分。match 臂里写的 rsx! 是另一次宏调用,有自己的 CallBody,序号从 0 重新数。

这样切的原因在运行期:循环体会跑 N 次、if 只会选中一个分支、children 由父组件生成却挂在子组件里,它们都不能和外层一起印进同一张静态树,只能各印一张,外层在对应位置留一个动态槽。没有 else 的 if 不生成 else 模板,展开时补的是 VNode::empty(),一个占位符。

下面这个例子贯穿本页:

fn app() -> Element {
  let n = 2;
  rsx! {
    div { class: "box", id: "item-{n}",
      h1 { "Hello" }
      "n = {n}"
      for i in 0..n {
        li { "{i}" }
      }
      if n > 1 {
        span { "big" }
      }
    }
  }
}

它有三个模板:根(div 那棵)、for 的循环体(li)、if 的分支(span)。debug 构建的展开里,每个模板都带着自己的序号(下一节讲这段代码是什么),三处分别是:

                5u32,
                3u32,
                0usize,
...
                                        5u32,
                                        3u32,
                                        1usize,
...
                                        5u32,
                                        3u32,
                                        2usize,

5u32, 3u32 是 rsx! 所在的行和列,三个模板共用;最后一个数就是 cascade_hotreload_info 发的序号。

编号:DynIdVisitor

每个 TemplateBody 在 TemplateBody::new 里给自己的节点编号,干活的是 DynIdVisitor。它维护一条 current_path(从根往下每一层的下标),深度优先走一遍,visit 的规则是:

    fn visit(&mut self, node: &BodyNode) {
        match node {
            // Just descend into elements - they're not dynamic
            BodyNode::Element(el) => {
                for (idx, attr) in el.merged_attributes.iter().enumerate() {
                    if !attr.is_static_str_literal() {
                        self.assign_path_to_attribute(attr, idx);
                        // ...
                    }
                }
                // ...
                self.visit_children(&el.children);
            }

            // Text nodes are dynamic if they contain dynamic segments
            BodyNode::Text(txt) => {
                if !txt.is_static() {
                    self.assign_path_to_node(node);
                    self.assign_formatted_segment(&txt.input);
                }
            }

            // Raw exprs are always dynamic
            BodyNode::RawExpr(_) | BodyNode::ForLoop(_) | BodyNode::IfChain(_) => {
                self.assign_path_to_node(node)
            }
            BodyNode::Component(component) => {
                self.assign_path_to_node(node);
                // ...
            }
        };
    }

assign_path_to_node 把 node_paths 当前的长度当作这个节点的动态序号,再把 current_path 压进 node_paths;属性那边同理,attr_paths 里存的是「元素的路径 + 它在 merged_attributes 里的下标」。所以:

  • 动态节点和动态属性各自从 0 编号,按深度优先的出现顺序。
  • 编号只在一个模板内部有效。for 循环体里的 "{i}" 是循环体模板的 0 号,和外层的 0 号无关。
  • 元素自己永远不是动态的;组件、for、if、{expr} 永远是动态的;文本和属性看内容。

例子的根模板编号结果(运行期打出来的,见后文):

动态部分序号路径
id: "item-{n}"dynamic_attrs[0]attr_paths[0] = [0](div)
"n = {n}"dynamic_nodes[0]node_paths[0] = [0, 1]
for i in 0..ndynamic_nodes[1]node_paths[1] = [0, 2]
if n > 1dynamic_nodes[2]node_paths[2] = [0, 3]

路径的第一个数是根的下标(div 是 0 号根),后面是一层层子节点的下标:h1 是 div 的 0 号孩子,"n = {n}" 是 1 号,所以是 [0, 1]。

什么算静态:只有字符串字面量

一段文本或一个属性值能不能印进模板,只有一条标准:它是不是一个不含任何 {} 洞的字符串字面量。文本的判断是 IfmtInput::is_static:

    pub fn is_static(&self) -> bool {
        self.segments
            .iter()
            .all(|seg| matches!(seg, Segment::Literal(_)))
    }

字符串先被切成 Literal 和 Formatted 两种片段,{{、}} 是转义,属于 Literal。属性的判断是 Attribute::as_static_str_literal:

    pub fn as_static_str_literal(&self) -> Option<(&AttributeName, &IfmtInput)> {
        match &self.value {
            AttributeValue::AttrLiteral(lit) => match &lit {
                HotLiteral::Fmted(input) if input.is_static() => Some((&self.name, input)),
                _ => None,
            },
            _ => None,
        }
    }

只有 HotLiteral::Fmted(字符串)可能是静态的;width: 100 是 HotLiteral::Int,hidden: false 是 HotLiteral::Bool,都走动态属性。原因在模板一侧:TemplateAttribute 的 value 是 &'static str,整数和布尔要变成 AttributeValue::Int / Bool 交给渲染器,没有字符串形式可存。

同名属性在编号之前先被 merge_attributes 合并:同名的几个值拼成一个格式化字符串,中间插一个空格。几个都是字面量时合并结果仍是静态的(class: "a", class: "b" 变成静态的 "a b");其中有一个是 if 表达式,合并结果就带一个洞,变成动态。

📝NOTE
没有蠢问题

问: lang: "{GREETING}" 里的 GREETING 是个 const,值在编译期就定了,为什么还是动态属性?

答: 过程宏只看得到 token,看不到类型和常量的值。"{GREETING}" 在宏眼里就是一个带洞的字符串,洞里是一个标识符,它不知道这个标识符是 const 还是局部变量,只能生成 format! 让它运行期求值。{"from expr"} 也一样:花括号里是任意表达式,一律是 RawExpr。代价是每次渲染多一次格式化和一个 String,diff 时再比一次字符串。想让它进模板,就直接写字面量。

cargo expand:release 下展开的样子

cargo expand --offline --release --bin expand_small 展开上面的例子。根模板的展开如下(删了 for 和 if 两段的内部,它们和根模板的结构相同):

fn app() -> Element {
    let n = 2;
    dioxus_core::Element::Ok({
        let __key = None;
        let __dynamic_nodes: [dioxus_core::DynamicNode; 3usize] = [
            dioxus_core::DynamicNode::Text(
                dioxus_core::VText::new(
                    ::alloc::__export::must_use({
                        ::alloc::fmt::format(format_args!("n = {0}", n))
                    }),
                ),
            ),
            {
                let ___nodes = dioxus_core::IntoDynNode::into_dyn_node(
                    (0..n)
                        .into_iter()
                        .map(|i| {
                            dioxus_core::Element::Ok({
                                // ... 循环体模板,dynamic_nodes[0] 是
                                // dioxus_core::VText::new((i).to_string())
                            })
                        }),
                );
                ___nodes
            },
            {
                let ___nodes = dioxus_core::IntoDynNode::into_dyn_node(
                    if n > 1 {
                        {
                            dioxus_core::Element::Ok({
                                // ... span 模板,没有动态部分
                            })
                        }
                    } else {
                        dioxus_core::VNode::empty()
                    },
                );
                ___nodes
            },
        ];
        let __dynamic_attributes: [Box<[dioxus_core::Attribute]>; 1usize] = [
            Box::new([
                {
                    dioxus_core::Attribute::new(
                        dioxus_elements::div::id.0,
                        ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!("item-{0}", n))
                        }),
                        dioxus_elements::div::id.1,
                        dioxus_elements::div::id.2,
                    )
                },
            ]),
        ];
        #[doc(hidden)]
        static __TEMPLATE_ROOTS: &[dioxus_core::TemplateNode] = &[
            {
                dioxus_core::TemplateNode::Element {
                    tag: dioxus_elements::elements::div::TAG_NAME,
                    namespace: dioxus_elements::div::NAME_SPACE,
                    attrs: &[
                        dioxus_core::TemplateAttribute::Static {
                            name: dioxus_elements::div::class.0,
                            namespace: dioxus_elements::div::class.1,
                            value: "box",
                        },
                        dioxus_core::TemplateAttribute::Dynamic {
                            id: 0usize,
                        },
                    ],
                    children: &[
                        {
                            dioxus_core::TemplateNode::Element {
                                tag: dioxus_elements::elements::h1::TAG_NAME,
                                namespace: dioxus_elements::h1::NAME_SPACE,
                                attrs: &[],
                                children: &[
                                    dioxus_core::TemplateNode::Text {
                                        text: "Hello",
                                    },
                                ],
                            }
                        },
                        dioxus_core::TemplateNode::Dynamic {
                            id: 0usize,
                        },
                        dioxus_core::TemplateNode::Dynamic {
                            id: 1usize,
                        },
                        dioxus_core::TemplateNode::Dynamic {
                            id: 2usize,
                        },
                    ],
                }
            },
        ];
        {
            #[doc(hidden)]
            static ___TEMPLATE: dioxus_core::Template = dioxus_core::Template {
                roots: __TEMPLATE_ROOTS,
                node_paths: &[&[0u8, 1u8], &[0u8, 2u8], &[0u8, 3u8]],
                attr_paths: &[&[0u8]],
            };
            #[allow(clippy::let_and_return)]
            let __vnodes = dioxus_core::VNode::new(
                __key,
                ___TEMPLATE,
                Box::new(__dynamic_nodes),
                Box::new(__dynamic_attributes),
            );
            __vnodes
        }
    })
}

逐段看,这些都来自 TemplateBody 的 ToTokens:

  • 外壳永远是 Element::Ok。 rsx! 本身不会失败,Err 只来自组件里的 ?(见 Suspense 与错误边界)。
  • 动态部分是两个定长数组。 长度在编译期就知道(3usize、1usize),每次渲染按编号顺序求值一遍。dynamic_attrs 的每个槽是 Box<[Attribute]> 而不是单个属性,因为 ..attrs 展开会往同一个槽里放多个属性。
  • 静态部分是 static。 __TEMPLATE_ROOTS 和 ___TEMPLATE 都是 static,整个程序里每个 rsx! 模板只有一份,Template 只是三个 &'static 切片,VNode::new 按值拷进去(它是 Copy)。路径直接是字面量数组。
  • 元素名和属性名经过 dioxus_elements。 dioxus_elements::elements::div::TAG_NAME、dioxus_elements::div::id.0 是 dioxus-html 里的常量,名字拼错就编译不过;属性的 .1 是命名空间,.2 是 volatile 标记。
  • for 变成 into_iter().map(...),if 变成 if ... else。 两者都交给 IntoDynNode::into_dyn_node 变成一个 DynamicNode(ForLoop、IfChain 的 ToTokens),迭代器和 Element 各自变成什么,在 Template 与 VNode 里有实验。
  • 单洞字符串在 release 下不走 format!。 循环体里的 "{i}" 展开成 (i).to_string(),"n = {n}" 才是 format!。这是 IfmtInput 的 ToTokens 里 if !cfg!(debug_assertions) 包着的 try_to_string。注意这个 cfg! 判断的是过程宏 crate 自己编译时的设置,cargo 的 release profile 下它是关的。

组件节点展开成 fc_to_builder(Row).label(..).count(..).build().into_vcomponent(Row),这一串在 #[component] 与 Props 里讲。

debug 构建:同一棵树,换一条路

同一个例子不加 --release 展开,文件从 199 行变成 528 行。__TEMPLATE_ROOTS 和两个动态数组完全一样,变的是最后怎么得到 VNode。TemplateBody::to_tokens 里 #[cfg(debug_assertions)] 的几段分别展开成:

        fn __original_template() -> &'static dioxus_core::internal::HotReloadedTemplate {
            static __ORIGINAL_TEMPLATE: ::std::sync::OnceLock<
                dioxus_core::internal::HotReloadedTemplate,
            > = ::std::sync::OnceLock::new();
            if __ORIGINAL_TEMPLATE.get().is_none() {
                _ = __ORIGINAL_TEMPLATE
                    .set(
                        dioxus_core::internal::HotReloadedTemplate::new(
                            None,
                            // ... 两个 vec:HotReloadDynamicNode::Dynamic(0..=2)、HotReloadDynamicAttribute::Dynamic(0)
                            ::alloc::vec::Vec::new(),
                            __TEMPLATE_ROOTS,
                        ),
                    );
            }
            __ORIGINAL_TEMPLATE.get().unwrap()
        }
        let __template_read = {
            use dioxus_signals::ReadableExt;
            // ... __NORMALIZED_FILE = "src/bin/expand_small.rs"
            static __TEMPLATE: dioxus_signals::GlobalSignal<
                Option<dioxus_core::internal::HotReloadedTemplate>,
            > = dioxus_signals::GlobalSignal::with_location(
                || None::<dioxus_core::internal::HotReloadedTemplate>,
                __NORMALIZED_FILE,
                5u32,
                3u32,
                0usize,
            );
            dioxus_core::Runtime::try_current()
                .map(|_| __TEMPLATE.read())
        };
        // ... 没有被热重载替换过就用 __original_template()
        let mut __dynamic_literal_pool = dioxus_core::internal::DynamicLiteralPool::new(
            // ... 一个 vec,两项都是 format!("{0}", n) 再 .to_string()
        );
        // ... __dynamic_nodes / __dynamic_attributes / __TEMPLATE_ROOTS 同 release
        {
            let mut __dynamic_value_pool = dioxus_core::internal::DynamicValuePool::new(
                Vec::from(__dynamic_nodes),
                Vec::from(__dynamic_attributes),
                __dynamic_literal_pool,
            );
            __dynamic_value_pool.render_with(__template_read)
        }

(宏里的 vec![...] 被 cargo expand 展开成很长的 box_assume_init_into_vec_unsafe(...),上面删掉了,用 // ... 注释说明里面是什么。)

这是热重载的接口:每个模板按「文件、行、列、序号」挂一个 GlobalSignal,dx serve 改了 rsx! 里的静态部分时,往这个 signal 里塞一个新的 HotReloadedTemplate,读过它的组件就会重跑。热重载本身不在本卷范围内,这里只关心两点。

第一,debug 下 Template 不是宏写死的。HotReloadedTemplate::new 只拿 roots,node_paths / attr_paths 在运行期从 roots 重新算一遍(node_paths 深度优先找 TemplateNode::Dynamic,debug_assert_eq!(node_paths.len(), *id) 核对顺序),用 Box::leak 换成 &'static,每个调用点只算一次(OnceLock)。release 展开里的 node_paths: &[...] 在 debug 下根本不生成。这个差别会影响模板的比较方式,见 Template 与 VNode。

第二,debug 下每个 {} 洞要多格式化一次。__dynamic_literal_pool 把每个洞单独 format! 再 to_string() 一遍,留给热重载改写文本时用;VText 和 Attribute 里的完整字符串照样格式化。例子里的 n 出现在两个洞里,debug 下每次渲染格式化四次,release 下两次。

两种构建得到的结构是一样的。实验 templates 在组件外直接调 app(),沿 VNode 打出模板地址、路径和动态部分(dump 函数递归进 Fragment,代码略),debug 输出:

debug_assertions = true
template roots@0x1047a04e8 node_paths=[[0, 1], [0, 2], [0, 3]] attr_paths=[[0]]
  dynamic_nodes[0] = Text("n = 2")
  dynamic_nodes[1] = Fragment(len=2)
    template roots@0x1047a01d8 node_paths=[[0, 0]] attr_paths=[]
      dynamic_nodes[0] = Text("0")
    template roots@0x1047a01d8 node_paths=[[0, 0]] attr_paths=[]
      dynamic_nodes[0] = Text("1")
  dynamic_nodes[2] = Fragment(len=1)
    template roots@0x1047a02a0 node_paths=[] attr_paths=[]
  dynamic_attrs[0] = ["id=Text(\"item-2\")"]

--release 下除了第一行和地址,逐字相同。三种 roots 地址对应三个模板;for 跑了两次,两个 li 的 VNode 指向同一个模板,只有 dynamic_nodes[0] 不同。

路径给谁用

node_paths 的第一个数是根的下标,渲染器那边看到的路径要去掉它。创建一个模板时,core 先让渲染器整块克隆静态树,再把每个动态槽里的占位节点换成真正的内容,diff/node.rs 里替换占位节点的那一行是:

                    // The path is one shorter because the top node is the root
                    let path = &self.template.node_paths[dynamic_node_id][1..];
                    to.replace_placeholder_with_nodes(path, m);

例子第一次渲染(rebuild_to_vec() 的 Mutations.edits,压成一行一条)里能看到这一点:根模板 node_paths 的 [0, 3]、[0, 2]、[0, 1] 变成了 ReplacePlaceholder 的 [3]、[2]、[1]:

[ LoadTemplate { index: 0, id: ElementId( 1, ), },
SetAttribute { name: "id", ns: None, value: Text( "item-2", ), id: ElementId( 1, ), },
LoadTemplate { index: 0, id: ElementId( 2, ), },
ReplacePlaceholder { path: [ 3, ], m: 1, },
LoadTemplate { index: 0, id: ElementId( 3, ), },
CreateTextNode { value: "0", id: ElementId( 4, ), },
ReplacePlaceholder { path: [ 0, ], m: 1, },
LoadTemplate { index: 0, id: ElementId( 5, ), },
CreateTextNode { value: "1", id: ElementId( 6, ), },
ReplacePlaceholder { path: [ 0, ], m: 1, },
ReplacePlaceholder { path: [ 2, ], m: 2, },
CreateTextNode { value: "n = 2", id: ElementId( 7, ), },
ReplacePlaceholder { path: [ 1, ], m: 1, },
AppendChildren { id: ElementId( 0, ), m: 1, },
]

这串指令的顺序(为什么动态节点倒着填、m 是什么)在 Mutations。Template 字段上的文档注释说存下来的路径「will be one segment shorter than the path sent to the renderer」,和代码正好相反:存下来的长一段,发给渲染器的短一段。

attr_paths 除了用来给动态属性定位,还用在事件上:Runtime::handle_event 拿事件目标的路径和每个 attr_paths 比 is_descendant,找出路径上挂着的监听器。

空 rsx! 和 key

core 要求每个模板至少有一个根,方便整块替换。normalized 在生成代码前检查:一个节点都没有,就当成写了 {()},而 () 的 IntoDynNode 是占位符。实验 empty_rsx:

let v = rsx! {}.unwrap();
println!("roots={:?} node_paths={:?} dynamic_nodes={:?}", v.template.roots, v.template.node_paths, v.dynamic_nodes);
println!("same Rc as VNode::placeholder(): {}", v == VNode::placeholder());
roots=[Dynamic { id: 0 }] node_paths=[[0]] dynamic_nodes=[Placeholder(VPlaceholder)]
same Rc as VNode::placeholder(): false

形状和 VNode::placeholder() 一样,但是一个新的 Rc,不是那个线程内共享的占位 VNode。

key 不进模板,也不进动态属性:merge_attributes 跳过它,展开里的 let __key = ... 取的是第一个根上的 key。validate_key 要求它是带洞的格式化字符串,写成静态字符串会报「Key must not be a static string. Make sure to use a formatted string like key: "{value}"」;写在第二个及以后的根上只给警告「Keys are only allowed on the first node in the block.」。key 在列表 diff 里怎么用,见 带 key 的列表。

❗IMPORTANT
动脑筋:下面这个 div 的模板里有几个 TemplateAttribute::Dynamic,dynamic_nodes 有几项?

M2 Max、macOS 27、rustc 1.99.0,debug 构建,实验 staticness。先按本页的规则数一遍,再看答案。

const GREETING: &str = "hi";

fn app() -> Element {
  let x = 7;
  let on = true;
  rsx! {
    div {
      class: "a",
      class: "b",
      title: "{{braces}}",
      width: 100,
      hidden: false,
      lang: "{GREETING}",
      dir: if on { "rtl" },
      "plain"
      "{{not a hole}}"
      "{x}"
      "{x:>3}"
      {"from expr"}
    }
  }
}

程序在组件外调 app(),递归打印 v.template.roots,再逐项打印 dynamic_nodes 和 dynamic_attrs。

动脑筋答案

Element <div>
  attr Static class="a b"
  attr Static title="{braces}"
  attr Dynamic { id: 0 }
  attr Dynamic { id: 1 }
  attr Dynamic { id: 2 }
  attr Dynamic { id: 3 }
  Text "plain"
  Text "{not a hole}"
  Dynamic { id: 0 }
  Dynamic { id: 1 }
  Dynamic { id: 2 }
dynamic_nodes[0] = Text(VText { value: "7" })
dynamic_nodes[1] = Text(VText { value: "  7" })
dynamic_nodes[2] = Text(VText { value: "from expr" })
dynamic_attrs[0] = width Int(100)
dynamic_attrs[1] = hidden Bool(false)
dynamic_attrs[2] = lang Text("hi")
dynamic_attrs[3] = dir Text("rtl")

4 个动态属性,3 个动态节点。

  • class 写了两次,两个都是字面量,merge_attributes 合成静态的 "a b",只占一个位置。
  • "{{braces}}"、"{{not a hole}}" 里的双花括号是转义,没有洞,是静态的;模板里存的是转义之后的 {braces}。
  • width: 100、hidden: false 是整数、布尔字面量,不是字符串,只能动态,值是 Int(100)、Bool(false)。
  • "{GREETING}" 引用的是 const,宏不知道,照样是动态的 Text("hi")。
  • dir: if on { "rtl" } 是 if 表达式,动态;on 为 false 时它的值是 AttributeValue::None,渲染器会删掉这个属性。
  • "{x}" 和 "{x:>3}" 都有洞,都是 DynamicNode::Text;格式参数照常生效(" 7")。
  • {"from expr"} 是花括号表达式,哪怕里面是字面量也是动态的,&str 的 IntoDynNode 把它变成 Text。

小结

  • rsx! 解析成 CallBody,节点分六种 BodyNode;根、组件 children、for 循环体、if 的每个分支各是一个模板,元素的子节点不另起模板。
  • DynIdVisitor 按深度优先给每个模板里的动态节点、动态属性各自从 0 编号,路径是从根开始的下标序列,第一个数是根的下标,发给渲染器时去掉。
  • 只有不含洞的字符串字面量是静态的;整数、布尔、const、花括号表达式都是动态的;同名字面量属性合并后仍是静态。
  • release 下每个模板是一个 static Template,路径是字面量;debug 下 Template 由 HotReloadedTemplate::new 在运行期从 roots 算出路径、每个调用点一次,每个洞还要多格式化一次,供热重载用。
  • 空 rsx! 变成一个占位符;key 只取第一个根上的,必须是格式化字符串。

💬 关联讨论

发起讨论
加载中...