Vue3-reactive响应式原理

469 阅读6分钟

依赖收集

版本依赖收集派发更新
Vue2dep.dependdep.notify
Vue3tracktrigger

响应式

版本响应式
Vue2data中设置、$set()
Vue3reactive,ref、shallowReactive、shallowReadonly

Vue3中使用高阶函数:函数柯里化


Vue3响应式源码

Vue3 中定义的几个用来标记目标对象 target 的类型的flag。

// packages/reactivity/src/reactive.ts -16行
export const enum ReactiveFlags {
  SKIP = '__v_skip',
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  RAW = '__v_raw'
}

export interface Target {
  [ReactiveFlags.SKIP]?: boolean // 不做响应式处理的数据
  [ReactiveFlags.IS_REACTIVE]?: boolean // target 是否是响应式
  [ReactiveFlags.IS_READONLY]?: boolean // target 是否是只读的
  [ReactiveFlags.RAW]?: any // 表示 proxy 对应的源数据,target 已经是 proxy 对象时会有该属性
}

reactive

// packages/reactivity/src/reactive.ts -87行
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // 如果 target 是只读类型的对象就直接返回
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target, // 需要创建响应式的目标对象 data
    false, // 不是只读类型
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap // const reactiveMap = new WeakMap<Target, any>()
  )
}

createReactiveObject

// packages/reactivity/src/reactive.ts -181行
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // typeof 不是 object 类型的,直接返回
  if (!isObject(target)) {
    if (__DEV__) console.warn(`value cannot be made reactive: ${String(target)}`)
    return target
  }
  // 已经是响应式的就直接返回
  if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
    return target
  }
  // 如果已经存在 map 中了,就直接返回
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 不做响应式的,直接返回
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // 把 target 转为 proxy
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 添加到 map 里
  proxyMap.set(target, proxy)
  return proxy
}

大概了解了这个方法里要做的事,可以看出一个很重要的东西,就是proxy的第二个参数 handlers。 baseHandlers 处理数组,对象;collectionHandlers处理 Map、Set、WeakMap、WeakSet。

在 basehandlers 中包含了四种 handler:

  • mutableHandlers 可变处理
  • readonlyHandlers 只读处理
  • shallowReactiveHandlers 浅观察处理(只观察目标对象的第一层属性)
  • shallowReadonlyHandlers 浅观察 && 只读处理 其中 readonlyHandlers、shallowReactiveHandlers、shallowReadonlyHandlers 都是 mutableHandlers 的变形版本

mutableHandlers 定义是这样的:

const get = /*#__PURE__*/ createGetter()
const set = /*#__PURE__*/ createSetter()
export const mutableHandlers: ProxyHandler<object> = {
  get, // 获取属性
  set, // 修改属性
  deleteProperty, // 删除属性
  has, // 是否拥有某个属性
  ownKeys // 收集 key,包括 symbol 类型或者不可枚举的 key
}

gethasownKeys 会触发依赖收集 track();
setdeleteProperty 会触发更新 trigger()。

使用weakMap的优点:

  • key是对象
  • 可以自动释放

createGetter()

// packages/reactivity/src/baseHandlers.ts -80行
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 (
      // receiver 指向调用者,这里判断是为了保证触发拦截 handle 的是 proxy 本身而不是 proxy 的继承者
      // 触发拦的两种方式:一是访问 proxy 对象本身的属性,二是访问对象原型链上有 proxy 对象的对象的属性,因为查询会沿着原型链向下找
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow ? shallowReadonlyMap : readonlyMap
          : shallow ? shallowReactiveMap : reactiveMap
        ).get(target)
    ) {
      // 返回 target 本身,也就是响应式对象的原始值
      return target
    }
    // 是否是数组
    const targetIsArray = isArray(target)
    // 不是只读类型 && 是数组 && 触发的是 arrayInstrumentations 工具集里的方法
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      // 通过 proxy 调用,arrayInstrumentations[key]的this一定指向 proxy
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // proxy 预返回值
    const res = Reflect.get(target, key, receiver)
    // key 是 symbol 或访问的是__proto__属性不做依赖收集和递归响应式处理,直接返回结果
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }
    // 不是只读类型的 target 就收集依赖。因为只读类型不会变化,无法触发 setter,也就会触发更新
    if (!isReadonly) {
      // 收集依赖,存储到对应的全局仓库中
      track(target, TrackOpTypes.GET, key)
    }
    // 浅比较,不做递归转化,就是说对象有属性值还是对象的话不递归调用 reactive()
    if (shallow) {
      return res
    }
    // 访问的属性已经是 ref 对象
    if (isRef(res)) {
      // 返回 ref.value,数组除外
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }
    // 由于 proxy 只能代理一层,如果子元素是对象,需要递归继续代理
    // 懒代理,Vue3的性能优化点(Vue2时,直接递归操作,Vue3的时候,不使用,就不会执行)
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

track() 依赖收集放到后面,和派发更新一起


createSetter()

