# 原始值的响应式方案

原始值指的是 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)
}

但为了概念上的统一,我们会将通过 toReftoRefs 转换后得到的结果视为真正的 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 函数即可