3.1 JSX的两种编译命运:模板克隆与createElement


3.1 JSX 的两种编译命运:模板克隆与 createElement

本节摘要:同样一段 JSX,React 编译成"每次渲染都执行的元素工厂调用",Solid 编译成"一份 HTML 模板加少量细粒度绑定"。本节把两份产物并排摊开,解释模板克隆为什么是首屏快的关键,并清点书写层语法差异——classonInput、style 键名——构成一张迁移期排雷清单。

「模板克隆」是理解 Solid 性能的钥匙词汇。它指的是:编译器把你 JSX 里的静态结构(标签层级、类名、绝大多数属性)预先做成一份真实的 DOM 模板,渲染时用一次克隆批量复制;只有 JSX 表达式位置(插值、事件、动态属性)被单独抽出来,挂成响应式绑定。React 的编译产物没有这个环节——它生成的是创建元素描述对象的函数调用,每次渲染都重新执行、重新生成一棵描述树。

同一段 JSX,两份产物

先写源码,一个带标题和计数的卡片:

function Card(props) { return ( <div class="card"> <h2 class="card-title">{props.title}</h2> <p class="card-desc">固定文案,永远不变</p> <button class="btn" onClick={props.onLike}> 赞 {props.count()} </button> </div> ); }

React 的编译产物(简化示意):

// 每次渲染都执行,生成一棵全新的对象描述树 function Card(props) { return jsx("div", { className: "card", children: [ jsx("h2", { className: "card-title", children: props.title }), jsx("p", { className: "card-desc", children: "固定文案,永远不变" }), jsx("button", { className: "btn", onClick: props.onLike, children: ["赞 ", props.count()] }) ] }); }

Solid 的编译产物(简化示意):

// 组件体执行一次:克隆模板 + 挂绑定 const _tpl = template(`<div class="card"><h2 class="card-title"></h2><p class="card-desc">固定文案,永远不变</p><button class="btn"></button></div>`); function Card(props) { const _el = _tpl.cloneNode(true); // 结构来自克隆,不是重建 insert(_el, () => props.title, _el.firstChild); // 文本绑定 onClick(_el.lastChild, props.onLike); // 事件委托 insert(_el.lastChild, () => ["赞 ", props.count()]); return _el; }

两份产物对照读,三个信息量最大的差异就浮出来了。差异一:静态内容的位置。"固定文案,永远不变"在 React 里是每次渲染都要传递的字符串实参,在 Solid 里它躺在模板字符串里,克隆之后根本不进任何执行路径——静态内容从此零成本。差异二:更新的组织方式。React 靠"新描述对旧描述"的 diff 找出变化,Solid 的每个表达式位置都换成了() =>包裹的取值函数,信号变化时这些函数重新执行、直接写入节点,对账环节不存在。差异三:事件处理。Solid 编译器把事件统一挂到文档根做委托,组件里的 onClick 不会成为 DOM 节点上的独立监听器——大量列表条目的事件内存开销因此骤降。

图:JSX 编译产物对照——对象工厂与模板克隆

图:JSX 编译产物对照——对象工厂与模板克隆

书写层排雷清单

编译目标不同,书写层的方言差异必须背下来,否则迁移期会撞上一堆"看起来一样却行为不同"的坑:

书写点 React 习惯 Solid 写法 说明
类名 className class 编译目标就是真实 DOM 属性
输入框 onChange onInput Solid 沿用原生事件名,onChange 不存在
style 驼峰键名对象 驼峰键名对象 一致,但建议配合 classList 指令
危险 HTML dangerouslySetInnerHTML innerHTML 属性 命名直白,别滥用
SVG 属性 驼峰转换 与 DOM 一致 stroke-width 直接写
事件对象 合成事件 e.target.value 原生事件 e.currentTarget.value 没有事件池与合成层

表里最容易踩的两格加粗提醒:onChange 在 Solid 里根本不存在,输入框必须用 onInput;事件对象是浏览器原生对象,取值用 e.currentTarget——React 的 e.target 习惯在事件委托下会拿到错误的节点(委托后 target 可能是内部子元素)。这两条能排掉迁移期一半的诡异 bug。

⚠️ class 写成 className 不会报错,但会被当成一个叫 className 的未知属性静默丢弃,样式不生效且难排查。团队迁移前把这条写进代码评审清单,性价比极高。

动态组件:两个框架的第三种形态

语法差异清单之外,还有一个"组件本身是变量"的场景值得单独说。React 里直接把组件存进变量再当标签用(const Tag = props.kind === "a" ? A : B; <Tag />),因为渲染就是执行,变量自然可用。Solid 里标签位是编译期静态分析的边界,动态组件走专门的 Dynamic 组件:

import { Dynamic } from "solid-js/web"; <Dynamic component={props.kind === "a" ? A : B} size="lg" /> // 组件变量变化时,旧组件销毁、新组件装载,作用域随之切换

配套地,"往子组件里塞内容"的插槽场景在两边形态也不同:React 的 props.children 是求值后的元素树,Solid 的 props.children 是可求值多次的响应式结构——需要把它当函数调用(配合工具函数)或直接渲染。这一差异是 render props 模式在 5.3 节"变形"的物理基础,先在语法层记住:Solid 的 children 是活的,React 的 children 是快照。

问题:能直接阅读编译产物吗?

能,而且推荐。开发构建的产物与源码在同一工程内可对照查看,编译产物的可读性是 Solid 的一个隐性优点:你在第 3 章看到的模板克隆与绑定代码就是真实产物的形状,不是教学示意。调试"为什么这里不更新"时,看一眼产物里这个表达式位有没有生成绑定函数,往往比改代码碰运气快得多。7.3 的学习路径里,"读一次自己项目的编译产物"被列为第一周的作业,理由正在于此。

本节要点回顾

  • 产物决定命运:对象工厂服务重渲染,模板克隆服务一次性装配加终身绑定。
  • 静态内容零成本:留在模板里的部分不进执行路径,这是首屏与更新双快的原因。
  • 事件走委托:节点上没有独立监听器,长列表事件内存大幅下降。
  • 方言要背:class、onInput、原生事件对象,迁移期排雷三件套。

下一章视角从编译期转回运行期:组件装载之后的生命周期,在"只跑一次"的世界里还剩下什么。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U