最近想再回顾下 Proxy 这一部分的内容, 顺便也看看他的应用场景, 刚好在 Vue3 的响应式 API 中有使用, 所以就结合着一起复习下, 顺便总结记录一番. 如果只对 Vue3 的响应式感兴趣的, 可以直接跳到文章的第二部分.
Proxy 和 Reflect 是 ES6中出来的, 已经很久了, 但是平时工作中写一些业务代码基本都不会去考虑用这两个语法 (不是业务太low了, 就是自己太low了), 太久了容易生疏, 这里结合 Vue3 来系统性的整理一下.
可以说 Proxy 和 Reflect 是贴近了函数式的编程思想, 特别是 Reflect, 均是采用函数式调用的写法, 下面先来看下这两者的概念.
话不多说, 先上语法
/**
* target: 目标对象
* handler: 一个对象, 是操作target时所对应的某些处理函数
*/
new Proxy(target, handler)
Proxy顾名思义是代理的意思, 其功能也名副其实, 在目标对象之前设置一层代理, 进行对象访问的拦截, 由此提供了一种机制,就是可以对外界的访问进行过滤和改写. 这个功能很强大, 等于可以改变一些对象原来底层的访问, 从而修改某些操作的默认行为.
具体可以拦截或修改对象的哪些访问? 目前官方提供了13个拦截操作, 均可以在参数 handler 对象中定义, 具体如下:
方法 | 说明 | 返回值 |
---|---|---|
get(target, propKey, receiver) | 拦截对象属性的读取 | 属性值 |
set(target, propKey, value, receiver) | 拦截对象属性的设置 | 布尔值 |
has(target, propKey) | 拦截propKey in proxy的操作,以及对象的hasOwnProperty方法 | 布尔值 |
deleteProperty(target, propKey) | 拦截delete proxy[propKey]的操作 | 布尔值 |
ownKeys(target) | 拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy) | 数组 |
getOwnPropertyDescriptor(target, propKey) | 拦截Object.getOwnPropertyDescriptor(proxy, propKey) | 属性的描述对象 |
defineProperty(target, propKey, propDesc) | 拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs) | 布尔值 |
preventExtensions(target) | 拦截Object.preventExtensions(proxy) | 布尔值 |
getPrototypeOf(target) | 拦截Object.getPrototypeOf(proxy) | 对象 |
isExtensible(target) | 拦截Object.isExtensible(proxy) | 布尔值 |
setPrototypeOf(target, proto) | 拦截Object.setPrototypeOf(proxy, proto) | 布尔值 |
apply(target, object, args) | 拦截Proxy实例作为函数调用的操作 | |
construct(target, args) | 拦截Proxy实例作为构造函数调用的操作 |
具体的用法不一一介绍, 具体可以参见MDN, 选 get 方法重点的说下
用于拦截属性的读取操作, 可以在读取过程中进行一系列的逻辑执行, 比如:
1) 可以拦截数组下标读取, 以及倒序获取数组元素
const arr = new Proxy([1, 2, 3], {
get(target, p, receiver) {
return Reflect.get(target, p < 0 ? `${+p + target.length}` : p, receiver);
},
});
console.log(arr[-1]); // 3
2) 函数名链式调用
// 定义全局方法
const globalFunc = {
double: (n) => n * 2,
pow: (n) => Math.pow(n, 2),
round: (n) => Math.round(n),
};
const pip = (value) => {
let funcList = [];
const func = new Proxy({}, {
get(target, p) {
if (p === "exec") {
return funcList.reduce((val, fn) => fn(val), value);
}
funcList.push(globalFunc[p]);
return func;
},
}
);
return func;
};
console.log(pip(3.4).double.pow.round.exec); // 46
3) get 函数有第三个参数 receiver , 可以用来改变读取的函数中 this 的指向
const testA = { _m: "m_A", _n: "n_A" };
const testB = {
_m: "m_B",
_n: "n_B",
get m() { return this._m; },
get n() { return this._n; },
};
const proxy = new Proxy(testB, {
get(target, p, receiver) {
if (p === 'm') return Reflect.get(target, p, testA);
return Reflect.get(target, p, receiver);
},
});
console.log(proxy.m, proxy.n); // m_A, n_B
4) 可以定义对象的私有属性, 禁止被外界直接访问, 这里有个小问题, 下面代码虽然禁止了私有属性的访问, 但是涉及到私有属性相关的方法也无法正常使用, 这个是否有好的解法?
const obj = {
_name: 'test',
getName() { return this._name; } // 这个也没有办法拿到_name
};
const proxy = new Proxy(obj, {
get(target, p, receiver) {
if (p.startsWith('_')) {
console.warn('cannot read private prop redirectly')
return null
}
return Reflect.get(target, p, receiver);
},
});
console.log(proxy._name); // warning
5) get 方法可以被继承
const proxy = new Proxy({ a: 1 }, {
get(target, p, receiver) {
console.log(`GET ${p}`);
return Reflect.get(target, p, receiver);
},
});
const obj = Object.create(proxy);
console.log(obj.a);
// GET a
// 1
英文大多翻译成反射的意思, 但是理解起来比较别扭, 其实就是替代了现有 Object 对象的某些方法, 对这些方法做了一些优化, 使这些方法更容易理解, 更函数式. 其目前实现的方法与 Proxy 中 handler 的方法一一对应, 都是13个
Reflect 是一个对象, 所有属性和方法都是静态的, 为什么有了 Object 还需要 Reflect ? 他与 Proxy 有什么关系?
1) 提升 Object 方法的合理性, 使方法的返回更加友好. Object.defineProperty 在出错时会跑出一个异常, 我们要手动去捕获他, 否则程序会中断, 而 Reflect.defineProperty 采用函数返回值的形式告诉调用方结果.
const obj = Object.freeze({});
try {
Object.defineProperty(obj, 'a', { value: 1 });
} catch (error) {
console.log('Object: ', error);
// Object: TypeError: Cannot define property foo, object is not extensible
}
const res = Reflect.defineProperty(obj, 'a', { value: 1 });
console.log('Reflect: ', res); // Reflect: false
2) 函数式编程思想, 不再采用 Object 的一些命令式语法
const obj = { a: 1, b: 2 };
console.log('Object: ', 'a' in obj);
console.log('Reflect: ', Reflect.has(obj, 'a'));
delete obj.a;
Reflect.deleteProperty(obj, 'b');
3) 为 Proxy 提供运行对象默认行为的方法, 作为修改行为的基础. 因为 Proxy 可以拦截修改对象的一些行为方法, 而这些方法都能在 Reflect 上找到, 保证原生的行为能力可以正常运行.
const proxy = new Proxy(obj, {
get(target, name) {
console.log('get ', target, name);
// 调用原生方法
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete ' + name);
// 调用原生方法
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has ' + name);
// 调用原生方法
return Reflect.has(target, name);
}
});
有了上面的基础, 我们接下来看下 Vue3 是如何使用的
众所周知, Vue3 使用 Proxy 替代了 Object.defineProperty 来做响应式. 因为 Object.defineProperty 的功能有限 (无法监听删除、数组下标、in事件、apply等), 所以 Vue2 做了很多功能补齐, 甚至有的就不支持. 而到了 Vue3 使用 Proxy 带来了全新的响应式解决方案, 我们来看看其中的核心: 响应式API (官网传送), 篇幅原因, 先介绍一部分
这几个方法先放一起说, 大多数响应式 API 都会以 reactive 为基础, 他返回一个对象的响应式代理. 直接源码看一下, reactvie 最终是使用 createReactiveObject 来创建一个响应式代理
function reactive(target) {
// 如果target已经是只读的响应式对象, 则直接返回只读的版本, 例如一般的computed类型
if (isReadonly(target)) {
return target;
}
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}
说明:
另外三种类型 readonly、shallowReactive、shallowReadonly 与 reactive 类似, 都是调用 createReactiveObject 方法进行代理创建, 只是传入的参数不同, 处理逻辑稍有差异. 不同代理创建、内部属性标识、缓存对象、拦截方法都不一样, 这里归纳整理一下
类型 | 创建函数 | 是否只读 | 缓存对象 | 普通类型handlers | 集合类型handlers |
---|---|---|---|---|---|
响应式代理 | reactive | false | reactiveMap | get, set, deleteProperty, has, ownKeys | get |
只读代理 | readonly | true | readonlyMap | readonlyGet、set、deleteProperty | get |
浅层响应式代理 | shallowReactive | false | shallowReactiveMap | shallowGet, shallowSet, deleteProperty, has, ownKeys | get |
浅层只读代理 | shallowReadonly | true | shallowReadonlyMap | shallowReadonlyGet、set、deleteProperty | get |
下面就来看下 createReactiveObject 这个方法具体是怎么创建这四种类型的 Proxy 的
再来看下 createReactiveObject 的实现
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// 只有非空的对象类型, 才会去创建代理, 否则会直接返回原始值
if (!shared.isObject(target)) {
return target;
}
// 如果目标对象已经是响应式的也直接返回, 除非是创建一个他的只读副本
if (target["__v_raw" /* RAW */] && !(isReadonly && target["__v_isReactive"])) {
return target;
}
// proxyMap即reactiveMap, 上面说了缓存了对象代理状态, 已代理过的就从缓存中直接获取, 并返回
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
// 只有符合条件的对象, 才会去进行代理监听, 具体哪些类型, 下面说明
const targetType = getTargetType(target);
if (targetType === 0) {
return target;
}
// 根据不同类型的对象, 创建不同程度的监听方法
const proxy = new Proxy(target, targetType === 2 ? collectionHandlers : baseHandlers);
// 缓存当前原始对象和代理对象之间的映射关系
proxyMap.set(target, proxy);
return proxy;
}
函数流程图如下
说明:
getTargetType: Vue3 会根据原始对象的类型对其进行归类, 并根据类型设置代理对象的 handler, 其依据用一张表来描述,
原始对象类型 | 返回值 | 返回值含义 | handler取值 |
---|---|---|---|
markRaw 标记不可被转为代理 | 0 | SKIP (无效) | - |
对象不可扩展 | 0 | INVALID (无效) | - |
Object 类型 | 1 | COMMON | mutableHandlers |
Array 类型 | 1 | COMMON | mutableHandlers |
Map 类型 | 2 | COLLECTION | mutableCollectionHandlers |
Set 类型 | 2 | COLLECTION | mutableCollectionHandlers |
WeakMap 类型 | 2 | COLLECTION | mutableCollectionHandlers |
WeakSet 类型 | 2 | COLLECTION | mutableCollectionHandlers |
不属于以上任何情况 | 0 | INVALID (无效) | - |
baseHandlers: 针对于普通 (COMMON) 类型的 handlers, 他定义了 get、set、deleteProperty、has、ownKeys 拦截方法. 也由此可见, 在 Vue2 的基础上扩展了除get、set的其他响应式控制.
collectionHandlers: 针对于集合 (COLLECTION) 类型的 handlers, 其只定义了 get 方法, 因为集合都是以函数式的方式调用, 例如 set.has、set.add, 所以只需要拦截 get 方法, 然后再到 get 方法中代理各集合的内建方法即可
下面分开解释, 先来看下 COMMON 的 handlers
Vue3 中针对于普通类型的对象一共有 4 种类型的 get 拦截, 除了普通响应式的 get, 还包括 shallowGet (浅层响应式)、readonlyGet (只读代理)、shallowReadonlyGet (浅层只读代理). 浅层的含义就是说所有的效果只作用域对象的根层级, 不做深层级的处理. 例如浅层响应式, 只有根层级被转化成了响应式, 对其深层级属性不做转换, 例如下面这个例子
const state = shallowReactive({ a: 1, b: { c: 2 } });
isReactive(state); // true, 当 state.a的值变化时, 可以被监听
isReactive(state.b); // false, 内层对象非响应式, 会原样返回
这几种类型都是通过 createGetter 来创建, 他接收两个布尔类型参数, 这两个参数的取值组合控制了不同的类型的 get 定义, 普通的 reactive 使用的是函数默认值创建, 一起来看下区别
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
// 获取响应式状态: 一个对象不是只读的, 就是响应式的
if (key === '__v_isReactive' /* IS_REACTIVE */) {
return !isReadonly;
}
// 获取只读状态
if (key === '__v_isReadonly' /* IS_READONLY */) {
return isReadonly;
}
// 获取浅层状态
if (key === '__v_isShallow' /* IS_SHALLOW */) {
return shallow;
}
// 获取target的原始对象
if (
key === '__v_raw' /* RAW */ && receiver
=== (isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target;
}
// 调用非只读数组的内置代理方法, arrayInstrumentations里有Vue3代理的方法, 下面详细说
const targetIsArray = shared.isArray(target);
if (!isReadonly && targetIsArray && shared.hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
// 直接先获取target对象的属性值
const res = Reflect.get(target, key, receiver);
// 获取的是默认Symbol或者内置的Symbol属性, 则直接返回
if (shared.isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res;
}
// 如果不是只读代理, 则有可能会改变属性值, 要进行依赖收集
if (!isReadonly) {
track(target, 'get' /* GET */, key);
}
// 如果是浅层代理, 则直接返回
if (shallow) {
return res;
}
// 如果获取的属性值是ref对象, 则判断是否需要自动解包
if (isRef(res)) {
// 数组或者数值类型, 则不解包
const shouldUnwrap = !targetIsArray || !shared.isIntegerKey(key);
return shouldUnwrap ? res.value : res;
}
// 获取的属性值是个对象, 因为不是浅层代理, 所以返回值也需要转为代理对象
if (shared.isObject(res)) {
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
说明:
同样来归纳一下不同代理之间 get 拦截方法的差异性
get拦截类型 | 对应函数 | 内部标识取值 | 是否依赖收集 | ||
---|---|---|---|---|---|
__v_isReactive | __v_isReadonly | __v_isShallow | |||
响应式代理 | get | true | false | false | 除改变数组长度方法 和 内建Symbol方法外 |
只读代理 | readonlyGet | false | true | false | 否 |
浅层响应式代理 | shallowGet | true | false | true | 除改变数组长度方法 和 内建Symbol方法外 |
浅层只读代理 | shallowReadonlyGet | false | true | true | 否 |
用四种方法创建一个 { a: 1 } 对象的响应式对象, 看下不同代理类型的区别
和 get 一样, set 有有一个公共方法, 用来创建不同类型的 set, 他叫 createSetter, set 主要分为3 种, 因为两种只读代理共用一个. 对于这种代理, 由于是只读, 所以不会进行任何设置的操作, 其set 就是一个空壳, 像下面这样
set(target, key) { return true; }
而另外两种响应式的代理, 则通过 createSetter 函数的参数来区分为普通 set 和 浅层 set (shallowSet), 下面来重点看下 createSetter
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
// 先获取一下对应key原来的值
let oldValue = target[key];
// 对于原来值是只读ref对象, 是不允许改变其本来的ref属性的
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false;
}
// 如果target是非浅层响应式代理, 并且新值value非只读
if (!shallow && !isReadonly(value)) {
// 如果新值value是非浅层响应式的, 则对新旧值进行解包, 拿到原始对象
if (!isShallow(value)) {
value = toRaw(value);
oldValue = toRaw(oldValue);
}
// 非数组, 值由响应式变为非响应式, 则直接赋值, 退出
if (!shared.isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
}
}
// 用于判断是不是要往数组或对象里添加新元素
const hadKey = shared.isArray(target) && shared.isIntegerKey(key)
? Number(key) < target.length
: shared.hasOwn(target, key);
// 设置target的key值
const result = Reflect.set(target, key, value, receiver);
// 只对当前实例属性做依赖处理, 如果是原型链中的某个元素,则不要触发
if (target === toRaw(receiver)) {
// 如果是新增元素, 则激活add类型的触发器,进行依赖处理
if (!hadKey) {
trigger(target, 'add' /* ADD */, key, value);
} else if (shared.hasChanged(value, oldValue)) {
// 如果是修改元素值, 并且新旧值不一样, 则激活set类型的触发器,进行依赖处理
trigger(target, 'set' /* SET */, key, value);
}
}
return result;
};
}
说明:
const state = reactive({ a: 1 });
const comp = computed(() => state.a + 1); // 只读的ref对象
const test = ref({ c: comp }); // 将其初始化给ref属性的c
test.value.c = 2; // 把c的值动态改为数字类型, 触发 proxy 的set
// 赋值不被允许, 直接返回 false, 立即报错
// TypeError: 'set' on proxy: trap returned falsish for property 'c'
同样来归纳一下不同代理之间 set 拦截方法的差异性
get拦截类型 | 对应函数 | 是获取原始值 | 处理trigger的add | 处理trigger的set |
---|---|---|---|---|
响应式代理 | set | 是 | 响应式对象值新增 | 响应式对象值变化 |
只读代理 | readonlySet | 否 | 否 | 否 |
浅层响应式代理 | shallowSet | 否 | 响应式对象值新增 | 响应式对象值变化 |
浅层只读代理 | readonlySet | 否 | 否 | 否 |
这几个代理方法比较简单, 放一起说, 在实现上, 主要区分了只读属性和非只读属性两种代理
只读属性的代理: 对 deleteProperty 删除属性这种操作都是禁止的, has、ownKeys没有进行拦截
非只读属性的代理: 会对三个方法进行依赖收集, 然后调用 Reflect 对应方法返回数据
get拦截类型 | deleteProperty | has | ownKeys |
---|---|---|---|
响应式代理 | 拦截, trigger delete | 拦截, track hase | 拦截, track iterate |
只读代理 | 拦截, return true | 否 | 否 |
浅层响应式代理 | 拦截, trigger delete | 拦截, track hase | 拦截, track iterate |
浅层只读代理 | 拦截, return true | 否 | 否 |
下面来看下 COLLECTION 的 handlers
Set 和 Map 这类的都是通过实例化对象的方式使用, 所以要对里面的值进行操作, 都是调用对象的一些属性和方法, 因此他们代理的 handlers 只需要进行 get 函数的实现. 但是在 get 函数中, 分别实现了集合方法的代理.
mutableCollectionHandlers 同样分了四种类型, 与 ceateGetter 一样都是通过一个公共函数实现, 这里是叫 createInstrumentationGetter 的函数. 这个函数很简单, 主要是进行了不同代理类型的分发处理, 分别给到 mutableInstrumentations、readonlyInstrumentations、shallowReadonlyInstrumentations、shallowInstrumentations 进行集合方法对代理, 其余的内部标识取值与 createGetter 类似.
这里不贴源码了, 用一张函数调用图来表示, 所有方法的实现都区分了四种类型
Vue3 代理了集合的 10 个方法和 1 个属性的获取, 其实现不复杂, 可以直接查看源码理解, 这里不做分析, 主要说明几点
1) 所有非只读的代理对象, 都会进行 track 的依赖收集
2) add、set、delete、clear 这种涉及到结合元素变更的, 都会进行 trigger 触发依赖处理
ref 函数创建一个响应式的、可更改的 ref 对象, 他是 RefImpl 类的实例化对象, 其对外只暴露一个 value 属性用户获取和更改.
shallowRef 函数是创建一个浅层 ref 对象, 浅层的函数在上面已经说过了, 浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式.
ref 和 shallowRef 都是通过 createRef(rawValue, shallow) 来创建的, 而 createRef 最终是进行 new RefImpl(rawValue, shallow) 实例化, 根据参数 shallow 区分是否是浅层对象, 下面来重点看下 RefImpl
class RefImpl {
constructor(value, __v_isShallow) {
this.__v_isShallow = __v_isShallow; // 浅层对象标识
this.dep = undefined; // 依赖收集器
this.__v_isRef = true; // ref标识
// 浅层对象会原样存储和暴露, 不转为响应式
this._rawValue = __v_isShallow ? value : toRaw(value);
this._value = __v_isShallow ? value : toReactive(value);
}
get value() {
trackRefValue(this); // 当有调用方时, 往this.dep中添加依赖
return this._value; // 返回对应响应式值
}
set value(newVal) {
// 设置新值
newVal = this.__v_isShallow ? newVal : toRaw(newVal);
// 当值发生变化的时候, 更新相关属性, 并且会根据收集的依赖进行逐个触发, 通知值变化
if (shared.hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = this.__v_isShallow ? newVal : toReactive(newVal);
triggerRefValue(this);
}
}
}
来看两个示例, 示例代码如下
const refA = ref({ a: 1 });
const shallowRefB = shallowRef({ a: 1 });
const c = computed(() => (refA.value.a + 1));
说明
比较熟悉的计算属性, 必然也是响应式的. 他可以接收一个函数或一个带有 get、set 的对象作为第一参数, 他返回一个只读的 ref 对象. computed 内部是通过实例化 ComputedRefImpl 对象创建的, 所以重点看下 ComputedRefImpl 类.
class ComputedRefImpl {
// 如果初始化computed传入的是函数, 则getter就是该函数, _setter是个空函数, isReadonly为true
// 如果初始化computed传入的是对象, 则分别将get、set赋值给getter、setter, isReadonly取决于是否定义了set
constructor(getter, _setter, isReadonly, isSSR) {
this._setter = _setter; // 初始化setter函数
this.dep = undefined; // 依赖收集齐
this.__v_isRef = true; // ref标识
this._dirty = true; // 是否被引用, true为未被引用
// 副作用处理器件, 主要是当有地方引用computed时, 进行依赖收集和处理
this.effect = new ReactiveEffect(getter, () => {
// 当有被其他对象引用, 且触发副作用时, 会执行这里
if (!this._dirty) {
this._dirty = true;
triggerRefValue(this);
}
});
this.effect.computed = this; // 在副作用处理器中关联
// 副作用处理器是否有效
this.effect.active = this._cacheable = !isSSR;
// 只读标识
this['__v_isReadonly' /* IS_READONLY */] = isReadonly;
}
get value() {
// 针对于只读的对象, reactive不会进行处理, 需要在对象自身中进行响应式处理
const self = toRaw(this);
trackRefValue(self);
// 如果存在被引用的情况, 则进行依赖处理, 并计算computed的value值
if (self._dirty || !self._cacheable) {
self._dirty = false;
self._value = self.effect.run();
}
return self._value;
}
set value(newValue) {
this._setter(newValue);
}
}
同样用两个示例说明, 示例代码如下, 在上面创建的 refA 的基础上, 生成计算属性
const computedC = computed(() => (refA.value.a + 1));
const computedD = computed(() => (refA.value.a + 2));
说明
class ReactiveEffect {
...
run() {
...
try {
this.parent = activeEffect;
activeEffect = this;
...
}
}
}
function trackEffects(dep, debuggerEventExtraInfo) {
...
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
Vue3 中初始化响应式对象的 API 大致先介绍到这里, 还有一些其他的响应式相关的工具、副作用等 API 有机会一起学习分享
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。