Vue3中如何监测数据变动
vue3使用Proxy
来进行数据拦截(关于Proxy的相关介绍,可以参见MDN,这里不做更多说明),但是在使用Proxy
的时候,有几个比较头痛的问题需要处理。
1、如何解决多次触发set
和get
问题。
2、Proxy只能代理一层数据,对于多层数据如何处理。
1. 如何解决多次触发set
和get
问题
当Proxy
代理一个数组时,会引起多次get
或set
的问题,看如下示例。
const p = new Proxy([1, 2, 3], {
get(target, p, receiver) {
console.log(`get ${p}`)
return Reflect.get(target, p, receiver)
},
set(target, p, value, receiver) {
console.log(`set ${p} ${value}`)
return Reflect.set(target, p, value, receiver)
}
})
p.push(10)
打印结果
get push
get length
set 3 10
set length 4
可以看到get
和set
分别执行了2次,一次是当前设置的值,还有一次是length
的改变触发了set
,执行2次set
就意味着会引起多次render
。那么,如何解决这个问题呢?
办法有很多,比如可以使用定时器setTimeout
来控制执行次数。
function reactive(data, fn) {
let timer
return new Proxy(data, {
get(target, p, receiver) {
return Reflect.get(target, p, receiver)
},
set(target, p, value, receiver) {
clearTimeout(timer)
timer = setTimeout(() => {
typeof fn === 'function' && fn()
})
return Reflect.set(target, p, value, receiver)
}
})
}
const array = [1, 2, 3, 4]
let proxyArray = reactive(array, () => {
console.log('trigger')
})
proxyArray.push(10)
// 输出:trigger
结果只输出一次trigger
,这就解决了多次触发的问题。当然,这只是其中一种解决办法。
那么,vue3是怎么做的呢?
/******节选baseHandlers中的部分代码******/
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
value = toRaw(value)
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
const result = Reflect.set(target, key, value, receiver)
if (target === toRaw(receiver)) {
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
} else {
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key)
}
}
}
return result
}
vue3的做法比较巧妙,首先通过hadKey
判断当前对象中是否包含要设置的key
。从上面已经知道,set
会执行一次当前赋值的一次set
操作,还会执行一个length
改变时的set
操作。
举个栗子,假如执行如下操作
const p = reactive([1, 2, 3]);
p.push(4);
首先hadKey
检测肯定为false
,因为在set
之前,key
肯定是不存在的,所以就会进入!hadKey
逻辑,成功trigger
。
第二次length
改变触发set
的时候,首先拿到oldValue
,也就是target[key]
,此时key
为length
,所以target[key]
的值是4
,而此时value
也为4
,所以oldValue === value
,那么hasChanged(value, oldValue)
这个逻辑就进不来了,所以就会执行一次trigger。
基于vue3的这个实现思路,我写了一个精简版的。
function reactive(data, fn) {
return new Proxy(data, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const hadKey = Object.hasOwnProperty.call(target, key)
const oldValue = target[key]
if (!hadKey || oldValue !== value) {
typeof fn === 'function' && fn()
}
return Reflect.set(target, key, value, receiver)
}
})
}
let array = [1, 2]
let p = reactive(array, () => {
console.log('trigger')
})
p.push(3)
// 输出trigger
2. 如何解决Proxy只能代理一层的问题
const p = new Proxy(
{ a: { b: 1 } },
{
get(target, p, receiver) {
return Reflect.get(target, p, receiver)
},
set(target, p, value, receiver) {
console.log('set')
return Reflect.set(target, p, value, receiver)
}
}
)
p.a.b = 3
运行后控制台没有任何输出,而执行p.a = 3
后会输出set
,可见Proxy
只能代理第一层数据,对于嵌套的数据是无法感知的。如何解决这个问题呢?
首先想到的肯定是递归Proxy
,简单实现一下。
function reactive(data) {
const results = Array.isArray(data) ? [] : {}
if (typeof data === 'object') {
for (let key in data) {
results[key] = reactive(data[key])
}
return new Proxy(results, {
get(target, k, receiver) {
return Reflect.get(target, k, receiver)
},
set(target, k, value, receiver) {
console.log('set')
return Reflect.set(target, k, value, receiver)
}
})
}
return data
}
let data = { a: { b: 1 }, c: { d: 2 } }
let p = reactive(data)
p.c.d = 2
// 输出:set
通过递归,给每一个对象用Proxy包装一下,这样即使给内层数据赋值,也能拦截到set
操作。但是,缺点也很明显,当数据量变大时,进行递归肯定会带来很大的性能问题。说到这里,vue3中是如何进行深度代理的?
不得不说,vue3的实现方式很巧妙,首先祭上vue3的代码
function createGetter(isReadonly: boolean, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
if (shallow) {
track(target, OperationTypes.GET, key)
return res
}
if (isRef(res)) {
return res.value
}
track(target, OperationTypes.GET, key)
return isObject(res)
? isReadonly
? readonly(res)
: reactive(res)
: res
}
}
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
if (readonlyToRaw.has(target)) {
return target
}
if (readonlyValues.has(target)) {
return readonly(target)
}
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
function createReactiveObject(
target: any,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
if (toRaw.has(target)) {
return target
}
if (!canObserve(target)) {
return target
}
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
因为每次set
被拦截之前都会拦截到get
操作,所以vue3在get
中直接对数据进行reactive
,这样就大大减少了递归reactive
带来的性能消耗。
isObject(res) ? (isReadonly ? readonly(res) : reactive(res)) : res
首先判断是否为object
类型,如果是一个Object,就对其包装一层Proxy
,否则直接返回原始值。同时,它还维护了几个WeakMap
,通过这些WeakMap
可以通过原始对象快速找到已经包装过Proxy
的对象,或者通过已经包装过Proxy
的对象找到原始对象,这将大大地提升了性能。
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
模拟vue的实现思路,可以实现一个精简版的大深度Proxy
function reactive(data) {
return new Proxy(data, {
get(target, k, receiver) {
const result = Reflect.get(target, k, receiver)
if (typeof result === 'object') {
return reactive(result)
}
return result
},
set(target, k, value, receiver) {
console.log('set')
return Reflect.set(target, k, value, receiver)
}
})
}
let data = { a: { b: 1 }, c: { d: 2 } }
let p = reactive(data)
p.c.d = 2
// 输出:set
本文只是针对vue3中如何解决Proxy的两个痛点做出总结,当然,这里只是粗略的理解,源码的实现更加地复杂,如有不正之处欢迎不吝指正。
- 本博客所有文章除特别声明外,均可转载和分享,转载请注明出处!
- 本文地址:https://www.leevii.com/?p=2501
if (!hadKey || oldValue !== value) {
typeof fn === ‘function’ && fn()
}你这个例子举的好像没有第二个条件也可以满足只执行一次
oldValue !== value判断数据变更的时候才执行操作,如果是一个对象话,去掉这个条件就不行了,比如: