ref与reactive使用疑问和源码解析

156 阅读7分钟

vue3使用响应式对象的同时,会有比较多疑问,类似:

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

reactive实现响应式就是基于ES2015 Proxy的实现的。那我们知道Proxy有几个特点:

  • 代理的对象是不等于原始数据对象
  • 原始对象里头的数据和被Proxy包装的对象之间是有关联的。即当原始对象里头数据发生改变时,会影响代理对象;代理对象里头的数据发生变化对应的原始数据也会发生变化。 需要记住:是对象里头的数据变化,并不能将原始变量的重新赋值,那是大换血了

demo测试

测试一

let tstring = reactive("xin"); //如果是在ts环境中,会有类型异常提示
const tObj = reactive({
  a: 1,
});
​
setTimeout(() => {
  tstring = "xin2";
  tObj.a = 5;
  console.log("======>tstring", tstring);
  console.log("======>tObj", tObj);
}, 1000);

image-20220803104004898.png

结论:

reactive绑定不是对象的数据类型时,会直接返回原始值,同时提示waring;绑定对象时,会返回响应式Proxy代理对象。

测试二

const tstring = ref("xin");
const tObj = ref({
  a: 1,
});
​
setTimeout(() => {
  tstring.value = "xin2";
  tObj.value.a = 5;
  console.log("======>tstring", tstring);
  console.log("======>tObj", tObj);
}, 1000);

image-20220803105042186.png

结论:

使用ref的话tstringtObj都变成响应式的了,页面引用会更新,需要使用.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({
    timer: null,
    happy: 100,
    money: 0
})
​
...
​
return people;

image.png

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

测试四

<template>
  <button @click="changeObj">changeObj</button>
  <div>obj:{{ obj.count }}</div>
  <button @click="changeReactiveObj">changeReactiveObj</button>
  <div>reactiveObj:{{ reactiveObj.count }}</div>
</template>
<script lang="ts">
import { reactive } from "vue";
export default {
  setup() {
    const obj = {
      count: 0,
    };
    const reactiveObj = reactive(obj);
    function changeObj() {
      obj.count++;
      console.log(obj);
      console.log(reactiveObj);
    }
    function changeReactiveObj() {
      reactiveObj.count++;
      console.log(obj);
      console.log(reactiveObj);
    }
    return { changeObj, changeReactiveObj, reactiveObj, obj };
  },
};
</script>

当点击changObj按钮时,可以看到objreactiveObj对应值都进行了改变,但视图未进行更新。

image-20220803120434574.png

当点击changeReactiveObj按钮时,可以看到objreactiveObj对应值都进行了改变,视图也进行更新,但是会从changObj最后一次记录数据开始更新视图。

image-20220803121732535.png

所以当响应式对象里头数据变化的时候原始对象的数据也会变化,同样,原始对象的数据变化也会引起响应式数据变化

测试五

测试reactive是否会深度监听每一层

const t2 = reactive({
  a: {
    b: {
      c: { name: "c" },
    },
  },
});
console.log(t2);
console.log(t2.a);
console.log(t2.a.b);
console.log(t2.a.b.c);

image-20220803122405661.png

结果reactive是递归会将每一层包装成Proxy对象的,深度监听每一层的property

源码解析

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)
    }
}

总结

创建响应式对象api有两个,refreactive。从各种影响总的概括来说,本质是因为两个api返回的响应式对象是不同的。

const refVal = ref(1);
const reactiveVal = reactive({ name: "xin" });
console.log("refVal", refVal);
console.log("reactiveVal", reactiveVal);
​
// refVal RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 1, _value: 1}
// reactiveVal Proxy {name: 'xin'}

如上图可看到,使用ref返回实例对象为RefImpl,而reactive返回的是一个Proxy对象,例如影响结构的原因,其实就是因为实例RefImpl对象结构value后,value指对应是个响应式数据,而Proxy对象解构后自然会失去响应式。vue官方建议使用toRefsapi来将Proxy转为一个个ref使用,转为RefImpl实例后,就可以使用结构了。