Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue3响应式 源码解析(一) #58

Closed
zhangyu1818 opened this issue Jun 14, 2021 · 0 comments
Closed

Vue3响应式 源码解析(一) #58

zhangyu1818 opened this issue Jun 14, 2021 · 0 comments
Labels

Comments

@zhangyu1818
Copy link
Owner

在正文开始之前,先简述一下响应式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的分析。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant