Vue3的源码学习系列之reactivity

875 阅读6分钟

image.png

前言

不久前,尤大官宣Vue3将在2022年2月7日成为新的默认版本。对大多数的前端人来说,Vue3的学习已是势在必行。

image.png 这篇文章将记录通过崔效瑞大佬mini-vue,学习Vue3源码过程中的那些事儿,今天主要讲讲reactivity的核心流程。开篇之前我们先来说说Vue3的源码结构

Vue3源码的结构

我认为,学习一门语言源码之前,了解其代码结构还是很有必要的。
从Vue3的源码工程来看,Vue3的源码结构主要分为编译阶段运行时阶段

image.png

编译阶段@vue/compiler-sfc包来解析*.vue文件,每个*.vue文件中<template>块的内容会被提取出来并传递给@vue/complier-dom包(依赖于@vue/compiler-core包),预编译为JavaScript的渲染函数,并附属到导出的组件上作为其render选项。

运行时阶段@vue/runtime-dom包来解析dom节点上的内容,其底层依赖于@vue/runtime-core包。

最后由@vue/reactivity实现Vue的响应式。

vue3的源码结构.png

reactivity的核心流程

进入reactivity的核心流程学习之前,我们可以先了解一下reactive的使用,以及官网对于reactive的描述。

image.png

这篇文章中,我将用断点调试的方法来梳理reactivity模块的核心流程。

如果你也使用vscode,那么你可以安装JestJest Runner这两个插件来启动测试用例。打好断点之后,按F5,测试用例便会运行,调试器就会停在断点的位置。

image.png

第一步:创建响应式对象

reactivity的单元测试文件packages/reactivity/_tests_/reactive.spec.ts入手。

describe('reactivity/reactive', () => {
  test('Object', () => {
    const original = { foo: 1 }
    // 创建响应式对象
    const observed = reactive(original)
    expect(observed).not.toBe(original)
    expect(isReactive(observed)).toBe(true)
    expect(isReactive(original)).toBe(false)
    // get
    expect(observed.foo).toBe(1)
    // has
    expect('foo' in observed).toBe(true)
    // ownKeys
    expect(Object.keys(observed)).toEqual(['foo'])
  })
  ...
})

通过代码我们可以知道,reactive函数接收到一个target对象,由createReactiveObject函数将其转换为一个Proxy对象

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 核心就是 proxy
  // 目的是可以侦听到用户 get 或者 set 的动作
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // baseHandlers -> 处理器
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 把创建好的 proxy 给存起来
  proxyMap.set(target, proxy)
  return proxy
}

collectionHandlers

collectionHandlers针对的是集合数据类型(即set、map、weakSet、weakMap),以此来解决Proxy的局限性。文件路径在packages/reactivity/src/collectionHandlers.ts

其处理器主要包含三种:

  • mutableCollectionHandlers(普通响应式数据)
  • shallowCollectionHandlers(浅层响应式数据)
  • readonlyCollectionHandlers(只读响应式数据)。 核心的代码如下:
//该方法的主要作用就是通过instrumentations实现数据劫持
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
    ? readonlyInstrumentations
    : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW) {
      return target
    }

    // 返回Reflect.get()处理后的方法
    // 如果是 get has add set delete clear forEach 的方法调用,或者是获取size,那么改为调用mutabelInstrumentations里的相关方法
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

baseHandlers

从上述的代码中,baseHandlers就是响应式对象的处理器。文件路径在packages/reactivity/src/baseHandlers.tsimage.png

该文件主要是将mutableHandlers变量导出,该变量含有get、set、deleteProperty、has、ownKeys等方法。

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
  • createGetter
// 创建get方法。isReadonly 是否只读,shallow是否是浅层
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    ... ...
    //判断是否为数组, 实际就是调用 Array.isArray(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
    // builtInSymbols: symbol 类 上的一些symbol类型的属性,例如迭代器 symbol.iterator
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    // 如果不是只读的,则进行依赖收集
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    //如果是浅层的,则直接返回
    if (shallow) {
      return res
    }
    ... ...
    return res
  }
}
  • createSetter
// 创建set方法。shallow 参数标识是否为浅层
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    //获取旧值
    ... ...
    // hadKey 进行取值有效判断,即key是否在target上存在
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    //获取结果
    const result = Reflect.set(target, key, value, receiver)
    // 如果目标是原型链中的某个东西,不要触发
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

第二步: 通过effect副作用函数,依赖收集

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  ... ...
  const _effect = new ReactiveEffect(fn)
  if (options) {
    // 把用户传过来的值合并到_effect对象上去
    // 缺点就是不是显式的,看代码的时候并不知道有什么值
    extend(_effect, options)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) {
    // 执行
    _effect.run()
  }
  // 让用户可以自行选择调用的时机(调用 fn)
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

effect函数通过接收一个fn函数,在run方法中调用了fn函数之后,初次触发reactive的get操作,从而执行track函数,进行依赖关系收集,把effect收集起来作为依赖。 image.png

// 调用get方法的时候,进行依赖收集
// target: 当前追踪对象 type:收集对象类型 key: 当前访问的key
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!isTracking()) {
    return
  }
  // 获取当前追踪对象targetMap是全局变量
  let depsMap = targetMap.get(target)
  // 判断是否存在依赖中,没有则添加
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }

  const eventInfo = __DEV__
    ? { effect: activeEffect, target, type, key }
    : undefined
  // 手动触发track
  trackEffects(dep, eventInfo)
}

第三步: 通过effect副作用函数,派发通知

当下次响应式对象发生改变时,就会触发reactive的set操作,从而执行trigger函数,重新运行effect函数,实现数据变更的派发通知,触发依赖的重新收集。

image.png

// 调用set方法,触发变更函数
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) {
    // never been tracked
    return
  }
  ... ...

  // 触发依赖函数
  if (deps.length === 1) {
    if (deps[0]) {
      if (__DEV__) {
        triggerEffects(deps[0], eventInfo)
      } else {
        triggerEffects(deps[0])
      }
    }
  } else {
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
}

总结

reactivity的核心流程分为:

  • 创建响应式对象
  • 初始化时,创建effect对象,执行fn函数,触发get操作,执行track函数,把effect收集起来作为依赖
  • 数据变更时,触发set操作,执行trigger函数重新进行依赖关系收集

image.png

结束语

新的一年已经来到,这是我今年的第一篇学习总结的输出。

过去的一年,感谢陪同我一起前行的小伙伴们!

与优秀的你们同行,希望我能坚持下去,不要掉队~

如果有写的不好的地方,请大家多多指教。