ZHANGYU.dev

October 14, 2023

Vue3响应式 源码解析(一)

Vue12.4 min to read

在正文开始之前,先简述一下响应式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拦截,还需要在其他操作中收集依赖,完善响应式的功能。

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


除去普通对象和数组的代理,还有一个难点是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的分析。