Vue3.2x源码解析(三):深入响应式原理

349 阅读25分钟

系列文章:

本节将深入理解vue3的响应式系统构成。

我们知道vue2的响应式数据依赖于原生方法Ojbect.defineProperty,因为这个方法在追踪集合数据类型变化有缺陷,所以vue3替换为了ES6的proxy方法,这也是本节讨论的重点。

vue2的响应式系统由三大核心模块构成:ObserverDepWatcher

vue3的响应式系统同样是由三个核心模块构成:DataDepEffect

vue3中watcher变成了effect,虽然effect的实现及定义都发生了变化,但是它们的作用是类似的:都是为了执行副作用函数钩子。在阅读源码的时候,你可以以vue2的watcher进行参考对比,方便理解。

1,Data

data即响应式数据,vue3的响应式API比较多,但ref/reactive是其核心方法,其他API基本都以这两个为基础派生而来。所以我们在分析vue3如何定义响应式数据时,主要就是分析这两个API的源码。

一,ref
// packages/reactivity/src/ref.ts
# 定义响应式数据API
function ref(value?: unknown) {
  return createRef(value, false)
}

继续查看createRef源码:

# 创建ref的函数
function createRef(rawValue: unknown, shallow: boolean) {
  // 如果传入的value已经是ref数据,直接返回
  if (isRef(rawValue)) {
    return rawValue
  }
  // 新建一个ref实例 并返回
  return new RefImpl(rawValue, shallow)
}

继续查看RefImpl源码:

# Ref类
/**
 * value:可以为基本类型,也可以为集合类型
 * 1,如果为基本类型,则将value直接赋值给_value属性
 * 2,如果为集合类型,则通过toReactive转换为响应式数据后,再赋值给_value属性
 */
class RefImpl<T> {
  // 存储当前值
  private _value: T 
  private _rawValue: T
  # dep属性默认为undefined,会在get的依赖收集时初始化为dep实例即依赖容器,存储effect元素
  public dep?: Dep = undefined
  // 是否是ref类型数据
  public readonly __v_isRef = true
  // public readonly __v_isShallow: boolean 属性值为第二个参数
​
  constructor(value: T, public readonly __v_isShallow: boolean) {
    // 初始化两个私有属性值
    // 原value值
    this._rawValue = __v_isShallow ? value : toRaw(value)
    // 新value值: 不是浅层的就转化为深层响应式数据
    this._value = __v_isShallow ? value : toReactive(value)
  }
​
  # ref类的核心是:定义了一个访问器value属性,获取value返回的是this._value
  get value() {
    // 收集依赖
    trackRefValue(this)
    return this._value
  }
​
  set value(newVal) {
    // 判断是否为浅层/只读
    const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    # 校验值是否变化
    if (hasChanged(newVal, this._rawValue)) {
      // 再次对两个私有属性值进行更新;
      this._rawValue = newVal
      # 重点:如果是浅层/只读的:就不会进行深层响应式创建 【数据类型的判断在toReactive方法里面】
      this._value = useDirectValue ? newVal : toReactive(newVal)
      // 触发依赖(副作用函数)
      triggerRefValue(this, newVal)
    }
  }
}

根据前面的源码可以看出,ref的原理其实比较简单,一句话理解就是通过ref函数创建并返回了一个ref实例对象,同时ref实例内部定义了几个重要的属性,通过这些属性,我们可以创建出不同需求的响应式数据。

当然其中最核心还是定义了一个访问器属性value,我们对ref数据的变化操作都是通过value属性来进行实现,同时我们也可以发现ref数据是在getter中收集依赖,在setter中触发依赖,这和vue2的思想是完全一致。

其实在这里我们也是可以明白一个观点:vue3的源码虽然更多更抽象了,但其很多功能的使用方法和实现思路还是和原来一致的,这也是框架升级的一个重要特点,避免用户在使用新版本框架时产生较大的割裂感。

