深入理解 Vue3 的响应式

404 阅读6分钟

深入理解 Vue3 的响应式

0 前言

随着 Vue3 的登台,各大博客论坛铺天盖地的涌来各种文章。我们组也是率先把 Vue3 应用到了工作中,刮起了一波学习热潮。上周组内一位大佬的分享也让我受益良多,同时也打开了我的好奇心,那么就抱着问到底的精神来看看让我疑惑的问题。

1 ref 和 reactive 的关系

Vue3 的文档中主要提供了两种比较常用的方法来把你的数据转换成响应式,refreactive。这两种方法的区别就在于 refreactive 的基础上对数据进行了二次分装,需要以 .value的形式获取值,可以从源码中看到:

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, public 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)
    }
  }
}
const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val

重点在于 convert 方法,能看出最后还是会通过 reactive 方法来转换成响应式。也知道了因为 Proxy 不能监听简单数据类型所以 Vue 在基础数据类型上又加了一层对象来实现响应式。

2. reactive 做了什么

既然 ref 只是 reactive的一层伪装,那我们就直接看看 reactive 做了什么吧:

export const reactiveMap = new WeakMap<Target, any>()
export const readonlyMap = new WeakMap<Target, any>()

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
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
  )
}


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
}

Vue 首先判断了一下如果 target 是只读对象那么直接返回。

然后就进入了 createReactiveObject 方法,代码很清晰,先判断了是否已经绑定过,没有就绑定。最后,通过判断目标的类型,如果是普通的对象 Object 或 Array,处理器对象就使用 baseHandlers,如果是 Set, Map, WeakMap, WeakSet 中的一个,就使用 collectionHandlers

这里有一个小知识点,为什么 Vue 存储使用的是 WeakMap ?WeakMap 的键值必须是对象,而且 WeakMap 的键值是不可枚举的,是弱引用。方便垃圾回收。

弱引用对应的当然就是强引用了,强引用有一种计数机制。举个例子当一个对象被创建时,计数为1,每有一个变量引用该对象,计数都会加1,只有当计数为0时,对象才会被垃圾回收。所以一旦造成循环引用等问题,就会造成内存泄漏。而 WeakMap 当它的键所指对象没有被其他地方引用时,就会被垃圾回收了。

3. baseHandlers

baseHandlers 针对不同类型的响应式做了不同的处理,我们重点看一下完全响应式是如何处理的:

export const mutableHandlers: ProxyHandler<object> = {
 get,
 set,
 deleteProperty,
 has,
 ownKeys
}

从 Vue2 中我们知道,如果 Vue 想要在数据更新时通知界面渲染那肯定要收集依赖,而在数据被修改后触发依赖,那么我们就知道:get、has、ownKeys 是收集依赖,set、deleteProperty 是触发依赖。

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)

    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      return res
    }

    if (!isReadonly) {
      // 只读不需要收集
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      // 浅层响应,直接返回
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

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

代码并不难理解,加了点注释。其中 arrayInstrumentations 是用来收集数组的依赖的,我们来看看具体怎么做的:

const arrayInstrumentations: Record<string, Function> = {}
// instrument identity-sensitive Array methods to account for possible reactive
// values
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
  const method = Array.prototype[key] as any
  arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
    const arr = toRaw(this)
    for (let i = 0, l = this.length; i < l; i++) {
      track(arr, TrackOpTypes.GET, i + '')
    }
    // we run the method using the original args first (which may be reactive)
    const res = method.apply(arr, args)
    if (res === -1 || res === false) {
      // if that didn't work, run it again using raw values.
      return method.apply(arr, args.map(toRaw))
    } else {
      return res
    }
  }
})

在这里 Vue 通过封装了数组方法来达到收集依赖的目的。

再回到之前的代码,代码中用到了 Reflect.get。Reflect 是一个内置对象,它与 Proxy 几乎共用方法。那为什么 Vue 要多此一举用 Reflect 呢? 我们来看个例子:

const data = new Proxy([1,2,3], {
    get(target, key) {
        console.log('get', key)
        return target[key];
    },
    set(target, key, value) {
        console.log('set', key, value)
        return true;
    }
})
data.push(4)

这是一个普通写法,set 方法为什么要返回一个 true,如果不返回 true 则会抛出一个 typeError。看上去是不是不太优雅,那么我们用 Reflect 改造一下。

const data = new Proxy([1, 2, 3], {
  get(target, key, receiver) {
    console.log('get', key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log('set', key, value);
    return Reflect.set(target, key, value, receiver);
  },
});
data.push(4);

改造完后,我们来看看打印结果,打印出来是这样的:

get push
get length
set 3 4
set length 4

这样达到的效果是一样的,不过还是有点问题。你会发现打印了两次 get 和 set,这是因为在 push 后,不仅改变了数组内的元素,同时操作了数组的 length 产生了两次 get 和 set,那么 Vue 是如何解决这种重复问题的?

export const hasOwn = (
  val: object,
  key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)
...
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
      	// key 不存在说明是新增操作,触发依赖
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
      	// 新旧值不相等,触发依赖
        // 同时也防止了刚才提到的 push 后触发了两次依赖的问题,新旧值相同则不触发依赖
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

Vue 通过判断 key 是否为 target 自身属性,以及设置 val 是否跟 target[key] 相等来阻止重复的响应。

继续看 set 和 get 方法中,用到了 trigger 和 track 方法。看一下源码中他们都来自于一个叫 effect 的文件,那么 effect 是做什么用的?

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

其中创建 effect 的其实是 createReactiveEffect 方法,我们来看看这个方法:

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      // 清除依赖
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        // 
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true	// 判断当前 effect 是否活跃
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

所以真正的依赖函数是 activeEffect,那我们继续来看看 trigger 和 track 做了什么

export function track(target: object, type: TrackOpTypes, key: unknown) {
  // activeEffect 为空代表没有依赖,直接返回
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // targetMap 是一个 WeakMap,用于收集依赖
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  // 没有收集依赖的话直接返回
  if (!depsMap) {
    // never been tracked
    return
  }

  // 依赖队列,这里的依赖队列是 Set 类型避免重复
  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
  	// 这里就能监听到数组的 length 变化了
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          add(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  // 触发依赖
  effects.forEach(run)
}

其实已经有 Vue2 内味儿了,其实就是一个改良版的发布订阅模式。get 时通过 track 收集依赖,而 set 时通过 trigger 触发了依赖,而 effect 收集了这些依赖并进行追踪,在响应后去触发相应的依赖。effect 也正是 Vue3 响应式的核心。

了解了这些后,那么剩下的三个方法就很明朗了,一看就能明白:

function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}

function ownKeys(target: object): (string | number | symbol)[] {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

那么 Vue3 的响应式相关的比较核心的源码基本就是这些了,至于 collectionHandlers 方法,基本是换汤不换药,核心也是通过 trigger 和 track 实现,有兴趣的大佬可以自己看看。

4 参考