Vue3响应式对象-reactive

1,671 阅读6分钟

一、reactive对象简要介绍

reactive对象是Vue3为最常用的响应式对象之一。其通过代理Proxy实现,是对象类型的一种响应式封装。

二、不同reactive对象的区别

  • reactive

    reactive对象是深层响应式对象,即嵌套的对象也会被转换为响应式的代理对象。示例如下:

    const proxy = reactive({
      out:{
        nest:{
          a:10
        }
      }
    })
    const nest = proxy.out.nest // nest为Proxy对象
    
  • shallowReactive

    shallowReactive对象是浅层响应式对象,只有访问外层属性才具有响应式,即访问内层属性,不会返回响应式的代理。示例如下:

    const proxy = shallowReactive({
      out:{
        nest:{
          a:10
        }
      }
    })
    const out = proxy.out // 非Proxy对象
    

    这是因为proxy对象是响应式的,所以可以感知到out属性的变更,但无法感知到nest属性的变更,因为out对象不是代理对象

  • readonlyReactive

    readonlyReactive返回一个对象的只读代理,即无法进行写操作

  • shallowReadonlyReactive

    shallowReadonlyReactive返回一个浅层只读代理,即针对外层属性只读,内层属性不限制。示例如下:

    const proxy = shallowReadonly({
      a:10,
      b:{
        c:20
      }
    })
    proxy.a = 20 //error
    proxy.b.c = 30
    

    实际是由于浅响应,深层属性不会转换为代理,因此不会产生拦截

三、创建对象核心源码解析

下面针对源码进行解析

  • 源码如下:

    function createReactiveObject(
      target: Target,
      isReadonly: boolean,
      baseHandlers: ProxyHandler<any>,
      collectionHandlers: ProxyHandler<any>,
      proxyMap: WeakMap<Target, any>
    ) {
      // reactive只作用于对象
      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
      }
      // 从代理map中获取
      const existingProxy = proxyMap.get(target)
      if (existingProxy) {
        return existingProxy
      }
      // 判断目标对象是否合法,
      // 有些对象被打上了标记,不能转换为代理,有些对象是不可扩展的,这些对象不是合法对象
      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. baseHandler针对普通对象的拦截处理

(1). get

