vue 3 响应式原理
在 Vue 3 中,响应式系统的核心是使用了 ES6 的 Proxy
对象来实现对数据的拦截和响应式更新。
简单的 Proxy
示例:
const data = { count: 0 };
const handler = {
get(target, key, receiver) {
// 当访问属性时触发
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
// 当设置属性时触发
const result = Reflect.set(target, key, value, receiver);
trigger(target, key);
return result;
}
};
const reactiveData = new Proxy(data, handler);
const proxy = new Proxy(target, handler);
console.log(proxy.message); // "Getting property message" -> "Hello, world!"
proxy.message = "Hello, Vue 3!"; // "Setting property message to Hello, Vue 3!"
在上面的代码中,handler
对象定义了两个拦截器方法:get
和 set
。这两个方法分别用于拦截属性的读取和设置操作。
-
get
方法在属性被访问时触发。它接收三个参数:目标对象(target
)、属性名(key
)和代理对象(receiver
)。在get
方法中,你可以执行一些操作,比如追踪依赖(track
),然后返回属性的值。 -
set
方法在属性被设置时触发。它接收四个参数:目标对象(target
)、属性名(key
)、新值(value
)和代理对象(receiver
)。在set
方法中,你可以执行一些操作,比如触发依赖更新(trigger
),然后返回一个布尔值表示设置操作是否成功。
在 Vue 3 中,Proxy
被用来实现响应式数据追踪和依赖收集。主要通过 reactive
和 ref
API 实现。
Reactive API 示例
import { reactive } from 'vue';
const state = reactive({
count: 0
});
state.count++; // 这将触发 Vue 的响应式系统
Ref API 示例:
import { ref } from 'vue';
const count = ref(0);
count.value++; // 这也将触发 Vue 的响应式系统
响应式系统的工作原理
-
依赖收集:当响应式对象的属性被访问时,
Proxy
的get
拦截器会触发依赖收集,将依赖(如组件或计算属性)记录下来。 -
触发更新:当响应式对象的属性被修改时,
Proxy
的set
拦截器会触发通知,所有依赖于该属性的组件或计算属性都会重新计算或重新渲染。
简化的响应式系统示例:
import { reactive, effect } from 'vue';
const state = reactive({ count: 0 });
effect(() => {
console.log(`Count is: ${state.count}`);
});
state.count++;
// 当 state.count 被修改时,上面的 effect 会重新执行,输出 "Count is: 1"
vue2 的响应式原理
Object.defineProperty
是 ES5 引入的一个方法,用于直接在一个对象上定义或修改一个属性,同时可以指定该属性的特性,如是否可枚举、是否可配置、是否可写等。
当你访问一个已经被 Vue 转换过的属性时,Vue 会将这个属性标记为“依赖”,然后当这个属性被修改时,Vue 会通知所有依赖于这个属性的地方进行更新。这也就实现了 Vue 的响应式系统。当你在模板中使用了一个 data 对象的属性时,比如 {{ message }}
,Vue 会在内部追踪 message 属性的依赖关系,当 message 发生变化时,Vue 会自动更新相关的视图。
定义响应式属性:Vue 定义响应式属性的核心逻辑如下:
function defineReactive(obj, key, val) {
// 创建一个依赖管理器(Dep),用于收集和通知依赖
const dep = new Dep();
// 获取对象属性的当前描述符
let property = Object.getOwnPropertyDescriptor(obj, key);
// 如果不可配置,则直接返回
if (property && property.configurable === false) {
return;
}
// 缓存属性的 getter 和 setter(如果存在)
const getter = property && property.get;
const setter = property && property.set;
// 使用 Object.defineProperty 重新定义属性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 获取值
const value = getter ? getter.call(obj) : val;
// 如果存在当前依赖目标,则添加依赖
if (Dep.target) {
dep.depend();
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
// 如果新值与旧值相同,则不触发更新
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
// 如果存在 setter,则调用 setter,否则直接设置值
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
// 通知所有依赖进行更新
dep.notify();
}
});
}
-
依赖管理器 (Dep):这是一个简单的依赖管理器,用于收集依赖并在数据变化时通知它们:
let uid = 0;
class Dep {
constructor() {
this.id = uid++;
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
const index = this.subs.indexOf(sub);
if (index > -1) {
this.subs.splice(index, 1);
}
}
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
const subs = this.subs.slice();
for (let i = 0; i < subs.length; i++) {
subs[i].update();
}
}
}
Dep.target = null;
假设我们有一个 Vue 实例,其 data
对象中有一个属性 message
:
var vm = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
});
当你在 Vue 实例的模板中使用 {{ message }}
时,Vue 会创建一个“Watcher”来追踪这个属性的依赖:
watcher = new Watcher(vm, function updateComponent() {
console.log(vm.message); // 读取 message 属性会触发 getter
});
当你修改 vm.message
的值时,setter 会被触发,并且 dep.notify()
会通知所有依赖于 message
的 watcher 更新,从而触发视图更新。
vm.message = 'Hello World!'; // 触发 setter,调用 dep.notify(),视图更新
Object.defineProperty与 proxy特点:
-
局限性:
Object.defineProperty
只能对对象的属性进行拦截,不能拦截对象的新增属性、删除属性等操作。 -
性能:在处理大量属性时,
Object.defineProperty
的性能可能不如Proxy
,因为Object.defineProperty
需要为每个属性单独设置拦截器。 -
兼容性:
Object.defineProperty
在旧版浏览器中可能不被支持,而Proxy
需要 ES6 环境。object.defineProperty 示例
const obj = {}; Object.defineProperty(obj, 'name', { get() { console.log('访问 name 属性'); return '张三'; }, set(value) { console.log('设置 name 属性', value); } }); console.log(obj.name); // 输出 '访问 name 属性' 和 '张三' obj.name = '李四'; // 输出 '设置 name 属性' 和 '李四'
proxy 示例
const handler = { get(target, propKey, receiver) { console.log(`访问 ${propKey} 属性`); return Reflect.get(target, propKey, receiver); }, set(target, propKey, value, receiver) { console.log(`设置 ${propKey} 属性为 ${value}`); return Reflect.set(target, propKey, value, receiver); } }; const proxy = new Proxy({}, handler); proxy.name = '王五'; // 输出 '设置 name 属性为 王五' console.log(proxy.name); // 输出 '访问 name 属性' 和 '王五'
需要注意的点:
1、Object.defineProperty 有一些限制,它只能监听对象的属性,并且需要遍历对象的属性来进行转换,这意味着在 Vue 2 中无法监听数组的变化。因此,在 Vue 2 中对数组的变化需要通过特定的方法来进行处理,比如使用 push
、pop
等方法,或者直接使用 Vue.set
方法。
2、因此vue2 可能出现的一个问题,数据更新视图未更新的情况。但是vue2 推出了this.$set(target,key,修改后的值)。在vue3中使用了proxy 代理就很好的解决了这个问题。
3、异步更新问题:有些情况下,如果数据的变化发生在 JavaScript 执行栈之外(比如在定时器回调、Promise 回调中),Vue 可能无法立即捕获到数据的变化。这时可以使用 this.$nextTick()
来确保在 DOM 更新之后再执行一段代码。