在对ref数据设置新值的时候,会对新值进行类型校验:如果是原始类型就直接赋值,如果是集合类型就会通过reactive方法创建proxy响应式代理对象并返回,这个逻辑都是在toReactive函数内部处理的:

// packages/reactivity/src/reactive.ts
// 如果使用ref创建响应式时:传入了对象类型参数,内部就会使用reactive方法生成proxy响应式代理对象
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

综上所述: 使用ref创建响应式数据时,如果传入原始类型数据,则直接赋值即可。如果传入的是引用类型数据,则需要通过reactive方法创建proxy代理对象再赋值,下面我们就开始分析针对对象类型的响应式数据创建。

二,reactive
// packages/reactivity/src/reactive.ts
# 定义响应式数据API
function reactive(target: object) {
  // 如果是只读数据,直接return
  if (isReadonly(target)) {
    return target
  }
  // 创建响应式数据对象
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

继续查看createReactiveObject源码:

# 创建响应式对象方法
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 如果target不是对象,直接返回目标,不做任何操作
  if (!isObject(target)) {
    return target
  }
  # 如果目标已经是proxy代理对象,直接返回
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
​
  // 从proxyMap中查询目标target
  # 如果目标已经存在对应的:proxy代理对象,直接返回对应的proxy对象【拒绝重复新建】
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 获取传入target的类型【基本类型视为无效类型】
  const targetType = getTargetType(target)
  // 如果目标为无效类型,不会进行响应式转换,直接返回
  if (targetType === TargetType.INVALID) {
    return target
  }
  # 新建proxy代理对象
  const proxy = new Proxy(
    target,
    // 判断传入的target是不是map,set集合类型,传入的不同的handlers处理对象
    // collectionHandlers:map/set ;  baseHandlers: obj/list
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  # 在map结构里,添加一对键值,存储目标与代理【作用是防止重复新建】
  proxyMap.set(target, proxy)
  // 返回代理对象,即响应式数据
  return proxy
}

根据上面的源码可以看出,reactive方法只能对对象类型进行代理转换,如果传入原始类型数据,不会有任何操作。同时在创建proxy代理对象的之前,还做了一系列的优化校验,比如proxyMap变量是一个map结构,存储的键值都是对应的target与proxy,每次新建后都会存入一对键值。而在新建之前可以在这个map结构中进行查询是否存在已有的proxy,如果存在就直接返回不再新建,避免重复创建,优化框架的性能。

去掉这些校验:整个方法就是创建了一个proxy代理对象并返回,创建代理很简单,复杂的是拦截对象的设置。

# 重点:baseHandlers
const proxy = new Proxy(target, baseHandlers)

注意:我们这里只展开分析常规的obj/arr类型的baseHandlers,针对map/set集合类型的有兴趣可以去阅读源码。

baseHandlers是处理程序对象,是创建proxy的核心,它可以拦截针对target的所有基本语义操作,它是vue3对象类型实现响应式功能的核心,下面我们就继续分析baseHandlers源码:

/**
 *  Vue3的响应式核心原理:
 *  利用proxy与Reflect的API,在get/has/ownKeys中收集依赖,在set/delete中触发依赖
 */
// packages/reactivity/src/baseHandlers.ts
# 处理程序对象handlers【针对普通对象】
const mutableHandlers: ProxyHandler<object> = {
  // 五个捕获器obj/arr
  get, // createGetter
  set, // createSetter
  deleteProperty,
  // 通过has拦截函数实现对 in 操作符的代理:
  has,
  // 通过ownKeys拦截函数代理for in循环
  // 使⽤for...in 循环遍历数组与遍历常规对象并⽆差异,因此同样可以使⽤ownKeys拦截数组
  ownKeys
}

注意:我们这里只分析常规的mutableHandlers对象,它是基础也是核心,其他的只读响应式,浅层响应式更简单。

mutableHandlers是作用于常规对象和数组的处理程序对象,它里面有五个捕获器,这五个捕获器拦截了针对目标对象的所有增删改查操作。我们要理解vue3的响应式实现,就得理解每个捕获器的内部实现原理。

(一)get

const get = /*#__PURE__*/ createGetter()
// get:默认是一个getter
function createGetter(isReadonly = false, shallow = false) {
  # 在获取属性值的操作中被调用
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 如果get获取的是proxy对象的几个内置属性:直接返回对应值
    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
    }
​
    # 下面开始常规的key逻辑:
    
    // 判断目标是不是数组类型
    const targetIsArray = isArray(target)
​
    if (!isReadonly) {
      # 拦截数组的部分原生API,重写
      // 如果是数组,并且key为数组的操作API,则进行拦截,重写对应的操作方法,并且加入依赖收集
      // ['includes', 'indexOf', 'lastIndexOf'] ['push', 'pop', 'shift', 'unshift', 'splice']
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
        // 返回数组API读取的结果res
        return Reflect.get(arrayInstrumentations, key, receiver)
      }
      if (key === 'hasOwnProperty') {
        return hasOwnProperty
      }
    }
