Vue3源码解析05--响应式reactive和ref

Vue3 源码解析 05–响应式 reactive 和 ref

前言

我们都知道 Vue 的响应式是通过数据劫持实现的,Vue2 的数据劫持是通过 Object.defineProperty 实现的,而 Vue3 升级的一部分原因就是将defineProperty替换成更为强大的 Proxy。
众所周知,Proxy 是 defineProperty 的升级版,之所以没有推广开来的原因还是适配性的问题。现在时机终于成熟啦 。

我们现在要分析的就是响应式的 reactive 和 ref,这两者的使用方式不尽相同。当我们使用的时候也许会有疑问:既然都是响应式,那么这两者的本质区别是什么呢。带着这个疑问我们来分析一下两者的源码吧。

reactive 源码分析

废话不多说,我们直接看一下 reactive 的源码:

//packages/reactivity/src/reactive.ts

export function reactive(target: object) {
  //如果传入的数据是readonly的,那么直接返回这个readonly的数据
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  //返回一个Reactive对象
  return createReactiveObject(
    target,
    false,
    mutableHandlers, //响应式数据的代理handler,一般Object和Array
    mutableCollectionHandlers //响应式集合的代理handler,一般是Set、Map、WeakMap、WeakSet
  )
}

上面这段代码很简单,就是当我们调用 reactive 的时候:

  • 首先判断当前数据是不是 readonly 对象,这里的 readonly 其实就是我们使用 [Vue.readonly 创建的对象
  • 然后调用 createReactiveObject()方法创建响应式的对象

至于上面代码提到了一个ReactiveFlags枚举,我们这里插播一下该参数的类型

export const enum ReactiveFlags {
  SKIP = '__v_skip', //用于标记对象不可进行代理
  IS_REACTIVE = '__v_isReactive', //reactive
  IS_READONLY = '__v_isReadonly', //readonly
  RAW = '__v_raw' //是proxy上面的原始target
}

接着就是使用 createReactiveObject 创建响应式对象,那么在看createReactiveObject之前,我们需要了解一下,

  • Vue3 中响应式的映射类型:
  //原始对象到响应式对象的映射
  export const reactiveMap = new WeakMap<Target, any>()
  //readonly对象到原始对象的映射
  export const readonlyMap = new WeakMap<Target, any>()

可以看出在 reactive.ts 中会预存两个 WeakMap:reactiveMap 和 readonlyMap。这两者分别代表了原始对象到响应式对象的映射readonly 对象到响应式对象的映射

  • 另外,还有 target 的数据类型:
  const enum TargetType {
    INVALID = 0, //其他类型
    COMMON = 1, //Array、Object类型的
    COLLECTION = 2 //Set 、 Map 、 WeakMap 、 WeakSet类型的
  }

上面代码告诉我们,target 目标数据的类型有三种其中COMMON表示 Array 和 Object 的基本类型,COLLECTION表示 Set、Map、WeakMap、WeakSet 四种集合类型,INVALID表示无效类型

下面我们继续顺着代码来看一下**createReactiveObject()**的实现:

首先,createReactiveObject接收四个参数目标对象、是否是 readonly、基本数据类型的劫持 handler、集合类型的数据劫持 handler。

  function createReactiveObject(
    target:Target,
    isReadonly:boolean,
    baseHandlers:ProxyHandler,
    collectionHandlers:ProxyHandler
  )

接下来,对目标数据进行判断。如果不是对象类型,则直接返回该数据并且在开发环境下给出警报。如果目标数据是 Proxy 直接返回该数据,但是有个例外情况,那就是当我们在 reactive 基础上调用 readonly 的时候。

//如果不是一个对象直接返回,开发环境下给出提示
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }

  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  //如果已经是一个响应式Proxy则直接返回响应式的Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

上面那句:当我们在 reactive 的基础上调用 readonly 的时候,听起来比较拗口,所以我们直接上一个测试用例看一下上述情况出现的时机:

//packages/reactivity/__tests__/readonly.spec.ts
test('readonly + reactive should make get() value also readonly + reactive', () => {
  const map = reactive(new Collection())
  const roMap = readonly(map)
  const key = {}
  map.set(key, {})

  const item = map.get(key)
  expect(isReactive(item)).toBe(true)
  expect(isReadonly(item)).toBe(false)

  const roItem = roMap.get(key)
  expect(isReactive(roItem)).toBe(true)
  expect(isReadonly(roItem)).toBe(true)
})

最后,是我们 reactive 创建响应式的核心逻辑了。使用 Proxy 实现数据劫持,传入我们的劫持 handler。同时将 Proxy 响应式收集起来更新响应式数据映射。

//只有普通类型和集合类型数据类型才可以被处理为响应式
  //主要类型:Object,Array,Map,Set,WeakMap,WeakSet
  const targetType = getTargetType(target)
  //如果数据类型是无效的
  if (targetType === TargetType.INVALID) {
    return target
  }
