lit-html 的 Part 更新机制详解:没有 Diff,它是如何精确更新 DOM 的?

在前两篇中已经明确了一个关键事实:

Lit / lit-html 不使用 Virtual DOM,也不存在 Diff 过程

那么问题就变成了:

没有 Diff,lit-html 是如何做到“只更新必要 DOM”的?

答案就在 lit-html 最核心、也是最容易被忽略的设计中:
Part(局部更新单元)

本文将从渲染流程、Part 类型、更新算法三个层面,拆解 lit-html 的底层机制。


一、lit-html 的设计前提(IImportant)

在深入 Part 之前,先明确 lit-html 的两个前提假设:

  1. 模板结构是稳定的
  2. 只有表达式(${})是动态的

这两个前提,决定了 lit-html 的整个架构方向。


二、从模板字符串到 Template 实例

2.1 模板字符串的本质

html`<p>Hello ${name}</p>`

在 JavaScript 层面,这会被拆成:

strings = ["<p>Hello ", "</p>"]
values  = [name]

lit-html 把这两部分封装成一个对象:

TemplateResult {
  strings,
  values
}

2.2 Template 的缓存机制

lit-html 并不会每次都重新解析模板。

内部会用 strings 作为 key 进行缓存:

Map<TemplateStringsArray, Template>

只要模板字面量不变,Template 就只会创建一次。


三、Template 内部发生了什么

3.1 生成 <template> DOM

在第一次渲染时,lit-html 会:

  1. 把字符串拼接成 HTML
  2. 插入特殊 marker(注释节点,作为 key)
  3. 创建 <template> 元素

示意:

<p>Hello <!--lit-part--></p>

这些 marker 就是 Part 的锚点


3.2 Part 的本质

Part = DOM 中一个“确定可更新的位置”

lit-html 在解析 template 时,会记录:

  • Part 的类型
  • Part 对应的 DOM 节点
  • Part 的索引顺序

这些信息在首次渲染阶段全部确定


四、Part 的几种核心类型

4.1 ChildPart(Most Common)

用于更新:

  • 文本节点
  • 子节点
  • 嵌套模板
html`<p>${value}</p>`

4.2 AttributePart

html`<div class="${cls}"></div>`

更新的是 attribute。


4.3 PropertyPart

html`<input .value=${val} />`

直接赋值给 DOM property。


4.4 BooleanAttributePart

html`<button ?disabled=${disabled}></button>`

4.5 EventPart

html`<button @click=${onClick}></button>`

特点:

  • 事件 handler 会被缓存
  • 不会重复 add/remove listener

五、首次渲染:建立 Part 与 DOM 的映射关系

5.1 首次 render 流程

TemplateResult
  ↓
Template(缓存 or 新建)
  ↓
clone template.content
  ↓
遍历 marker
  ↓
创建 Part 实例
  ↓
插入真实 DOM

此时,lit-html 已经知道:

哪个表达式 → 对应哪个 DOM 节点


六、更新阶段:没有 Diff,只有定点更新

6.1 更新入口

在 Lit 中,状态变化最终会触发:

render(templateResult, container)

6.2 更新的核心循环

伪代码如下(高度简化):

for (let i = 0; i < parts.length; i++) {
  parts[i].setValue(values[i])
}

关键点:

  • 顺序一一对应
  • 没有 DOM 遍历
  • 没有结构比较

6.3 Part.setValue 内部逻辑

以 ChildPart 为例:

if (value === oldValue) return

if (value is primitive) {
  updateTextNode()
} else if (value is TemplateResult) {
  renderNestedTemplate()
} else if (value is Array) {
  updateList()
}

每一种 value 类型,对应一条非常明确的更新路径


七、为什么这套机制非常高效

7.1 时间复杂度优势

  • Virtual DOM:O(n) 遍历 + Diff
  • lit-html:O(k),k = 表达式数量

通常:

k << n

7.2 内存与 GC 压力极低

  • 没有 VDOM 对象树
  • Part 实例数量固定
  • 更新阶段几乎不分配新对象

八、列表更新:lit-html 也不是“天真替换”

html`
  ${items.map(item => html`<li>${item}</li>`)}
`

lit-html 内部:

  • 为数组创建一个父 ChildPart
  • 每一项是一个嵌套 Part
  • 支持 keyed directive(如 repeat

8.1 repeat 指令的意义

repeat(items, item => item.id, item => html`...`)

作用:

  • 复用已有 DOM
  • 精确控制节点移动
  • 避免整体销毁重建

九、lit-html 的边界在哪里?

9.1 结构高度动态的代价

html`
  ${cond ? html`<A />` : html`<B />`}
`
  • 会创建多个 Template
  • 多层 Part 嵌套
  • 可读性和性能都会下降

9.2 lit-html 更擅长“稳定结构 + 数据变化”

这也是为什么 Lit 非常适合:

  • Design System
  • UI 组件库
  • 长期稳定组件

十、一个核心结论(Remember)

lit-html 的性能不是来自“聪明的算法”
而是来自“极强的约束与确定性”

它提前消灭了“不确定性”,
因此不需要 Diff。


十一、总结

  • lit-html 用 Part 替代了 Virtual DOM
  • Part 在首次渲染时就与 DOM 绑定
  • 更新阶段只做定点赋值
  • 这是一个极其“工程化”的设计

如果你理解了这一套机制,就会明白:

Lit 的高性能不是魔法,而是设计选择的结果

posted @ 2025-12-22 10:45  幼儿园技术家  阅读(10)  评论(0)    收藏  举报