Vue3手写系列之ref

1,334 阅读5分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

hello 大家好,🙎🏻‍♀️🙋🏻‍♀️🙆🏻‍♀️

我是一个热爱知识传递,正在学习写作的作者,ClyingDeng 凳凳!

本文给大家带来的是vue3 中 ref API的实现。

ref的用法

ref类似reactive。接收一个值,返回一个响应式、可更改的ref对象。该对象进行取值或者设置值的时候可以通过.value来实现。

先看一个用法案例:

<div id="app"></div>
<script src="../../../../node_modules/@vue/reactivity/dist//reactivity.global.js"></script>
<script>
    const { reactive, effect, computed, ref } = VueReactivity
    const name = ref('Deng')
    effect(() => {
        app.innerHTML = name.value
        console.log(name.value);
    })
    setTimeout(() => {
        name.value = 'Clying'
    }, 1000)
</script>

1.gif

我们可以看出页面上先展示了Deng,修改name的值,间隔一秒之后展示 Clying

ref的实现

既然要实现ref这个API,那我们就需要先看看这个ref内部藏的哪些东西。输出这个ref对象可以看到:

image.png

它是一个RefImpl类,里面存在一些_value_rawValue等属性。那我们就应该可以想到,它应该是和computed类似,创建一个实例,通过属性访问器进行读取和设置值。

ref 功能函数初始化

在写ref这个功能的时候,我们按照之前写法,通过对外暴露RefImpl实例。

export function ref(value) {
    return new RefImpl(value, false)
}

在此,第二个参数留给refshallowRef类中留有一个作区分用的,暂时我们可以先忽略,ref默认值是false。

RefImpl 类初始化

既然功能函数架子搭好了,那就看看上图输出的RefImpl这个类中的属性吧。内部和computed一样,存在一个收集依赖的dep、一个__v_isRef用来区分是否是ref这个对象类型、一个原值_rawValue、一个代理值_value、还有一个__v_isShallow用来区分是否是shallow。

export class RefImpl {
    private _value
    public readonly __v_isRef = true // 是否是ref类
    private _rawValue
    public dep = undefined
    // __v_isShallow true表示shallowRef
    constructor(value, public readonly __v_isShallow) {
        this._rawValue = value
        this._value = toReactive(value) // _value是一个代理对象
    }
    get value() {
        // 收集
        return this._value
    }
    set value(newVal) {
        // 触发
    }
}

这边的toReactive是将接收的对象包装成reactive类的对象,这就符合官网文档中对ref这个API的说明了。

// 将接收的对象包装成reactive类的对象
export const toReactive = <T extends unknown>(value: T): T =>
    isObject(value) ? reactive(value) : value

image.png

注意:为什么使用两个值_rawValue_value呢?

image.png 使用原vue3框架,我们可以看到输出的结果。对于非对象类型的值,原值和value是一样的,但是对于对象而言,输出的value我们可以看到它其实已经是一个代理对象了。

所以,我们能够很容易的理解:_value保存的是我们加工后具有响应式的对象;_rawValue保存的是原始的值,未经处理过的。

收集和触发

有这些属性是远远不够的,我们还需要读取ref对象的值和设置该值。那么该怎么搞呢?🤔🤔🤔

当然是依葫芦画瓢,照着computed来啦!通过RefImpl类的属性访问器,在get中读取值并收集相关依赖,在set中设置值并触发更新。

在get中我们可以依旧使用effect文件中的trackEffects来收集。

get value() {
    // 收集
    trackEffects(this.dep || (this.dep = new Set()))
    return this._value
}

设置值:在值发生变化的时候,我们更新值与该值相依赖的属性。

这里我们需要注意的是不仅要更新我们操作的值对象_value,还要更原值_rawValue。在原值更新的时候直接赋值新的值,但是在更新_rawValue时,我们需要对对象进行一个重新代理,即我们需要将该对象toReactive

set value(newVal) {
    // 触发
    if (hasChanged(newVal, this._rawValue)) {
        this._rawValue = newVal // 原值更新
        this._value = toReactive(newVal) // 相应的对象重新代理
        triggerEffects(this.dep || (this.dep = new Set())) // 触发依赖更新
    }
}

trackEffectstriggerEffects的实现详细可以点击Vue3手写系列之computed这篇文章查看。

shallowRef

除了一个ref,vue3中还有一个shallowRef这个API。 我们看下这个例子:

const shallowInfo = shallowRef({
    info: {
        name: 'Deng'
    }
})
effect(() => {
    app.innerHTML = JSON.stringify(shallowInfo.value)
    console.log('更改了:', shallowInfo);
})
setTimeout(() => {
    shallowInfo.value.info = { name: 'Clying', age: 12 }
}, 1000)
setTimeout(() => {
    shallowInfo.value = { name: 'Clying', sex: '女' }
}, 4000)

在更改shallowRef信息时,虽然触发了两次effect,但是只有通过shallowInfo.value形式的赋值方式才会触发页面更新。

2.gif

这下可以看出shallowRef是ref的浅层形式了吧,它不会深层递归将其转为响应式数据,只有通过.value的形式可以。

shallowRef 实现

根据上文实现ref的基础上,我们有在RefImpl类中留有一个__v_isShallow字段来区分是否是shallowRef,此时就派上用场啦!

在创建RefImpl实例时,我们第二个参数就该传入true,并表示我们需要创建一个shallowRef的实例对象。

export function shallowRef(value) {
    return new RefImpl(value, true)
}

我们在对shallowInfo这个对象赋值的时候发现,通过shallowInfo.value.info这种深层赋值时是不会触发更新的,那么在我们对值操作的时候,如果是shallowInfo类型的对象,对内部操作的值_value就不应该转成响应式代理对象。

// RefImpl类
constructor(value, public readonly __v_isShallow) {
    this._rawValue = value
    this._value = __v_isShallow ? value : toReactive(value) // _value是一个代理对象
}

在源码中,原值_rawValue赋值时其实也是需要判断的:this._rawValue = __v_isShallow ? value : toRaw(value)。在数据内部会存在__v_raw来标志是否是原始对象。

同样在设置值的时候,也可以判断下,如果是浅层代理,那么就直接使用新值不需要触发更新,如果是通过shallowInfo.value形式那就需要重新代理对象。

set value(newVal) {
    const useDirectValue = this.__v_isShallow
    if (hasChanged(newVal, this._rawValue)) {
        this._rawValue = newVal // 原值更新
        this._value = useDirectValue ? newVal : toReactive(newVal) // 相应的对象重新代理
        triggerEffects(this.dep || (this.dep = new Set()))
    }
}

源码中设置值会考虑的情况比我们现有的要多,在设置值的时候会考虑传入的值和设置的新值的类型还有是否可读属性,根据这些再去判断是否需要重新代理。

image.png

最后验证一下我们自己写的shallowRef API是否可用:

image.png

此图为证,最后渲染的只有通过.value格式设置的值。.value.内部属性设置值虽然更改了但是并不会触发更新。

感兴趣的朋友可以关注 手写vue3系列 专栏或者点击关注作者ClyingDeng哦(●'◡'●)!。 如果不足,请多指教。