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 的两个前提假设:
- 模板结构是稳定的
- 只有表达式(
${})是动态的
这两个前提,决定了 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 会:
- 把字符串拼接成 HTML
- 插入特殊 marker(注释节点,作为 key)
- 创建
<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 的高性能不是魔法,而是设计选择的结果

浙公网安备 33010602011771号