深刻源码理解reactive和ref

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战javascript

前言

最近一直从新学习Vue3,看到composition API了,尝试结合源码看看,理解深入一些。本文先来看看 reactiveref 两个APIhtml

1、reactive

官方定义

咱们先来看官方对于reactive的解释,官方的解释也很是简单vue

返回对象的响应式副本java

但从这句话咱们能够获得如下信息react

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

产生一些疑问

但一样会有一些疑问web

好比,reactive的参数只能传递一个对象吗,若是传递其余值会怎么样?api

好比,返回的响应式数据的本质是什么,为啥就能让数据变成响应式?markdown

好比,"副本"是否是意味着响应式数据与原始数据没有关联?app

好比,返回的响应式副本里头的数据是深度响应式吗,便是否递归监听对象的全部属性?等等函数

经过测试解决疑问

带着这些疑问咱们一块儿来看 首先,经过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对象

2、ref

官方定义

关于ref,官方的解释是:

接受一个内部值并返回一个响应式且可变的 ref 对象

www.vue3js.cn/docs/zh/api…

为了方便理解,下文中将内部值都称为原始数据(orgin

简单来讲ref就是:原始数据=>响应式数据 的过程

产生疑问

但有几个问题得搞明白

  1. ref接受的原始数据是什么类型?是原始值仍是引用值,仍是都行?
  2. 返回的响应式数据本质具体是什么?根据传递的数据类型不一样,返回的响应式对象是否不一样?
  3. 响应式数据改变会触发界面更新,那原始数据改变会触发界面更新吗?即原始数据和返回的响应式数据是否有关联

测试解决疑问

示例代码1:

let origin = 0; //原始数据为原始值
let count = ref(origin);
function add() {
  count.value++;
}
复制代码

示例代码2:

let origin = { val: 0 };//原始数据为对象
let count = ref(origin);
function add() {
  count.value.val++;
}
复制代码

经测试,咱们发现,传递的原始数据orgin能够是原始值也能够是引用值,可是须要注意,若是传递的是原始值,指向原始数据的那个值保存在返回的响应式数据的.value中,如上count.value;若是传递的一个对象,返回的响应式数据的.value中对应有指向原始数据的属性,如上count.value.val

为了测试第二个问题,咱们将上述示例中的count打出来,看返回的具体是什么

console.log(count)
console.log(count.constructor)
复制代码

对比发现,无论传递数据类型的数据给ref,不管是原始值仍是引用值,返回的响应式数据对象本质都是由RefImpl类构造出来的对象。但不一样的是里头的value,一个是原始值,一个是Proxy对象

源码分析

到这里,不妨来读一下RefImpl类的源码

目录:vue-next\packages\reactivity\src\ref.ts

class RefImpl<T> {
  private _value: T
  public readonly __v_isRef = true
  constructor(private _rawValue: T, private readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }
  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }
  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}
复制代码

能够看见RefImpl class传递了一个泛型类型T,里头具体包含:

  1. 有个私有属性_value,类型为T,有个公开只读属性__v_isRef值为true
  2. 有两个方法,get value(){}set value(){},分别对应私有属性的读写操做,用于供外界操做value
  3. 有一个构造函数constructor,用于构造对象。构造函数接受两个参数:
    • 第一个参数_rawValue,要求是T类型
    • 第二个参数_shallow,默认值为true

当经过它构建对象时,会给对象的_value属性赋值为 _rawValue或者convert(_rawValue) 再看convert源码以下:

const convert = <T extends unknown>(val: T): T => isObject(val) ? reactive(val) : val 复制代码

经过源码咱们发现,最终Vue会根据传入的数据是否是对象isObject(val),若是是对象本质调用的是reactive,不然返回原始数据

下面再来验证最后一个问题就是:经过ref包装的结果,当原始数据改变时会触发界面更新吗?即原始数据和返回的响应式数据是否有关联?

示例代码3

let origin = 0; //原始值
let count = ref(origin);
function add() {
  origin++
  console.log(count.value)
}
复制代码

示例代码4

let origin = { val: 0 }; //引用值
let count = ref(origin);
function add() {
  origin++
  console.log(count.value.val)
}
复制代码

发现,不管传入给ref的原始数据是原始值仍是引用值,当原始数据发生修改时,并不会影响响应式数据,更不会触发界面UI的更新

实例代码5

let origin = 0; 
let count = ref(origin);
function add() {
  count.value++
  console.log(origin)
}
复制代码

上述代码,不管count修改多少次,origin一直是0

若是响应式数据发生改变,对应界面UI是会自动更新的,注意不影响原始数据

小结

简单小结一下:

  1. ref本质是将一个数据变成一个对象,这个对象具备响应式特色

  2. ref接受的原始数据能够是原始值也能够是引用值,返回的对象本质都是RefImpl类的实例`

  3. 不管传入的原始数据时什么类型,当原始数据发生改变时,并不会影响响应数据,更不会触发UI的更新。但当响应式数据发生改变,对应界面UI是会自动更新的,注意不影响原始数据。因此ref中,原始数据和通过ref包装后的响应式数据是无关联的

END

以上就是关于和reactiveref全部内容~

源码看得比较少,若有问题欢迎留言告知~

相关文章
相关标签/搜索