首先来介绍一下Proxy
Proxy
它的第一个参数接收的是一个被代理对象target
,第二个参数是一个配置对象,包含一组捕获器handle
,将会返回一个代理对象ProxyTarget
.
这些handle
定义了在ProxyTarget
上触发的各种基本操作的行为.每个handle
都是一个函数,用来处理相应的操作
例如:
读取ProxyTarget
中的属性.
读取(get)是基本操作的一种,所以它将被get捕获器拦截.并且在get函数内可以重新定义返回结果.target
是被代理对象,key
是访问的属性.
因为 重新定义了返回结果return target[key];
访问ProxyTarget
中的属性就等于直接访问了被代理对象的属性一样.
const ProxyTarget = new Proxy(target, {
get(target, key) {
return target[key];
},
});
Reflect
它是一个全局方法,其中有
- reflect.get
- reflect.set等
任何在 Proxy 的拦截器中能够找到的方法,都能够在 Reflect 中找到同名函数
,它们的操作等价.不同的是
Reflect函数还能接收一个参数,即指定接收者 receiver,你可以把它理解为函数调用过程中的 this
既然操作等价,为什么Vue使用的是Reflect下的函数呢.举个例子
对下面的对象进行代理: 当我们使用 proxyTarget.bar 访问 bar属性时,它的 getter 函数内的 this 指向的其实是原始对象 target,这说明我们最终访问的其实是 target.foo。很显然,在副作用函数内通过原始对象访问它的某个属性是不会建立响应联系的
const target = {
foo: 1,
get bar() {
// 现在这里的 this 为代理对象 p
return this.foo
}
}
//等价于
effect(() => {
// obj 是原始数据,不是代理对象,这样的访问不能够建立响应联系
target.foo
})
代理Object
响应系统应该拦截一切读取操作,以便当数据变化时能够正确地触发响应 前面我们使用 get 拦截函数去拦截对属性的读取操作。但在响应系统中,“读取”是一个很宽泛的概念,下面列出了所有读取操作
- 访问属性:obj.foo
- 判断对象或原型上是否存在给定的 key:key in obj
- 使用 for...in 循环遍历对象:for (const key in obj) {}
对于in操作符
在has上就可以拦截
has(target, key) {
track(target, key)
return Reflect.has(target, key)
}
对于forin
在ownKeys上拦截,因为ownKeys的参数里没有key,ownKey只判断是否存在属性,并不对应与某一个key,所以参数没有key也很正常.但是没有key,forin
对应的副作用
应该怎么存放呢?那就再声明一个新的symbol(ITERATE_KEY),把它当成key.
那么什么时候触发与ITERATE_KEY对应的副作用函数呢? 给对象添加新属性
会影响forin循环的次数
所以只需要修改时把ITERATE_KEY对应的副作用函数也执行一遍
let ITERATE_KEY = Symbol();
ownKeys(target) {
// 将副作用函数与 ITERATE_KEY 关联
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
}
effect(() => {
for (const key in proxyData) {
console.log('sss')
}
}
)
function trigger(target, key) {
let effects = bucket?.get(target)?.get(key);
const iterateEffects = bucket?.get(target).get(ITERATE_KEY)//新增
const effectsToRun = new Set([...(effects?effects:[]),...(iterateEffects?iterateEffects:[])]);//新增
effectsToRun &&
effectsToRun.forEach((effectFn) => {
if (activeEffect !== effectFn) {
effectFn.options.scheduler
? effectFn.options.scheduler(effectFn)
: effectFn();
}
});
}
当只是修改属性不会对 for...in 循环产生影响,我们不需要触发副作用函数重新执行,否则会造成不必要的性能开销。所以要想解决上述问题,当设置属性操作发生时,就需要我们在set 拦截函数内能够区分操作的类型
,到底是添加新属性还是设置已有属性,再把type参数传递给trigger函数。
set(target, key, newVal) {
// 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
const type = Object.prototype.hasOwnProperty.call(target,key) ? 'SET' : 'ADD'//新增
target[key] = newVal;
trigger(target, key,type);//新增
},
function trigger(target, key,type) {
//新增type参数
let effects = bucket?.get(target)?.get(key);
const iterateEffects = bucket?.get(target).get(ITERATE_KEY)
if (type === 'ADD') {
//新增
effectsToRun = new Set([...(effects?effects:[]),...(iterateEffects?iterateEffects:[])]);
}
else {
//新增
effectsToRun=new Set(effects)
}
effectsToRun &&
effectsToRun.forEach((effectFn) => {
if (activeEffect !== effectFn) {
effectFn.options.scheduler
? effectFn.options.scheduler(effectFn)
: effectFn();
}
});
}
删除键值
也会影响forin,所以需要监听deleteProperty,那么当操作类型为 ADD 或 DELETE 时,需要触发与 ITERATE_KEY 相关联的副作用函数重新执行
(type === "ADD"||type === "DELETE")
deleteProperty(target, key) {
// 检查被操作的属性是否是对象自己的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, y);
// 使用 Reflect.deleteProperty 完成属性的删除
const res = Reflect.deleteProperty(target, key);
if (res && hadKey) {
// 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新,传递DELETE
trigger(target, key, "DELETE");
}
return res;
},
深度监听
下面这种情况就监听不到变化,需要借助递归,reactive函数返回封装之后的proxy
effect(() => {
console.log(proxyData.obj.a);
});
proxyData.obj.a=222;
get(target, key, receiver) {
res=Reflect.get(target, key, receiver)
track(target, key);
if (typeof res === 'object' && res !== null) {
return reactive(res)
}
return res
},
当然有时不需要深度监听,可以在封装一层函数,而给里面的createReactive传递不同参数用于区分
对于只读属性,只需添加上第三个参数即可
function reactive(obj) {
return createReactive(obj);
}
function shallowReactive(obj) {
return createReactive(obj, true);
}
//在get中。isShallow是第二个参数代表是否深度监听,直接返回未封装的
if (isShallow) {
return res }
//对应只读,需要在触发set deleteProperty最开始判断第三个参数isReadonly,抛出错误信息并立即退出
//对应深只读,需在get中将返回包装成只读
if (typeof res === 'object' && res !== null) {
// 如果数据为只读,则调用 readonly 对值进行包装
return isReadonly ? readonly(res) : reactive(res)
}
代理数组
对数组的读取操作有
- 通过索引访问
- 访问数组的length
- forin forof 循环
- 数组的原型方法,如 concat/join/every/some/find/findIndex/includes 等,以及其他所有不改变原数组的原型方法。
设置操作
- 通过索引修改
- 修改length
- 数组的栈方法:push/pop/shift/unshift。
- 修改原数组的原型方法:splice/fill/sort
代理数组其实是代理对象很相似,大部分代码都不需要额外做处理
但是例如通过索引设置元素,可能会修改length,也就需要触发length相关的副作用执行
let arr = reactive([0])
effect(() => {
console.log(arr.length)
})
arr[1]=100
在set中判断是对数组的操作是add还是set,在trigger中判断如果是add并且是数组,要触发length相关的副作用
set(target, key, newVal) {
const type = Array.isArray(target)//新增
? // 如果代理目标是数组,则检测被设置的索引值是否小于数组长度,
// 如果是,则视作 SET 操作,否则是 ADD 操作
Number(key) < target.length
? "SET"
: "ADD"
: Object.prototype.hasOwnProperty.call(target, key)
? "SET"
: "ADD";
target[key] = newVal;
trigger(target, key, type);
},
function trigger(target, key, type) {
let effects = bucket?.get(target)?.get(key);
const iterateEffects = bucket?.get(target)?.get(ITERATE_KEY);
const lengthEffects = bucket?.get(target)?.get('length');
if (type === 'ADD' && Array.isArray(target)) {
//新增
effectsToRun = new Set([
...(effects ? effects : []),
...(lengthEffects ? lengthEffects : []),
]);
}
else if (type === "ADD" || type === "DELETE") {
effectsToRun = new Set([
...(effects ? effects : []),
...(iterateEffects ? iterateEffects : []),
]);
} else {
effectsToRun = new Set(effects);
}
effectsToRun &&
effectsToRun.forEach((effectFn) => {
if (activeEffect !== effectFn) {
effectFn.options.scheduler
? effectFn.options.scheduler(effectFn)
: effectFn();
}
});
}
然而当修改 length 属性值时也会隐式地影响数组元素
,会对只有那些索引值大于或等于新的 length 属性值的元素产生影响
,需要触发响应。所以给trigger再传入一个参数也就是修改之后的长度length,再将相关的副作用添加再执行
const arr = reactive(['foo'])
effect(() => {
// 访问数组的第 0 个元素
console.log(arr[0]) // foo
})
// 将数组的长度修改为 0,导致第 0 个元素被删除,因此应该触发响应
arr.length = 0
set(target, key, newVal,receiver) {
const type = Array.isArray(target)
?
Number(key) < target.length
? "SET"
: "ADD"
: Object.prototype.hasOwnProperty.call(target, key)
? "SET"
: "ADD";
trigger(target, key, type, newVal);//多传一个值
return Reflect.set(target, key, newVal, receiver);
},
function trigger(target, key, type, newVal) {
let effects = bucket?.get(target)?.get(key);
const iterateEffects = bucket?.get(target)?.get(ITERATE_KEY);
const lengthEffects = bucket?.get(target)?.get("length");
if (type === "ADD" && Array.isArray(target)) {
effectsToRun = new Set([
...(effects ? effects : []),
...(lengthEffects ? lengthEffects : []),
]);
} else if (type === "ADD" || type === "DELETE") {
effectsToRun = new Set([
...(effects ? effects : []),
...(iterateEffects ? iterateEffects : []),
]);
} else {
effectsToRun = new Set(effects);
}
if (Array.isArray(target) && key === "length") {
//新增
let depsMap = bucket?.get(target);
// 对于索引大于或等于新的 length 值的元素,
// 需要把所有相关联的副作用函数取出并添加到 effectsToRun 中待执行
depsMap.forEach((effects, key) => {
if (key >= newVal) {
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
});
}
effectsToRun &&
effectsToRun.forEach((effectFn) => {
if (activeEffect !== effectFn) {
effectFn.options.scheduler
? effectFn.options.scheduler(effectFn)
: effectFn();
}
});
}
为了追踪对普通对象的 for...in 操作,人为创造了 ITERATE_KEY 作为追踪的 key,但是对应遍历数组,只需要使用 length 作为 key 去建立响应联系,因为只有修改length才会影响forin
对于for of与for in的代码相同,但是它还会读取数组的 Symbol.iterator 属性。该属性是一个 symbol值,为了避免发生意外的错误,以及性能上的考虑,我们不应该在副作用函数与Symbol.iterator 这类 symbol 值之间建立响应联系,因此需要修改 get 拦截函数
ownKeys(target) {
track(target, Array.isArray(target) ? 'length' :ITERATE_KEY)//修改
return Reflect.ownKeys(target);
},
//get
if (typeof key !== 'symbol') {
track(target, key) }
重写数组方法,为什么要重写
首先要知道当调用数组的 push 方法向数组中添加元素时,既会读取数组的 length 属性值,也会设置数组的 length 属性值。这会导致两个独立的副作用函数互相影响。以下面的代码为例:
// 第一个副作用函数
effect(() => {
arr.push(1);
});
// 第二个副作用函数
effect(() => {
arr.push(1);
});
执行过程
- 第一个副作用函数执行。在该函数内,调用 arr.push 方法向数 组中添加了一个元素。我们知道,调用数组的 push 方法会间接读取数组的 length 属性。所以,当第一个副作用函数执行完毕后,会与 length 属性建立响应联系。
- 接着,第二个副作用函数执行。同样,它也会与 length 属性建立响应联系。但不要忘记,调用 arr.push 方法不仅会间接读取数组的 length 属性,还会间接设置 length 属性的值。
- 第二个函数内的 arr.push 方法的调用设置了数组的 length 属性值。于是,响应系统尝试把与 length 属性相关联的副作用函 数全部取出并执行,其中就包括第一个副作用函数。问题就出在这里,可以发现,第二个副作用函数还未执行完毕,就要再次执 行第一个副作用函数了。
- 第一个副作用函数再次执行。同样,这会间接设置数组的 length 属性。于是,响应系统又要尝试把所有与 length 属性相关联的副作用函数取出并执行,其中就包含第二个副作用函数。
- 如此循环往复,最终导致调用栈溢出。
问题的原因是 push 方法的调用会间接读取 length 属性。所以,只要我们“屏蔽”对 length 属性的读取,从而避免在它与副作用函数之间建立响应联系
并且因为数组的 push 方法在语义上是修改操作,而非读取操作,所以避免建立响应联系并不会产生其他副作用
let shouldTrack = true;
// 重写数组的 push、pop、shift、unshift 以及 splice 方法
["push", "pop", "shift", "unshift", "splice"].forEach((method) => {
const originMethod = Array.prototype[method];
arrayInstrumentations[method] = function (...args) {
shouldTrack = false;
let res = originMethod.apply(this, args);
shouldTrack = true;
return res;
};
});
function track(target, key) {
// 当禁止追踪时,直接返回
if (!activeEffect || !shouldTrack) return;
// 省略部分代码
}
代理set map
使用 Proxy 代理集合类型的数据不同于代理普通对象,因为集合类型数据的操作与普通对象存在很大的不同,这里暂不往下深入
Set 类型的原型属性和方法如下。
size:返回集合中元素的数量。
add(value):向集合中添加给定的值。
clear():清空集合。
delete(value):从集合中删除给定的值。
has(value):判断集合中是否存在给定的值。
keys():返回一个迭代器对象。可用于 for...of 循环,迭代器对象产生的值为集合中的元素值。
values():对于 Set 集合类型来说,keys() 与 values() 等价。
entries():返回一个迭代器对象。迭代过程中为集合中的每一个元素产生一个数组值 [key, value]。
forEach(callback[, thisArg]):forEach 函数会遍历集合中的所有元素,并对每一个元素调用 callback 函数。
forEach 函数接收可选的第二个参数 thisArg,用于指定callback 函数执行时的 this 值。Map 类型的原型属性和方法如下。
size:返回 Map 数据中的键值对数量。
clear():清空 Map。
delete(key):删除指定 key 的键值对。
has(key):判断 Map 中是否存在指定 key 的键值对。
get(key):读取指定 key 对应的值。
set(key, value):为 Map 设置新的键值对。
keys():返回一个迭代器对象。迭代过程中会产生键值对的 key值。
values():返回一个迭代器对象。迭代过程中会产生键值对的value 值。
entries():返回一个迭代器对象。迭代过程中会产生由 [key,value] 组成的数组值。
forEach(callback[, thisArg]):forEach 函数会遍历Map 数据的所有键值对,并对每一个键值对调用 callback 函数。forEach 函数接收可选的第二个参数 thisArg,用于指定callback 函数执行时的 this 值。