# 原始值的响应式方案
原始值指的是 Boolean、Number、BigInt、String、Symbol、undefined 和 null 等类型的值。
Proxy无法提供对原始值的代理,因此想要将原始值变成响应式数据,就必须对其做一层包裹。
# 引入 ref 的概念
let str = 'vue'
// 无法拦截对值的修改
str = 'vue3'
解决方案:使用一个非原始值去“包裹”原始值,例如使用一个对象包裹原始值
const wrapper = {
value: 'vue'
}
// 可以使用 Proxy 代理 wrapper,间接实现对原始值的拦截
const name = reactive(wrapper)
name.value // vue
// 修改值可以触发响应
name.value = 'vue3'
导致的问题:
用户为了创建一个响应式的原始值,不得不顺带创建一个包裹对象;
包裹对象由用户定义,而这意味着不规范。用户可以随意命名,例如 wrapper.value、wrapper.val 都是可以的;
解决方案:
// 封装一个 ref 函数
function ref(val) {
// 在 ref 函数内部创建包裹对象
const wrapper = {
value: val
}
// 将包裹对象变成响应式数据
return reactive(wrapper)
}
判断当前值是否是为 ref 类型
function ref(val) {
const wrapper = {
value: val
}
// 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef, 并且值为 true
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
return reactive(wrapper)
}
// __v_isRef 属性来判断一个数据是否是 ref 了
function isRef(r) {
return Boolean(r && r.__v_isRef === true);
}
# 响应丢失问题
<template>
<p>{{ foo }} / {{ bar }}</p>
</template>
export default {
setup() {
// 响应式数据
const obj = reactive({
foo: 1,
bar: 2
})
setTimeout(() => {
// 异步修改数据 响应式不会生效
obj.foo = 3
}, 1000)
// 将数据暴露到模板中
return {
...obj
}
}
}
原因:这是由展开运算符(...)导致
return {
...obj
}
// 等价于:
return {
foo: 1,
bar: 2
}
//返回的是普通对象
// obj 是响应式数据
const obj = reactive({
foo: 1,
bar: 2
})
// 将响应式数据展开到一个新的对象 newObj
const newObj = {
...obj
}
effect(() => {
// 在副作用函数内通过新的对象 newObj 读取 foo 属性值
console.log(newObj.foo)
})
// 很显然,此时修改 obj.foo 并不会触发响应
obj.foo = 100
解决方案:
// obj 是响应式数据
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(() => {
// 在副作用函数内通过新的对象 newObj 读取 foo 属性值
console.log(newObj.foo.value)
})
// 这时能够触发响应了
obj.foo = 100
封装一下toRef函数
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key]
}
}
return wrapper
}
const newObj = {
foo: toRef(obj, 'foo'),
bar: toRef(obj, 'bar')
}
封装一下toRefs函数
function toRefs(obj) {
const result = {}
for (const key in obj) {
result[key] = toRef(obj, key)
}
return result
}
const newObj = {
...toRefs(obj)
}
但为了概念上的统一,我们会将通过 toRef 或 toRefs 转换后得到的结果视为真正的 ref 数据。
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key]
}
}
// 定义 __v_isRef 属性
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 会把响应式数据的第一层属性值转换为 ref,因此必须通过 value 属性访问值
const obj = reactive({
foo: 1,
bar: 2
})
obj.foo // 1
obj.bar // 2
const newObj = {
...toRefs(obj)
}
// 必须使用 value 访问值
newObj.foo.value // 1
newObj.bar.value // 2
我们想要的效果是:
<p>{{ foo }} / {{ bar }}</p>
现在的效果
<p>{{ foo.value }} / {{ bar.value }}</p>
解决方案:
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
// 自动脱 ref 实现:如果读取的值是 ref,则返回它的 value 属性值
return value.__v_isRef ? value.value : value
}
})
}
// 调用 proxyRefs 函数创建代理
const newObj = proxyRefs({
...toRefs(obj)
})
我们在编写组件时,组件中的 setip 函数所返回的数据会传递 proxyRefs 函数处理。
const MyComponent = {
setup() {
const count = ref(0)
// 返回的这个对象会传递给 proxyRefs
return {
count
}
}
}
ref 设置值
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
return value.__v_isRef ? value.value : value
},
set(target, key, newValue, receiver) {
// 通过 target 读取真实值
const value = target[key]
// 如果值是 Ref,则设置其对应的 value 属性值
if (value.__v_isRef) {
value.value = newValue
return true
}
return Reflect.set(target, key, newValue, receiver)
}
})
18
}
** reactive 函数也有自动脱 ref 的能力**
const count = ref(0)
const obj = reactive({
count
})
obj.count // 0
用户不需要关心 ref 类型,只需要使用 reactive 函数即可