// 数组的封装方法
function createArrayInstrumentations() {
  const instrumentations: Record<string, Function> = {}
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      const arr = toRaw(this) as any
      for (let i = 0, l = this.length; i < l; i++) {
        // 针对数组遍历类型的方法,直接先把所有的元素都收集,
        // 在数组较大时,这意味着会产生很多依赖,需要考虑别的替代优化方案
        track(arr, TrackOpTypes.GET, i + '')
      }
      // 通过数组的原始方法获取数据
      const res = arr[key](...args)
      if (res === -1 || res === false) {
        // 如果未找到,则获取参数的原始数据在获取一次
        // 传入的匹配参数可能是响应式数据,判断是否存在时,只需要比对原始数据即可
        return arr[key](...args.map(toRaw))
      } else {
        return res
      }
    }
  })
  ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      // 针对会更改数据数组长度的方法,暂停依赖收集,
      // 因为这些方法会"隐式"的访问和修改length属性
      // 不暂停收集可能会导致无限递归
      pauseTracking()
      const res = (toRaw(this) as any)[key].apply(this, args)
      resetTracking()
      return res
    }
  })
  return instrumentations
}
// 拦截的核心代码
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 根据特殊key获取特定值  1.是否响应式 2.是否只读 3.是否浅代理 4.被代理对象(原始值)
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return shallow
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target
    }
    // 判断原始数据是否数组
    const targetIsArray = isArray(target)

    if (!isReadonly) {
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
        // 调用数组的封装方法,由于访问数组元素时,需要进行依赖收集,
        // 因此针对数组的一些方法进行了二次封装
        return Reflect.get(arrayInstrumentations, key, receiver)
      }
      if (key === 'hasOwnProperty') {
        return hasOwnProperty
      }
    }
    // 获取对应值
    const res = Reflect.get(target, key, receiver)
    // 一些特殊属性直接返回
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    if (!isReadonly) {
      // 非只读需要进行依赖收集
      track(target, TrackOpTypes.GET, key)
    }
    // 浅代理直接返回结果
    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // 如果访问的是数组元素,且该元素是ref对象,则直接返回。
      // 访问其他ref对象需要进行解包,返回值
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }

    if (isObject(res)) {
      // 如果返回值是对象,则需要转换为代理返回,并且继承当前代理对象是否响应式的特性
      // 比如当前对象是只读的,那么其嵌套对象的代理也是只读的
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

get方法的代码相对而言还是比较简单。流程图如下:

reactive读流程.png

(2). set

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // 获取变更前数据
    let oldValue = (target as any)[key]
    // 如果前数据是只读且是ref对象,但新数据不是ref对象,则不能更改
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false
    }
    if (!shallow) {
      if (!isShallow(value) && !isReadonly(value)) {
        // 正常响应式数据,分别获取原始值
        oldValue = toRaw(oldValue)
        value = toRaw(value)
      }
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        // 前对象不是数组,且前数据是ref对象,新数据不是ref对象,则直接更改value属性值
        oldValue.value = value
        return true
      }
    } else {
      // 浅代理下,对象被设置成原样,不论是否响应式
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // 变更值
    const result = Reflect.set(target, key, value, receiver)
    // 正常情况下,target就是代理对象receiver的原始对象,这个判断是为了防止多重代理情况下的修改
    // 比如target是a,然后有proxyA代理对象a,有proxyB代理proxyA对象,此时通过proxyB修改数据则会在某一阶段存在判断不相等
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 如果不存在key,则触发新增类型的依赖更新
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 如果存在key,则触发修改类型的依赖更新
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

set方法的流程相对于get方法,要简洁许多,这儿不再提供流程图。简单来说呢,分为以下几步:

  1. 判断是否可以修改
  2. 针对是否浅代理,分别处理
  3. 在浅代理中,针对ref对象,判断是否只用修改其value值即可
  4. 根据之前是否存在对应键来触发相应的依赖更新类型

(3). 其他拦截方法

除开最为主要的get,set方法,其实还存在其余几个方法用于依赖收集以及依赖更新,但都不是主要方法,下面简要贴上源码及注释。

// delete关键字
function deleteProperty(target: object, key: string | symbol): boolean {
  // 是否存在对应key值
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  // 是否删除成功
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    // 如果存在key值且删除成功,触发delete类型依赖更新
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

// in关键字
function has(target: object, key: string | symbol): boolean {
  // 判断是否存在对应key
  const result = Reflect.has(target, key)
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    // 如果不是一些内置key值以及特殊symbol类型的key值,则触发has类型依赖收集
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}

// Object.keys 
function ownKeys(target: object): (string | symbol)[] {
  // 直接触发一个iterate类型的依赖收集,如果数组,则键值说length,如果是对象,则键值是特殊键 ITERATE_KEY
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

2. collectionHandler 针对集合类型的拦截处理

集合类型是一些内置类型,主要是针对以下四类:

  • Set
  • Map
  • WeakSet
  • WeakMap

使用集合时,一般都是使用其封装好的方法,类似于使用数组的方法.因此,主要针对这些方法的一个重写做一个简要的解析。

1. get

// get方法只存在于Map和WeakMap中
function get(
  target: MapTypes,
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  // 获取map对象的原始值
  target = (target as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  // 获取原始key值,传入的key可以是个代理对象
  const rawKey = toRaw(key)
  if (!isReadonly) {
    // 非只读需要依赖收集,原始key以及代理key都收集
    if (key !== rawKey) {
      track(rawTarget, TrackOpTypes.GET, key)
    }
    track(rawTarget, TrackOpTypes.GET, rawKey)
  }
  const { has } = getProto(rawTarget)
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  // 判断是否存在相应键值,如果存在则获取对应值,并将其根据map的特性转换为相应的代理对象
  // map浅代理-不做转换
  // map只读-转换只读代理
  // map响应式-转换响应式代理
  if (has.call(rawTarget, key)) {
    return wrap(target.get(key))
  } else if (has.call(rawTarget, rawKey)) {
    return wrap(target.get(rawKey))
  } else if (target !== rawTarget) {
    // #3602 readonly(reactive(Map))
    // ensure that the nested reactive `Map` can do tracking for itself
    target.get(key)
  }
}

get方法是Map和WeakMap集合所拥有的方法。在通过get方法获取对应值时,先收集相关依赖key,然后将对应值进行相应的响应式转换。代码中的key可能是一个代理对象,所以获取了其原始值rawKey,2个key值都进行判断是为了兼容传入代理对象的情况。

2. has

// 集合类型都存在的方法
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
  const target = (this as any)[ReactiveFlags.RAW]
  // 获取原始集合对象以及原始键值
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  // 非只读进需要收集依赖
  if (!isReadonly) {
    if (key !== rawKey) {
      track(rawTarget, TrackOpTypes.HAS, key)
    }
    track(rawTarget, TrackOpTypes.HAS, rawKey)
  }
  // 调用集合原始has方法返回结果
  return key === rawKey
    ? target.has(key)
    : target.has(key) || target.has(rawKey)
}

3. size

// 获取集合大小,非只读触发依赖收集
function size(target: IterableCollections, isReadonly = false) {
  target = (target as any)[ReactiveFlags.RAW]
  !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  // 调用原始方法获取集合大小
  return Reflect.get(target, 'size', target)
}

4. add

// 添加元素,Set,WeakSet类型的方法
function add(this: SetTypes, value: unknown) {
  // 获取原始值
  value = toRaw(value)
  // 获取原始几乎
  const target = toRaw(this)
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  if (!hadKey) {
    // 集合中不存在对应的值,则添加,并且触发依赖更新
    target.add(value)
    trigger(target, TriggerOpTypes.ADD, value, value)
  }
  return this
}

5. set

// 设置键值对,Map和WeakMap类型的方法
function set(this: MapTypes, key: unknown, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  const { has, get } = getProto(target)

  // 判断是否存在对应key值
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    checkIdentityKeys(target, has, key)
  }

  const oldValue = get.call(target, key)
  // 设置对应值
  target.set(key, value)
  // 根据是否存在对应键值触发添加和修改的依赖更新
  if (!hadKey) {
    trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue)) {
    // 修改需要判断前后值是否变更
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  return this
}

6. delete

// 集合都拥有的方法 delete触发
function deleteEntry(this: CollectionTypes, key: unknown) {
  const target = toRaw(this)
  const { has, get } = getProto(target)
  let hadKey = has.call(target, key)
  // 获取是否存在key
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    checkIdentityKeys(target, has, key)
  }

  const oldValue = get ? get.call(target, key) : undefined
  // forward the operation before queueing reactions
  // 直接删除,不论是否存在,因为不存在也无影响
  const result = target.delete(key)
  if (hadKey) {
    // 只有存在key,才触发delete依赖更新
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

7. clear

// 清空集合,集合都存在的方法
function clear(this: IterableCollections) {
  const target = toRaw(this)
  const hadItems = target.size !== 0
  const oldTarget = __DEV__
    ? isMap(target)
      ? new Map(target)
      : new Set(target)
    : undefined
  // forward the operation before queueing reactions
  // 直接清空集合
  const result = target.clear()
  if (hadItems) {
    // 如果集合之前存在元素,则触发clear依赖更新
    trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
  }
  return result
}

8. forEach

// 集合的遍历方法 forEach
function createForEach(isReadonly: boolean, isShallow: boolean) {
  return function forEach(
    this: IterableCollections,
    callback: Function,
    thisArg?: unknown
  ) {
    const observed = this as any
    const target = observed[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    // 根据集合特性获取集合元素的包装方法 1.不处理 2.只读 3.响应式
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    // 非只读进行依赖收集
    !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
    return target.forEach((value: unknown, key: unknown) => {
      // 返回包装后的遍历值
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
}

9. 疑惑点阐述

在上述所有集合,都用到了一个方法toRaw,这是获取原始值的方法,本质上是递归获取ReactiveFlags.RAW这个属性的值。方法实现如下:

export function toRaw<T>(observed: T): T {
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

这个方法的实际作用就是获取一个代理对象的原始值,比如存在一个对象a,代理对象proxyA是a的代理,那么toRaw(proxyA)最终能获取到对象a,并且当存在多级代理时,也可以获取到最原始的那个被代理对象,因为toRaw是个递归调用。直接访问ReactiveFlags.RAW只会获取当前代理的原始值,如果存在多级代理,则获取到的值还是代理对象。那么为什么需要获取原始值?

本质上讲,使用代理只是为了新增一些自定义逻辑,主要是依赖的收集和更新,原本的针对集合的读写逻辑不应该被修改。因此不管读写,最终操作的都是原始数据以及原始集合,使用toRaw转换,是为了支持代理key的传入。这个模型如下图:

读写本质逻辑.png

从上图可知,不管读写,本质上都是先转换为原始数据,在进行操作。那么为什么需要转换?因为在Vue3中,我们绝大多数时候使用的都是响应式对象,这种设计方法就是为了不论传入响应式对象还是原始对象,都能正确的存储,简单测试代码如下:

const map = reactive(new Map())
let obj = {}
let obj1 = {}
map.set(reactive(obj),1)
map.set(obj1,2)
expect(map.get(obj)).toBe(1)
expect(map.has(obj)).toBe(true)
expect(map.get(reactive(obj1))).toBe(2)
expect(map.has(reactive(obj))).toBe(true)

在上述测试代码中,设置的key值和获取的key值,分别是代理对象和非代理对象,但这完全不影响正常的读写逻辑,并且还有正常的依赖收集

五、总结

Vue3响应式对象reactive的分析就到这儿了,本质上就是一个代理对象,在拦截操作的过程中,加入了依赖更新和依赖收集的流程,后续博客会介绍ref对象的代码解析。