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 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 建立响应联系

  1. size属性是一个只读属性,Set的add、delete会改变它的值。
  2. 调用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进行迭代。

  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.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)
        },
    })
}
Vue 3中的`ref`和`reactive`是两个核心的概念,它们在数据绑定和响应式系统中起着重要作用。 1. `ref`: - `ref`是一个特殊的对象,它包装了一个基本类型的值(如字符串、数字或对象)和一个当前值的getter/setter。当你使用`ref`时,Vue会自动跟踪它的值并确保视图更新。 - 它主要用于存储和操作复杂的数据结构,例如对象和数组,因为这些类型不能直接用作模板的绑定目标。 - 示例: ```javascript const count = ref(0); // 创建一个数字类型的引用 ``` - 相关问题: 1. `ref`主要适用于哪种数据类型? 2. 什么时候会在Vue组件中使用`ref`而不是直接绑定变量? 3. 如何使用`ref.value`获取内部存储的原始值? 2. `reactive`: - `reactive`是Vue提供的一个更高阶的功能,用于将整个对象转换为响应式的。当你调用`reactive(obj)`时,Vue会对对象的所有属性进行深度观察,并将其转换为响应式数据。 - 对象内部的变化会自动更新视图,反之亦然。`reactive`通常用于初始化复杂的组件状态,它可以包含嵌套的对象和数组。 - 示例: ```javascript const user = reactive({ name: 'Alice', age: 30 }); ``` - 相关问题: 1. 什么情况下你会选择使用`reactive`而不是单独创建多个`ref`? 2. `reactive`能处理哪些数据类型? 3. `reactive`如何确保视图与数据的一致性? 总结一下,`ref`侧重于管理单个值,而`reactive`用于管理整个可变对象,它们都是Vue3实现数据绑定和响应式设计的关键工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值