Renderer 更新 -> 文本节点的处理与优化
在之前的章节中,我们一直通过虚拟 DOM 创建元素节点并渲染到页面中。然而,Vue 不仅支持元素节点,还支持文本节点的独立渲染。本章将深入探讨文本节点的实现原理,以及如何通过虚拟节点标准化来简化开发体验。
从元素节点到文本节点
传统的元素节点渲染
在此前的实现中,我们通过虚拟 DOM 创建元素节点:
import { h, render, Text } from '../../../node_modules/vue/dist/vue.esm-browser.js'
const vNode = h('div', null, ['hello', 'world'])
render(vNode, app)
渲染结果:

独立的文本节点
但实际上,Vue 也支持将文本作为独立的虚拟节点进行渲染。观察 Vue 官方的 vnode 结构,可以看到 children 的 type 是一个 Symbol(v-txt):

这意味着文本节点在 Vue 中有自己独立的节点类型标识,而不是简单地作为字符串存在。
实现文本节点支持
核心思路
要支持文本节点,我们需要:
- 定义文本节点类型:创建一个唯一的标识符来区分文本节点
- 处理文本节点挂载:在
patch函数中添加文本节点的处理逻辑 - 处理文本节点更新:实现文本内容变化时的更新机制
第一步:创建文本节点标记
首先,我们需要定义一个唯一的标识符来表示文本节点:
// vnode.ts
export const Text = Symbol('v-txt')
为什么使用 Symbol?
| 特性 | 说明 |
|---|---|
| 唯一性 | Symbol 保证了标识符的全局唯一性 |
| 类型安全 | 避免与字符串类型混淆,类型检查更严格 |
| 语义化 | 使用 Symbol('v-txt') 清晰表达意图 |
第二步:在渲染器中处理文本节点
文本节点的核心逻辑都在 render.ts 中实现。我们需要在 patch 函数中添加对文本节点的特殊处理:
基本使用
import { h, render, Text } from '../dist/vue.esm.js'
const vNode = h(Text, null, 'hello')
render(vNode, app)
初次挂载时,执行流程为:render → patch
改造 patch 函数
在 patch 函数中使用 switch 语句来区分不同类型的节点:
function patch(n1, n2, container, anchor = null) {
// 相同节点,直接返回
if (n1 === n2)
return
// 节点类型不同,先卸载旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
unmount(n1)
n1 = null
}
const { shapeFlag, type } = n2
switch (type) {
case Text:
// 处理文本节点
processText(n1, n2, container, anchor)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 处理 DOM 元素 -> div span p h1
processElement(n1, n2, container, anchor)
}
else if (shapeFlag & ShapeFlags.COMPONENT) {
// TODO: 组件处理
}
}
}
设计要点:
- 类型优先判断:通过
switch (type)优先处理特殊节点类型(如Text) - 后备处理:在
default分支中处理普通元素和组件 - 节点复用检查:在处理前先判断节点类型是否变化
第三步:实现 processText 函数
processText 函数负责文本节点的挂载和更新:
// 文本节点的更新与挂载
function processText(n1, n2, container, anchor) {
if (n1 == null) {
// 初次挂载:创建文本节点
const el = hostCreateText(n2.children)
n2.el = el
hostInsert(el, container, anchor)
}
else {
// 更新:复用 DOM 节点,仅更新文本内容
n2.el = n1.el
if (n1.children !== n2.children) {
hostSetText(n2.el, n2.children)
}
}
}
逻辑分析
挂载阶段(n1 == null):
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | hostCreateText(n2.children) | 创建真实的文本 DOM 节点 |
| 2 | n2.el = el | 将 DOM 节点引用保存到 vnode 中 |
| 3 | hostInsert(el, container, anchor) | 将节点插入到容器中 |
更新阶段(n1 != null):
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | n2.el = n1.el | 复用旧节点的 DOM 引用 |
| 2 | 比较文本内容 | 判断 children 是否变化 |
| 3 | hostSetText(n2.el, n2.children) | 只在内容变化时更新,避免不必要的 DOM 操作 |
性能优化点:
- 节点复用:更新时不创建新的 DOM 节点,直接复用旧节点
- 按需更新:只在文本内容真正变化时才调用
hostSetText - 最小化操作:文本更新是最轻量的 DOM 操作之一
虚拟节点标准化:提升开发体验
问题场景
现在我们有了文本节点的支持,但在实际使用中会遇到这样的问题:
之前的写法:
const vnode = h('div', null, [
h('span', null, 'hello'),
h('span', null, 'world')
])
理想的写法:
const vnode = h('div', null, ['hello', 'world'])
第二种写法更简洁直观,但我们的系统目前无法识别字符串 'hello' 和 'world' 应该被当作文本节点处理。
解决方案:虚拟节点标准化
我们需要一个函数,将各种类型的 children 标准化为统一的虚拟节点格式:
// vnode.ts
// 虚拟节点标准化函数
/**
* 将原始值转换为标准的虚拟节点
* @param vnode - 可能是虚拟节点、字符串或数字
* @returns 标准化后的虚拟节点
*/
export function normalizeVNode(vnode) {
// 如果是字符串或数字,转换为文本节点
if (isString(vnode) || isNumber(vnode)) {
return createVNode(Text, null, String(vnode))
}
// 已经是虚拟节点,直接返回
return vnode
}
处理的类型映射
| 输入类型 | 处理方式 | 输出 |
|---|---|---|
string | createVNode(Text, null, vnode) | 文本节点虚拟对象 |
number | createVNode(Text, null, String(vnode)) | 文本节点虚拟对象(数字转字符串) |
vnode | 直接返回 | 原始虚拟节点 |
应用到 diff 算法
我们需要在所有处理子节点的地方应用标准化,确保系统能正确识别各种格式的 children:
双端 diff 中的应用
function patchKeyedChildren(c1, c2, container) {
let i = 0
let e1 = c1.length - 1
let e2 = c2.length - 1
// 前序比对
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = (c2[i] = normalizeVNode(c2[i])) // 标准化
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container)
}
else {
break
}
i++
}
// 后序比对
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = normalizeVNode(c2[e2])) // 标准化
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container)
}
else {
break
}
e1--
e2--
}
// 新增节点处理
if (i > e1) {
const nextPos = e2 + 1
const anchor = nextPos < c2.length ? c2[nextPos].el : null
while (i <= e2) {
patch(null, (c2[i] = normalizeVNode(c2[i])), container, anchor) // 标准化
i++
}
}
// ... 其他逻辑
}
乱序 diff 中的应用
else {
// 乱序处理
for (let j = s2; j <= e2; j++) {
const n2 = (c2[j] = normalizeVNode(c2[j])) // 标准化
keyToNewIndexMap.set(n2.key, j)
}
// ...
}
标准化的好处
开发体验提升:
| 场景 | 优化前 | 优化后 |
|---|---|---|
| 纯文本列表 | [h(Text, null, 'a'), h(Text, null, 'b')] | ['a', 'b'] |
| 混合内容 | [h('div'), h(Text, null, 'text')] | [h('div'), 'text'] |
| 数字内容 | [h(Text, null, String(123))] | [123] |
系统健壮性提升:
- 类型容错:自动处理多种输入类型
- 统一处理:内部统一使用虚拟节点格式
- 减少错误:避免直接传递字符串导致的类型错误
完整代码汇总
vnode.ts
// 文本节点类型标识
export const Text = Symbol('v-txt')
/**
* 虚拟节点标准化函数
* 将字符串、数字等原始类型转换为文本节点虚拟对象
*/
export function normalizeVNode(vnode) {
if (isString(vnode) || isNumber(vnode)) {
return createVNode(Text, null, String(vnode))
}
return vnode
}
render.ts
/**
* patch 函数 - 节点对比和更新的核心
*/
function patch(n1, n2, container, anchor = null) {
if (n1 === n2)
return
// 节点类型不同,卸载旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
unmount(n1)
n1 = null
}
const { shapeFlag, type } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor)
}
else if (shapeFlag & ShapeFlags.COMPONENT) {
// 组件处理
}
}
}
/**
* 文本节点的挂载和更新
*/
function processText(n1, n2, container, anchor) {
if (n1 == null) {
// 挂载阶段
const el = hostCreateText(n2.children)
n2.el = el
hostInsert(el, container, anchor)
}
else {
// 更新阶段
n2.el = n1.el
if (n1.children !== n2.children) {
hostSetText(n2.el, n2.children)
}
}
}
总结
通过本章的学习,我们完成了文本节点的完整支持和虚拟节点标准化优化。让我们回顾一下核心要点:
1. 文本节点实现
- 类型标识:使用
Symbol('v-txt')定义文本节点唯一标识 - 挂载逻辑:通过
hostCreateText创建真实文本 DOM 节点 - 更新逻辑:复用节点,仅在内容变化时更新文本
- 性能优化:最小化 DOM 操作,按需更新
2. 渲染器改造
- 类型分发:在
patch中使用switch优先处理特殊节点类型 - 独立处理:
processText专门处理文本节点的生命周期 - 统一接口:遵循与元素节点相同的
n1、n2参数模式
3. 虚拟节点标准化
- 类型转换:自动将字符串、数字转换为文本节点
- 开发体验:简化 API 使用,支持更直观的语法
- 系统健壮:统一内部数据格式,减少类型错误
- 全面应用:在所有
children处理位置应用标准化
4. 使用场景对比
标准化前:
h('div', null, [
h(Text, null, 'hello'),
h(Text, null, 'world'),
h(Text, null, String(123))
])
标准化后:
h('div', null, ['hello', 'world', 123])
5. 关键技术点
| 技术点 | 实现方式 | 作用 |
|---|---|---|
| 节点标识 | Symbol('v-txt') | 唯一性标识,避免冲突 |
| 节点复用 | n2.el = n1.el | 减少 DOM 创建开销 |
| 按需更新 | if (n1.children !== n2.children) | 避免不必要的文本更新 |
| 类型标准化 | normalizeVNode(vnode) | 统一数据格式,提升开发体验 |
6. 性能优化效果
| 优化项 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| DOM 操作 | 每次都重新创建 | 复用节点 | 减少创建开销 |
| 文本更新 | 无判断直接更新 | 按需更新 | 减少不必要的 DOM 操作 |
| API 简洁度 | 冗长的虚拟节点创建 | 直接使用字符串 | 代码量减少 60%+ |
至此,我们的渲染器已经支持了文本节点的完整生命周期管理,并通过虚拟节点标准化大幅提升了开发体验。这为后续组件系统的实现奠定了坚实的基础。