文章参考了霍春阳的《Vue.js设计与实现》,是自己在阅读过程中的一些思考和理解
由于Proxy的代理目标必须是非原始值,因此对原始值的修改Proxy无法拦截。因此需要用一个非原始值去将原始值包裹起来,这样就能拦截了。
const wrapper = {
value: 'vue'
}
// 这样对value的修改就能触发响应式
const name = reactive(wrapper)
但是这样做会导致两个问题,首先就是如果对原始值添加响应式就不得不用一个非原始值来包裹他;同时包裹对象如果由用户定义,那么存在多样性。因此引入ref的封装函数,用于封装原始值。代码如下:
// 原始值的响应式
function ref(val) {
// 包裹原始值
const wrapper = {
value: val
}
// 返回包裹的响应式
return reactive(wrapper)
}
这样对原始值value的操作就能触发响应式。这里其实就展示了ref的底层实际上用reactive实现的。
但是这样的方式与下面代码的操作没有任何区别:
const t = reactive({value: 'vue'})
用户的操作与其功能没有任何区别,因此就需要区分这个数据是ref的还是reactive的。解决方法就是在wrapper里面加一个独特的标识,来表明这是一个ref数据。代码如下:
// 原始值的响应式
function ref(val) {
// 包裹原始值
const wrapper = {
value: val
}
// 创建不可枚举属性'_v_isRef'代表其为ref数据
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
// 返回包裹的响应式
return reactive(wrapper)
}
响应式丢失问题
这里的丢失问题是,如果将响应式对象通过解构赋值给一个普通对象的话,通过普通对象读取和修改拿到的值,是没有响应式的。代码如下:
// 响应式对象
const obj = reactive({foo: 1, bar: 2})
// 普通对象通过展开运算符拿到响应式对象的属性
const newObj = {
...obj
}
// effect函数监视普通对象的属性
effect(() => {
console.log(newObj.foo)
})
// 这时修改响应式数据,是不会触发副作用函数的重新执行的
obj.foo = 100
这里的问题就是,在副作用函数中,通过普通对象来访问响应式数据,也能有响应式的功能。解决方法如下:
//
const obj = reactive({foo: 1, bar: 2})
// 通过设置同名属性的方法,在获取其值时,通过get函数获取原响应式的值,达到响应式的目的
const newObj = {
foo: {
get value() {
return obj.foo
}
},
bar: {
get value() {
return obj.bar
}
}
}
// 副作用函数中读取普通对象的值,其实读取的是响应式的值,这样就能建立联系
effect(() => {
console.log(newObj.foo)
})
// 在设置时就能触发响应式
obj.foo = 12
通过观察newObj的结构,将其公共部分封装出一个函数。代码如下:
// 单个属性转换
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key]
}
}
return wrapper
}
const newObj = {
foo: toRef(obj, 'foo'),
bar: toRef(obj, 'bar')
}
对于多属性,不可能一个一个的去调用toRef,因此需要一个循环来操作。封装一个新的函数,代码如下:
// 多属性一次转换完成
function toRefs(obj) {
const ret = {}
// for..in循环遍历属性,完成转换
for(const key in obj) {
ret[key] = toRef(obj, key)
}
return ret
}
到现在,响应式的问题已经解决,实际上还是通过读取原响应式的方式来达到普通对象具有响应式的能力。不过还需要对转换为ref的数据打上ref的标识,因为通过toRef和toRefs方式得到的结果将视为ref。代码如下:
// 单个属性转换
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key]
}
}
// 加上标识
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
return wrapper
}
由于toRefs也是通过toRef得到的,就不再添加新的标识。最后由于toRef中没有set函数,因此数据是只读的,需要加上set函数,才能真正达到响应式的功能。代码如下:
get value() {
return obj[key]
},
// 添加set函数,完成ref修改响应式
set value(val) {
obj[key] = val
}
}
自动脱ref
至此完成了普通对象的响应式丢失问题,但是由于toRefs会把响应式的数据的第一层变为ref,而ref又必须通过value来读取,这样就增加了用户的负担。如果是一个深层的对象,那么访问其深层数据时如果每层都要读取value,就会使得代码臃肿,因此需要在读取时替用户读取value属性,使得其不必通过value来获取值。代码如下:
// 脱value函数
function proxyRefs(target) {
return new Proxy(target, {
// 代理拦截get操作,当对象是ref时,返回其身上的value,否则直接返回
get(target, p, receiver) {
const res = Reflect.get(target, p, receiver)
return res.__v_isRef ? res.value : res
},
set(target, p, value, receiver) {
// 获取真实值
const val = target[p]
// 判断如果是ref类型,则在其value上更改
if(val.__v_isRef) {
val.value = value
return true
}
// 否则直接返回set操作
return Reflect.set(target, p, value, receiver)
}
})
}
上述代码也同时对set操作进行了拦截,同样是判断如果为ref数据,则直接在value属性上修改,不需要用户来访问value属性进行修改。实际上,在Vue3的setup函数中,最后的return对象就会传递给proxyRef函数进行处理,这样在模版中就可以直接通过属性名来读取值,不必通过.value属性了。同样reactive也有这样的功能。