Vue3 reactivity 葫芦里是什么药

228 阅读10分钟

本篇文章我们将阅读 Vue3 最新源码的 reactivity 部分,了解几个 vue 响应式核心 API 的实现。

  • 建议大家先阅读我贴出的源码(我添加了部分中文注释帮助大家理解),自己思考其实现原理
  • 然后看下我给出的要点,才疏学浅,难免有所疏漏,请大家多多补充

话不多说,我们马上进入正题。

Ref

ref 和 shallowRef 的实现依赖下面这个类型, 实际上他们就是返回一个 RefImpl 实例,如果本身就是 RefImpll 实例了就直接返回。 shallowRef 只是实例化的入参不同, 控制只对对象第一层实现响应式。

  • 可以看到, RefImpl 的 _value 属性其实是由 toReactive 得来的。我们先按住不表。
  • 在 getter 中调用 dep.trace() 收集 active link
  • 在 setter 中调用 dep.trigger() 触发订阅的更新
 /**
* @internal
*/
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() {
    if (__DEV__) {
      this.dep.track({
        target: this,
        type: TrackOpTypes.GET,
        key: 'value',
      })
    } else {
      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)
      if (__DEV__) {
        this.dep.trigger({
          target: this,
          type: TriggerOpTypes.SET,
          key: 'value',
          newValue,
          oldValue,
        })
      } else {
        this.dep.trigger()
      }
    }
  }
}

与 RefImpl 的区别在于他没有自己的 Dep ,而是依赖于从原本 object 上获取 dep

class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly [ReactiveFlags.IS_REF] = true
  public _value: T[K] = undefined!

  constructor(
    private readonly _object: T,
    private readonly _key: K,
    private readonly _defaultValue?: T[K],
  ) {}

  get value() {
    const val = this._object[this._key]
    return (this._value = val === undefined ? this._defaultValue! : val)
  }

  set value(newVal) {
    this._object[this._key] = newVal
  }

  get dep(): Dep | undefined {
    return getDepFromReactive(toRaw(this._object), this._key)
  }
}

Dep 类实际上是一个双向链表。有收集依赖,依次(反向)触发的功能

class Dep {
  version = 0 // 当前依赖的版本号,用于优化依赖变更检测

  activeLink?: Link = undefined // 当前 dep 与活跃 effect 的链接(依赖关系)

  subs?: Link = undefined // 订阅者链表的尾部(双向链表,指向最后一个 effect/computed)

  subsHead?: Link // 订阅者链表的头部(仅开发环境,用于 onTrigger 钩子顺序调用)

  map?: KeyToDepMap = undefined // 用于对象属性依赖清理的映射表
  key?: unknown = undefined     // 当前 dep 关联的属性 key

  sc: number = 0 // 订阅者计数器

  readonly __v_skip = true // 内部标记,跳过响应式处理

  constructor(public computed?: ComputedRefImpl | undefined) {
    // 构造函数,computed 仅用于 computed ref
    if (__DEV__) {
      this.subsHead = undefined
    }
  }

  // 依赖收集:将当前活跃的 effect/computed 加入依赖链表
  track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
    // ...见源码逻辑...
  }

  // 依赖触发:当响应式数据变更时,通知所有订阅者
  trigger(debugInfo?: DebuggerEventExtraInfo): void {
    this.version++
    globalVersion++
    this.notify(debugInfo)
  }

  // 通知所有订阅者(effect/computed),依次执行响应
  notify(debugInfo?: DebuggerEventExtraInfo): void {
    startBatch()
    try {
      // 开发环境下,先顺序调用 onTrigger 钩子
      for (let head = this.subsHead; head; head = head.nextSub) {
        // ...
      }
      // 逆序遍历订阅者链表,依次调用 notify
      for (let link = this.subs; link; link = link.prevSub) {
        if (link.sub.notify()) {
          // 如果是 computed,还要递归通知其依赖
          (link.sub as ComputedRefImpl).dep.notify()
        }
      }
    } finally {
      endBatch()
    }
  }
}

Reactive

reactive 是对象进行代理,实现响应式的过程。Ref 在拓展了原始类型的响应式之外,内部也是使用 toReactive 实现深层的响应式

  • reactive & shallowReactive 和 ref 类似,不过这里是通过传入不同的 proxy handler ,调用 createReactiveObject 实现区分
  • createReactiveObject 中跟据不同的数据类型传入不同 proxy Handler 在 handler 中拦截 get set has 等属性操作
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
}

以 reactive 类型的非 collection(set,map) handler 为例。对比 ref 的依赖收集来看

  • ref 的依赖收集 dep 实例是挂载在 RefImpl 实例上的 ( this.dep.trigger )
  • Reactive handler 的 dep 实例是通过 WeakMap (直接传入key 调用 trigger / track函数)
// 可变响应式对象的 Proxy handler,继承自 BaseReactiveHandler
class MutableReactiveHandler extends BaseReactiveHandler {
  constructor(isShallow = false) {
    super(false, isShallow) // false 表示不是只读,isShallow 是否浅层响应
  }

  // 拦截属性设置
  set(target, key, value, receiver): boolean {
    let oldValue = target[key]
    if (!this._isShallow) {
      // 非浅层模式下,处理只读和响应式解包
      const isOldValueReadonly = isReadonly(oldValue)
      if (!isShallow(value) && !isReadonly(value)) {
        oldValue = toRaw(oldValue)
        value = toRaw(value)
      }
      // 如果原值是 ref,新值不是 ref,直接赋值到 ref.value
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        if (isOldValueReadonly) {
          return false // 只读 ref 不可赋值
        } else {
          oldValue.value = value // 赋值到 ref.value
          return true
        }
      }
    } else {
      // 浅层模式下,直接赋值,不做响应式处理
    }

    // 判断属性是新增还是修改
    const hadKey = isArray(target) && isIntegerKey(key)
      ? Number(key) < target.length
      : hasOwn(target, key)
    // 通过 Reflect 设置属性
    const result = Reflect.set(
      target,
      key,
      value,
      isRef(target) ? target : receiver,
    )
    // 只在本体上触发依赖
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 新增属性,触发 ADD
 trigger (target, TriggerOpTypes . ADD , key, value)
      } else if (hasChanged(value, oldValue)) {
        // 修改属性,触发 SET
 trigger (target, TriggerOpTypes . SET , key, value, oldValue)
      }
    }
    return result
  }

  // 拦截属性删除
  deleteProperty(target, key): boolean {
    const hadKey = hasOwn(target, key)
    const oldValue = target[key]
    const result = Reflect.deleteProperty(target, key)
    if (result && hadKey) {
      // 删除成功且原本有该属性,触发 DELETE
 trigger (target, TriggerOpTypes . DELETE , key, undefined , oldValue)
    }
    return result
  }

  // 拦截 in 操作符
  has(target, key): boolean {
    const result = Reflect.has(target, key)
    if (!isSymbol(key) || !builtInSymbols.has(key)) {
      // 依赖收集 HAS 操作
 track (target, TrackOpTypes . HAS , key)
    }
    return result
  }

  // 拦截 Object.keys/Object.getOwnPropertyNames 等
  ownKeys(target): (string | symbol)[] {
    // 依赖收集 ITERATE 操作
    track(
      target,
      TrackOpTypes.ITERATE,
      isArray(target) ? 'length' : ITERATE_KEY,
    )
    return Reflect.ownKeys(target)
  }
}
  • export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap() 每一个 object 对应一个 DepMap
  • const depsMap = targetMap.get(target) DepMap 中以属性层级保存每一个 Dep 实例
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>,
): void {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    globalVersion++
    return
  }

  const run = (dep: Dep | undefined) => {
    if (dep) {
      if (__DEV__) {
        dep.trigger({
          target,
          type,
          key,
          newValue,
          oldValue,
          oldTarget,
        })
      } else {
        dep.trigger()
      }
    }
  }

  startBatch()

  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(run)
  } else {
    const targetIsArray = isArray(target)
    const isArrayIndex = targetIsArray && isIntegerKey(key)

    if (targetIsArray && key === 'length') {
      const newLength = Number(newValue)
      depsMap.forEach((dep, key) => {
        if (
          key === 'length' ||
          key === ARRAY_ITERATE_KEY ||
          (!isSymbol(key) && key >= newLength)
        ) {
          run(dep)
        }
      })
    } else {
      // schedule runs for SET | ADD | DELETE
      if (key !== void 0 || depsMap.has(void 0)) {
        run(depsMap.get(key))
      }

      // schedule ARRAY_ITERATE for any numeric key change (length is handled above)
      if (isArrayIndex) {
        run(depsMap.get(ARRAY_ITERATE_KEY))
      }

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

  endBatch()
}
  • myRef(RefImpl 实例)有一个 dep,负责 .value 的依赖。
  • myRef.value(reactive 对象)每个属性也有自己的 dep,负责各自属性的依赖。
  • 这两套 dep 互不干扰,分别管理不同层级的响应式依赖。

Computed

compoted 函数的类型声明有两个,这叫做类型重载,允许跟据传入的参数不同而使用不同的声明

  • 我们可以直接传入 getter, 此时 computed 是只读了
  • 如果我们传入的是一个有 get set 的对象,此时computed 是可写的
  • computed 其实只是返回 get set 去生成的一个 ComputedRefImpl 实例
export function computed<T>(
  getter: ComputedGetter<T>,
  debugOptions?: DebuggerOptions,
): ComputedRef<T>
export function computed<T, S = T>(
  options: WritableComputedOptions<T, S>,
  debugOptions?: DebuggerOptions,
): WritableComputedRef<T, S>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false,
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T> | undefined

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  const cRef = new ComputedRefImpl(getter, setter, isSSR)

  if (__DEV__ && debugOptions && !isSSR) {
    cRef.onTrack = debugOptions.onTrack
    cRef.onTrigger = debugOptions.onTrigger
  }

  return cRef as any
}

