lastModified: 2024-08-14
缘由
众所周知的, 在vue3中, 响应式的方式由Proxy
实现的.
相比于vue2的defineProperty
, Proxy
为响应式提供了更好的性能和灵活性.
proxy-example
typescript
const $app = document.querySelector('#app') as HTMLDivElement;
const $value = document.querySelector('#value') as HTMLDivElement;
const $input = document.querySelector('#input') as HTMLInputElement;
const obj = {
value: '',
oldValue: '',
};
const write = () => {
$value.innerHTML = `oldValue: ${obj.oldValue} <br/> value: ${obj.value}`;
};
// Proxy的基本示例
const proxyObj = new Proxy(obj, {
get(target, key) {
return target[key];
},
set(target, key, value) {
const oldValue = target[key];
target[key] = value;
obj.oldValue = oldValue;
obj.value = value;
write();
return true;
},
});
$input.addEventListener('input', function () {
proxyObj.value = this.value;
});
defineProperty-example
typescript
const $app = document.querySelector('#app') as HTMLDivElement;
const $value = document.querySelector('#value') as HTMLDivElement;
const $input = document.querySelector('#input') as HTMLInputElement;
const obj = {
value: '',
oldValue: '',
};
const write = () => {
$value.innerHTML = `oldValue: ${obj.oldValue} <br/> value: ${obj.value}`;
};
const defineProperty = () => {
let value = obj.value;
Object.defineProperty(obj, 'value', {
get() {
return value;
},
set(newValue) {
obj.oldValue = obj.value;
value = newValue;
write();
},
});
};
defineProperty();
$input.addEventListener('input', function () {
obj.value = this.value;
});
可以看到相比Proxy
, defineProperty
的实现要更加麻烦, 需要遍历一个对象的所有属性, 如果对象比较大就会消耗性能, 同时也无法对对象新增和删除的属性做出响应.
而Proxy
则作为浏览器提供的一个新特性, 能够对对象的所有操作做出响应, 并且基于此特性, vue3不再需要遍历对象的所有属性, 能够大大提高性能.
但是在源码中有这么一段代码:
src/reactive/baseHandlers.ts
typescript
class BaseReactiveHandler implements ProxyHandler<Target> {
constructor(
protected readonly _isReadonly = false,
protected readonly _isShallow = false,
) {}
get(target: Target, key: string | symbol, receiver: object) {
....
const res = Reflect.get(target, key, receiver);
...
return res;
}
}
这个Reflect
为什么会在这里使用呢?
Reflect和Proxy
Reflect
是ES6中新增的一个对象, 它提供了一些操作对象的方法, 比如Reflect.get
, Reflect.set
, Reflect.deleteProperty
等.
typescript
const obj = { a: 1 }; Reflect.get(obj, 'a') === obj.a; Reflect.set(obj, 'a', 2); // obj.a = 2; Reflect.deleteProperty(obj, 'a') === true; // delete obj.a;
以上的操作方式时相等的, 那么为什么不直接使用obj.a
呢?
正常情况下, obj.a
和Reflect.get(obj, 'a')
确实是相等的, 但是当obj
是一个Proxy
对象时, 情况就有所不同了.
Proxy的一些问题
javascript
const $app = document.querySelector('#app');
const base = {
__name: 'base',
get name() {
return this.__name;
},
};
const baseProxy = new Proxy(base, {
get(target, key) {
return target[key];
},
});
const child = {
__proto__: baseProxy,
__name: 'child',
};
$app.innerHTML = child.name;
可以看到, 理论上应该调用的this
应该是child
, 但是实际上输出的是base
的值.
这是因为Proxy
代理了base
的get
操作, 所以调用时的target
为base
而不是child
.
ECMA 规范的定义
根据ecma-get的规定, 在访问对象的时候, 会调用对象的[[GET]]
方法获取值. 这个方法不会暴露给用户, 也不会显示在对象下.
同时, 对于Proxy
创建的特殊对象, ecma-proxy-get也规定了单独的[[GET]]
方式.
一般对象的GET调用步骤
先看一般对象调用时, [GET]
的规定:
- 先在自身查找是否有对应的属性, 如果没有则递归调用父级的
[[GET]]
方法, 直到找到对应的属性描述符. - 判断属性描述符结果是不是数据描述符, 即描述符不为空, 描述符包含
[[VALUE]]
也就是值字段或者[[Writable]]
也就是可以写入的话, 则返回true
否则false
. - 如果是数据描述符, 则返回对应的
[[VALUE]]
字段 - 如果是操作描述符, 则查找描述符的
[[GET]]
方法, 如果没有则返回undefiend
有则调用并将Receiver
对象作为this
传入. - 重复以上步骤, 直到找到对应的值或者
parent
父级为空.
方法中, Receiver
对象是调用该方法的对象, O
是对象本身, P
是查找的属性.
Proxy对象的GET调用步骤
Proxy
对象中的[[ProxyTarget]]
和[[ProxyHandler]]
是在创建Proxy
对象时设置的内部属性. 对应传入的原始对象和处理函数对象.
然后再看Proxy
的[[GET]]
就要简单一点:
可以看到在Proxy
代理对象的[[GET]]
上, 会先获取[[ProxyHandler]]
中的get
方法, 如果没有获取到则直接调用且返回原始对象的[[GET]]
.
如果获取到了则会调用该方法, 并且将target
P
Receiver
作为参数传进去, 也就是Proxy
的get
方法接收的几个参数:
target
: 被代理的原始对象P
: 属性名Receiver
: 调用该方法的对象
之后则是判断这个属性是否可以被访问可以被写入之类, 如果不可以则抛出异常, 否则返回属性的值, 也就是被代理对象的对应的值.
到这里也就知道了为什么target[key]
会返回base
而不是child
了, 因为Proxy
的get
方法中, target
被代理的对象是base
, 而Receiver
是child
, 所以返回的就是base
的值.
同时, 如果直接调用receiver[key]
则会造成对调用对象的递归调用(不断循环调用同一是代理对象的属性), 导致调用栈溢出.
Reflect.get的调用
相比于上面的Proxy
对象的调用, ecma-reflection的Reflect.get
的调用就更加简单的多.
就只是简单的调用一下, 因为无法直接操作对象的[[GET]]
方法传递receiver
, 所以需要通过Reflect
来调用.
上面已经看到了, 对于普通对象, 如果是自身属性则返回, 否则查找父级属性返回. 如果是操作符取值则将receiver
作为this
对[[GET]]
进行调用.
对于代理对象, 如果有则经过get
方法后返回, 否则调用源对象的[[GET]]
方法返回.
总结
通过以上的分析, 可以看到Proxy
和Reflect.get
在实现上其实非常相似, 都是通过调用对象的[[GET]]
方法获取值, 只是Proxy
在获取值时, 会优先调用自定义的get
方法.
同时也知道为什么vue3要在Proxy
中使用Reflect
进行操作, 是为了避免一些情况下的代理对象取值错误, 比如操作符取值时(目前也不知道是否有其他的可能)的this
能够准确指向执行对象.
由于Proxy
的target
是被代理的对象, 所以会出现使用操作符this
指向错误的问题, 所以使用Reflect
来进行操作.
不过在实际业务中, 使用get
set
之类的操作符还是非常少见的. 在vue3的源码中使用倒是用了不少, 比如ref
.
reactivity/src/ref.ts
typescript
/**
* @internal
*/
class RefImpl<T = any> {
_value: T;
private _rawValue: T;
dep: Dep = new Dep();
public readonly [ReactiveFlags.IS_REF] = true;
public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false;
constructor(value: T, isShallow: boolean) {
this._rawValue = isShallow ? value : toRaw(value);
this._value = isShallow ? value : toReactive(value);
this[ReactiveFlags.IS_SHALLOW] = isShallow;
}
get value() {
if (__DEV__) {
this.dep.track({
target: this,
type: TrackOpTypes.GET,
key: 'value',
});
} else {
this.dep.track();
}
return this._value;
}
set value(newValue) {
const oldValue = this._rawValue;
const useDirectValue = this[ReactiveFlags.IS_SHALLOW] || isShallow(newValue) || isReadonly(newValue);
newValue = useDirectValue ? newValue : toRaw(newValue);
if (hasChanged(newValue, oldValue)) {
this._rawValue = newValue;
this._value = useDirectValue ? newValue : toReactive(newValue);
if (__DEV__) {
this.dep.trigger({
target: this,
type: TriggerOpTypes.SET,
key: 'value',
newValue,
oldValue,
});
} else {
this.dep.trigger();
}
}
}
}
而且类的继承也不会出现这种问题, 可以算是在业务中基本遇不到的问题了.