Vue的响应式代理和数据改造
在 Vue 中,响应式代理是实现数据驱动的核心机制之一。当开发者改变 Vue 实例的数据时,视图会立即进行渲染,以显示最新的数据。以下是对 Vue 中响应式代理和数据改造的解析。
# 初始化阶段
在 Vue 的初始化阶段,会执行 initData
,对 data
属性进行改造。如果 data
是一个函数,会立即执行该函数获取 data
对象,并将其赋值给 this._data
。
// proxy data on instance
const keys = Object.keys(data);
const props = vm.$options.props;
const methods = vm.$options.methods;
let i = keys.length;
while (i--) {
const key = keys[i];
// ...
if (props && hasOwn(props, key)) {
__DEV__ &&
warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
);
} else if (!isReserved(key)) {
proxy(vm, `_data`, key);
}
}
export function proxy(target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
这段代码的作用是将 data
对象上的所有属性代理到 Vue 实例上,使得在 Vue 组件中可以通过 this.xxx
的方式获取 data
属性上的值。实际上, this.xxx
的访问路径是: this.xxx → this._data.xxx → data对象
。
代理完之后,就要对 data 对象进行响应式改造,递归 data 对象,如果 data 的属性中有对象或者数组的,则继续对其进行响应式改造。
# 对象的响应式改造
对于对象的响应式改造,首先会创建一个 dep
对象, 让其跟随 Observer
对象, dep
的作用是监听该对象是否发生变化。
然后遍历属性,为每个属性定义一个 dep
,用于监听属性变化。通过 Object.defineProperty
改造 get
和 set
方法(vue3 通过 proxy
),当视图取值时,进行监听;当修改属性时,通知视图更新。
dep 的作用可以看这篇文章
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key,
});
} else {
dep.depend();
}
if (childOb) {
childOb.dep.depend();
if (isArray(value)) {
dependArray(value);
}
}
}
return isRef(value) && !shallow ? value.value : value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
if (!hasChanged(value, newVal)) {
return;
}
if (__DEV__ && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else if (getter) {
// #7981: for accessor properties without setter
return;
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal;
return;
} else {
val = newVal;
}
childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock);
if (__DEV__) {
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value,
});
} else {
dep.notify();
}
},
});
# 数组改造
对于数组,Vue 并没有对数组中所有元素进行监听改造,而是只对数组的增删 API 和数组中的对象元素进行改造。具体的方法如下:
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
];
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted);
// notify change
if (__DEV__) {
ob.dep.notify({
type: TriggerOpTypes.ARRAY_MUTATION,
target: this,
key: method,
});
} else {
ob.dep.notify();
}
return result;
});
});
def(value, '__ob__', this);
if (isArray(value)) {
if (!mock) {
if (hasProto) {
/* eslint-disable no-proto */
(value as any).__proto__ = arrayMethods;
/* eslint-enable no-proto */
} else {
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i];
def(value, key, arrayMethods[key]);
}
}
}
if (!shallow) {
this.observeArray(value);
}
}
这段代码的作用是对数组的常用操作方法(如 push
、 pop
、 shift
等)进行了改造,使得修改数组时能够通知视图更新。
对于这些实现,其实不能覆盖所有情况。
例如以下这些情况不会更新。
- 修改数组上的元素,
a = [1, 2, 3], a[0] = 100
- 新增对象属性,
a = { b: 1 }, a.c = 1
官方则通过 $set
去设置,内部原理其实就是修改完值之后,会调用一次 a.__ob__.dep.notify()
,主动刷新视图
# 总结
通过响应式代理和数据改造,Vue 实现了数据的双向绑定。当数据发生变化时,视图会自动更新,保持数据和视图的同步。这是 Vue 实现响应式的核心机制,也是 Vue 在前端框架中的一大特色。