现在我们仔细来看下 ComputedRefImpl

  • 和 RefImpl 一样,每一个 computedRefImp 拥有一个自己的 Dep。在调用 getter 的时候通过 track 追踪当前 activeEffect 和 ref 类似。

  • Computed 如何感知依赖项的变化 ?其实这个能力依赖于 Ref / Reactive 而不是 Comuputed 本身。computed 调用了 ref / reactive 的getter ,自然就被记录到 ref 的或者 reactive 的属性的 dep 上,当他们更新, computed 就会被 ref/reactive 的 trigger 函数后续链路标记为 dirty。

    • 注意这里只是标记为 dirty,不会马上更新,而是访问他的 getter 的时候才重新计算,这其实就是 lazy evaluation
    • computed 相当于 react 的 useMemo,useCallback 可以充当缓存函数使用
  • 而 Computed 本身的 dep 记录的是调用 computed 的getter 的内容,比如组件。computed 被 ref / reactive 标记为 dirty 的同时,会调用他的 notify 函数,触发 batch 函数(传入this),batch 通过 this 后续链路相当于调用了 this.dep.trigger,触发依赖的更新。(只不过 batch 有批处理优化)

  • 调用他的 getter 的时候会执行 refreshComputed(this),每次都是从新的依赖计算结果。

export class ComputedRefImpl<T = any> implements Subscriber {
  /**
* @internal
*/
  _value: any = undefined
  /**
* @internal
*/
  readonly dep: Dep = new Dep(this)
  /**
* @internal
*/
  readonly __v_isRef = true
  // TODO isolatedDeclarations ReactiveFlags.IS_REF
  /**
* @internal
*/
  readonly __v_isReadonly: boolean
  // TODO isolatedDeclarations ReactiveFlags.IS_READONLY
  // A computed is also a subscriber that tracks other deps
  /**
* @internal
*/
  deps?: Link = undefined
  /**
* @internal
*/
  depsTail?: Link = undefined
  /**
* @internal
*/
  flags: EffectFlags = EffectFlags.DIRTY
  /**
* @internal
*/
  globalVersion: number = globalVersion - 1
  /**
* @internal
*/
  isSSR: boolean
  /**
* @internal
*/
  next?: Subscriber = undefined

  // for backwards compat
  effect: this = this
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  /**
* Dev only
* @internal
*/
  _warnRecursive?: boolean

  constructor(
    public fn: ComputedGetter<T>,
    private readonly setter: ComputedSetter<T> | undefined,
    isSSR: boolean,
  ) {
    this[ReactiveFlags.IS_READONLY] = !setter
    this.isSSR = isSSR
  }

  /**
* @internal
*/
  notify(): true | void {
    this.flags |= EffectFlags.DIRTY
    if (
      !(this.flags & EffectFlags.NOTIFIED) &&
      // avoid infinite self recursion
      activeSub !== this
    ) {
      batch(this, true)
      return true
    } else if (__DEV__) {
      // TODO warn
    }
  }

