从vue3.0源码推导reactive与ref 的用法及场景

2,118 阅读7分钟

vue3正式发布以后,相信有很多小伙伴都迫不及待的体验了一波,在熟悉新的composition api的同时,我们发现官方文档提供了两种设置响应式数据的API:ref()和reactive(),官方文档中对这两个API的一些描述总感觉差点意思,有很多同学得出结论ref()用于基本类型的响应式处理,reactive()用于引用类型的响应式处理,实际是否有这样呢?今天让我们从源码看看是不是能推导出相对简洁实用的用法


体验了一段时间vue3,在阅读官方文档时,有些新的API说明一带而过,导致在使用中大家的用法五花八门,也会出现一些疑问,比如:

  • ref可以生成基本类型的响应式副本,也可以定义引用类型的响应式副本吗
  • reactive只能接收对象吗,接收其他类型的值会有什么问题
  • 具体什么时候使用ref或reactive
  • 被ref包装的值为什么要用.value操作,reactive包装的值使用.value会有什么现象
  • 解构会断开响应式的追踪

等等问题

一、测试

测试一
import { reactive, ref } from "vue";

let test = reactive('zhangsan');
let testObj = reactive({
    a: 1
})

setTimeout(() => {
    test = 'lisi'
    testObj.a = 5
    console.log('======>test', test)
    console.log('======>testobj', testObj)
}, 2000)

2.jpg

结论是会发现testObj都变成响应式的了,test并没有响应式更新,打印变量发现test就是一个普通的字符串,而testObj是一个Proxy对象,同时报了一个waring

测试二
import { reactive, ref } from "vue";

let test = ref('zahngsan');
let testObj = ref({
    a: 1
})

setTimeout(() => {
    test.value = 'lisi'
    testObj.value.a = 5
    console.log('======>test', test)
    console.log('======>testobj', testObj)
}, 2000)

1.jpg

使用ref的话testtestObj都变成响应式的了,页面引用会更新,不过得需要使用.value对其进行修改,打印两个变量返回了RefImpl对象

测试三
// Composition API逻辑抽离  useGrowUp.js
import { ref, onMounted, onUnmounted } from "vue";

export default function useGrowUp() { 
    const timer = ref(null);
    const happy = ref(100);
    const money = ref(0);

    function growUp() {
        money.value++;
        happy.value--;
    };
    
    onMounted(() => {
        timer.value = setInterval(growUp, 5000); 
    })
    
    onUnmounted(() => {
        clearInterval(timer.value);
    })

    return { happy,money };
}

你可以在Vue文件中像下面这样使用

<template>
    <h2>模拟人生</h2>
    <div>快乐:{{ happy }},财富:{{ money }}</div>
</template>
<script>
    import useGrowUp from './useGrowUp.js'
    setup() {
        const { money, happy } = useGrowUp();
        console.log('money', money)
        console.log('happy', happy)

        return {
            money,
            happy
        }
    }
</script>

1.gif

可以看到moneyhappy是RefImpl对象,现象是响应式的;但如果我们不想使用很多ref(),像如下这种用一个reactive()定义一个people返回,看看会有什么效果

const people = reactive({
    timernull,
    happy100,
    money0
})

...

return people;

image.png

这次只是打印出了money, happy的具体值,页面已经失去了响应式,是因为使用了解构const { money, happy } = useGrowUp();

下面就针对这些现象,看看源码里做了什么事情

二、源码解析

1、reactive
// 源码位置 packages/reactivity/src/reactive.ts
export function reactive(target: object) {
    // if trying to observe a readonly proxy, return the readonly version.
    if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
        return target
    }

    return createReactiveObject(
        target,
        false,
        mutableHandlers,
        mutableCollectionHandlers,
        reactiveMap
    )
}

// ReactiveFlags枚举 该枚举类型在后续源码中戏份很大
export const enum ReactiveFlags {
    SKIP = '__v_skip', // 标记是否跳过响应式
    IS_REACTIVE = '__v_isReactive', // 标记是否是reactive对象
    IS_READONLY = '__v_isReadonly', // 标记是否是只读对象
    RAW = '__v_raw' // 标记是否原始值
}

首先入口判断是否是只读,是则直接返回target,后执行createReactiveObject操作,下面看下createReactiveObject的代码

// reactive核心创建逻辑
function createReactiveObject(
    target: Target,
    isReadonly: boolean,
    baseHandlers: ProxyHandler<any>,
    collectionHandlers: ProxyHandler<any>,
    proxyMap: WeakMap<Target, any>
) {
    if (!isObject(target)) {
        if (__DEV__) {
            console.warn(`value cannot be made reactive: ${String(target)}`)
        }
        return target
    }
    // target is already a Proxy, return it.
    // exception: calling readonly() on a reactive object
    if (
        target[ReactiveFlags.RAW] &&
        !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
    ) {
        return target
    }

    // target already has corresponding Proxy
    const existingProxy = proxyMap.get(target)
    if (existingProxy) {
        return existingProxy
    }
    // only a whitelist of value types can be observed.
    const targetType = getTargetType(target)
    if (targetType === TargetType.INVALID) {
        return target
    }
    const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
    )
    proxyMap.set(target, proxy)
    return proxy
}
  1. 如果不是object类型,Dev环境下会报警告,并且返回target,也就是reactive不支持基本类型的响应式转换;

  2. 如果target有原始值标记,说明是已经响应化了,直接返回target,并且有个例外:可以对proxy对象再进行readonly操作;

