第9章 9.2 Vue 3 源码:组件渲染

前置知识:上一章我们揭开了 Vue 3 响应式的秘密——数据变了,界面自动跟着变,就像变色龙自动变色一样自然。

本章目标:这一章我们要深入 Vue 3 的"组装车间",看看一个 .vue 文件是怎么变成屏幕上真实可见的像素的。


🎯 开场 3 分钟:为什么要学这个?

你有没有想过这个问题:Vue 凭什么知道 {{ message }} 要渲染在哪个位置?

<div>{{ message }}</div>

这一行代码背后,Vue 做了大量工作:
1. 把 {{ message }} 解析成一个"指令"
2. 去响应式数据里找到 message 的值
3. 创建 DOM 节点,塞进去

如果你写过原生 JS,可能会这样干:

const div = document.createElement('div')
div.innerText = message
document.body.appendCh\n\n![Simple tech illustration expla](https://blog.xxyye.com/wp-content/uploads/2026/06/ac61cbff46cbd93.png)\n\n![AI comic creation scene, creat](https://blog.xxyye.com/wp-content/uploads/2026/06/3ca6487222afe0b.png)\n\nild(div)

问题来了:当 message 变了,你得手动再执行一遍这套流程。10 个变量还好,100 个呢?

学完这章,你能说清楚 Vue 的组件渲染管线是怎么工作的,遇到"为什么我的界面没更新"这种 bug,你也能自己 Debug 了。


🧱 基础 25 分钟:核心概念

概念 1:虚拟 DOM——组件的"蓝图"

是什么:虚拟 DOM(Virtual DOM)就是一个描述 UI 长什么样的普通 JS 对象

举个例子,你在相亲网站上填的"个人信息表"就是你的虚拟 DOM——它不是真实的你,但是一个可以展示、可以修改的"描述"。

// 虚拟 DOM 节点(简化版)
const vnode = {
type: 'div',
props: { class: 'container' },
children: [
{ type: 'h1', children: '标题' },
{ type: 'p', children: '这是一段文字' }
]
}

为什么要用:如果每次改数据都直接操作真实 DOM,浏览器要反复"重绘",性能很差。虚拟 DOM 让你先在内存里算出最优方案,再一次性更新。

类比一下:装修房子之前,先画设计图,确认没问题了再动手砸墙。

概念 2:h 函数——创建虚拟 DOM 的"工厂"

是什么h 函数是 Vue 提供的工具,用来创建虚拟 DOM 节点。它的名字来自 hyperscript(写 JS 脚本创建 HTML)。

为什么要用:手写上面那种嵌套对象太累了,h 函数让创建过程更声明式。

import { h } from 'vue'

// 之前的写法
const vnode1 = { type: 'div', props: { class: 'box' }, children: ['Hello'] }

// 用 h 函数的写法
const vnode2 = h('div', { class: 'box' }, 'Hello')

看,h('div', props, children) 三参数,简单明了。

概念 3:createApp——启动 Vue 应用的"钥匙"

是什么createApp 是 Vue 3 的入口函数,它接收一个根组件,返回一个应用实例。

类比:createApp 就像电影开场的制片公司 logo——它不负责拍电影,但它是一切的前提。

import { createApp } from 'vue'
import App from './App.vue'

// 这就是启动 Vue 应用的那行代码
const app = createApp(App)

// 告诉 Vue 把 App 挂载到哪个 DOM 元素上
app.mount('#app')

执行完这行代码,App.vue 里的模板就开始渲染到页面上了。

概念 4:patch 算法——对比蓝图找不同

是什么:当数据变了,Vue 需要重新生成虚拟 DOM,然后和上一次的对比,找出哪里变了,只更新需要改的地方。

为什么要用:不然每次数据变都要重新创建整个 DOM 树,太慢了。

类比:就像老师批改作业,不用把学生整篇作文重写一遍,只需要圈出写错的地方让学生改。

概念 5:组件的本质——返回虚拟 DOM 的函数

是什么:在 Vue 3 里,组件本质上就是一个返回虚拟 DOM 的函数

// 这就是一个最最简单的组件
const MyButton = {
setup() {
const count = ref(0)
return () => h('button', { onClick: () => count.value++ }, `点击了 ${count.value} 次`)
}
}

注意看,setup 返回的是一个函数,这个函数返回 h('button', ...)——也就是虚拟 DOM。


🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):用 h 函数手动创建一个页面

目标:体验一下"声明式"创建 UI 的感觉

// 创建一个完整的页面组件
import { h, createApp } from 'vue'

const App = {
setup() {
const message = 'Hello Vue 3!'
const count = ref(0)

return () => h('div', { class: 'container' }, [
  h('h1', message),
  h('p', `计数器: ${count.value}`),
  h('button', {
    onClick: () => count.value++
  }, '点我加一')
])
}
}

createApp(App).mount('#app')

预期输出:页面上显示标题、计数器数字、一个按钮。点击按钮,数字+1。

