知

模块一 · 起步与基础

第 3 / 17 节16 分钟阅读型

Vue 到底是什么:声明式渲染与响应式

从命令式 DOM 操作讲到声明式渲染,再拆开响应式的三件事:依赖收集、触发更新、调度。这一节建立整门课的心智模型。

读完这一节,你能做到

  1. 说清声明式渲染与命令式操作的本质差异。
  2. 用"数据 → 依赖 → 更新"解释一次视图刷新。
  3. 说出模板最终被编译成了什么。
  4. 判断一个"数据变了但页面没变"的问题出在哪一环。

一、从命令式到声明式

命令式的写法是"一步一步告诉浏览器怎么改":

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 更重要的原因。

九、常见坑

  1. 以为 ref 包装后原对象也变成响应式(ref(obj) 内部会转成 reactive,但替换 .value 会换掉引用)。
  2. 在 setup 里读取了数据,却期望它在渲染时自动追踪(依赖只记录执行期间的读取)。
  3. 直接给数组下标赋值,误以为不会触发更新(Vue 3 的 Proxy 可以拦截,Vue 2 不行)。
  4. 把"同一轮改多次只渲染一次"误判为 bug。
  5. 脱离响应式上下文(如普通函数里)读取数据,却期望建立依赖。

十、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 写一个计数器,分别用命令式和 ref 声明式实现,比较代码量。
  2. 在 watchEffect 里读取一个 ref,再在外部修改它,观察触发次数。
  3. 用 console.log 打印组件的 render 执行次数,验证"同一轮改三次只渲染一次"。

② 让 AI 写同一题

把第 1 题(计数器两种实现)交给 AI,并要求它解释:为什么直接改普通变量界面不动。

③ 对比要盯的点

  • 它的解释停在“Vue 会追踪依赖”吗?没讲到 Proxy 的 get 收集、set 触发,就不算机制。
  • 它有没有说清 ref 为什么要 .value?这是最常被含糊过去的一点。
  • 代码里它写的是 count.value++ 还是 count++?后者在 script 里直接失效。

④ 一句话取舍

它答对了用法,但能不能回答“为什么 ref 要 .value、reactive 不要”?

十一、自测

  1. 声明式渲染的本质是什么?
  2. 响应式三件事分别解决什么问题?
  3. 模板最终被编译成了什么?
  4. 为什么同一轮改三次只渲染一次?
  5. "数据变了页面没变"应该按什么顺序排查?

参考答案要点:描述数据与视图的对应关系;收集/触发/调度;render 函数;调度器合并;先查响应式,再查依赖,最后查调度。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解