既如下这种情况是会被允许的

let obj = {
    a: 1
}
let testReactive = reactive(obj)
let testReadonly = readonly(testReactive)

console.log('======>reactive', testReactive)
console.log('======>readonly', testReadonly)

4.png

  1. 检查target是不是已经有了proxy对象的映射,有的话直接取出返回,后边代码会看到,会对生成的proxy对象在WeakMap结构中添加一条target,proxy的映射;

  2. 通过getTargetType获取target的类型,只有白名单里的类型才能白响应式,看代码知道也就是支持ObjectArrayMapSetWeakMapWeakSet;

const enum TargetType {
    INVALID = 0,
    COMMON = 1,
    COLLECTION = 2
}

function targetTypeMap(rawType: string) {
    switch (rawType) {
        case 'Object':
        case 'Array':
            return TargetType.COMMON
        case 'Map':
        case 'Set':
        case 'WeakMap':
        case 'WeakSet':
            return TargetType.COLLECTION
        default:
            return TargetType.INVALID
    }
}

function getTargetType(value: Target) {
    return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
        ? TargetType.INVALID
        : targetTypeMap(toRawType(value))
}
  1. 执行响应化,Proxy构造函数的第二个参数handler,针对不同的类型使用不同的handler,集合类型MapSetWeakMapWeakSet使用collectionHandlersObjectArray使用baseHandlers,我们日常开发大部分情况会用到baseHandlers,整个的响应式系统还有收集依赖(track),触发依赖(trigger),这些操作会在handler中执行,关于handler可以单开一篇,不是本文重点

  2. 最后会在WeakMap结构中添加一条target,proxy的映射;

以上仅仅展示了reactive的过程,还有其他一些API,像shallowReactivereadonly等等过程类似,都是调用createReactiveObject

2、ref
// 仅展示源码中需要的代码
export function ref<T extends object>(value: T): ToRef<T>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
    return createRef(value, false)
}

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

// 根据__v_isRef判断是否是ref
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
    return Boolean(r && r.__v_isRef === true)
}

ref定义这里的写法是TS的重载写法,重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。

这里很简单,看看传进来的值是不是有__v_isRef标记,有就代表已经响应化了,直接返回值,没有就new一个RefImpl类的实例,然后看下RefImpl

class RefImpl<T> {
    private _value: T
    private _rawValue: T
    
    public dep?: Dep = undefined
    public readonly __v_isRef = true
    
    constructor(value: T, public readonly _shallow: boolean) {
        this._rawValue = _shallow ? value : toRaw(value)
        this._value = _shallow ? value : convert(value)
    }

    get value() {
        trackRefValue(this)
        return this._value
    }

    set value(newVal) {
        newVal = this._shallow ? newVal : toRaw(newVal)
        if (hasChanged(newVal, this._rawValue)) {
            this._rawValue = newVal
            this._value = this._shallow ? newVal : convert(newVal)
            triggerRefValue(this, newVal)
        }
    }
}

通过RefImpl类可以看出,ref传入的值,会通过一个value进行获取,修改,这样做是因为js本身没有对基本类型监听的能力,所以需要将值放到一个载体上,对其进行get,set操作时才能进行收集依赖(track),触发依赖(trigger)。

_shallow决定当前值的响应式是否由reactive()创建

内部值_rawValue 存放而如果_shallow为false直接等于value,否则对 value进行toRaw处理成proxy对象的原始值或基本类型的值

内部值_value 存放而如果_shallow为false直接等于value,否则通过convert方法判断是否是Object,是则调用reactive()创建

getset方法同理,加入了对比新旧值及收集依赖(track),触发依赖(trigger)过程

const convert = <T extends unknown>(val: T): T =>
    isObject(val) ? reactive(val) : val

值得提一句的是,ref还支持自定义get,set方法,既CustomRefImpl,看了源码后,官方用例也就好理解了

class CustomRefImpl<T> {
    public dep?: Dep = undefined
    private readonly _get: ReturnType<CustomRefFactory<T>>['get']
    private readonly _set: ReturnType<CustomRefFactory<T>>['set']
    public readonly __v_isRef = true
    
    constructor(factory: CustomRefFactory<T>) {
        const { get, set } = factory(
            () => trackRefValue(this),
            () => triggerRefValue(this)
        )
        this._get = get
        this._set = set
    }

    get value() {
        return this._get()
    }

    set value(newVal) {
        this._set(newVal)
    }
}

三、结论及建议

至此,本文对vue3.0中重要的两个apirefreactive主要的执行过程就分析完了,可以看出

  1. reactive只对引用类型进行响应化处理;

  2. ref可以对任何类型进行响应化处理,但是缺点也很明显,需要用.value来操作,这个问题在最新的提案(中文RFC英文原版知乎讨论)中尤大提出解决方案(ref sugar)了,使用一种新的语法糖,具体如下:

ref:count = 1
count++

目前社区还在争论中,不过最后无论以什么方式出现,这个.value应该会解决的;

使用建议:

1、很多时候可以使用ref来创建任何响应式数据;

2、如果有很多基本类型需要定义的,可以包装成一个对象,一次性使用reactive转化成响应式

3、解决reactive生成的proxy对象解构会失去响应式的问题,可以使用toRefs响应式对象转换为一个个的ref使用,在测试三中,改成如下即可

const people = reactive({
    timernull,
    happy100,
    money0
})

...

return toRefs(people)

如果有任何问题,欢迎留言,也欢迎关注我的博客

(完)