zhangyu1818.

Tag »Vue

Vue3响应式 源码解析(一)

Jun 14, 2021View on Github

在正文开始之前,先简述一下响应式Proxy的原理。

原理简述


ProxyES6新增的对象,可以对指定对象做一层代理。

我们通过Proxy创建的对象,无非会经过2个步骤,达成我们的响应式需求。

  1. 收集watch函数中的依赖。
  2. 改变后再次调用watch函数。

如何收集依赖

在对象通过Proxy代理后,我们就可以在读取对象的属性时加上一层拦截,通常形式为:

const p = new Proxy({}, {
  get(target, key, receiver) {
    // 拦截
  }
});

get拦截方法中,我们即可以拿到对象本身target,读取的属性key,和调用者receiver(就是p对象),在这里我们就能够获取当前访问的属性key

通常我们会在方法里访问代理后的对象:

function fn(){
  console.log(p.value);
}
fn();

当我们执行了fn函数后,我们就会触发我们的get拦截,只需要在get拦截中记录下当前执行的函数,就可以建立一个key => fn的映射,后续可以在属性值发生改变后再次调用fn函数。

所以比较疑惑的点是如何在执行我们的get拦截的同时,还能获取到是哪一个函数调用了这个代理对象。

Vue3的实现中,是使用了一个effect函数来包装我们自己的函数。

effect(()=>{
  console.log(p.value)
})

为的就是能将调用了代理对象的函数保存下来。

let activeEffect;

function effect(fn){
  activeEffect = fn;
  fn(); // 执行
  activeEffect = null;
}
// ...
get(target, key, receiver) {
  // get拦截中访问全局的activeEffect,就是当前调用的函数
  // key => activeEffect
}

get拦截中还有一个需要注意的点,如果我们需要代理的对象是数组,那么在调用如pushpopincludes等大部分数组方法时,其实都会触发get拦截,这些方法都会访问数组的length属性。

触发Watch函数

我们会在值修改后触发保存下来的key => fn映射的函数。set拦截会在设置属性值的时触发。

const p = new Proxy({}, {
  set(target, key, value, receiver) {
    // 取出key对应的fn来执行
  }
});

其他的拦截方式

除去我们读取属性时的get拦截,还需要在其他操作中收集依赖,完善响应式的功能。

  • hasin操作符拦截。
  • ownKeys
    • 拦截Object.getOwnPropertyNames()
    • 拦截Object.getOwnPropertySymbols()
    • 拦截Object.keys()
    • 拦截Reflect.ownKeys()

除去设置属性的set拦截来触发依赖函数,还需要在删除属性时也触发。

  • deleteProperty,删除属性时拦截。

除去普通对象和数组的代理,还有一个难点是MapSet对象的代理。

详细的原理实现可以我之前的链接,本文中就不再实现了。

  1. 如何利用Proxy实现一个响应式对象
  2. 如何使用Proxy拦截Map和Set的操作

接下来进入正文部分。

源码浅析

Vue3是Monorepo,响应式的包reacitvity是单独的一个包。

image-20210612124937655

reactivity受了以上3个包的启发,刚好我也拜读过observer-util的源码,reactivity相对“前辈”做了很多巧妙的改进和功能的增强。

  1. 增加了shallow模式,只有第一层值为响应式。
  2. 增加了readonly模式,不会收集依赖,不能修改。
  3. 增加了ref对象。

文件结构

├── baseHandlers.ts
├── collectionHandlers.ts
├── computed.ts
├── effect.ts
├── index.ts
├── operations.ts
├── reactive.ts
└── ref.ts

baseHandlerscollectionHandlers为功能的主要实现文件,也就是Proxy对象对应的拦截器函数,effect为观察者函数文件。

本文主要分析的也是这3部分。

对象数据结构

Target类型为需要Proxy的原始对象,上面定义了4个内部属性。

export interface Target {
  [ReactiveFlags.SKIP]?: boolean
  [ReactiveFlags.IS_REACTIVE]?: boolean
  [ReactiveFlags.IS_READONLY]?: boolean
  [ReactiveFlags.RAW]?: any
}

targetMap为内部保存收集的依赖函数的一个WeakMap

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

它的键名是未经过Proxy响应式操作的原始对象,值为key => Set<依赖函数>Map

我们会通过targetMap获取当前对象对应的key => Set<依赖函数>Map,从中取出key对应的所有依赖函数,然后在值发生改变后调用它们。

以下4个Map是内部记录原始对象Targetreactivereadonly后对象的映射关系。

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

baseHandlers

baseHandlers这个文件里主要是创建了针对普通对象,数组的Proxy拦截器函数。

先看收集依赖的get拦截器。

get

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // Target 内部键名并没有储存在对象上,而是通过get拦截闭包的返回
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
            ? shallowReactiveMap
            : reactiveMap
        ).get(target)
    ) {
      // target就是raw value,前提是receiver和raw => proxy里的对象一样
      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)

    // 忽略内置symbol和non-trackable键
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    // readonly不能改不用追踪
    if (!isReadonly) {
      // 收集依赖
      track(target, TrackOpTypes.GET, key)
    }

    // shallow响应式直接返回结果,不对嵌套对象再做响应式
    if (shallow) {
      return res
    }

    // ref的处理
    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)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