​
    // 常规obj/arr的key读取:
    # 核心原理API就是Reflect.get
    const res = Reflect.get(target, key, receiver)
​
    // 添加判断:如果key的类型是symbol,???
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }
​
    if (!isReadonly) {
      # 收集依赖
      track(target, TrackOpTypes.GET, key)
    }
​
    // 浅层响应式:直接返回res
    if (shallow) {
      return res
    }
​
    # 如果读取的结果是一个ref类型数据
    if (isRef(res)) {
      // ref unwrapping - skip unwrap for Array + integer key.
      # 如果是数组 且 Key为整数即索引:直接返回res   否则返回ref.value
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }
​
    # 重点:这里key读取的最常见的两种结果就是obj和原始值;
    // 深层响应式:如果读取的结果是一个obj,我们需要用reactive把res包装为响应式后再返回
    # 每一层的访问结果 都会被reactive包装后返回
    if (isObject(res)) {
      // 深只读readonly
      return isReadonly ? readonly(res) : reactive(res)
    }
​
    // 读取的结果是一个原始值,直接返回
    return res
  }
}

get捕获器对应的反射API方法为Reflect.get()。在get捕获器中有比较多的逻辑校验,首先就是对数组的校验,如果是数组类型,并且是数组原生API的操作读取,就进行拦截,重写为新定义的方法,并且在方法内部加入依赖收集。然后就是对常规obj/arr的key读取,使用了Reflect.get这个方法,这个方法是get捕获器的核心,读取目标对象的数据。

mutableHandlers处理程序对象的五个捕获器,每个捕获器内部都依赖于Reflect对象的对应API方法,这些方法与捕获器相互配合实现了Vue3的响应式核心。

最后要重点注意一下对读取结果的处理,如果是原始类型的数据直接返回,如果是对象类型的数据则需要使用reactive方法包装为响应式数据后再返回。

(二)set

const set = /*#__PURE__*/ createSetter()
function createSetter(shallow = false) {
  # 在设置属性值的操作中被调用
  return function set(target: object,key: string | symbol,value: unknown,receiver: object): boolean {

    # 保存旧值
    let oldValue = (target as any)[key]
    // 如果是只读的且 旧值为ref数据,禁止设置,直接return
    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)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }
	
      # 针对key的存在进行判断
    // isIntegerKey:是否为整数key,即数组的索引
    // const hadKey = Number(key) < target.length 结果为布尔值
    // const hadKey = hasOwn(target, key) 结果为布尔值
    const hadKey =(isArray(target) && isIntegerKey(key)) ?
        # 数组情况,key小于length说明在数组中存在
        Number(key) < target.length 
    	# 对象情况:是否存在属性key
    	: hasOwn(target, key)
    
    # 常规key的value设置
    // 核心原理API就是Reflect.set
    const result = Reflect.set(target, key, value, receiver)

    // toRaw(receiver) 判断receiver必须是target的代理对象才触发更新,如果是原型链上的则不触发更新,避免性能浪费
    if (target === toRaw(receiver)) {
      # 新增和修改:都要触发依赖(即:dep依赖容器中收集的effect实例)
      // 区分ADD和SET,执行不同的触发逻辑
      if (!hadKey) {
        // 新增
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 修改
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    // 返回 true 表示设置成功;返回 false 表示失败
    return result
  }
}

