VUE3响应式之reactive、ref篇

29 阅读5分钟

之前一直使用vue开发项目,没有去研究底层源码,俗话说要站在巨人的肩膀上学习,那现在抽出时间去学习研究一下,写文章只是记录我学习的过程,如有错误欢迎批评指正。vue为3.5版本

废话不多说,直接开怼!!!

reactive

vue3采用组合式API,reactive和ref是响应式的基础,那就从这俩入手,先来看下定义reactive的源码:

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap,
  )
}

从以上代码可以看出,reactive只接受类型为object的参数,如果target为只读则直接返回。再来看看createReactiveObject函数

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>,
) {
  if (!isObject(target)) {
    if (__DEV__) {
      warn(
        `value cannot be made ${isReadonly ? 'readonly' : '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
  }
  // only specific value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
  )
  proxyMap.set(target, proxy)
  return proxy
}

从上面代码我们可以得到以下几点:

已经代理过的目标对象和只读的代理对象直接返回
通过getTargetType函数判断target类型:INVALID无效、COMMON(object、array)、COLLECTION(mapset、weakMap、weakSet)
proxyMap存储已经代理过的对象避免重复代理,影响性能

说一下这个proxyMap,它使用了WeakMap弱引用,当没有任何引用时,将会被垃圾回收。在这里看到Proxy本尊,也就是它让数据“活”起来了。主要看下Proxy第二个参数,我们研究一下在实际项目中使用最多的object和array,就直接看baseHandlers,先来看下get拦截函数(有删减)

class BaseReactiveHandler implements ProxyHandler<Target> {
  constructor(
    protected readonly _isReadonly = false,
    protected readonly _isShallow = false,
  ) {}
  get(target: Target, key: string | symbol, receiver: object): any {
      if (key === ReactiveFlags.IS_REACTIVE) {
          return !isReadonly
      }
      .
      .
      .
    const res = Reflect.get(
      target,
      key,
      // if this is a proxy wrapping a ref, return methods using the raw ref
      // as receiver so that we don't have to call `toRaw` on the ref in all
      // its class methods
      isRef(target) ? target : receiver,
    )
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}

代理对象访问属性时,会走get方法,可自定义一些属性,比如访问target[ReactiveFlags.IS_REACTIVE],就会返回(!isReadonly)的值。通过Reflect.get()获取值,如果是对象且不是只读,就会继续调用reactive(),达到深度响应。最后这个track()在这里就先不讲,暂时记得它是建立依赖,把这个响应对象放在一个桶里(dep)

再来看看baseHandlers中set拦截函数(有删减)

class MutableReactiveHandler extends BaseReactiveHandler {
  constructor(isShallow = false) {
    super(false, isShallow)
  }
  set(
    target: Record<string | symbol, unknown>,
    key: string | symbol,
    value: unknown,
    receiver: object,
  ): boolean {
    let oldValue = target[key]
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(
      target,
      key,
      value,
      isRef(target) ? target : receiver,
    )
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }

来先看看注释的一行,翻译过来就是如果目标是原型链上的东西,不要触发,为什么要做这样的判断呢?因为如果对象自身不存在该属性,就会获取对象原型,并调用原型的[[Get]]方法得到最终结果。比如创建两个响应式对象obj1 = reactive({})和obj2 = reactive({a: 1}),将obj2设置为obj1的原型,当设置obj1.a = 2时,如果不加判断,副作用函数就会执行两次,造成不必要的更新,两次更新是由于set拦截函数触发两次导致,只要屏蔽一次更新就可以。

当设置obj1.a的值时会执行obj1代理对象的set拦截函数

// boj1的 set拦截函数
set(target,key,value,receiver){
    // target是原始对象{}
    // receiver是代理对象obj1
}

由于obj1上不存在a属性,所以会取得obj1的原型obj2,并执行obj2代理对象的set拦截函数

// boj2的 set拦截函数
set(target,key,value,receiver){
    // target是原始对象{a:1}
    // receiver是代理对象仍是obj1
}

通过toRaw()方法获取原始值,这样就可以屏蔽由原型引起的更新了。接下来就是根据变量hadKey来判断是否是新增还是更新,更新的话则会跟老值判断是否相等,不一样则触发更新,trigger()同样在这里不做讲解,记住它是触发更新的,从桶里拿出来更新。

ref

由于Proxy的代理目标必须是非原始值,所以必须使用一个非原始值去“包裹”原始值。来看下ref源码(有删减)

class RefImpl<T = any> {
  _value: T
  private _rawValue: T
  dep: Dep = new Dep()
  public readonly [ReactiveFlags.IS_REF] = true
  public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
  constructor(value: T, isShallow: boolean) {
    this._rawValue = isShallow ? value : toRaw(value)
    this._value = isShallow ? value : toReactive(value)
    this[ReactiveFlags.IS_SHALLOW] = isShallow
  }

  get value() {
    this.dep.track()
    return this._value
  }

  set value(newValue) {
    const oldValue = this._rawValue
    const useDirectValue =
      this[ReactiveFlags.IS_SHALLOW] ||
      isShallow(newValue) ||
      isReadonly(newValue)
    newValue = useDirectValue ? newValue : toRaw(newValue)
    if (hasChanged(newValue, oldValue)) {
      this._rawValue = newValue
      this._value = useDirectValue ? newValue : toReactive(newValue)
      this.dep.trigger()
    }
  }
}

这代码看起来好理解的多,通过toReactive()方法如果是对象则调用reactive进行响应式代理,原始值直接返回。 在 Class 内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。代码逻辑已经很清楚了,就不多讲了。这里涉及到的Dep不在这里讲解,会专门出一篇文章。

第一次写文采不行,如果有错误的欢迎指出。大佬们,轻点喷...