//创建响应式Proxy
  const proxy = new Proxy(
    target,
    //根据类型传入handler(collection:Set、Map、WeakMap、WeakSet,common:Object,Array)
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  //响应式数据收集起来,更新响应式数据映射
  proxyMap.set(target, proxy)
  return proxy

看完上面的代码,我们来简单总结一下 crateReactiveObject:

  • 首先判断我们传入的数据是否是对象类型的,如果不是直接返回该数据。
  • 判断是否已经是 Proxy,如果是的话直接返回该数据。但是需要注意一个例外情况:当我们使用 Vue.readonly()处理 reactive 数据的时候不适用该判断条件。
  • 最后,在满足我们数据类型的情况下,使用 Proxy 完成我们的数据劫持和响应式数据的映射。

ref 源码

上面我们看完了 reactive 的源码,知道了 reactive 的创建方式,下面我们来看一下 ref 的源码实现,相信我们看到最后也就能解答出我们上面提出的问题:ref 和 reactive 的本质区别是什么了。

和 reactive 相同的暴露方法:
ref 是通过 createRef方法创建 ref 对象,和createReactiveObject不同的是,createRef 调用 new RefImpl()来完成 ref 对象的创建。

//packages/reactivity/src/ref.tss
export function ref(value?: unknown) {
  return createRef(value)
}
//创建ref对象
function createRef(rawValue: unknown, shallow = false) {
  //如果本身就是一个ref的话,直接返回值
  if (isRef(rawValue)) {
    //如果目标数据是ref数据类型,直接返回
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

下面我们看一下 RefImpl class 的源码:

class RefImpl {
  //当前数据
  private _value: T

  //这个属性会标记对象是否是ref
  public readonly __v_isRef = true

  constructor(private _rawValue: T, public readonly _shallow = false) {
    //如果shallow为false,直接让ref.value等于value,否则对rawValue进行convert转换成reactive
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    //读取value的时候会出发track
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    //先判断新老value是否相同,如果不相同则再赋值并触发trigger
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

这里,我们首先分析一下其中的constructor

  • 该 RefImpl 接收两个参数,第一个是我们的原始数据,第二个参数是 shallow 的标识
  • 根据我们的 shallow 判断我们是否调用convert方法转化为 reactive

接下来就是 RefImpl 中 value 的 get 和 set 方法,这两个方法很简单:

  • 当触发 get 方法的时候触发 track 方法
  • 当触发 set 方法的时候,要先判断 newValue 和 oldValue 是否相同,不相同进行赋值并且触发 trigger 方法

♣ 友情提示,上面提到的 trigger 和 track,大家可以参考上一篇文章
♣ 从这里我们也可以看出来 ref 对象将响应式的数据挂载到了value属性上

接下来我们再看一下 convert 的实现:

//数据类型转换
const convert = (val: T): T =>
  //最终还是通过reactive()方法处理数据
  isObject(val) ? reactive(val) : val

这个方法的实现也是非常简单,就是单纯的判断一下我们的数据是不是 Object,如果是的话直接reactive处理我们的数据,否则直接返回该数据。

好了,看完 ref 的代码之后我们就可以总结出 ref 和 reactive 的本质区别了:

  • 两者的底层响应式实现是一样的,都是通过 createReactiveObject 来实现的数据响应式
  • 不同的是,ref 在创建的时候添加了一个**__v_isRef**标记来标识当前为 ref 对象
  • ref 对自身的 value 属性添加了数据劫持,get 的时候触发 track 方法,set 的时候触发 trigger 方法

这里我们贴一部分 ref 的测试用例:
这个测试用例也证实了我们源码中的set部分,相同的数据赋值不会触发赋值和 trigger

describe('reactivity/ref', () => {
  it('should hold a value', () => {
    const a = ref(1)
    expect(a.value).toBe(1)
    a.value = 2
    expect(a.value).toBe(2)
  })

  it('should be reactive', () => {
    const a = ref(1)
    let dummy
    let calls = 0
    effect(() => {
      calls++
      dummy = a.value
    })
    expect(calls).toBe(1)
    expect(dummy).toBe(1)
    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
    // same value should not trigger
    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
  })

总结

这节比较简单明了,主要是解析了一下 reactive 和 ref 的一个源码实现。其实我们这里就算涉及到的 reactive 和 ref 源码部分也仅仅是比较核心的一点,至于一些衍生的方法,例如 readonly,shallowReactive,isReactive 等方法。这里均没有涉及
是因为考虑到这些方法都是从其中衍生出来的,所以没有必要详细的解释了。

因为最近搬砖比较忙,所以文章总是断断续续的写,也没有太强的连贯性 。希望后面会好起来啦,也算是对自己美好的期望吧 。

你可能感兴趣的:(Vue3,Vue源码,前端,vue.js,javascript,前端)