mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
590 字
2 分钟
Vue 响应式原理:从数据变化到视图更新
2023-05-24

前言#

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 2Vue 3
响应式实现Object.definePropertyProxy
数组监听重写数组方法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; // 读取 count
proxy.count = 1; // 设置 count = 1

Proxy 拦截操作

拦截方法触发时机
get读取属性
set设置属性
hasin 操作符
deletePropertydelete 操作符
ownKeysObject.keys 等
getOwnPropertyDescriptorObject.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;
}

计算属性特点

  1. 惰性求值:只有被访问时才计算
  2. 缓存:依赖不变时返回缓存值
  3. 自动追踪:执行时自动收集依赖
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 类型

标记说明
ELEMENT1原生元素
STATEFUL_COMPONENT2有状态组件
TEXT_CHILDREN8文本子节点
ARRAY_CHILDREN16数组子节点
SLOTS_CHILDREN32插槽子节点

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 函数#

// 组合式 API
export 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);
// PatchFlags
export 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 mutations

9.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

关键要点#

  1. 响应式:Proxy 拦截 + 依赖收集
  2. 计算属性:惰性求值 + 缓存
  3. 调度:异步更新 + 批量处理
  4. 渲染:VNode + Diff + Patch
  5. 优化:PatchFlag + 静态提升

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Vue 响应式原理:从数据变化到视图更新
https://blog.souloss.com/posts/principles/vue-reactivity-principles/
作者
Souloss
发布于
2023-05-24
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时