解释:这三行代码完成了之前原生 JS 需要 20 行才能搞定的事,而且还自带"响应式"——点击按钮,界面自动更新。


项目 2(15 分钟):用组件嵌套做一个待办清单

目标:理解组件之间的嵌套关系

import { h, createApp, ref } from 'vue'

// 单个待办项组件
const TodoItem = {
props: ['todo'],
setup(props) {
return () => h('li', { 
  class: ['todo-item', props.todo.done ? 'done' : '']
}, props.todo.text)
}
}

// 待办清单组件
const TodoList = {
setup() {
const todos = ref([
  { id: 1, text: '买菜', done: false },
  { id: 2, text: '做饭', done: true },
  { id: 3, text: '洗碗', done: false }
])

const addTodo = () => {
  todos.value.push({ 
    id: Date.now(), 
    text: '新任务', 
    done: false 
  })
}

return () => h('div', { class: 'todo-list' }, [
  h('h2', '我的待办'),
  h('ul', todos.value.map(todo => h(TodoItem, { todo }))),
  h('button', { onClick: addTodo }, '添加待办')
])
}
}

createApp(TodoList).mount('#app')

预期输出
- 页面显示"我的待办"标题
- 列表里有 3 个待办项,其中"做饭"有删除线样式(因为 done: true)
- 底部有一个"添加待办"按钮,点击会在列表末尾添加新项目

解释:重点在于 h(TodoItem, { todo })——组件作为 h 函数的第一个参数,实现嵌套。


项目 3(15 分钟):实现一个简易的"数据驱动的表格渲染器"

目标:综合运用虚拟 DOM + 组件化思维,做一个有点实用价值的小工具

这个场景很常见:后端返回了一批数据,你要渲染成表格展示。

import { h, createApp, ref } from 'vue'

// 表格单元格组件
const TableCell = {
props: ['value', 'column'],
setup(props) {
return () => {
  // 根据列类型渲染不同的样式
  const style = props.column.type === 'number' 
    ? { textAlign: 'right', color: '#2563eb' }
    : { textAlign: 'left' }

  return h('td', { style }, String(props.value))
}
}
}

// 可排序的表格组件
const DataTable = {
props: ['columns', 'data'],
setup(props) {
const sortKey = ref(null)
const sortOrder = ref('asc')

const sortedData = () => {
  if (!sortKey.value) return props.data

  return [...props.data].sort((a, b) => {
    const valA = a[sortKey.value]
    const valB = b[sortKey.value]
    const modifier = sortOrder.value === 'asc' ? 1 : -1
    return valA > valB ? modifier : -modifier
  })
}

const toggleSort = (key) => {
  if (sortKey.value === key) {
    sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
  } else {
    sortKey.value = key
    sortOrder.value = 'asc'
  }
}

return () => h('table', { class: 'data-table' }, [
  // 表头
  h('thead', [
    h('tr', props.columns.map(col => 
      h('th', { 
        onClick: () => toggleSort(col.key),
        style: { cursor: 'pointer' }
      }, col.label)
    ))
  ]),
  // 表体
  h('tbody', sortedData().map(row =>
    h('tr', props.columns.map(col =>
      h(TableCell, { value: row[col.key], column: col })
    ))
  ))
])
}
}

// 根组件:演示数据
const App = {
setup() {
const columns = [
  { key: 'name', label: '姓名', type: 'text' },
  { key: 'age', label: '年龄', type: 'number' },
  { key: 'city', label: '城市', type: 'text' }
]

const data = ref([
  { name: '张三', age: 28, city: '北京' },
  { name: '李四', age: 22, city: '上海' },
  { name: '王五', age: 35, city: '深圳' }
])

return () => h('div', { class: 'app' }, [
  h('h1', '员工信息表'),
  h(DataTable, { columns, data: data.value })
])
}
}

createApp(App).mount('#app')

预期输出
- 显示一个三列表格:姓名、年龄、城市
- 点击表头可以排序(再点击反转顺序)
- 数字列右对齐且蓝色显示

解释:这个例子展示了 Vue 虚拟 DOM 的真正威力——你可以用纯 JS 对象来描述任意复杂的 UI 结构,然后 Vue 会帮你高效地渲染到屏幕上。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:忘记 setup 返回的是函数

// ❌ 错误写法:直接返回虚拟 DOM 对象
setup() {
return h('div', 'Hello')  // 错!这里返回的是对象,不是函数
}

// ✅ 正确写法:返回函数
setup() {
return () => h('div', 'Hello')  // 对!返回的是函数
}

坑 2:在 setup 里直接修改 props

// ❌ 错误写法
setup(props) {
props.count = 100  // Vue 不允许直接修改 props
}

// ✅ 正确做法:通过 emit 通知父组件修改
setup(props, { emit }) {
const update = () => emit('update', 100)
}

坑 3:循环渲染时忘记加 key

