目录
- 引入 ref
- 解决响应丢失的问题
- 自动脱 ref
引入 ref
在之前实现的 reactive 方法,其代理的目标必须是一个非原始值才行,例如:
let str = 'vue'
// 无法拦截 str 的修改
str = 'vue3'
上述这个例子表达的意思就是,我们还缺少一个能够对原始值实现响应式数据的手段。
对于这个问题,基于现有的 reactive 就一定毫无办法吗,其实也不尽然,也有一些曲线救国的手段,如下:
const wrapper = {value: 'vue'
}// 使用 proxy 代理 wrapper,间接实现对原始值的拦截
const name = reactive(wrapper)
name.value // vue
// 修改值-触发响应拦截
name.value = 'vue3'
这样确实是可以进行拦截,但是它存在两个问题:
- 用户为了创建一个响应式对象的原始值,不得不顺带创建一个包裹对象,使用不方便
- 包裹对象由用户定义,而这意味着不规范,用户可以随意命名,例如:wrapper.value、wrapper.v、wrapper.v,甚至如果用户愿意,可以写成语义错乱的情况,比如:wrapper.size。
而为了解决以上两个问题,我们需要封装一个函数,来进行规范,如下:
// 封装一个 ref 函数
function ref(val){// 在 ref 函数内创建包裹对象const wrapper = {value: val}// 将包裹对象变成响应式数据return reactive(wrapper)
}
这样就解决了上述的两个问题,我们来添加一段测试代码如下:
const refVal = ref(1)effect(() => {console.log('effect-ref:', refVal.value)
})refVal.value++
结果如图:
这段代码可以按照我们预期的工作,他还存在诸多的问题,第一个面临的就是如何区分这是一个 ref 对象,即如何区分 refVal 到底是原始值的包裹对象,还是一个非原始值的响应式数据,如如下代码所示:
const refVal1 = ref(1)
const refVal2 = reactive({value: 1})
如果放入 effect 函数中执行,这两个代码都是一样的效果。但是我们有必要区分一个数据到是是不是 ref,因为这对于后续的自动脱 ref 能力至关重要。
而想区分也并不难,在 reactive 中我们是在 get 拦截器中通过一个内置的 Symbol 为 key 来实现的,这里我们可以使用类似的方式,不过由于没有使用代理,我们可以使用 Object.defineProperty,如下:
function ref(val) {const wrapper = {value: val}// 添加一个特殊属性,用于标识这个对象是否是一个 ref 对象// - 且这个属性是不可枚举的,防止被遍历到Object.defineProperty(wrapper, '__v_isRef', {value: true})return reactive(wrapper)
}
通过这个属性就可以实现是否是一个 ref 对象
解决响应丢失的问题
ref 除了能够用于原始值的响应式方案之外,还可以用来解决响应式丢失的问题,首先,我们来看一下什么叫响应式丢失,在编写 vue.js 组件时,我们通常把数据暴露到模板中使用,例如:
export default {setup(){// 响应式数据const obj = reactive({ foo: 1, bar: 2 })// 将数据暴露到模板中return {...obj}}
}
然后在模板中可以访问从 setup 中暴露的数据,如下:
<template><p> {{ foo }} / {{ bar }} </p>
</template>
然而,这样书写就会失去响应式,当我们修改数据时,不会触发渲染,这是为什么?因为我们使用了展开运算符,而经过展开之后,等价于:
return {foo: 1,bar: 2
}
而这,就是一个普通对象,自然不具备响应式能力,我们也可以通过我们熟悉的 effect 案例来描述这个例子,如下:
const obj = reactive({ foo: 1, bar: 2 })// 将响应式数据战展开到一个新的对象 newObj
const newObj = {...obj
}effect(()=>{// 在副作用函数内使用新的对象 newObj 读取 foo 属性值console.log(newObj.foo)
})// 修改-无法触发响应
obj.foo = 100
如何解决这个问题呢?或者说:在副作用函数中,即时通过普通对象 newObj 来访问属性值,也能够建立响应联系,如下:
const obj = reactive({ foo: 1, bar: 2 })// newObj 具备 obj 的同名属性,并且每个属性值都是一个对象,这个对象具备一个访问器属性 value,当读取 value 属性,其实读取的就是 obj 下对应的属性值
const newObj = {foo: {get value(){return obj.foo}},bar: {get value(){return obj.bar}}
}effect(()=>{console.log(newObj.foo)
})// 可以触发响应
obj.foo = 100
这样就可以实现响应的触发了,仔细观察就不难发现 foo 和 bar 的处理都是一致的,对于这种我们可以通过封装一个函数来提高效率,如下:
function toRef(obj, key) {const wrapper = {get value() {return obj[key]}}return wrapper
}
有了这个函数之后,我们的 newObj 就可以大大简化了,如下:
const newObj = {foo: toRef(obj, 'foo'),bar: toRef(obj, 'bar')
}
但是如果这样 foo 的属性非常多,处理起来也显得麻烦,我们可以在编写一个 toRefs 函数来完成,如下:
function toRefs(obj) {const ret = {}// 遍历对象中的每一个属性for (const key in obj) {// 调用 toRef 函数完成转换ret[key] = toRef(obj, key)}return ret
}
现在,我们的使用会更加的简洁,如下:
const newObj = { ...toRefs(obj) }
而由上述可得 toRef 会将一个数据转为 ref 数据,所以还需要为 toRef 函数补充一点,如下:
function toRef(obj, key) {const wrapper = {get value() {return obj[key]}}// 添加一个特殊属性,用于标识这个对象是否是一个 ref 对象// - 且这个属性是不可枚举的,防止被遍历到Object.defineProperty(wrapper, '__v_isRef', {value: true})return wrapper
}
此时的 toRef 还只是只读的,因此我们还需要为其加上 setter,如下:
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
}
自动脱 ref
toRefs 解决了响应式丢失问题,也带了新的问题,由于 toRefs 会把第一层属性值转为 ref,因此必须通过 value 属性来访问,这样就会导致用户的心智负担,如下:
<!-- good -->
<div>{{ foo }} / {{ bar }}</div><!-- bad -->
<div>{{ foo.value }} / {{ bar.value }}</div>
因此,我们需要脱 ref 能力,所谓自动脱 ref,就是指的属性访问行为,即 ref 数据无需通过 xx.value 来访问。
而实现这个功能其实也不难,如下:
function proxyRefs(target) {return new Proxy(target, {get(target, key, receiver) {const value = Reflect.get(target, key, receiver)// 自动脱 ref 实现:如果 value 是一个 ref 对象,则返回它的 value 属性return isRef(value) ? value.value : value}})
}
可以看到,我们利用 proxy 的 get 拦截器作为插入代码逻辑的地方,在这里我们通过判断一个值是否是 ref 数据,如果是在自动的在这里读取 refVal.value。
而实际上,在编写 vue.js 组件的时候,组件中的 setup 函数所返回的数据会传递给 proxyRefs 来处理:
const myComp = {setup(){const count = ref(0)// 返回的这个对象会交给 proxyRefs 来处理return { count }}
}
这样在模板中,我们就无需通过 xx.value 来访问属性值。而可以读取,自然也需要添加修改,如下:
function proxyRefs(target) {return new Proxy(target, {get(target, key, receiver) {const value = Reflect.get(target, key, receiver)return isRef(value) ? value.value : value},set(target, key, newValue, receiver) {const oldValue = target[key]// 如果是 ref,则设置其对应的 value 属性值if (isRef(oldValue)) {oldValue.value = newValuereturn true}return Reflect.set(target, key, newValue, receiver)}})
}
有了自动脱 ref 的能力之后,就可以降低用户在使用时的心智负担,无需关心那个属性是 ref 那个属性是普通数据或者是 reactive。