set捕获器对应的反射API方法为Reflect.set(),在set捕获器中,首先校验了只读数据和ref数据,如果是这两种情况,则禁止设置。然后需要判断key有没有存在于目标对象之中。如果存在则为修改操作,如果不存在则为新增操作,区分不同的操作逻辑,需要执行不同的依赖触发逻辑。

(三)deleteProperty

# delete 操作符的代理
// 删除一个属性key
function deleteProperty(target: object, key: string | symbol): boolean {
  # 校验target是否存在属性key
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  // 核心原理API:Reflect.deleteProperty
  // 删除成功返回true
  const result = Reflect.deleteProperty(target, key)
  # 只有当被删除的属性是对象⾃⼰的属性并且成功删除时,才触发更新
  if (result && hadKey) {
    // 触发依赖
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

deleteProperty捕获器对应的反射 API 方法为Reflect.defineProperty(),首先校验目标对象是否存在属性key,不存在则会删除失败,返回false。只有存在属性key且删除成功的情况下,才会触发依赖。

(四)has

# in 操作符的代理
// 判断obj中有没有属性key
function has(target: object, key: string | symbol): boolean {
  // 核心原理API:Reflect.has
  const result = Reflect.has(target, key)
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    # 收集依赖
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}

has对应的反射 API 方法为 Reflect.has()

(五)ownKeys

# for in循环,Object.keys()的代理
function ownKeys(target: object): (string | symbol)[] {
  // 如果target是数组,则使⽤length属性作为 key 并建⽴响应联系
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

ownKeys对应的反射 API 方法为 Reflect.ownKeys()

综上所述: 这五个捕获器就是mutableHandlers处理程序对象的全部内容,通过这五个捕获器,reactive定义的响应式数据对象:可以拦截目标对象target的所有增删改查操作,并在相应的操作中收集依赖与触发依赖,以此为基础实现了vue3的响应式功能核心。

2,Dep

首先我们先分析dep的定义和创建:

# 定义了dep的类型
// &:交叉类型:将多个类型合并成一个类型,该类型具有所有类型的特性(取他们类型的合集)
// 相当于and符号
type Dep = Set<ReactiveEffect> & TrackedMarkers

ReactiveEffectTrackedMarkers的源码这里省略。

# 创建dep实例:依赖容器
// vue2收集的是watcher实例, vue3收集的是各种effect实例
// ReactiveEffect[]和string[]一样:代表effect数组:意思传入的是一个数组,数组的每个元素都是effect实例
const createDep = (effects?: ReactiveEffect[]): Dep => {
  // effects ?参数是可选的
  // 当没有参数时:这里的dep = Set(0) { w: 0, n: 0 },代表有没有元素,只有两个属性
  // 当有参数时:假如这里的dep = Set(3) {{…}, {…}, {…}  w:0, n:0},代表有三个元素,同时也有w,n属性
  const dep = new Set<ReactiveEffect>(effects) as Dep
  // set也是对象,可以给他定义属性
  dep.w = 0
  dep.n = 0
  # dep实例是一个set数据结构
  return dep
}

dep的源码非常少也比较简单,根据上面的源码我们可以看出,dep就是一个set集合结构,里面存储的是effect实例,同时定义了w,n两个属性。dep的作用依然和原来一样:专门用于收集依赖【effect实例】的容器

下面我们再去分析使用dep的地方,是如何收集依赖的。

3,Effect

effect在vue3称为副作用,其实它的作用和vue2的watcher是类似的,在阅读源码的时候可以参考对比。在vue3里面,dep就是收集的effect实例,最终的目的就是:在响应式数据变化的时候,执行effect实例中的回调函数。

下面我们分别来分析ref/reactive响应式数据是如何收集和触发依赖的:

一,ref

我们回到之前ref是如何收集和触发依赖的:

class RefImpl<T> {
  get value() {
    // 收集依赖
    trackRefValue(this)
  }
  set value() {
    // 触发依赖
    triggerRefValue(this, newVal)
  }
}

(一)收集依赖

在查看trackRefValue源码之前,有两个全局变量我们需要理解一下:

# 默认需要收集
export let shouldTrack = true

# 创建一个全局的activeEffect,存储当前创建/运行的组件renderEffect实例
export let activeEffect: ReactiveEffect | undefined

继续查看trackRefValue源码:

// 追踪ref值
function trackRefValue(ref: RefBase<any>) {
  # activeEffect为undefined时无法执行,我们需要去查看activeEffect的赋值地方
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
	...
    # createDep() 创建了一个dep实例,用于收集依赖
    // 刚开始ref.dep=undefined; 第一次收集依赖,初始化ref.dep为set集合对象
    // dep = Set(0) { w: 0, n: 0 },set0代表它没有元素,只有两个属性
    trackEffects(ref.dep || (ref.dep = createDep()))
  }
}

注意:文中的源码都会省略部分DEV环境下的代码。

很明显shouldTrack默认允许追踪的,但是activeEffect默认undefined,我们需要去查看它在什么情况下才会被赋值,只有在同时满足两个条件的情况下,trackEffects才会执行收集逻辑。

同时我们可以看见activeEffect被定义为ReactiveEffect类型,我们需要去查看ReactiveEffect源码:

/**
 * effect类
 * 相当于vue2的watcher
 */
# 重点:effect类
class ReactiveEffect<T = any> {
  active = true
  // 存储dep实例: [dep, dep, dep] dep实例:Set(1) {ReactiveEffect n:0 w:0 size: 1}
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined
  computed?: ComputedRefImpl<T>
  // 允许递归
  allowRecurse?: boolean
  // 延迟暂停
  private deferStop?: boolean
​
  onStop?: () => void
  // dev only 省略
​
  constructor(
    # 回调函数Fn:和watcher实例中的fn一样,最终都是为了执行这个回调函数
    public fn: () => T,
    # 调度程序scheduler:非常重要,调度函数决定了如何执行fn
    public scheduler: EffectScheduler | null = null,
    // 作用域
    scope?: EffectScope
  ) {
    // 记录effect作用域
    recordEffectScope(this, scope)
  }
​
  # 执行回调函数fn:返回值为fn的执行结果
  run() {
    if (!this.active) {
      return this.fn()
    }
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    try {
      this.parent = activeEffect
      # 设置activeEffect为 当前effect实例
      activeEffect = this
      shouldTrack = true
​
      trackOpBit = 1 << ++effectTrackDepth
​
      if (effectTrackDepth <= maxMarkerBits) {
        initDepMarkers(this)
      } else {
        cleanupEffect(this)
      }
      return this.fn()
    } finally {
      if (effectTrackDepth <= maxMarkerBits) {
        finalizeDepMarkers(this)
      }
​
      trackOpBit = 1 << --effectTrackDepth
​
      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      this.parent = undefined
​
      if (this.deferStop) {
        this.stop()
      }
    }
  }
​
  stop() {
    // stopped while running itself - defer the cleanup
    if (activeEffect === this) {
      this.deferStop = true
    } else if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

ReactiveEffect类和Vue2的watcher类非常类似:ReactiveEffect类生成effect实例,effect实例收集dep实例,dep实例又收集effect实例。

同时我们发现activeEffect是在run函数中被赋值,只有run函数被调用了,activeEffect才会被赋值,ref数据的trackEffects才会执行收集逻辑。所以我们必须要理解activeEffect被赋值的这个逻辑过程。

解析之前,我们回顾上一节的组件初始化过程: 组件在创建renderEffect实例之前,会先调用setup函数完成组件初始化。而通过上一节我们知道Vue3的响应式数据都是在setup调用过程中初始化的,包括计算属性以及watch。所以在组件的renderEffect创建之前,组件内的ref,reactive,computed,watch就已经完成了初始化。

一,首先ref/reactive一定是最开始初始化的,因为computed和watch一般都需要依赖于响应式数据,所以我们的computed和watch一般都会定义在响应式数据之后,所以ref/reactive在初始化创建完成之时,是没有收集任何依赖的,因为他们没有还没有被引用,就不会触发get拦截处理。

重点注意: 在vue3中:只有三种类型的依赖即effect实例:computedEffect,watchEffect和renderEffect。所以ref/reactive响应式数据只有在被computed,watch和template模板中被引用了,才能够收集到对应的依赖。

二,在computed和watch初始化过程中,如果引用了ref/reactive数据,那么这时候ref/reactive就能够收集到对应的computedEffect和watchEffect实例,我们分开来讲解:

  • computed:我们定义了一个computed计算属性,getter内部引用了一个ref数据。然后在computed实例初始化时,内部会创建computedEffect,同时将这个effect实例存储到了computed自身的effect属性上。计算属性虽然被创建完成了,但是还没有被访问,所以就没有执行一次getter,也就不会触发ref数据的get,也就不能收集到computedEffect。而计算属性被访问有两种情况:1.在后续的函数调用中被访问【也包括watch的回调函数调用,watch的回调函数中可以使用计算属性值】,2.在组件渲染时的模板中访问。假如以前面的情况为例,这时候计算属性被访问了,就会执行一次effect.run(),在run函数中就会将当前的computedEffect实例赋值给activeEffect。这时候activeEffect就有值了,然后run函数中还会调用fn回调函数,在这里就是getter。所以也就会触发ref的get拦截操作,ref数据收集依赖的两个变量条件都已经满足了,就能够继续触发后续的收集逻辑。【也就是收集这个computedEffect实例】
# 案例
const str = ref({name: 1});
const countTxt = computed(()=> {
	return str.value.name + 'ms'
})
function fn() {
	console.log(countTxt.value)
}
// 调用fn
// 1, 首先触发countTxt实例的get,countTxt收集自身computedEffect
// 2, computed调用getter,就会触发str的get,str就能够收集到computedEffect
fn()
  • watch:我们定义一个watch,监听一个ref数据。在watch初始化过程中,内部也会创建一个watchEffect实例,同时在最后的初始化过程中会执行一个initial run,在这里会执行effect.run()effect的run方法每次调用都会将自身实例赋值给activeEffect】,将watchEffect赋值给activeEffect,这时候的activeEffect就已经是我们需要的effect依赖了,然后执行Fn即getter就会触发ref的get,所以这时候ref数据就可以收集到正确的依赖了。
# 案例
const str = ref({name: 1});
const countTxt = computed(()=> {
	return str.value.name + 'ms'
})
function fns() {
	console.log(countTxt.value)
}
fns()
// 初始化最后,执行getter 即()=>str.value.name,就会触发ref数据的get,即可收集到watchEffect依赖
watch(()=>str.value.name, (val)=> {
	console.log(val)
})
// 【dep是一个set结构,可以对内容自动去重,不会存在重复收集】

注意: computed实例和ref实例一样,也是响应式数据,也可以收集相关的effect依赖实例。同理如果watch或者watchEffect中引用了computed变量,那么这个computed变量也可以收集到watchEffect依赖实例。

下面我们继续收集逻辑,继续查看trackEffects源码:

function trackEffects(dep: Dep) {
  # 默认不收集
  let shouldTrack = false
  // 当:当前递归追踪的层级小于JS引擎最大递归追踪层级时,可以继续追踪
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit // set newly tracked
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    // 清理所有追踪
    shouldTrack = !dep.has(activeEffect!)
  }
​
  # 可以收集的情况下:
  if (shouldTrack) {
    // 默认的dep = Set(0) { w: 0, n: 0 },dep是一个set集合实例,这里用作收集依赖
    # dep收集effect依赖实例
    dep.add(activeEffect!)
    # 同时,effect实例又将dep添加到自己的deps属性中【和vue2一样,互相收集】
    activeEffect!.deps.push(dep)
  }
}

根据上面的代码我们可以看出,除去前面的边界情况代码,trackEffects函数的作用就是向ref.dep属性添加当前的effect实例,同时又把自身dep实例添加到activeEffect.deps属性中,这和vue2的思想是一致的。

(二)触发依赖

// 触发ref的依赖
function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref)
  if (ref.dep) {
    // 触发依赖
    triggerEffects(ref.dep)
  }
}

继续查看triggerEffects源码:

function triggerEffects(dep: Dep | ReactiveEffect[]) {
  # 如果是数组直接使用,如果不是:则为set实例,扩展set中的元素到数组
  // [...dep]=[...Set(3)]:是set集合中的元素,展开为数组[effect, effect, effect]
  const effects = isArray(dep) ? dep : [...dep]
  // 循环依赖列表:先触发计算属性的依赖,是为了更新计算属性的值,从而让后续的依赖如果引用了计算属性能够获取到最新的值
  for (const effect of effects) {
    # 触发为计算属性的依赖effect
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    # 触发其他依赖effect
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

根据上面的源码可以看出:triggerEffects首先做了一个数据的格式处理,然后就是循环触发不同类型的effect实例,而triggerEffect函数就是具体执行effect实例的方法。

继续查看triggerEffect源码:

# 具体执行effect
function triggerEffect(effect: ReactiveEffect) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (effect.scheduler) {
      # 存在调度程序的情况下;优先执行effect的调度任务,添加job到异步队列
      // vue3的更新主要都是走调度程序
      effect.scheduler()
    } else {
      // 或者直接run方法,调用fn
      effect.run()
    }
  }
}

到这里我们也可以发现,触发依赖的最终目标就是执行effect实例的调度任务scheduler或者回调函数fn

具体的调度任务和回调函数执行我们这里暂时不展开,后面异步更新队列会详细讲解。这里主要是分析ref数据的依赖收集与触发。

二,reactive

这节我们继续解析reactive响应式数据的收集依赖触发依赖逻辑:

在分析之前,我们先看两个类型定义:

// 收集类型:查询
export const enum TrackOpTypes {
  GET = 'get',
  HAS = 'has',
  // 迭代
  ITERATE = 'iterate'
}
​
// 触发类型:增删改
export const enum TriggerOpTypes {
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear'
}

在这里我们可以发现,reactive响应式数据收集依赖有三种类型 【GET,HAS,ITERATE】 ,触发依赖有四种类型 【SET, ADD, DELETE, CLEAR】 。不同的操作类型对应的不同的依赖操作逻辑,这也是体现了reactive创建的响应式数据:即proxy代理对象能够对目标对象实现完全数据拦截的强大功能,这也是Vue3的响应式核心采用ES6 Proxy的原因。

注意:ref数据只有get/set,因为ref响应式数据本质就只是对ref.value属性的操作。而一旦使用ref创建的响应式数据传入了集合类型,底层就会被reactive托管,所以说reactive是vue3响应式功能的底层核心。

(一)收集依赖

get {
  track(target, TrackOpTypes.GET, key)
}
has {
  track(target, TrackOpTypes.HAS, key)
}
# 追踪收集
function track(target: object, type: TrackOpTypes, key: unknown) {
  // 这两个判断条件还是和之前一样
  if (shouldTrack && activeEffect) {
    # 从targetMap结构中 查询target对应的值
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      // 如果depsMap不存在,则添加一个新的键值对
      targetMap.set(target, (depsMap = new Map()))
    }
    # depsMap存在:则从中取出key对应的dep实例
    let dep = depsMap.get(key)
    if (!dep) {
      // 如果dep实例不存在,则向depsMap添加一个新的键值对
      depsMap.set(key, (dep = createDep()))
    }
    # dep实例存在,收集依赖
    trackEffects(dep, eventInfo)
  }
}