get拦截器中,首先是一个巧妙的处理返回ReactiveFlags对应的值,不需要将它对应的值真正的赋值在对象上,接着会对数组做特殊的处理,收集依赖的函数为track,它定义在effect.ts中,在后文会分析这一模块。如果返回的值是对象,则会延迟转换对象。

set

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) {
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      // 如果旧值是ref的情况时,ref内部也有set拦截,
      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)
    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 ADD
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 有key SET
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

set拦截器里主要是判断设置的key是否存在,然后分2种参数去triggertrigger函数为触发收集effect函数的方法,同样定义在effect.ts中,这里先暂且不提。

ownKeys

function ownKeys(target: object): (string | symbol)[] {
  // 对于数组来说key是length,对象的话是ITERATE_KEY只作为一个key的标识符
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

ownKeys拦截器同样是收集依赖,需要注意的是传入的key参数,在target为数组的时候keylength,对象的时候keyITERATE_KEYITERATE_KEY仅为一个symbol值的标识符,后续会通过这个值来取到对应的effect函数,实际是不存在这个key的。

effect

本文的原理简述中提到,如果我们想要知道对象被哪一个函数调用了,需要将函数放入我们自己的运行函数中来调用。实际代码中我们是将传入effect方法的函数做了一层新的包装,它的类型为ReactiveEffect

数据结构

export interface ReactiveEffect<T = any> {
  (): T
  _isEffect: true
  id: number
  active: boolean // 是否有效
  raw: () => T // 原始函数
  deps: Array<Dep> // 依赖了该effect的key所对应的保存effect的Set
  options: ReactiveEffectOptions
  allowRecurse: boolean
}	

其中比较重要的字段为deps,如果我们在执行该effectFn函数收集依赖时,得到了如下的依赖结构:

{
  "key1": [effectFn] // Set
  "key2": [effectFn] // Set
}

那么我们的ReactiveEffect方法effectFndeps属性保存的值就是这2个key所对应的Set

export function effect<T = any>(
  fn: () => T, // 传入的函数
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options) // 创建ReactiveEffect
  if (!options.lazy) {
    effect() // 执行ReactiveEffect
  }
  return effect
}

effect函数中通过createReactiveEffect创建了ReactiveEffect

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      return fn()
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        // 将activeEffect赋值为当前effect
        activeEffect = effect
        // 执行函数,对应的拦截器可以通过activeEffect保存对应的effect
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        // 重置activeEffect
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

在执行effect函数前,先将函数保存到全局变量activeEffect中,这样在函数执行的同时,对应的拦截器在收集依赖的时候就能知道当前是哪一个函数在执行。

cleanup

cleanup方法清除依赖关系。

function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

上文提到了deps属性的结构,保存的是依赖了effectFnSet,遍历它们,将effectFn从所有的Set中删除。

track

track方法收集依赖,功能非常简单,将activeEffect添加进Dep

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  // 初始化 target => Map<key,Dep>
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  // 初始化 key => Set<Effect>
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  // 当前key对应的Set中不存在activeEffect
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect) // 添加进Set
    activeEffect.deps.push(dep) // 同时添加进Effect的deps
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

Trigger

trigger方法执行ReactiveEffect,内部会做一些类型判断,比如TriggerOpTypes.CLEAR只存在于MapSet

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) {
    return
  }

  // 将需要执行的effect都拷贝到effects 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)
        }
      })
    }
  }

  // CLEAR类型存在于Map和Set的collectionHandlers中
  if (type === TriggerOpTypes.CLEAR) {
    // Map的forEach第一个参数是值,也就是key对应对Dep Set
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) { // 数组
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // key !== undefined SET | ADD | DELETE
    if (key !== void 0) {
      // 只加入当前key的effect函数
      add(depsMap.get(key))
    }

    // ITERATE_KEY是一个内置的标识变量 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
          // 新索引 => length 改变
          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)
}

数组的特殊处理

虽然使用了Proxy,但是数组方法还是需要特殊处理,避免一些边界情况,它们并没有重写数组方法。

includes, indexOf,lastIndexOf

这3个方法的特殊处理是为了同时能够判断是否存在响应式数据。

  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 + '')
    }
    // 先用当前参数执行
    const res = method.apply(arr, args)
    if (res === -1 || res === false) {
      // 如果没有结果,将参数转为raw值再执行
      return method.apply(arr, args.map(toRaw))
    } else {
      return res
    }
  }
})

为了确保响应式的值和非响应式的值都可以被判断,所以可能会遍历两次。

避免循环依赖

;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
  const method = Array.prototype[key] as any
  arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
    pauseTracking()
    const res = method.apply(this, args)
    resetTracking()
    return res
  }
})

数组的方法基本都会隐式的依赖lengh属性,在某些情况可能会出现循环依赖(#2137)。

总结

以上为Vue 3响应式的对象和数组拦截的源码浅析,本文只简单分析了baseHandlers中重要的拦截器,后续会带来collectionHandlers的分析。