Vue2 与 Vue3 响应式数据原理详解
一、核心本质
Vue 响应式核心目标:数据变化 → 自动更新视图,底层实现思路统一:
- 劫持/监听数据的读取(get):收集依赖(记录哪些视图、函数依赖当前数据)
- 劫持/监听数据的修改(set) :触发依赖(通知所有收集的函数更新视图) 二者最大区别:Vue2 使用
Object.defineProperty,Vue3 使用Proxy + Reflect。
二、Vue2 响应式:Object.defineProperty
1. 原理介绍
Object.defineProperty(obj, prop, descriptor) 给对象单个属性劫持 get/set:
- get() :组件渲染、计算属性、watch 读取数据时触发,收集当前正在执行的渲染函数/副作用函数,存入Dep(依赖收集器)。
- set():数据被赋值修改时触发,Dep 遍历内部所有依赖,执行更新函数,刷新视图。
2. 基础简易模拟
js
// 依赖收集容器
class Dep {
constructor() {
this.subs = []; // 存放依赖(渲染watcher)
}
// 收集依赖
addSub(watcher) {
this.subs.push(watcher);
}
// 派发更新
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
// 观察者:数据劫持
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return;
Object.keys(obj).forEach(key => {
let val = obj[key];
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
// 有正在执行的watcher则收集依赖
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
observe(newVal); // 新值递归劫持
dep.notify(); // 通知更新
}
});
observe(val); // 递归深层劫持对象
})
}
// 渲染Watcher:视图更新函数
class Watcher {
constructor(fn) {
Dep.target = this;
this.updateFn = fn;
this.update();
Dep.target = null;
}
update() {
this.updateFn();
}
}
// 测试
const data = { name: "vue2" };
observe(data);
new Watcher(() => console.log("视图更新:", data.name));
data.name = "更新了";
3. Vue2 致命缺陷
- 无法监听对象新增、删除属性
defineProperty只能劫持初始化存在的属性,新增this.obj.a=1、删除delete this.obj.a无法触发set,因此需要Vue.set / this.$set、this.$delete。 - 数组监听缺陷 无法监听数组下标修改、数组长度修改,只能重写7个数组方法(push/pop/shift/unshift/splice/sort/reverse)做拦截。
- 递归遍历性能差 初始化时递归遍历对象所有层级,深层大对象初始化开销大。
三、Vue3 响应式:Proxy + Reflect
1. 原理介绍
new Proxy(target, handler) 代理整个对象,而非单个属性:
- 拦截操作:读取、修改、新增、删除、下标、长度等全部对象操作。
- 搭配
Reflect:规范对象原生操作,保证this指向、返回值稳定。 - 依旧沿用 Dep收集依赖、effect副作用函数 的模式。
2. 简易模拟
js
class Dep {
constructor() {
this.effects = new Set();
}
collect() {
activeEffect && this.effects.add(activeEffect);
}
trigger() {
this.effects.forEach(fn => fn());
}
}
let activeEffect = null;
// 副作用函数:视图更新逻辑
function effect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
// 存储所有对象-key对应的dep
const targetMap = new WeakMap();
function getDep(target, key) {
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 Dep()));
return dep;
}
// 数据劫持
function reactive(target) {
return new Proxy(target, {
get(target, key) {
const dep = getDep(target, key);
dep.collect();
return Reflect.get(target, key);
},
set(target, key, val) {
const dep = getDep(target, key);
const res = Reflect.set(target, key, val);
dep.trigger();
return res;
},
deleteProperty(target, key) {
const dep = getDep(target, key);
const res = Reflect.deleteProperty(target, key);
dep.trigger();
return res;
}
})
}
// 测试
const state = reactive({ name: "vue3" });
effect(() => console.log("视图更新:", state.name));
state.name = "新版vue";
state.age = 18; // 新增属性可监听
delete state.name; // 删除属性可监听
3. Proxy 对比 defineProperty 的优势
- 全覆盖监听 支持属性新增、删除、数组下标、数组长度、
in运算符等全部操作,不再需要$set。 - 懒劫持 初始化只代理外层对象,深层属性用到才递归代理,初始化性能大幅优化。
- 数组完美监听 原生拦截数组下标、length 修改,不用重写数组方法。
- 可拦截更多操作:
has、ownKeys、apply等13种对象行为。
4. 小缺点
Proxy 不兼容 IE 浏览器,Vue3 直接放弃IE支持;Vue2 为兼容老旧浏览器选择 defineProperty。
四、完整响应式闭环流程(Vue2/Vue3通用)
- 初始化劫持:对data数据做劫持(defineProperty/Proxy),定义get/set拦截。
- 依赖收集 组件挂载执行渲染函数 → 读取响应式数据触发get → 把当前渲染函数存入Dep。
- 派发更新 修改数据触发set → Dep取出所有收集的渲染函数 → 执行函数重新渲染DOM。
- 异步更新队列 多次连续修改数据不会多次渲染,Vue 用nextTick维护异步队列,合并更新,只刷新一次视图。
五、衍生概念梳理
- Watcher:Vue2 中渲染Watcher、计算属性Watcher、侦听器Watcher,存放更新逻辑。
- Dep:依赖管理器,一对一绑定数据属性与Watcher。
- computed:惰性依赖,只有取值才执行,缓存结果。
- watch:主动监听,数据变化立刻执行回调。
- nextTick:DOM异步更新,等待DOM刷新完成后执行回调。