看了官网让我对自己关于ref、reactive的理解产生了怀疑!

1,889 阅读6分钟

我一直觉得 ref 与 reactive 有各自的适用场景,单纯的使用 ref 一把梭与定义一个 reactive 把所有变量都塞进去都不是一个推荐的做法。

但是官网有明确说在组合式 API 中,推荐使用 ref() 函数来声明响应式状态,这就让我对自己的理解产生了怀疑,难道自己这么久真的用错了吗?猛击查看官网-声明响应式状态

image.png

官网也说明了使用 reactive 定义响应式变量的一些局限性,算是解释了为什么推荐使用 ref。

image.png

日常使用

在日常开发中我一直是使用 reactive 定义对象,使用 ref 定义基本数据类型及数组。

使用 ref 定义数组是因为数组有时需要直接赋值,上边图里指出 reactive 是不能直接赋值的会失去响应性。如果按照这个理解使用 ref 定义对象也说的通。

那使用 reactive 定义的对象如何直接赋值还不会丢失响应呢?

image.png

按照官网的应该使用 ref 来定义可能会重新赋值的对象。但是我在实际使用中 Object.assign(oldObj, newObj); 一把梭。

第一种方式大概会有人这么用:

const state = reactive({  
    form: {  
        dateType: 30,  
        time: [],  
    },  
    unitList: [],  
    indicatorsList: [],  
    titleLineDom: null  
})

把一个页面的所有变量都塞进一个 state 变量中当成 sfc 写法中 data 来用,使用 const { proxy } = getCurrentInstance() 获取下 proxy 来当 this,完美搭配!但我不是很喜欢,也不是很理解这种写法!

扯的有点远了,下边来看下 ref、 reactive 的具体实现。

reactive

先说 reactive 是因为 ref 的实现依赖了 reactive。

我们都知道 vue3 的响应式是通过 proxy 来实现的,那我们先来实现一个最简单的 reactive。

代码来自 Vue.js设计与实现,当然这只是个简单的实现,实际实现要比这个复杂很多考虑很多边界情况。感兴趣的可查看 Vue.js设计与实现-对应的代码仓库查看,相比于 vue3 源码要更容易理解一些。

ref

在读 Vue.js设计与实现 时以为 ref 访问需要使用 .value 是因为 proxy 只能代理对象。基本数据类型需要封装为对象来实现数据的响应。代码来源

function ref(val) {  
    const wrapper = {  
        value: val,  
    };  
    // 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为 true  
    Object.defineProperty(wrapper, "__v_isRef", {  
        value: true,  
    });  

    return reactive(wrapper);  
}

在读源码的时候发现自己的理解好像有点不对,还有更多的深意,比如让你知道自己在使用的是一个响应式的变量。

源码位置:packages/reactivity/src/ref.ts :134

export function ref(value?: unknown) {  
    return createRef(value, false)  
}

export function shallowRef(value?: unknown) {  
    return createRef(value, true)  
}

function createRef(rawValue: unknown, shallow: boolean) {  
    if (isRef(rawValue)) {  
        return rawValue  
    }  
    return new RefImpl(rawValue, shallow)  
}

class RefImpl<T> {  
    private _value: T  
    private _rawValue: T  

    // 响应依赖集合
    public dep?: Dep = undefined  
    // 用来判断是否是 ref
    public readonly __v_isRef = true  

    // 类构造函数接收两个参数,初始值,是否是浅响应
    constructor(  
        value: T,  
        public readonly __v_isShallow: boolean  
    ) {  
        // 设置原始值,toRaw 根据一个 Vue 创建的代理返回其原始对象。
        this._rawValue = __v_isShallow ? value : toRaw(value)
        // 浅响应设置初始值即可,否则使用 toReactive 设置为响应数据
        // toReactive 判断值是否是对象,对象类型调用 reactive,非对象直接返回传入的值
        this._value = __v_isShallow ? value : toReactive(value)  
    }  

    get value() {  
        // 设置响应依赖
        trackRefValue(this)  
        // 获取数据时 返回存储的响应式数据
        return this._value  
    }  

    set value(newVal) {  
        // 判断是否直接使用传递的值
        const useDirectValue =  
        this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)  
        
       
        newVal = useDirectValue ? newVal : toRaw(newVal)  
        
        // 判断数据是否有变化
        if (hasChanged(newVal, this._rawValue)) { 
            // 设置原始值
            this._rawValue = newVal  
            // 设置响应式值
            this._value = useDirectValue ? newVal : toReactive(newVal)  
            
            // 触发更新
            triggerRefValue(this, newVal)  
        }  
    }  
}

Vue.js设计与实现 的实现或许是为了让读者更容易理解与源码有些差异,但最终都实现了数据的响应。

当然 ref、reactive 的实现细节还有很多这个需要各位读者自行参悟,比如 reactive 对对象类型的响应是如何实现的 Vue.js设计与实现 书中给出了具体的实现细节建议阅读。

toReactive 函数

这里可以看出来 toReactive 的处理逻辑,是对象就进行包装都则直接返回传进来的值

export const toReactive = <T extends unknown>(value: T): T =>  
isObject(value) ? reactive(value) : value

有什么区别呢?

const refVal1 = ref(1)

// or

const refVal2 = reactive({ value: 1 })

我的理解

我依然坚持 ref、reactive 各有各的的适用场景,官网的话不要过度解读。reactive 存的问题 ref 就不会存在吗?

有限的值类型

我觉得也没什么问题设计如此,最初在学习 composition Api 使用的时候接收到的概念就是 ref 定义基本类型 reactive 定义对象。

使用 .value 来访问对象的属性我可能不太能接受。

const userInfo = ref({  
    name: '唐诗'  
})  
console.log(userInfo.value.name)  

const userInfo = reactive({  
    name: '唐诗'  
})  
console.log(userInfo.name)

不能替换整个对象

那 ref 不能直接赋值啊!ref 直接对 value 属性直接赋值是不会丢失响应性,那使用 reactive 定义个一个 value,不也是一样的效果吗?

const refVal1 = ref(1)

// ref 直接赋值,一样会丢失响应
refVal1 = a

// or

const refVal2 = reactive({ value: 1 })

对解构操作不友好

这个其实我在日常使用中并没有什么感觉,使用 toRefs 包装一层也不算很麻烦。

ref 解构不也是一样吗?

总结

ref 与 reactive 有各自的适用场景,单纯的使用 ref 一把梭与定义一个 reactive 把所有变量都塞进去都不是一个推荐的做法。

至于官网的推荐,看个人理解了。之前的文章也说过 vue3 的 composition Api 是一把双刃剑,用的好与坏全凭个人本事。

当然你说官网推荐 ref 我也只能说你说的对,毕竟官网是一座大山!

文中我也有一些疑问,各位大佬若能提供解答不胜感激!

往期文章