欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 文化 > vue3 reactive原理(二)-代理Set和Map及ref原理

vue3 reactive原理(二)-代理Set和Map及ref原理

2024/10/24 11:25:10 来源:https://blog.csdn.net/qq_25308331/article/details/140677273  浏览:    关键词:vue3 reactive原理(二)-代理Set和Map及ref原理

 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 valuereturn 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 valuereturn typeof value === 'object' && value !== null ? reactive(value) : value
},

1.1 建立响应联系

  1. size属性是一个只读属性,Set的add、delete会改变它的值。
  2. 调用Set的add方法时,如果元素已存在于Set中,就不需要触发响应。调用Set的delete方法时,如果元素不存在于Set中,也不需要触发响应。
const mutableInstrumentation = {add(key) {const target = this.rawconst hadKey = target.has(key)const res = target.add(key)if (!hadKey && res) {trigger(target, key, 'ADD')}return res},delete(key) {const target = this.rawconst 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.rawconst 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.rawconst wrap = (val) => typeof val === 'object' ? reactive(val) : valtrack(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进行迭代。

  1. 使用for...of迭代一个代理对象时,内部会调用[Symbol.iterator]()方法,返回一个迭代器。迭代产生的值不具备响应性,所以需要把这些值包装成响应体。
  2. 可迭代协议指一个对象实现了Symbol.iterator方法,迭代器协议是指一个对象实现了next方法。而entries方法要求返回值是一个可迭代对象,即该对象要实现了Symbol.iterator方法。
  3. values 方法,返回的仅是Map的值,而非键值对。
  4. 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.rawconst itr = target[Symbol.iterator]()const wrap = (val) => typeof val === 'object' && val != null ? reactive(val) : valtrack(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.rawconst itr = target.values()const wrap = (val) => typeof val === 'object' && val != null ? reactive(val) : valtrack(target,ITERATE_KEY)return {next() {const {value,done} = itr.next()return {value: wrap(value),done}},[Symbol.iterator]() {return this}}
}
function keyIterationMethod() {const target = this.rawconst itr = target.keys()const wrap = (val) => typeof val === 'object' && val != null ? reactive(val) : valtrack(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 = newValuereturn true}return Reflect.set(target,p,newValue,receiver)},})
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com