模块一 · 起步与基础
Vue 到底是什么:声明式渲染与响应式
从命令式 DOM 操作讲到声明式渲染,再拆开响应式的三件事:依赖收集、触发更新、调度。这一节建立整门课的心智模型。
读完这一节,你能做到
- 说清声明式渲染与命令式操作的本质差异。
- 用"数据 → 依赖 → 更新"解释一次视图刷新。
- 说出模板最终被编译成了什么。
- 判断一个"数据变了但页面没变"的问题出在哪一环。
一、从命令式到声明式
命令式的写法是"一步一步告诉浏览器怎么改":
const el = document.querySelector("#count");
let count = 0;
button.onclick = () => {
count++;
el.textContent = count;
};
声明式只描述"数据与视图的对应关系",由框架负责更新:
<template>
<button @click="count++">{{ count }}</button>
</template>
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
差别不在写法,而在谁负责一致性:命令式由你保证每一次状态变化都同步到 DOM;声明式由框架保证"状态是什么,视图就是什么"。
代价是:你必须理解框架的更新时机,否则会遇到"我明明改了数据,页面却还是旧的"。
二、响应式的三件事
1. 依赖收集:渲染时读到哪些响应式数据,就记录"谁依赖了谁"
2. 触发更新:数据变化时,通知依赖它的副作用重新执行
3. 调度:把多次通知合并,在合适的时机批量更新
- 依赖收集发生在渲染或
watchEffect执行期间。没被读到的数据,不会建立依赖。 - 触发更新靠 Proxy 的
get/set拦截。 - 调度决定"什么时候更新",Vue 默认把更新推到微任务队列,所以同一轮里改三次,只渲染一次。
三、模板编译成了什么
模板不是字符串替换,而是先编译成渲染函数,再由运行时执行:
// <div>{{ msg }}</div> 编译后近似于
function render(ctx) {
return h("div", ctx.msg);
}
三段时间线:
| 阶段 | 做什么 | 产物 |
|---|---|---|
| 编译 | 解析模板、生成 AST、转换、生成代码 | render 函数 |
| 挂载 | 执行 render,创建 VNode,patch 成真实 DOM | 首次视图 |
| 更新 | 依赖触发,重新执行 render,diff 新旧 VNode | 增量更新 |
编译期能做优化的前提,是模板是静态可分析的。 这也是为什么 Vue 鼓励写模板而不是手写 render。
四、一次更新的完整链路
count.value++
→ Proxy set 拦截
→ 通知依赖的 effect
→ 调度器放入微任务队列
→ 刷新时重新执行组件 render
→ 生成新 VNode
→ patch 对比新旧 VNode
→ 只更新变化的 DOM
任何一环断了,都会表现为"页面不更新":
- 数据不是响应式的(普通对象属性、未声明的变量)。
- 依赖没被收集(在渲染之外读取,或在异步回调里读取)。
- 更新被调度器合并(同一轮多次修改,只渲染一次,这是正常的)。
五、为什么需要虚拟 DOM
直接操作 DOM 的问题不在“慢”,而在“规模化”:当多个状态共同影响同一块视图时,手动同步容易漏;频繁读写 DOM 会触发重排重绘;视图逻辑散落在事件回调里,难以复用与测试。
虚拟 DOM 的价值是把“状态 → 视图”变成可计算、可比较的过程:
| 价值 | 说明 |
|---|---|
| 可预测 | 视图是状态的纯函数结果,同一状态必得同一视图 |
| 可跨平台 | 渲染器可替换为小程序、Canvas、服务端 |
| 可优化 | 编译期标记 + 运行时 diff,把更新缩小到动态节点 |
代价是多了一层 VNode 分配与比较开销。极简单的场景(比如只改一个文本)直接操作 DOM 确实更快,但一旦页面变复杂,维护成本会迅速超过这点性能差异。
六、两个构建版本
| 版本 | 是否含编译器 | 适用 |
|---|---|---|
| runtime-only | 否 | 生产环境,模板在构建期编译 |
| runtime + compiler | 是 | 运行时编译字符串模板 |
生产项目应使用 runtime-only,避免把编译器打进包体;只有“模板来自后端字符串”这类场景才需要完整版。
七、响应式不等于双向绑定
双向绑定只是 v-model 的语法糖,本质是“绑定值 + 监听输入事件”;响应式解决的是“数据变化后视图更新”,不负责“视图变化后数据更新”。把两者混为一谈,就会在自定义组件里写出错误的数据流:子组件直接改 props,父组件却不知道数据从哪来。
八、学习顺序
先跑通项目与模板(第 1-2 节),再理解渲染链路(本节),接着学组件与边界(第 4-5 节)和生命周期(第 6 节),然后深入响应式与编译(第 7-8 节),最后进入路由、请求、类型、动画、性能、测试与表达(第 9-15 节)。顺序反了,容易变成背 API。判断是否真正理解的标准:能否用三句话解释一次点击到 DOM 更新的全过程;如果只能说“数据变了页面就变”,说明还停在表面。一句话总结:Vue 让你描述结果,响应式负责让结果保持最新。这也是“心智模型”比 API 更重要的原因。
九、常见坑
- 以为
ref包装后原对象也变成响应式(ref(obj)内部会转成 reactive,但替换.value会换掉引用)。 - 在
setup里读取了数据,却期望它在渲染时自动追踪(依赖只记录执行期间的读取)。 - 直接给数组下标赋值,误以为不会触发更新(Vue 3 的 Proxy 可以拦截,Vue 2 不行)。
- 把"同一轮改多次只渲染一次"误判为 bug。
- 脱离响应式上下文(如普通函数里)读取数据,却期望建立依赖。
十、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 写一个计数器,分别用命令式和
ref声明式实现,比较代码量。 - 在
watchEffect里读取一个ref,再在外部修改它,观察触发次数。 - 用
console.log打印组件的 render 执行次数,验证"同一轮改三次只渲染一次"。
② 让 AI 写同一题
把第 1 题(计数器两种实现)交给 AI,并要求它解释:为什么直接改普通变量界面不动。
③ 对比要盯的点
- 它的解释停在“Vue 会追踪依赖”吗?没讲到 Proxy 的 get 收集、set 触发,就不算机制。
- 它有没有说清
ref为什么要.value?这是最常被含糊过去的一点。 - 代码里它写的是
count.value++还是count++?后者在 script 里直接失效。
④ 一句话取舍
它答对了用法,但能不能回答“为什么 ref 要 .value、reactive 不要”?
十一、自测
- 声明式渲染的本质是什么?
- 响应式三件事分别解决什么问题?
- 模板最终被编译成了什么?
- 为什么同一轮改三次只渲染一次?
- "数据变了页面没变"应该按什么顺序排查?
参考答案要点:描述数据与视图的对应关系;收集/触发/调度;render 函数;调度器合并;先查响应式,再查依赖,最后查调度。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
提示:读完可以顺手做几道对应题目,检验是否真的理解