// ❌ 错误写法:没有 key,Vue 不知道谁是谁
h('ul', items.map(item => h('li', item.text)))

// ✅ 正确写法:给每个节点唯一的 key
h('ul', items.map(item => h('li', { key: item.id }, item.text)))

坑 4:组件名写错大小写

// ❌ 错误:HTML 标签不区分大小写,但组件名区分
h('MyComponent', ...)  // 在模板里会变成 <mycomponent>

// ✅ 正确: PascalCase 用于 JSX/ render 函数
import { MyComponent } from './MyComponent'
h(MyComponent, ...)

坑 5:忘记 ref.value

// ❌ 错误:以为 count 自动解包
const count = ref(0)
count++  // 错!这样改不了响应式数据

// ✅ 正确:要加 .value
count.value++

性能小贴士:避免不必要的父组件重渲染

如果子组件接收的 props 没变,可以用 markRaw 标记不会被修改的对象:

import { markRaw } from 'vue'

setup() {
// 这个对象永远不会变,不需要 Vue 追踪它
const staticConfig = markRaw({
apiUrl: 'https://api.example.com',
timeout: 5000
})

return () => h(ChildComponent, { config: staticConfig })
}

调试技巧:用 Vue DevTools 可视化虚拟 DOM

打开浏览器 DevTools 的 Vue 面板,你可以看到:
- 组件树结构
- 每个组件的 props、data、computed
- 虚拟 DOM 的变化历史

或者在代码里加 log:

setup() {
const count = ref(0)

// 监控 count 的变化
watch(count, (newVal) => {
console.log('count 变了:', newVal)
})

return () => h('div', count.value)
}

✏️ 练习题

练习 1(2 分钟):改标题

  • 输入:把项目 1 的 message 改成 "你好 Vue"
  • 预期输出:页面标题变成"你好 Vue"
  • 提示:直接在 setup 里改 const message = '你好 Vue' 就行

练习 2(2 分钟):加个判断

  • 输入:在项目 1 里,当 count >= 5 时,按钮文字变成"不能再点了"
  • 预期输出:点够 5 次后,按钮文字变化
  • 提示:用三元表达式:count.value >= 5 ? '不能再点了' : '点我加一'

练习 3(3 分钟):新数据排序

  • 输入:给项目 2 的待办清单添加一个功能:已完成的排到最后面
  • 预期输出:列表顺序变成:买菜、洗碗、做饭(做饭因为 done:true 排到后面)
  • 提示:在 sortedData 的排序函数里加一个条件:done 为 true 的权重更低

练习 4(5 分钟):串联两个项目

  • 输入:把项目 2 的待办清单嵌入到项目 3 的表格里,作为每行的"操作列"
  • 预期输出:表格的最后一列显示"完成/删除"按钮
  • 提示:新建一个操作列的配置 column,类型用 'actions',在 TableCell 里特殊处理

练习 5(5 分钟):分析报错

  • 输入:运行下面的代码,看看会报什么错
const App = {
setup() {
const items = ref(['a', 'b', 'c'])
return () => h('ul', items.map(item => h('li', item)))
}
}
createApp(App).mount('#app')
  • 预期输出:应该正常运行。但如果改成 return h('ul', items.map(...))(不加 ())会报错
  • 提示:报错信息大概是 "Objects are not valid as a valid child"(对象不能作为有效的子元素)

作业:做一个「组件渲染调试器」

需求描述:做一个可视化工具,实时显示你创建的虚拟 DOM 结构。

功能点
1. 输入一个虚拟 DOM 描述(可以用简化语法),实时渲染出对应的 UI
2. 显示这个虚拟 DOM 的嵌套树状结构图
3. 当你修改描述时,高亮显示哪些 DOM 节点发生了变化

加分项
1. 支持导出/导入虚拟 DOM 配置(JSON 格式)
2. 支持查看 patch 算法的对比过程

验收标准
- 能跑起来
- 输入简单配置后能正确渲染 UI
- 树状结构图清晰展示组件层级

提交方式:评论区贴代码或 GitHub 链接


📚 总结 + 资源

一句话总结:Vue 3 的组件渲染管线是:模板 → 虚拟 DOM → patch 对比 → 真实 DOM,理解了这个流程,你就能看透大多数"界面不更新"的问题。

延伸学习资源
1. Vue 3 官方文档 - 渲染函数——最权威的参考
2. Vue 3 Design RFCs——可以看到 Vue 每一个特性是怎么设计出来的
3. 《深入浅出 Vue.js》——刘建国著,对虚拟 DOM 和响应式讲得很透

互动钩子:你在实际项目里遇到过"改了数据但界面没更新"的情况吗?当时是怎么解决的?评论区聊聊,老粉优先回复!


📌 下章预告:学会了组件渲染,你有没有想过一个问题——.vue 文件里的 <template> 标签,Vue 是怎么"认识"它的?下一章「第 9.3 Vue 3 源码:编译器」我们将揭晓这个秘密。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。