Vue响应式进化:从Object.defineProperty到Proxy
Vue3已经发布了一段时间了,那么相比于Vue2做了哪些升级,解决了什么问题呢?
众所周知Vue的响应式系统是其核心特性之一,它能够自动追踪数据变化并更新视图。从Vue 2到Vue 3,响应式系统经历了一次重要的技术升级,从使用Object.defineProperty到采用Proxy,本文将结合关键代码来分析它的进化过程。
Vue 2的响应式实现:Object.defineProperty
基本原理
Vue 2使用Object.defineProperty来实现响应式,其核心原理如下:
/**
* Vue 2响应式实现的核心代码
* 主要包含以下几个部分:
* 1. 数组方法拦截器:重写数组的七个方法
* 2. 观察者函数:递归观察对象的所有属性
* 3. 响应式定义:使用Object.defineProperty定义getter/setter
* 4. 依赖收集器:管理依赖关系
*/
// 1. 创建数组方法拦截器
const oldArrayProto = Array.prototype; // 保存数组原始的原型对象
// 创建新的原型对象,继承自Array.prototype
const newArrayProto = Object.create(oldArrayProto);
// 需要重写的数组方法列表,这些方法会改变原数组,需要特殊处理
const arrayMethods = [
'push', // 在数组末尾添加元素
'pop', // 删除数组最后一个元素
'shift', // 删除数组第一个元素
'unshift', // 在数组开头添加元素
'splice', // 从数组中添加/删除元素
'sort', // 对数组进行排序
'reverse' // 反转数组
];
// 重写数组方法
arrayMethods.forEach(method => {
// 保存原始方法
const original = oldArrayProto[method];
// 重写方法
newArrayProto[method] = function(...args) {
// 调用原始数组方法,保持原有功能
const result = original.apply(this, args);
// 获取新增的元素,只有push、unshift、splice这三个方法会新增元素
let inserted;
switch (method) {
case 'push':
case 'unshift':
// 这两个方法的所有参数都是新增的元素
inserted = args;
break;
case 'splice':
// splice方法的第三个参数开始才是新增的元素
inserted = args.slice(2);
break;
}
// 对新插入的元素进行响应式处理
if (inserted) {
observer(inserted);
}
// 触发更新
console.log('数组更新了');
return result;
};
});
/**
* 2. 观察者函数
* 用于递归观察对象的所有属性
* @param {Object|Array} data - 需要观察的数据
* @returns {Object|Array} - 返回观察后的数据
*/
function observer(data) {
// 如果不是对象或者是null,直接返回
if (typeof data !== 'object' || data === null) {
return data;
}
// 如果是数组,重写数组的原型
if (Array.isArray(data)) {
// 将数组的原型指向我们重写后的原型
data.__proto__ = newArrayProto;
// 对数组中的每个元素进行观察
data.forEach(item => observer(item));
return;
}
// 遍历对象的所有属性
for (let key in data) {
defineReactive(data, key, data[key]);
}
}
/**
* 3. 定义响应式
* 使用Object.defineProperty定义getter/setter
* @param {Object} target - 目标对象
* @param {string} key - 属性名
* @param {any} value - 属性值
*/
function defineReactive(target, key, value) {
// 递归观察value,处理嵌套对象
observer(value);
// 为每个属性创建依赖收集器
const dep = new Dep();
Object.defineProperty(target, key, {
get() {
// 依赖收集,当有watcher访问这个属性时,将watcher添加到依赖中
if (Dep.target) {
dep.depend();
}
return value;
},
set(newValue) {
// 如果新值和旧值相同,不做处理
if (newValue === value) return;
// 对新值进行观察,处理新值是对象的情况
observer(newValue);
value = newValue;
// 通知所有依赖进行更新
dep.notify();
}
});
}
/**
* 4. 依赖收集器
* 用于管理依赖关系,实现发布订阅模式
*/
class Dep {
constructor() {
// 使用Set存储依赖,避免重复
this.subscribers = new Set();
}
// 添加依赖
depend() {
if (Dep.target) {
this.subscribers.add(Dep.target);
}
}
// 通知所有依赖进行更新
notify() {
this.subscribers.forEach(watcher => watcher.update());
}
}
// 5. 使用示例
let data = {
name: 'zdx',
age: 26,
address: {
city: 'hangzhou',
area: 'xihu'
},
hobbies: ['reading', 'coding']
};
// 将数据转换为响应式
observer(data);
局限性
-
对象属性监听限制:
- 无法检测对象属性的添加或删除
- 需要使用
Vue.set或this.$set来添加新属性 - 需要使用
Vue.delete或this.$delete来删除属性
-
数组操作限制:
- 无法直接监听数组索引和长度的变化
- 需要重写数组方法(push、pop、shift、unshift、splice、sort、reverse)
-
性能开销:
- 需要递归遍历对象的所有属性
- 每个属性都需要单独的getter/setter
Vue 3的响应式实现:Proxy
基本原理
Vue 3使用Proxy来实现响应式,其核心原理如下:
/**
* Vue 3响应式实现的核心代码
* 主要包含以下几个部分:
* 1. 响应式对象创建:使用Proxy创建响应式对象
* 2. 依赖收集:使用WeakMap存储依赖关系
* 3. 触发更新:通知依赖进行更新
* 4. 副作用函数:处理副作用
*/
/**
* 1. 创建响应式对象
* 使用Proxy创建响应式对象
* @param {Object} target - 目标对象
* @returns {Proxy} - 返回响应式对象
*/
function reactive(target = {}) {
// 如果不是对象或者是null,直接返回
if (typeof target !== 'object' || target === null) {
return target;
}
// 如果已经是响应式对象,直接返回,避免重复代理
if (target.__isReactive) {
return target;
}
// Proxy的处理器对象
const proxyConfig = {
/**
* 拦截对象属性的读取操作
* @param {Object} target - 目标对象
* @param {string} key - 属性名
* @param {Object} receiver - Proxy实例
*/
get(target, key, receiver) {
// 获取原始值
const result = Reflect.get(target, key, receiver);
// 依赖收集
track(target, key);
// 递归处理嵌套对象
return reactive(result);
},
/**
* 拦截对象属性的设置操作
* @param {Object} target - 目标对象
* @param {string} key - 属性名
* @param {any} value - 新值
* @param {Object} receiver - Proxy实例
*/
set(target, key, value, receiver) {
// 获取旧值
const oldValue = target[key];
// 设置新值
const result = Reflect.set(target, key, value, receiver);
// 如果值发生变化,触发更新
if (oldValue !== value) {
trigger(target, key);
}
return result;
},
/**
* 拦截对象属性的删除操作
* @param {Object} target - 目标对象
* @param {string} key - 属性名
*/
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
if (result) {
trigger(target, key);
}
return result;
}
};
// 创建Proxy实例
const observed = new Proxy(target, proxyConfig);
// 标记为响应式对象
observed.__isReactive = true;
return observed;
}
/**
* 2. 依赖收集
* 使用WeakMap存储依赖关系
* 结构:WeakMap -> Map -> Set
* WeakMap: target -> Map
* Map: key -> Set
* Set: effect functions
*/
const targetMap = new WeakMap();
/**
* 依赖收集函数
* @param {Object} target - 目标对象
* @param {string} key - 属性名
*/
function track(target, key) {
// 如果没有活动的副作用函数,直接返回
if (!activeEffect) return;
// 获取target对应的依赖Map
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 获取key对应的依赖Set
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 将当前活动的副作用函数添加到依赖中
dep.add(activeEffect);
}
/**
* 3. 触发更新
* 通知依赖进行更新
* @param {Object} target - 目标对象
* @param {string} key - 属性名
*/
function trigger(target, key) {
// 获取target对应的依赖Map
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 获取key对应的依赖Set
const dep = depsMap.get(key);
if (dep) {
// 执行所有依赖的副作用函数
dep.forEach(effect => effect());
}
}
/**
* 4. 副作用函数,用于处理副作用
*/
let activeEffect = null;
/**
* 创建副作用函数
* @param {Function} fn - 副作用函数
*/
function effect(fn) {
// 保存当前活动的副作用函数
activeEffect = fn;
// 执行副作用函数
fn();
// 清除当前活动的副作用函数
activeEffect = null;
}
// 5. 使用示例
const data = reactive({
name: 'zdx',
age: 26,
info: {
city: 'hangzhou',
detail: {
address: 'xihu'
}
},
hobbies: ['reading', 'coding']
});
// 监听数据变化
effect(() => {
console.log('数据更新:', data.name);
});
// 修改数据
data.name = 'zdx1';
data.info.city = 'shanghai';
data.hobbies.push('gaming');
优势
-
更强大的拦截能力:
- 可以监听对象属性的添加和删除
- 可以监听数组索引和长度的变化
- 可以监听Map、Set、WeakMap、WeakSet等数据结构
-
更好的性能:
- 不需要递归遍历对象的所有属性
- 按需响应,只有在访问属性时才会进行依赖收集
-
更简洁的API:
- 不需要特殊的API(如Vue.set)来添加或删除属性
- 可以直接使用原生JavaScript操作对象
实际应用示例
Vue 2中的响应式
// Vue 2
const vm = new Vue({
data: {
user: {
name: 'John'
}
}
})
// 需要特殊API
vm.$set(vm.user, 'age', 25)
vm.$delete(vm.user, 'name')
Vue 3中的响应式
// Vue 3
const state = reactive({
user: {
name: 'John'
}
})
// 直接操作即可
state.user.age = 25
delete state.user.name
实现响应式的技术演进:
- Vue 2:
Object.defineProperty,Vue 3:Proxy - 主要改进:更强大的数据监听能力和性能表现
- 注意Proxy的浏览器兼容性要求

浙公网安备 33010602011771号