这里我们要先看一个targetMap的定义:

# type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

targetMap是一个weakMap结构,它的键为target目标对象,它的值为KeyToDepMap。同时KeyToDepMap自身也是一个map结构,它的键为属性key,它的值为dep实例

理解了这些我们才可以继续向下分析:

在收集依赖时,需要先从targetMap里面查询target对应的值depsMap。

如果depsMap不存在,就向targetMap里面添加一个新的键值对:

# 键为 target  值为depsMap:一个空的map
targetMap.set(target, (depsMap = new Map()))

如果depsMap存在,则从depsMap结构中取出属性Key对应的dep实例:

# 如果depsMap存在:则从中取出key对应的dep实例
let dep = depsMap.get(key)

继续向下解析:

如果dep实例不存在,则向depsMap结构中添加一个新的键值对:

# 初始化一个dep实例
depsMap.set(key, (dep = createDep()))

如果dep实例存在,则开始执行依赖收集:

trackEffects(dep, eventInfo)

最后执行trackEffects方法,这个方法在ref里面已经解析,这里就不在重复。

注意: get / has / iterate三个收集依赖逻辑是一样的,区分主要是为了方便DEV环境调试,而触发依赖的逻辑是不同的,需要严格区分触发类型。

(二)触发依赖

set {
  trigger(target, TriggerOpTypes.SET, key, value, oldValue)
},
add {
  trigger(target, TriggerOpTypes.ADD, key, value)
},
delete {
  trigger(target, TriggerOpTypes.DELETE, key)
},
clear {
  trigger(target, TriggerOpTypes.CLEAR)
}
# 触发依赖
function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
      
  # 从targetMap结构中 查询target对应的值
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 不存在depsMap,直接return,
    return
  }