// packages/reactivity/src/baseHandlers.ts -141行
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    if (!shallow) {
      // 拿新值和老值的原始值,因为新传入的值可能是响应式数据,如果直接和 target 上原始值比较是没有意义的
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      // 不是数组 && 老值是 ref && 新值不是 ref,更新 ref.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 值
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // 赋值,相当于 target[key] = value
    const result = Reflect.set(target, key, value, receiver)
    // receiver 是 proxy 实例才派发更新,防止通过原型链触发拦截器触发更新
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 如果  target 没有 key,表示新增
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 如果新旧值不相等
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

为什么用 Reflect.get() 和 Reflect.set(),而不是直接用 target[key]?

  • Reflect.get(target, prop, receiver)中的参数receiver:如果target对象中指定了getter,receiver则为getter调用时的this值。
  • handler.get(target, prop, receiver)中的参数receiver:Proxy或者继承Proxy的对象。 详细解答

ReactiveEffect()

// 临时存储响应式函数 
const effectStack: ReactiveEffect[] = [] 
// 依赖收集栈 
const trackStack: boolean[] = [] 
// 最大嵌套深度 
const maxMarkerBits = 30

这里主要做的就是在依赖收集前用栈数据结构 effectStrack 来做 effect 的执行调试,保证当前 effect 的优先级最高,并及时清除己收集依赖的内存。

// packages/reactivity/src/effect.ts -53行
export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []

  // can be attached after creation
  computed?: boolean
  allowRecurse?: boolean
  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope | null
  ) {
    recordEffectScope(this, scope)
  }

  run() {
    if (!this.active) {
      return this.fn()
    }
    // 如果栈中没有当前的 effect
    if (!effectStack.includes(this)) {
      try {
        // 先把当前 effect 设置为全局全局激活的 effect,在 getter 中会收集 activeEffect 持有的 effect, 然后入栈
        effectStack.push((activeEffect = this))
        // 恢复依赖收集,因为在setup 函数自行期间,会暂停依赖收集
        enableTracking()
        // 记录递归深度位数
        trackOpBit = 1 << ++effectTrackDepth
        // 如果 effect 嵌套层数没有超过 30 层,一般超不了
        if (effectTrackDepth <= maxMarkerBits) {
          // 给依赖打标记,就是遍历 _effect 实例中的 deps 属性,给每个 dep 的 w 属性标记为 trackOpBit 的值
          initDepMarkers(this)
        } else {
          // 超过就 清除当前 effect 相关依赖 通常情况下不会
          cleanupEffect(this)
        }
        return this.fn()
      } finally {
        if (effectTrackDepth <= maxMarkerBits) {
          // 完成依赖标记
          finalizeDepMarkers(this)
        }
        // 恢复到上一级
        trackOpBit = 1 << --effectTrackDepth
        // 重置依赖收集状态
        resetTracking()
        // 出栈
        effectStack.pop()
        // 获取栈长度
        const n = effectStack.length
        // 将当前 activeEffect 指向栈最后一个 effect
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }

  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

该函数调用在runtime-core/src/renderer.ts,初始化ReactiveEffect对象,调用run函数。
需要注意的是标记完成后就会执行 fn() 函数,这个 fn 函数就是副作用函数封闭的函数,如果是在组件渲染,就是 fn 就是组件渲染函数,执行的时候就会就会访问数据,就会触发 target[key]getter,然后触发 track 进行依赖收集,这也就是 Vue3 的依赖收集过程。


track()

track 就是依赖收集器,负责把依赖收集起来统一放到一个依赖管理中心

const targetMap = new WeakMap<any, KeyToDepMap>() 

export function isTracking() { 
  return shouldTrack && activeEffect !== undefined
}

targetMap 为依赖管理中心,用于存储响应式函数、目标对象、键之间的映射关系。结构类似下面代码:

// targetMap(weakmap)={ 
//   target1(map):{ 
//     key1(dep):[effect1,effect2] 
//     key2(dep):[effect1,effect2] 
//   } 
// } 
// 给每个 target 创建一个 map,每个 key 对应着一个 dep 
// 用 dep 来收集依赖函数,监听 key 值变化,触发 dep 中的依赖函数

track()

// packages/reactivity/src/effect.ts -188行
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 如果当前没有激活 effect,就不用收集,首次创建时未执行get操作,故为false
  if (!isTracking()) {
    return
  }
  // 从依赖管理中心里获取 target
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // 如果没有就创建一个
    targetMap.set(target, (depsMap = new Map()))
  }
  // 获取 key 对应的 dep 集合
  let dep = depsMap.get(key)
  if (!dep) {
    // 没有就创建
    depsMap.set(key, (dep = createDep()))
  }
  // 开发环境和非开发环境
  const eventInfo = __DEV__
    ? { effect: activeEffect, target, type, key }
    : undefined

  trackEffects(dep, eventInfo)
}

trackEffects()

这里把当前激活的effect收集进对应的 effect 集合,也就是 dep

dep.n:n 是 newTracked 的缩写,表示是否是最新收集的(是否当前层)
dep.w:w 是 wasTracked 的缩写,表示是否已经被收集,避免重复收集

// packages/reactivity/src/effect.ts -212行
export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  // 如果 effect 嵌套层数没有超过30层
  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) {
    // 收集当前激活的 effect 作为依赖
    dep.add(activeEffect!)
    // 当前激活的 effect 收集 dep 集合
    activeEffect!.deps.push(dep)
    // 开发环境下触发 onTrack 事件
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        Object.assign(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo
        )
      )
    }
  }
}

以上为依赖收集的过程。


trigger()

trigger 是 track 收集的依赖对应的触发器,也就是负责根据映射关系,获取响应式函数,再派发通知 triggerEffects 进行更新。


未完待续~