一、vue2的响应式原理
Vue 2 的响应式原理是其核心特性之一,通过 数据劫持 和 发布-订阅模式 实现数据驱动视图更新。以下是 Vue 2 响应式原理的详细解析,涵盖其核心机制、实现细节以及实际应用中的限制。
一、响应式核心思想
Vue 2 的响应式系统基于以下两个核心概念:
-
数据劫持(Data Observe):
- 使用
Object.defineProperty
劫持对象属性,拦截get
和set
操作。 - 当访问或修改属性时,自动触发依赖收集和派发更新。
- 使用
-
发布-订阅模式(Pub/Sub):
- 在数据变化时通知所有依赖该数据的订阅者(Watcher),触发视图更新。
二、手写一个简易的响应式
1. 数据劫持(Object.defineProperty)
observe
:递归处理对象的每个属性。defineReactive
:使用Object.defineProperty
劫持属性:get
:收集依赖(通过Dep.target
)。set
:触发更新(调用dep.notify()
)
Vue 2 通过递归遍历对象的所有属性,使用 Object.defineProperty
将每个属性转换为响应式属性:
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (!data || typeof data !== 'object') return;
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
defineReactive(obj, key, val) {
const dep = new Dep(); // 每个属性对应一个 Dep
// 递归处理嵌套对象
this.observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get: () => {
// 依赖收集:如果存在当前订阅者(Watcher),添加到 Dep 在 get 操作中,通过全局的 Dep.target 判断当前是否处于订阅者上下文中,如果是,则将订阅者添加到 Dep.subs 中。
if (Dep.target) {
dep.addSub(Dep.target); // 收集订阅者
}
Dep.target = null;
return val;
},
set: (newVal) => {
if (newVal === val) return;
val = newVal;
// 派发更新:触发所有订阅者的 update 方法
dep.notify();
}
});
}
}
2.Dep 类(依赖管理器)
subs
:存储所有订阅者(Watcher)。addSub
:添加订阅者。notify
:通知所有订阅者执行update
。
class Dep{
constructor() {
this.subs = []; // 存储订阅者
}
addSub(watcher){
this.subs.push(watcher)
}
noticefy(){
this.subs.forEach(sub=>sub.update())
}
}
3. Watcher 类(订阅者)
cb
:更新回调函数(模拟视图更新)。update
:触发回调函数- Watcher 是响应式系统的执行单元,负责监听数据变化并执行回调(如更新视图)。
- 在 Vue 中,每个组件实例都有一个 Watcher,用于监听数据变化并触发组件重新渲染。
class Watcher {
constructor(cb) {
this.cb = cb; // 更新回调
}
update() {
this.cb(); // 执行回调
}
}
const app = new Vue({
data: {
message: 'Hello Vue!',
user: {
name: 'Alice',
age: 25
}
}
});
// 模拟视图更新(订阅者)
const watcher = new Watcher(() => {
console.log('数据变化了,视图需要更新!');
});
/*
/*
订阅者(Watcher)执行时,设置 Dep.target = this。
读取响应式属性时,触发 get,将订阅者添加到 Dep.subs。
完成后清空 Dep.target。
*/
// 订阅 message 属性(手动绑定)
Dep.target = watcher; // 设置当前订阅者
app.$data.message; // 触发 getter,收集依赖
Dep.target = null; // 清空当前订阅者
// 修改数据,触发更新
app.$data.message = 'Hello World!'; // 控制台输出:数据变化了,视图需要更新!
1. 依赖收集的核心机制
- 当一个
Watcher
(订阅者)访问响应式数据时,会触发Object.defineProperty
的getter
。 - 在
getter
中,通过检查Dep.target
是否存在,判断当前是否有订阅者正在监听该数据。 - 如果存在订阅者(
Dep.target
不为null
),则将该订阅者添加到当前属性对应的Dep
的subs
(订阅者列表)中。
2. 避免“误收集”依赖
Dep.target
是一个临时变量,仅在订阅者执行get
操作时短暂存在。- 在
get
操作完成后,Dep.target
会被清空(设为null
),防止后续无关的依赖被错误地收集。
三、Vue 2 的响应式限制
1. 无法检测数组索引和长度的变化
Vue 2 通过 Object.defineProperty
劫持属性,但数组的索引(如 arr[0]
)和长度(length
)是动态的,无法被劫持。因此,直接修改数组索引不会触发更新。
解决方案:
- Vue 2 提供了 数组变异方法(如
push
,pop
,splice
)的重写,修改数组时需使用这些方法。 - 使用
this.$set
修改数组索引或对象属性:
this.$set(this.arr, 0, 100); // ✅ 有效
2. 无法检测对象属性的动态添加/删除
由于 Object.defineProperty
只能劫持已存在的属性,动态添加的新属性不会被劫持:
this.obj.newKey = 'value'; // 无效
解决方案:
- 使用
this.$set
或this.$delete
动态添加/删除属性:
this.$set(this.obj, 'newKey', 'value'); // ✅ 有效
this.$delete(this.obj, 'newKey'); // ✅ 有效
二、vue3的响应式原理
在 Vue 3 中,响应式系统是其核心机制之一,通过 Proxy
和 Reflect
实现数据劫持,并结合 依赖收集 和 发布-订阅模式 实现数据驱动视图更新。相比 Vue 2 的 Object.defineProperty
,Vue 3 的响应式系统更强大、灵活,解决了 Vue 2 的诸多限制。
一、Vue 3 响应式原理的核心思想
1. Proxy 替代 Object.defineProperty
- Vue 2 使用
Object.defineProperty
劫持对象属性,但存在无法检测数组索引变化和动态属性添加的问题。 - Vue 3 使用
Proxy
对整个对象进行代理,可以拦截所有操作(如get
,set
,deleteProperty
,has
等),从而解决 Vue 2 的限制。
2. 依赖收集与派发更新
- 依赖收集:在访问响应式属性时,收集当前的副作用函数(
effect
)。 - 派发更新:在修改响应式属性时,通知所有依赖该属性的副作用函数重新执行。
二、手写一个简易的响应式
1.依赖管理器(Dep
)
Dep
类
effects
:存储所有依赖该属性的副作用函数(effect
)。track()
:收集当前激活的副作用函数(activeEffect
),将其添加到effects
中。trigger()
:触发所有依赖该属性的副作用函数重新执行。
//当访问 state.count 时,track() 会被调用,将当前 effect 收集到 Dep 中;当 state.count 被修改时,trigger() 会执行所有相关的 effect。
class Dep {
constructor() {
this.effects = new Set(); // 存储依赖该属性的副作用函数
}
track() {
if (activeEffect) {
this.effects.add(activeEffect);
}
}
trigger() {
for (const effect of this.effects) {
effect();
}
}
}
2.全局激活副作用函数(activeEffect
)
activeEffect
变量
- 全局变量:保存当前正在执行的副作用函数(
effect
)。 - 作用:在访问响应式数据时,用于依赖收集(将
activeEffect
添加到Dep.effects
中)。
//在 effect(() => console.log(state.count)) 中,activeEffect 会被临时设置为该 effect 函数,从而在访问 state.count 时完成依赖收集。
let activeEffect = null;
3.副作用函数封装(effect
)
effect
函数
runner
:包装副作用函数fn
,并在执行前设置activeEffect
,执行后重置。return runner
:允许手动调用runner()
重新执行副作用函数。
/*
第一次调用 effect 时,runner 会被执行,activeEffect 被设置为 runner,从而在访问 state.count 时完成依赖收集。
后续修改 state.count 会触发 Dep.trigger(),再次执行 runner。
*/
function effect(fn) {
const runner = () => {
activeEffect = runner;
fn(); // 执行副作用函数
activeEffect = null;
};
runner();
}
4.响应式对象创建(reactive
)
reactive
函数
Proxy
拦截get
和set
:get
:访问属性时,递归处理嵌套对象,收集依赖(调用dep.track()
)。set
:修改属性时,如果值变化,触发依赖更新(调用dep.trigger()
)。
proxyMap
:缓存已代理的对象,避免重复代理。
function reactive(target) {
// 使用 WeakMap 缓存已代理的对象
const proxyMap = new WeakMap();
function createReactiveObject(target) {
// 如果已代理,直接返回
if (proxyMap.has(target)) {
return proxyMap.get(target);
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
// 递归处理嵌套对象
if (typeof result === 'object' && result !== null) {
return createReactiveObject(result);
}
// 依赖收集
const dep = getDep(target, key);
dep.track();
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (result && oldValue !== value) {
// 派发更新
const dep = getDep(target, key);
dep.trigger();
}
return result;
}
});
proxyMap.set(target, proxy);
return proxy;
}
return createReactiveObject(target);
}
5.依赖管理(getDep
)
getDep
函数
targetMap
:使用WeakMap
存储目标对象与Map
的映射。depsMap
:存储属性名与Dep
的映射。- 作用:为每个对象属性分配一个独立的
Dep
实例,用于管理依赖。
/*
对于 state.count,getDep(state, 'count') 会返回对应的 Dep。
如果多次访问 state.count,会复用同一个 Dep。
*/
const targetMap = new WeakMap(); // 存储目标对象与 Map 的映射
function getDep(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
const state = reactive({
count: 0,
user: {
name: 'Alice',
age: 25
}
});
effect(() => {
console.log('Count changed:', state.count);
});
effect(() => {
console.log('User name changed:', state.user.name);
});
state.count = 1; // 输出: Count changed: 1
state.user.name = 'Bob'; // 输出: User name changed: Bob
流程分析:
-
创建响应式对象:
state
是reactive({ count: 0, user: { ... } })
创建的响应式对象。user
是嵌套对象,reactive
会递归处理,使其属性变为响应式。
-
定义副作用函数:
- 第一个
effect
依赖state.count
。 - 第二个
effect
依赖state.user.name
。
- 第一个
-
修改数据:
state.count = 1
:- 触发
set
拦截。 - 找到
state.count
对应的Dep
,调用trigger()
。 - 执行第一个
effect
,输出Count changed: 1
。
- 触发
state.user.name = 'Bob'
:- 触发
set
拦截。 - 找到
state.user.name
对应的Dep
,调用trigger()
。 - 执行第二个
effect
,输出User name changed: Bob
。
- 触发
+----------------+ +------------------+ +-----------------+
| 响应式数据 | | Proxy 拦截操作 | | 副作用函数 |
| (reactive/ref) |<----->| (get/set/delete) |<----->| (effect/渲染函数)|
+----------------+ +------------------+ +-----------------+
| ^
| |
v |
+---------------------+
| 依赖收集与触发更新 |
| (track/trigger) |
+---------------------+