Set和Map类型的数据也属于异质对象,它们有特定的属性和方法用来操作自身。因此创建代理时,针对特殊的方法需要特殊的对待。
Vue 的ref 是基于reactive函数实现的,它在其基础上,增加了基本类型的响应性、解决reactive在解构时丢失响应性的问题及在模版字面量中自动脱落Ref.
1 代理Set和Map
function createReactive(obj,isShallow = false) {
return new Proxy(obj, {
get(target, p, receiver) {
track(target,p)
const value = Reflect.get(target,p,receiver)
if (isShallow) return value
return typeof value === 'object' && value !== null ? reactive(value) : value
},
// 省略其他代码
})
}
const proxyObj = reactive(new Set())
// 报错 Method get Set.prototype.size called on incompatible receiver
console.log(proxyObj.size)
上面代码报错:receiver上不兼容size方法。这是因为上面代码中,receiver 指向Proxy的代理对象,它是没有size方法的。下面是对get方法改进。
get(target, p, receiver) {
if (p === 'size') return Reflect.get(target,p,target)
// 省略其他代码
},
但是,改进后再执行proxyObj.add(1),又报错:receiver 上不兼容add方法。因为是proxyObj执行add函数,add函数里的this始终指向proxyObj。这是可以使用函数的bind方法,来绑定函数中this的值。
get(target, p, receiver) {
// 省略其他代码
const value = Reflect.get(target,p,receiver)
if (typeof value === 'function') return value.bind(target)
if (isShallow) return value
return typeof value === 'object' && value !== null ? reactive(value) : value
},
1.1 建立响应联系
- size属性是一个只读属性,Set的add、delete会改变它的值。
- 调用Set的add方法时,如果元素已存在于Set中,就不需要触发响应。调用Set的delete方法时,如果元素不存在于Set中,也不需要触发响应。
const mutableInstrumentation = {
add(key) {
const target = this.raw
const hadKey = target.has(key)
const res = target.add(key)
if (!hadKey && res) {
trigger(target, key, 'ADD')
}
return res
},
delete(key) {
const target = this.raw
const hadKey = target.has(key)
const res = target.delete(key)
if (hadKey && res) {
trigger(target, key,'DELETE')
}
return res
}
}
// Proxy 中的get代理
get(target, p, receiver) {
// 省略其他代码
track(target,p)
if (mutableInstrumentation.hasOwnProperty(p)) {
return mutableInstrumentation[p]
}
// 省略其他代码
},
1.2 避免污染原始数据
const proxySet = reactive(new Set())
const map = new Map()
const proxyMap = reactive(map)
proxyMap.set('set',proxySet)
effect(() => {
console.log(map.get('set').size)
})
console.log("----------------")
proxySet.add(1) // 触发响应
上面原始数据具有了响应性,这不符合需求(原始数据应不具备响应性)。产生这个的原因是,在设置值时,直接把响应体对象也添加进原始对象了。所以,解决的关键在于:设置值时,如果该对象是响应体对象,则取其目标对象。
// mutableInstrumentation 对象的set方法
set(key,value) {
const target = this.raw
const had = target.has(key)
const oldValue = target.get(key)
target.set(key,value.raw || value) // 去目标对象
if (!had) {
track(target, 'key', 'ADD')
} else if (oldValue !== value && (oldValue === oldValue || value === value)) {
trigger(target,key,'SET')
}
}
1.3 处理forEach
1)forEach 只与键值对的数量有关,所以当forEach被调用时,让ITERATE_KEY与副作用函数建立联系。
2)当set方法设置的属性存在时,但属性值不同时,也应该触发forEach。
3)forEach函数的参数callback,它是有原始对象调用的,这意味着callback函数中的value及key两个参数不具有响应性,但是它们应该都具备响应性,需要将这两个参数转成响应体。
// mutableInstrumentation 对象的forEach方法
forEach(callback) {
const target = this.raw
const wrap = (val) => typeof val === 'object' ? reactive(val) : val
track(target,ITERATE_KEY)
target.forEach((v,k) => {
callback(wrap(v),wrap(k),this)
})
}
function trigger(target,p,type,newValue) {
const map = effectMap.get(target)
if (map) {
// 省略其他代码
if (type === 'ADD' || type === 'DELETE' || (type === 'SET' && Object.prototype.toString.call(target) === '[object Map]')) {
// 省略其他代码
}
// 省略其他代码
}
}
1.4 迭代器方法
集合类型有三个迭代器方法:entries、keys、values,还可以使用 for...of进行迭代。
- 使用for...of迭代一个代理对象时,内部会调用[Symbol.iterator]()方法,返回一个迭代器。迭代产生的值不具备响应性,所以需要把这些值包装成响应体。
- 可迭代协议指一个对象实现了Symbol.iterator方法,迭代器协议是指一个对象实现了next方法。而entries方法要求返回值是一个可迭代对象,即该对象要实现了Symbol.iterator方法。
- values 方法,返回的仅是Map的值,而非键值对。
- keys 方法,与上面不同的是,调用set时,如果非添加值,则不应该触发响应。
function trigger(target,p,type,newValue) {
const map = effectMap.get(target)
if (map) {
// 省略其他代码
if (type === 'ADD' || type === 'DELETE' && Object.prototype.toString.call(target) === '[object Map]') {
const tempSet = map.get(MAP_KEY_ITERATE_KEY)
tempSet && tempSet.forEach(fn => {
if (activeEffectFun !== fn) addSet.add(fn)
})
}
addSet.forEach(fn => fn())
}
}
const mutableInstrumentation = {
// 省略其他代码
[Symbol.iterator]: iterationMethod,
entries: iterationMethod,
values: valueIterationMethod,
keys: keyIterationMethod
}
function iterationMethod() {
const target = this.raw
const itr = target[Symbol.iterator]()
const wrap = (val) => typeof val === 'object' && val != null ? reactive(val) : val
track(target,ITERATE_KEY)
return {
next() {
const {value,done} = itr.next()
return {
value: value ? [wrap(value[0]),wrap(value[1])] : value,
done
}
},
[Symbol.iterator]() {
return this
}
}
}
function valueIterationMethod() {
const target = this.raw
const itr = target.values()
const wrap = (val) => typeof val === 'object' && val != null ? reactive(val) : val
track(target,ITERATE_KEY)
return {
next() {
const {value,done} = itr.next()
return {
value: wrap(value),
done
}
},
[Symbol.iterator]() {
return this
}
}
}
function keyIterationMethod() {
const target = this.raw
const itr = target.keys()
const wrap = (val) => typeof val === 'object' && val != null ? reactive(val) : val
track(target,MAP_KEY_ITERATE_KEY)
return {
next() {
const {value,done} = itr.next()
return {
value: wrap(value),
done
}
},
[Symbol.iterator]() {
return this
}
}
}
2 原始值的响应方案ref
Proxy 的代理目标必须是非原始值,如果要让原始值具有响应性,那么要对它进行包装。Vue3 的ref函数就负责这个工作。
function ref(val) {
const wrapper = {
value: val
}
// 为了区分数据是经过ref包装的,还是普通对象
Object.defineProperty(wrapper,'_v_isRef',{
value: true
})
return reactive(wrapper)
}
2.1 reactive 解构时丢失响应性
const proxyObj = reactive({name: 'hmf',num: 1})
const obj = {...proxyObj} // obj 不再具有响应性。这是因为解构时
{…proxyObj} 等价于 {name: 'hmf',num: 1}
要让obj 具有响应性,则需要使其属性值为一个对象。如下所示:
const obj = {
name: {
get value() {
return proxyObj.name
},
set value(val) {
proxyObj['name'] = val
}
},
num: {
get value() {
return proxyObj.num
},
set value(val) {
proxyObj['num'] = val
}
}
}
ref函数优化如下:
function toRefs(obj) {
const ret = {}
for (const key in obj) {
ret[key] = toRef(obj,key)
}
return ret
}
function toRef(obj,key) {
const wrapper = {
get value() {
return obj[key]
},
set value(val) {
obj[key] = val
}
}
Object.defineProperty(wrapper,'_v_isRef',{
value: true
})
return wrapper
}
2.2 自动脱ref
经过toRefs处理的对象,都需要通过对象的value属性来访问,例如
const proxyObj = ref({name: 'hmf',num: 1})
console.log(proxyObj.name.value)
proxyObj.name.value = 'hi'
访问任何属性都需要通过value属性访问,这增加了用户的心智负担。我们需要自动脱ref的能力,即上面proxy.name就可直接访问。
function proxyRefs(target) {
return new Proxy(target, {
get(target, p, receiver) {
const value = Reflect.get(target,p,receiver)
return value._v_isRef ? value.value : value
},
set(target, p, newValue, receiver) {
const value = target[p]
if (value._v_isRef) {
value.value = newValue
return true
}
return Reflect.set(target,p,newValue,receiver)
},
})
}