深入源码理解Vue3 reactive

作者: 深度剖析JavaScript | 来源:发表于2020-11-23 21:07 被阅读0次

    我们先来看官方对于reactive的解释,官方的解释也非常简单

    返回对象的响应式副本

    但从这句话我们可以得到以下信息

    1. reactive接受一个对象作为参数
    2. 其返回值是经reactive函数包装过后的数据对象,这个对象具有响应式

    但同样会有一些疑问
    比如,reactive的参数只能传递一个对象吗,如果传递其他值会怎么样?
    比如,返回的响应式数据的本质是什么,为啥就能让数据变成响应式?
    比如,"副本"是不是意味着响应式数据与原始数据没有关联?
    比如,返回的响应式副本里头的数据是深度响应式吗,即是否递归监听对象的所有属性?等等

    带着这些疑问我们一起来看
    首先,通过reactive创建一个响应数据

    import { reactive } from "vue";
    export default {
      setup() {  
        const state = reactive({
          count: 0,
        });
      },
    };
    

    如上代码就可以创建一个响应式数据state,我具体来看一下这个

    console.log(state)
    

    可以看见,返回的响应副本state其实就是Proxy对象。所以reactive实现响应式就是基于ES2015 Proxy的实现的。那我们知道Proxy有几个特点:

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

    因此,既然reactive实现响应式是基于Proxy的实现的,那我们大胆猜测,原始数据与相应数据也是有关联的。那我们来测试一下

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

    以上代码测试结果如下

    验证,确实当响应式对象里头数据变化的时候原始对象的数据也会变化
    如果反过来,结果也是一样

     // ++state.count
    ++obj.count;
    

    当响应式对象里头数据变化的时候原始对象的数据也会变化
    那问题来了,我们操作数据的时候通过谁来操作呢?
    官方的建议是

    建议只使用响应式代理,避免依赖原始对象

    再来解决另外一个问题看看reactive是否会深度监听每一层呢?

    const state = reactive({
        a:{
            b:{
                c:{name:'c'}
            }
        }
    });    
    console.log(state);  
    console.log(state.a);
    console.log(state.a.b);  
    console.log(state.a.b.c); 
    

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

    最后测试一下如果reactive传递是非对象而是原始值会怎么样

    const state = reactive(0);  
    console.log(state)
    

    结果是,原始值并不会被包装,所以也没有响应式特点

    下面,我们看看reactive的源码吧
    源码目录位置:vue-next\packages\reactivity\src\reactive.ts
    直接找到reactive的类型声明:

    export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
    

    可以看到reactive接受一个参数targettarget的类型是泛型T,而T类型是extends object,简单来说接受的参数target的类型是object类型或者时继承自object类的子类类型
    返回值的类型的UnwrapNestedRefs<T>
    看看UnwrapNestedRefs<T>类型

    type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
    

    使用type关键字声明类型UnwrapNestedRefs<T>,这里有个三目运算符,用于进一步判断T;如果传入的T属于Refs类或者其子类,那么返回传入的T,否者就是UnwrapRef<T>

    下面具体看看reactive方法的定义

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

    接受一个类型为object参数,当传入对象是只读,返回本身。这里的as关键字是断言,表示传入的值一定是Target类型,里头有个ReactiveFlags.IS_READONLY,用于判断是否是只读的属性

    export interface Target {
      [ReactiveFlags.SKIP]?: boolean
      [ReactiveFlags.IS_REACTIVE]?: boolean
      [ReactiveFlags.IS_READONLY]?: boolean
      [ReactiveFlags.RAW]?: any
    }
    

    如果传递的对象是普通对象(不是readonly),则执行创建响应式对象函数createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers)
    该方法比较长,是reactive的核心方法,所以还是得读一下源码

    function createReactiveObject(
      target: Target,
      isReadonly: boolean,
      baseHandlers: ProxyHandler<any>,
      collectionHandlers: ProxyHandler<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 proxyMap = isReadonly ? readonlyMap : reactiveMap
      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
    }
    

    可以看到除了几种特殊情况返回target本身之外,就返回proxyproxy就是通过new Proxy构造函数构建出来的。这里也进一步证明了reactive的响应式功能确实是通过Proxy实现的
    可以看一样Proxy的定义

    interface ProxyHandler<T extends object> {
        getPrototypeOf? (target: T): object | null;
        setPrototypeOf? (target: T, v: any): boolean;
        isExtensible? (target: T): boolean;
        preventExtensions? (target: T): boolean;
        getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined;
        has? (target: T, p: PropertyKey): boolean;
        get? (target: T, p: PropertyKey, receiver: any): any;
        set? (target: T, p: PropertyKey, value: any, receiver: any): boolean;
        deleteProperty? (target: T, p: PropertyKey): boolean;
        defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean;
        enumerate? (target: T): PropertyKey[];
        ownKeys? (target: T): PropertyKey[];
        apply? (target: T, thisArg: any, argArray?: any): any;
        construct? (target: T, argArray: any, newTarget?: any): object;
    }
    interface ProxyConstructor {
        revocable<T extends object>(target: T, handler: ProxyHandler<T>): { proxy: T; revoke: () => void; };
        new <T extends object>(target: T, handler: ProxyHandler<T>): T;
    }
    declare var Proxy: ProxyConstructor;
    

    里面的具体实现方法,在createReactiveObject传参的时候就传入进来了
    mutableHandlers和mutableCollectionHandlers,具体可以去`vue-next\packages\reactivity\src\baseHandlers.ts文件中看

    经过上面的了解,我们可以总结和回答一下最开始几个疑问了
    1. reactive的参数可以传递对象也可以传递原始值。但是原始值并不会包装成响应式数据
    2. 返回的响应式数据的本质Proxy对象
    3. 返回的响应式"副本"与原始数据有关联,当原始对象里头的数据或者响应式对象里头的数据发生,会彼此相互影响。两种都可以触发界面更新,操作时建议只使用响应式代理对象
    4. 返回的响应式对象里头时深度递归监听每一层的,每一层都会被包装成Proxy对象

    以上就是Vue3中reactive基本内容
    注:本文示例代码可在github查阅
    https://github.com/jCodeLife/learn-vue3/tree/master/learn-vue3-reactive

    相关文章

      网友评论

        本文标题:深入源码理解Vue3 reactive

        本文链接:https://www.haomeiwen.com/subject/fshviktx.html