  get value(): T {
    const link = __DEV__
      ? this.dep.track({
          target: this,
          type: TrackOpTypes.GET,
          key: 'value',
        })
      : this.dep.track()
    refreshComputed(this)
    // sync version after evaluation
    if (link) {
      link.version = this.dep.version
    }
    return this._value
  }

  set value(newValue) {
    if (this.setter) {
      this.setter(newValue)
    } else if (__DEV__) {
      warn('Write operation failed: computed value is readonly')
    }
  }
}

Watch

Watch 方法的实现逻辑比较复杂,他还顺带实现了 WatchEffect 等拓展 API。为了啃下这个硬骨头,我们先来看下用法

const stop2 = watch(
  count,
  (newVal, oldVal, onCleanup) => {
    const id = setTimeout(() => {
      console.log('异步操作')
    }, 1000)
    onCleanup(() => {
      clearTimeout(id)
      console.log('定时器已清除')
    })
  }
)

// 停止 watch
stop2()
  • 传入一个响应式值或者数组作为依赖数组
const stopEffect = watchEffect((onCleanup) => {
  const id = setInterval(() => {
    console.log('定时器执行')
  }, 1000)
  onCleanup(() => {
    clearInterval(id)
    console.log('定时器已清除')
  })
})

// 停止 watchEffect,会自动执行清理函数
stopEffect()
  • 自动追踪其中的响应式内容

ok,复习好了 watch API 的用法,我们来看下他的核心实现的签名

export function watch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb?: WatchCallback | null,
  options: WatchOptions = EMPTY_OBJ,
): WatchHandle
  • 如何实现响应式 ?

    • 总结来说其实是根据传入的 source 直接计算出依赖的所有属性然后访问他们的,这个执行访问的局部函数的函数叫 getter

    • 通过 effect = new ReactiveEffect(getter) ,并执行 effect.run(),让响应式数据收集 ReactiveEffect 为依赖,让我们看下 ReactEffect 做啥。其实他就是响应式数据收集的依赖项,也就是收集的时候 activeLink

      •   class ReactiveEffect {
            constructor(fn, scheduler?) {
              this.fn = fn
              this.scheduler = scheduler
              // ...
            }
        
            run() {
              // 1. 设置当前激活副作用
              // 2. 执行 fn,收集依赖
              // 3. 恢复上一个激活副作用
            }
        
            stop() {
              // 1. 移除所有依赖
              // 2. 标记为已停止
            }
          }
        
    • 当响应式数据变更的时候,会触发 effect.shceduler, 在 watch 源码种, effect.scheduler 包含了 job 相关的逻辑

      •    effect.scheduler = scheduler
              ? () => scheduler(job, false)
              : (job as EffectScheduler)
        
    • job 的逻辑是执行 getter(被挂载在 effect.run() 上) ,比较新旧依赖值是否相同,然后有条件的执行 cb(传入的副作用函数)

总结一下触发流程👇

  1. 响应式数据 set 时,deps 里的 ReactiveEffect 被触发。
  2. 如果有 scheduler,则调用 scheduler(即 job)。
  3. job 内部会执行 getter,比较新旧值,然后执行 cb。

现在我们已经大致了解了 watch 的核心逻辑,现在来看下上面提到的 watchEffect 是如何实现的

  • 可以看到其实 watchEffect 调用了 doWatch ,doWatch 的签名和 watch 一模一样,其实就是一个收口各种 watchApi 的地方,比如 watchEffect、watchPostEffect、watchSyncEffect,还处理了一下 SSR 环境的操作(笔者没接触过 SSR 就不深入研究了)
  • 最终doWatch 中还是调用 baseWatch (也就是上面提到的 watch 函数) 实现响应式逻辑
export function watchEffect(
  effect: WatchEffect,
  options?: WatchEffectOptions,
): WatchHandle {
  return doWatch(effect, null, options)
}
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  options: WatchOptions = EMPTY_OBJ,
): WatchHandle
  • 调用 WatchEffect 的时候,其实传入的副作用函数被赋值给个 source ,source 被计算成 getter
  • getter 被挂载在 effect.run() 上,执行 effect.scheduler 的时候,先执行 effect.run() ,如果没有传入 cb,说明调用的是 WatchEffect ,如果传入了 cb,比较一下 effect.run() 返回的新值和旧的是否有变化,有变化则执行 cb

画出流程图,再梳理一遍,就清晰多了。

image.png