590 字
2 分钟
Vue 响应式原理:从数据变化到视图更新
前言
Vue 的响应式系统是其核心特性,实现了数据变化自动更新视图的能力。本文深入剖析 Vue 3 的响应式原理,从 Proxy 代理到视图更新的完整流程。
Vue 响应式流程概览
flowchart TB
A[数据变化] --> B[Proxy 拦截]
B --> C[触发 setter]
C --> D[依赖触发]
D --> E[调度队列]
E --> F[异步更新]
F --> G[组件重渲染]
G --> H[虚拟 DOM Diff]
H --> I[DOM 更新]
subgraph 响应式系统
B
C
D
end
subgraph 调度系统
E
F
end
一、响应式基础
1.1 Vue 2 vs Vue 3 对比
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式实现 | Object.defineProperty | Proxy |
| 数组监听 | 重写数组方法 | Proxy 原生支持 |
| 新增属性 | 需要 $set | 自动响应 |
| 性能 | 初始化递归遍历 | 惰性响应 |
| Map/Set | 不支持 | 支持 |
1.2 Proxy 基础
const target = { count: 0 };const proxy = new Proxy(target, { get(target, key, receiver) { console.log(`读取 ${key}`); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { console.log(`设置 ${key} = ${value}`); return Reflect.set(target, key, value, receiver); },});
proxy.count; // 读取 countproxy.count = 1; // 设置 count = 1Proxy 拦截操作:
| 拦截方法 | 触发时机 |
|---|---|
| get | 读取属性 |
| set | 设置属性 |
| has | in 操作符 |
| deleteProperty | delete 操作符 |
| ownKeys | Object.keys 等 |
| getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor |
二、响应式系统实现
2.1 reactive 函数
// 简化实现function reactive(target) { return new Proxy(target, reactiveHandlers);}
const reactiveHandlers = { get(target, key, receiver) { const res = Reflect.get(target, key, receiver);
// 依赖收集 track(target, key);
// 深层响应式 if (isObject(res)) { return reactive(res); }
return res; },
set(target, key, value, receiver) { const oldValue = target[key]; const res = Reflect.set(target, key, value, receiver);
if (oldValue !== value) { // 触发更新 trigger(target, key); }
return res; },
deleteProperty(target, key) { const res = Reflect.deleteProperty(target, key); trigger(target, key); return res; },};2.2 依赖收集
Vue 使用 WeakMap 存储依赖关系:
// 全局依赖栈let activeEffect = null;const targetMap = new WeakMap();
// 依赖收集function track(target, key) { if (!activeEffect) return;
// 获取目标对象的依赖映射 let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); }
// 获取属性的依赖集合 let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); }
// 添加当前副作用 dep.add(activeEffect);}
// 触发更新function trigger(target, key) { const depsMap = targetMap.get(target); if (!depsMap) return;
const dep = depsMap.get(key); if (dep) { dep.forEach(effect => effect.run()); }}数据结构:
flowchart TB
A[targetMap: WeakMap] --> B[target1: Map]
A --> C[target2: Map]
B --> D[key1: Set]
B --> E[key2: Set]
D --> F[effect1]
D --> G[effect2]
E --> H[effect3]
2.3 effect 函数
class ReactiveEffect { constructor(fn, scheduler = null) { this.fn = fn; this.scheduler = scheduler; this.deps = []; // 反向依赖 }
run() { // 保存上一个 effect const parent = activeEffect; activeEffect = this;
try { return this.fn(); } finally { // 恢复 activeEffect = parent; } }
stop() { // 从所有依赖中移除 this.deps.forEach(dep => dep.delete(this)); }}
function effect(fn, options = {}) { const _effect = new ReactiveEffect(fn, options.scheduler);
// 立即执行一次 _effect.run();
// 返回 runner const runner = _effect.run.bind(_effect); runner.effect = _effect; return runner;}2.4 computed 实现
function computed(getter) { let value; let dirty = true;
const effect = new ReactiveEffect(getter, () => { if (!dirty) { dirty = true; trigger(computed, "value"); } });
const computed = { get value() { if (dirty) { value = effect.run(); dirty = false; } track(computed, "value"); return value; }, };
return computed;}计算属性特点:
- 惰性求值:只有被访问时才计算
- 缓存:依赖不变时返回缓存值
- 自动追踪:执行时自动收集依赖
sequenceDiagram
participant U as 用户代码
participant C as computed.value
participant E as Effect
participant D as 依赖数据
U->>C: 读取 value
C->>E: 首次访问,执行 getter
E->>D: 读取依赖,收集依赖
E-->>C: 返回计算值
C-->>U: 返回缓存值
Note over D: 依赖变化
D->>E: 触发 scheduler
E->>E: 标记 dirty = true
U->>C: 再次读取 value
C->>E: dirty 为 true,重新计算
E->>D: 重新收集依赖
2.5 watch 实现
function watch(source, cb, options = {}) { let getter; if (isReactive(source)) { getter = () => traverse(source); } else if (isFunction(source)) { getter = source; }
let oldValue; let cleanup;
const onCleanup = fn => { cleanup = fn; };
const job = () => { const newValue = effect.run(); if (cleanup) cleanup(); cb(newValue, oldValue, onCleanup); oldValue = newValue; };
const effect = new ReactiveEffect(getter, job); oldValue = effect.run();}
// 深度遍历function traverse(value, seen = new Set()) { if (!isObject(value) || seen.has(value)) return; seen.add(value);
for (const key in value) { traverse(value[key], seen); }
return value;}三、调度系统
3.1 异步更新队列
Vue 使用异步更新策略避免重复渲染:
const queue = [];let isFlushing = false;let isFlushPending = false;const resolvedPromise = Promise.resolve();
function queueJob(job) { if (!queue.includes(job)) { queue.push(job); queueFlush(); }}
function queueFlush() { if (!isFlushing && !isFlushPending) { isFlushPending = true; resolvedPromise.then(flushJobs); }}
function flushJobs() { isFlushPending = false; isFlushing = true;
// 按优先级排序(父组件先更新) queue.sort((a, b) => a.id - b.id);
try { for (let i = 0; i < queue.length; i++) { queue[i](); } } finally { queue.length = 0; isFlushing = false; }}3.2 nextTick
const p = Promise.resolve();
function nextTick(fn) { return fn ? p.then(fn) : p;}
// 使用示例this.count++;this.count++;this.count++;// 组件只更新一次
this.$nextTick(() => { console.log("DOM 已更新");});sequenceDiagram
participant S as 数据变化
participant Q as 更新队列
participant T as 微任务队列
participant D as DOM
S->>Q: 第一次修改
S->>Q: 第二次修改
S->>Q: 第三次修改
Note over Q: 合并为一次更新
Q->>T: 添加 flushJobs
T->>D: 微任务执行,更新 DOM
Note over D: DOM 更新完成
T->>T: nextTick 回调执行
3.3 调度器 API
const pendingPreFlushCbs = [];const pendingPostFlushCbs = [];
// 前置钩子(更新前执行)function queuePreFlushCb(cb) { queueCb(cb, pendingPreFlushCbs);}
// 后置钩子(更新后执行)function queuePostFlushCb(cb) { queueCb(cb, pendingPostFlushCbs);}
// watchEffect 使用前置调度watchEffect( () => { console.log("数据变化了"); }, { flush: "pre", // 'pre' | 'post' | 'sync' });四、虚拟 DOM
4.1 VNode 结构
// VNode 结构(简化)const vnode = { type: "div", // 标签名或组件 props: { // 属性 id: "app", class: "container", }, children: [ // 子节点 { type: "span", children: "Hello" }, ], key: null, // key ref: null, // ref el: null, // 实际 DOM 元素 shapeFlag: 0, // 节点类型标记 patchFlag: 0, // 更新标记(优化用)};ShapeFlag 类型:
| 标记 | 值 | 说明 |
|---|---|---|
| ELEMENT | 1 | 原生元素 |
| STATEFUL_COMPONENT | 2 | 有状态组件 |
| TEXT_CHILDREN | 8 | 文本子节点 |
| ARRAY_CHILDREN | 16 | 数组子节点 |
| SLOTS_CHILDREN | 32 | 插槽子节点 |
4.2 h 函数
function h(type, propsOrChildren, children) { const l = arguments.length;
if (l === 2) { if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { // h('div', { props }) if (isVNode(propsOrChildren)) { return createVNode(type, null, [propsOrChildren]); } return createVNode(type, propsOrChildren); } else { // h('div', children) return createVNode(type, null, propsOrChildren); } } else { if (l > 3) { children = Array.prototype.slice.call(arguments, 2); } else if (l === 3 && isVNode(children)) { children = [children]; } return createVNode(type, propsOrChildren, children); }}五、渲染器
5.1 渲染流程
flowchart TB
A[render] --> B{旧 VNode 存在?}
B -->|是| C[patch]
B -->|否| D[挂载]
C --> E{类型相同?}
E -->|是| F[patchElement]
E -->|否| G[卸载旧节点]
G --> D
D --> H[createVNode]
H --> I[mountElement]
I --> J[设置属性]
J --> K[处理子节点]
5.2 patch 函数
function patch(n1, n2, container, anchor = null) { // 类型不同,直接卸载重建 if (n1 && !isSameVNodeType(n1, n2)) { unmount(n1); n1 = null; }
const { type, shapeFlag } = n2;
switch (type) { case Text: processText(n1, n2, container); break; case Comment: processComment(n1, n2, container); break; case Fragment: processFragment(n1, n2, container); break; default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement(n1, n2, container, anchor); } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent(n1, n2, container, anchor); } }}5.3 mountElement
function mountElement(vnode, container, anchor) { const { type, props, shapeFlag } = vnode;
// 创建 DOM 元素 const el = (vnode.el = document.createElement(type));
// 设置属性 if (props) { for (const key in props) { patchProp(el, key, null, props[key]); } }
// 处理子节点 if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { el.textContent = vnode.children; } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren(vnode.children, el); }
// 插入 DOM container.insertBefore(el, anchor);}5.4 patchProp
function patchProp(el, key, prevValue, nextValue) { if (key === "class") { patchClass(el, nextValue); } else if (key === "style") { patchStyle(el, prevValue, nextValue); } else if (isOn(key)) { patchEvent(el, key, prevValue, nextValue); } else { patchAttr(el, key, nextValue); }}
// 类名处理function patchClass(el, value) { if (value == null) { el.removeAttribute("class"); } else { el.className = value; }}
// 事件处理function patchEvent(el, key, prevValue, nextValue) { const invokers = el._vei || (el._vei = {}); const existingInvoker = invokers[key];
if (nextValue && existingInvoker) { // 更新事件 existingInvoker.value = nextValue; } else { const name = key.slice(2).toLowerCase();
if (nextValue) { // 添加事件 const invoker = (invokers[key] = createInvoker(nextValue)); el.addEventListener(name, invoker); } else { // 移除事件 el.removeEventListener(name, existingInvoker); invokers[key] = undefined; } }}六、Diff 算法
6.1 Diff 策略
Vue 3 的 Diff 算法分为几种情况:
function patchKeyedChildren(c1, c2, container) { let i = 0; const l2 = c2.length; let e1 = c1.length - 1; let e2 = l2 - 1;
// 1. 从头部开始同步 while (i <= e1 && i <= e2) { if (isSameVNodeType(c1[i], c2[i])) { patch(c1[i], c2[i], container); } else { break; } i++; }
// 2. 从尾部开始同步 while (i <= e1 && i <= e2) { if (isSameVNodeType(c1[e1], c2[e2])) { patch(c1[e1], c2[e2], container); } else { break; } e1--; e2--; }
// 3. 新增节点 if (i > e1) { if (i <= e2) { const nextPos = e2 + 1; const anchor = nextPos < l2 ? c2[nextPos].el : null; while (i <= e2) { patch(null, c2[i], container, anchor); i++; } } }
// 4. 删除节点 else if (i > e2) { while (i <= e1) { unmount(c1[i]); i++; } }
// 5. 复杂情况:移动/新增/删除混合 else { // ...使用最长递增子序列 }}6.2 最长递增子序列
对于复杂情况,Vue 3 使用最长递增子序列(LIS)优化移动:
function getSequence(arr) { const p = arr.slice(); const result = [0]; let i, j, u, v, c; const len = arr.length;
for (i = 0; i < len; i++) { const arrI = arr[i]; if (arrI !== 0) { j = result[result.length - 1]; if (arr[j] < arrI) { p[i] = j; result.push(i); continue; }
u = 0; v = result.length - 1; while (u < v) { c = ((u + v) / 2) | 0; if (arr[result[c]] < arrI) { u = c + 1; } else { v = c; } }
if (arrI < arr[result[u]]) { if (u > 0) { p[i] = result[u - 1]; } result[u] = i; } } }
u = result.length; v = result[u - 1]; while (u-- > 0) { result[u] = v; v = p[v]; }
return result;}6.3 Diff 过程图解
flowchart TB
subgraph 旧列表
A1["A (key='a')"]
B1["B (key='b')"]
C1["C (key='c')"]
D1["D (key='d')"]
E1["E (key='e')"]
end
subgraph 新列表
A2["A (key='a')"]
C2["C (key='c')"]
F2["F (key='f')"]
B2["B (key='b')"]
G2["G (key='g')"]
end
A1 -.->|"相同"| A2
B1 -.->|"移动"| B2
C1 -.->|"相同"| C2
D1 -.->|"删除"| X
E1 -.->|"删除"| X
F2 -.->|"新增"| F
G2 -.->|"新增"| G
七、组件系统
7.1 组件实例
const componentOptions = { props: { count: Number }, data() { return { local: 0 }; }, computed: { double() { return this.count * 2; }, }, methods: { increment() { this.local++; }, }, render() { return h("div", this.local); },};
// 组件实例结构const instance = { vnode, // 组件 VNode type, // 组件选项 props, // props 对象 attrs, // 非 prop 属性 state, // data 响应式数据 render, // 渲染函数 subTree, // 渲染的子树 ctx, // 上下文对象 proxy, // 代理对象(this 指向) isMounted, // 是否已挂载 bc: null, // beforeCreate 钩子 c: null, // created 钩子 bm: null, // beforeMount 钩子 m: null, // mounted 钩子 bu: null, // beforeUpdate 钩子 u: null, // updated 钩子 um: null, // unmounted 钩子};7.2 组件渲染流程
sequenceDiagram
participant P as 父组件
participant C as 子组件实例
participant R as 渲染函数
participant S as 响应式数据
P->>C: 传递 props
C->>C: 初始化 setup
C->>S: 创建响应式数据
C->>R: 执行 render
R->>S: 读取数据(收集依赖)
R-->>C: 返回 VNode
C->>C: patch 子节点
Note over S: 数据变化
S->>C: 触发更新
C->>R: 重新执行 render
R->>C: 返回新 VNode
C->>C: Diff 更新
7.3 setup 函数
// 组合式 APIexport default { props: { count: Number }, setup(props, { emit, slots, attrs }) { // 创建响应式数据 const local = ref(0);
// 计算属性 const double = computed(() => local.value * 2);
// 方法 const increment = () => { local.value++; emit("change", local.value); };
// 生命周期 onMounted(() => { console.log("mounted"); });
// 返回渲染函数或对象 return () => h("div", local.value); // 或 return { local, double, increment }; },};八、优化技术
8.1 PatchFlag
Vue 3 使用 PatchFlag 标记动态内容:
// 模板<div class="static" :class="dynamic">{{ text }}</div>
// 生成的渲染代码_createVNode("div", { class: ["static", _ctx.dynamic], textContent: _ctx.text}, null, PatchFlags.PROPS | PatchFlags.TEXT);
// PatchFlagsexport const enum PatchFlags { TEXT = 1, // 动态文本 CLASS = 2, // 动态 class STYLE = 4, // 动态 style PROPS = 8, // 动态属性 FULL_PROPS = 16, // 有动态 key 的属性 HYDRATE_EVENTS = 32, // 有事件监听器 STABLE_FRAGMENT = 64, // 稳定的 fragment KEYED_FRAGMENT = 128, // 有 key 的 fragment UNKEYED_FRAGMENT = 256, // 无 key 的 fragment NEED_PATCH = 512, // 需要非 props 比较 DYNAMIC_SLOTS = 1024, // 动态插槽 HOISTED = -1, // 静态提升的节点 BAIL = -2 // 退出优化}8.2 静态提升
// 模板<div> <span class="static">静态内容</span> <span>{{ dynamic }}</span></div>;
// 静态提升const _hoisted_1 = /*#__PURE__*/ _createVNode( "span", { class: "static" }, "静态内容", -1);
function render() { return _createVNode("div", null, [ _hoisted_1, // 复用静态节点 _createVNode("span", null, _ctx.dynamic, 0), ]);}8.3 缓存事件处理函数
// 模板<button @click="count++">{{ count }}</button>
// 编译结果export function render(_ctx) { return (_openBlock(), _createBlock("button", { onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.count++ && _ctx.count++(...args))) }, _toDisplayString(_ctx.count), 1))}九、调试工具
9.1 Vue DevTools
Inspector 面板:├── 组件树├── 组件状态(data, props, computed)├── Pinia 状态└── 路由信息
Timeline 面板:├── 组件事件├── 性能分析├── 路由变化└── Pinia mutations9.2 性能调试
// 开发模式下开启性能追踪app.config.performance = true;
// 自定义性能追踪import { performance } from "perf_hooks";
const start = performance.now();// ...渲染操作const end = performance.now();console.log(`渲染耗时: ${end - start}ms`);总结
Vue 响应式完整流程
flowchart TB
subgraph 响应式系统
A[reactive/ref] --> B[Proxy 代理]
B --> C[track 收集依赖]
B --> D[trigger 触发更新]
end
subgraph 调度系统
D --> E[queueJob 入队]
E --> F[异步批量更新]
F --> G[nextTick]
end
subgraph 渲染系统
G --> H[执行渲染函数]
H --> I[生成 VNode]
I --> J[Diff 算法]
J --> K[patch 更新]
end
subgraph DOM
K --> L[DOM 更新]
end
关键要点
- 响应式:Proxy 拦截 + 依赖收集
- 计算属性:惰性求值 + 缓存
- 调度:异步更新 + 批量处理
- 渲染:VNode + Diff + Patch
- 优化:PatchFlag + 静态提升
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
Vue 响应式原理:从数据变化到视图更新
https://blog.souloss.com/posts/principles/vue-reactivity-principles/ 部分信息可能已经过时
相关文章 智能推荐
1
React 渲染流程:从 setState 到 DOM 更新
原理 深入剖析 React 的渲染流程,从 setState 调用到 DOM 更新的完整链路,涵盖 Fiber 架构、调度器、协调器、渲染器的核心原理。
2
MySQL 查询执行流程:从 SQL 到结果集
原理 深入剖析 MySQL 执行 SQL 查询的完整流程,从连接器到存储引擎,涵盖分析器、优化器、执行器的核心机制,揭示查询优化的底层原理。
3
Java JVM 运行机制:从 .class 到机器码
原理 深入剖析 JVM 的完整运行流程——从类加载到字节码执行,涵盖运行时数据区、垃圾回收、JIT 编译等核心机制。
4
JavaScript 引擎原理:V8 执行流程
原理 深入剖析 V8 引擎执行 JavaScript 代码的完整流程,从源码解析到字节码生成,从解释执行到 JIT 编译优化,揭示 JavaScript 高性能运行的秘密。
5
技术内幕揭秘
原理 技术内幕揭秘系列——深入剖析各类技术的底层原理与执行流程,从浏览器渲染到数据库查询,从容器启动到框架响应,揭示黑盒背后的秘密。






