
lit-html 原理深度拆解静态模板 Part 定位如何跳过 diff【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit长列表一滚动就掉帧、每次状态变化整块 DOM 重算——这是用过 diff 型框架的开发者都遇到过的瓶颈。lit-html 是 Lit 的底层模板引擎它用静态模板只解析一次、动态值各占一个 Part 定点更新的思路绕开了整树 diff本文直接拆开 packages/lit-html/src/lit-html.ts 看它怎么落地。一次渲染的真实路径五阶段流程图图中计数器首次渲染时 Prepare、Create、Update 全走一遍之后每次点击按钮只剩 Update 阶段在跑且只碰值真正变化的那个 Part。整个机制的支点就是strings数组的引用缓存同一个模板字面量无论调用多少次JS 引擎返回的都是同一个 strings 对象它天然就是缓存键。核心机制拆解Prepare 阶段为什么只做一次设计意图模板里 90% 是静态 HTML没必要每个状态周期都重新解析。getTemplateHtml用正则状态机扫描静态字符串判断每个${...}落在文本位还是属性位文本位换成注释标记节点属性位改写属性名并加$lit$后缀避免style、class等特殊属性被浏览器在标记阶段就解析。这一步的产出是带标记的template元素加一份TemplatePart元数据记录第几个动态值、在哪个节点、什么类型。// Template 构造函数静态字符串 → 带标记 HTML → 元数据 constructor({strings, [_$litType$]: type}, options) { const [html, attrNames] getTemplateHtml(strings, type); this.el Template.createElement(html, options); // ... 省略深度优先遍历标记节点生成 TemplatePart 数组 ... }源码见 packages/lit-html/src/lit-html.ts 的Template类和 packages/lit-html/src/lit-html.ts 的getTemplateHtml。由于 Prepare 只看静态字符串、不看动态值同一条模板字面量在页面生命周期内只执行这一次。Part 如何让 Update 阶段跳过没变的值设计意图更新不该是比较新旧 UI 树而应是逐个检查每个动态值的落点没变就什么都不做。Create 阶段把template内容克隆进 DocumentFragment按深度优先索引把每种TemplatePart实例化为挂在真实节点上的Part对象存进TemplateInstance._$parts_update(values) { let i 0; for (const part of this._$parts) { if (part ! undefined) { if ((part as AttributePart).strings ! undefined) { (part as AttributePart)._$setValue(values, part as AttributePart, i); } else { part._$setValue(values[i]); } } i; } }见 packages/lit-html/src/lit-html.ts。每种 Part 内部都记着_$committedValue——上次写进 DOM 的值。ChildPart._$setValue开头就是value ! this._$committedValue判断相同直接返回packages/lit-html/src/lit-html.ts变化时也只写最小动作文本改 Text 节点、属性走setAttribute、事件监听器复用同一个 listener 对象只换回调。所以一次状态变化DOM 写入次数 实际变化的绑定数与模板多大无关。用计数器跑一遍全流程import {html, render} from lit; const container document.querySelector(#container); const counterUi (count) html span class${count % 2 ? odd : }${count}/span button click${() render(counterUi(count 1), container)} Increment /button; render(counterUi(0), container); // ① Define RendercounterUi(0)求值只是对象字面量分配html标签函数不碰 DOM首次render在容器上找不到_$litPart$于是 ② Prepare生成带标记模板全程只发生一次→ ③ Create克隆节点、实例化三个 PartAttributePart / ChildPart / EventPart→ ④ Update写入、0、监听器点击按钮后再render容器上已有 Part模板缓存命中只剩 ④且AttributePart与EventPart的值判断后各自写入ChildPart写入新数字。这就是开头那张 GIF 里点按钮只动 Update的完整路径。与虚拟 DOM 方案的取舍方案更新单元变化检测方式典型开销虚拟 DOMReact/Preact 等整棵 vdom 树逐节点递归 diff每次更新都重建 vdom 并比对全树lit-html单个 Part每个 Part 比较_$committedValue首渲染一次性 Prepare之后零树操作这种取舍的收益是更新路径上没有任何树的生成与遍历——状态越多、模板越大与 vdom 方案的差距越明显这也是它在 JS Frameworks Benchmark 里更新类项目常年领先的原因。代价同样明确每个动态绑定都要维护一个 Part 对象和一对标记节点且 Part 直接钉在真实 DOM 上绕过 lit-html 手动改 DOM 会破坏 marker 节点虚拟 DOM 因为始终经过自己的抽象层对这类外部干扰反而更鲁棒。回看开头的滚动卡顿场景lit-html 的答案不是更快地 diff而是不 diff静态 HTML 解析一次就缓存动态值各占一个 Part更新时只碰变化点。最值得记住的一条——strings数组的引用稳定性是整个模板复用机制的地基。想继续深挖设计文档 dev-docs/design/how-lit-html-works.md 和 packages/lit-html/src/lit-html.ts 的render入口是最佳起点。【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考