Vue响应式之依赖收集-vue3

3/7/2022, 12:15:02 AM

Vue响应式之依赖收集-vue3

前言

上篇写了Vue2 的依赖收集和依赖更新的大致逻辑,这篇开始看Vue3的依赖收集,Vue3写法上兼容Vue2,为了快速了解核心流程,这里只看setup写法的逻辑。

vue3这篇相较于vue2的会简略很多,一是2中已经理清了依赖收集和更新的逻辑,所以这篇只关心依赖收集相关的,篇幅会较短,二是到现在还未细看过3的源码,也会忽略一些细节。

正文

Vue3源码版本为3.2.37

setup 写法中响应式数据通常以reactive 或 ref 定义,依赖收集逻辑就以 reactive 为入口来看

export function reactive(target: object) {
  // ...
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

 

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // ...
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

 

 

直入响应式核心 - proxy,reactive方法经 createReactiveObject 处理返回一个 Proxyproxy 的 handle 为 collectionHandlers 或者 baseHandlers,前者为 Map、Set、WeakMap、WeakSet 数据结构的 handle,这里只看 baseHandlers

export const mutableHandlers = {
  get: createGetter(),
  set: createSetter(),
  deleteProperty,
  has,
  ownKeys
}

baseHandlers 中定义了五类操作时的代理

依赖收集的原理都是一样的,不管是通过 Object.defineProperty 还是 Proxy,都是在 getter 中收集依赖,在 setter 中通知更新

createGetter 用来创建 getter

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // ...
    const res = Reflect.get(target, key, receiver)
	track(target, TrackOpTypes.GET, key)
     //...
    return res
  }
}

track 是收集的核心逻辑,类似 Vue2 中一系列 Dep 操作

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
    // 记录所有的响应对象
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep()))
    }

    const eventInfo = { effect: activeEffect, target, type, key }

    trackEffects(dep, eventInfo)
  }
}

targetMap是一个 WeakMap 缓存区,记录所有的响应对象,防止被重复代理,键就是对应的响应式对象(这里使用 weakmap 的好处是它的键是个弱引用,不会一直持有 target 的引用导致内存泄漏),effect 记录着的是当前调用这个proxy 的函数,类似 Vue2 中的 Dep.target

depsMap 的作用就是 Vue2中的 dep.subs,不同的2中的数组,Vue3中是个 Set,由 createDep 创建,记录响应式对象 key 的所有依赖函数

到这里能看出来,抛开了IE兼容包袱的Vue3 运用各种es6 的标准,单从这些代码层面优化也能看到性能也会优于Vue2.

export function trackEffects(dep) {
  dep.add(activeEffect!)
  // ...
}

搞清楚 activeEffect 如何更改指向,何时更改,就完成了整个依赖的收集过程

export class ReactiveEffect<T = any> {
  constructor(fn, cheduler, scope) {
    this.fn = fn
    // ..
    recordEffectScope(this, scope)
  }

  run() {
  	activeEffect = this
    return this.fn()
    // ...
  }
}

activeEffect 指向由 ReactiveEffect 实例 run 方法修改。ReactiveEffect 的作用和Vue2 中的 Watcher 作用类似,最终还是在 mount阶段通过 mountComponent 实例化

  const mountComponent = (
    initialVNode,
    ...
  ) => {
    // ...
    setupRenderEffect(
      instance,
      ...
    )
  }
  
  
  const setupRenderEffect = (
    instance,
    ...
  ) => {
    // 
    // ...
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(update),
      instance.scope // track it in component's effect scope
    ))

    const update = (instance.update = () => effect.run())
    // ...
    update()
  }

componentUpdateFn 就是组件的更新函数,会创建 vnode,并进行patch 最终渲染dom

以上就是整个 get 过程,响应式数据定义和代理脱离组件,通过暴露 ReactiveEffect.run 允许修改 activeEffect 指向,组件 mount 阶段实例化 effect,将响应式数据与组件关联,并将 instance.update 指向 effect.run

依赖更新的核心逻辑和2中是一样的,代理 setter ,遍历 dep 调用 effect.run

function createSetter(shallow = false) {
  return function set(target, key, value,receiver) {
    // ...
    const result = Reflect.set(target, key, value, receiver)
    // ...
    trigger(target, TriggerOpTypes.ADD, key, value)
    return result
  }
}


export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  let deps = []
  deps = [...depsMap.values()]
  const eventInfo =  target, type, key, newValue, oldValue, oldTarget }
  const effects  []
  for (const dep of deps) {
     effects.push(...dep)
  }
  triggerEffects(createDep(effects), eventInfo)
  }
}
export function triggerEffects(dep) {
  for (const effect of effects) {
    // ...
    effect.run()
  }
}