​
  # depsMap存在的情况下:
  // 新建一个空数组:用于存储需要触发的dep实例
  let deps: (Dep | undefined)[] = []
  # 重点:区分不同的触发类型
  if (type === TriggerOpTypes.CLEAR) {
     # clear清空类型
    // 展开所有dep实例, 需要触发target所有依赖
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    # 修改数组的length属性的情况下:
    // 保留新的length
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      // 循环depsMap结构:将符合条件的dep实例添加到deps数组
      if (key === 'length' || key >= newLength) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    # 其他类型触发情况:SET | ADD | DELETE
    // void 0 代替 undefined:void 0无论什么时候都是返回undefined
    if (key !== void 0) {
      // 常规对象的key触发都在这里处理:
      # 如果存在key,则从depsMap中取出key对应的dep实例,添加到deps数组
      deps.push(depsMap.get(key))
    }
​
    // also run for iteration key on ADD | DELETE | Map.SET
    # 兼容迭代结构处理:ADD | DELETE | Map.SET 
    switch (type) {
      case TriggerOpTypes.ADD:
        // 对象新增:处理迭代的情况
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          # 数组通过索引新增元素的情况:会触发length属性的变化,
          // 取出length属性对应的dep实例添加到deps数组
          deps.push(depsMap.get('length'))
        }
        break
​
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
​
      case TriggerOpTypes.SET:
        // 兼容map结构迭代
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }
​
  # 触发之前,判断deps有没有数据
  if (deps.length === 1) {
    if (deps[0]) {
      // 常规情况下:都是走的这里
      triggerEffects(deps[0])
    }
  } else {
    // 创建一个effects数组
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        // 取出deps中的deps实例,添加到effects
        effects.push(...dep)
      }
    }
    # 触发依赖
    triggerEffects(createDep(effects))
  }
}

根据上面的源码可以看出:trigger函数针对不同的数据类型,不同的触发类型都做了对应的兼容处理,保证任何操作都能正确的触发响应式数据的副作用,在对数据处理之后,依然是调用了triggerEffects方法来执行具体的触发逻辑。

vue3的响应式就分析到这里了,下节我们继续分析vue3的异步更新队列。