烂大街的Vue3响应式解析

446 阅读8分钟

这是19年底fork的仓库,不是最新的,旨在助小伙伴了解框架提供微少的帮助。
不喜勿喷,错误请友好指正。

Vue中的观察者模式

统一认知

观察者模式的定义:

一个对象中存在一对多的依赖关系,
当对象的状态发生改变时,它所有的依赖都得到通知并被自动更新。


观察者模式在Vue框架使用:
响应式数据是观察对象,而视图、计算属性、监听对象是依赖数据。
首先响应式数据视图、计算属性、监听对象建立依赖关系。
响应式数据状态改变时,对应的视图、计算属性、监听对象会得到通知并自动更新。

这时候问题来了😁,分为如下步骤

  1. 如何创建响应式数据或依赖数据
  2. 响应式数据如何与依赖数据建立关系
  3. 响应式数据的状态改变时,如何通知依赖数据
  4. 依赖数据收到通知如何做更新
  5. 响应式数据的状态有多少种改变情况

解答正文

如何创建响应式数据

框架提供创建响应式数据的函数reactive,我们从改函数入手。

// reactivity/src/effect.ts
// reactive声明
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

从声明得知。参数只能是Object类型,并返回一个,每层嵌套的属性值不为Ref类型的对象。(如何做到?①)

// reactivity/src/reactive.ts
// 打开reactive函数
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()

export function reactive(target: object) {
  ...(省略与判断readonly有关逻辑)
  ...
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  ...(省略原始数据已经转化判断逻辑)
  ...

  //只转化 Object,Array,Map,Set,WeakMap,WeakSet 类型
  if (!canObserve(target)) {
    return target
  }
  //target 为Set, Map, WeakMap, WeakSet 用collectionHandlers
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  if (!targetMap.has(target)) {
    // 在全局数据targetMap中分配收集依赖的Map (结构=动作->Set(依赖))
    targetMap.set(target, new Map()) 
  }
  return observed
}

梳理逻辑前,先统一认知。
rawToReactivereactiveToRaw是WeakMap数据,
主要用原始数据响应式数据相互查询。

我们知道Vue3使用Proxy实现数据劫持【文档】
所以mutableHandlers、mutableCollectionHandlers,是实例Proxy时不同类型用到的handle。

响应式数据创建的大概流程是:

  1. 检验原始数据能转化(是否readonly、是否已经为响应式数据、是否能被proxy劫持)
  2. 使用Proxy劫持原始数据,转化成响应式数据
  3. 把原始数据和响应式数据保存相互查询WeakMap
  4. 全局数据targetMap为原始数据分配收集依赖Map

创建依赖数据

不管是视图、计算属性还是监听对象,其实内部通过effect函数来创建的。

// reactivity/src/effect.ts
export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions { //配置项
    lazy?: boolean
    computed?: boolean
    scheduler?: (run: Function) => void
    onTrack?: (event: DebuggerEvent) => void
    onTrigger?: (event: DebuggerEvent) => void
    onStop?: () => void
   }
): ReactiveEffect<T = any> { //返回effect对象
  (): T
  _isEffect: true
  active: boolean
  raw: () => T
  deps: Array<Dep>
  options: ReactiveEffectOptions
}

从声明得知,
effect第一个参数是函数,第二参数是可选配置对象,然后返回依赖数据

// reactivity/src/effect.ts
export function effect(fn, options){ //函数体
  ...
  ...
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect() //执行
  }
  return effect
}

function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect(...args) {
    return run(effect, fn, args)
  }
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

与reactive函数一样,入口函数处理各种兼容情景,重点逻辑在createReactiveEffect核心函数。
从核心函数看出,依赖数据是一个函数,有各种特有属性。
最后执行依赖数据,即调用run方法。

// reactivity/src/effect.ts
export const effectStack: ReactiveEffect[] = []

function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  if (!effect.active) { // 失活,不走响应式逻辑,直接调用
    return fn(...args)
  }
  if (!effectStack.includes(effect)) { 判断该依赖数据不存在与全局依赖栈里 ②why
    cleanup(effect) //清除与与所有响应式对象建立的依赖关系 ③why
    try {
      effectStack.push(effect)
      return fn(...args)
    } finally {
      effectStack.pop()
    }
  }
}

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
  }
}

其实run核心操作是,把当前依赖数据保存到依赖栈的尾部,再执行fn函数。
因此响应式数据就可通过依赖栈获取依赖数据


响应式数据如何与依赖数据建立关系

建立关系,其实在创建响应式数据依赖数据时,就已埋下伏笔。
我们回顾创建这两种数据的关键“伏笔”。

在创建依赖最后,依赖数据被执行。所以触发对应响应式数据的代理事务中get
我们查看代理handle。

// reactivity/src/baseHandlers.ts
import { track, trigger } from './effect'

export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}

function createGetter(isReadonly: boolean, unwrap: boolean = true) {
  return function get(target: object, key: string | symbol, receiver: object) {
    let res = Reflect.get(target, key, receiver)
   // 如果target是symbol类型下属性,直接返回
    if (isSymbol(key) && builtInSymbols.has(key)) { 
      return res
    }
    if (unwrap && isRef(res)) {
      res = res.value
    } else {
      // track用于收集依赖
      track(target, OperationTypes.GET, key)
    }
    return isObject(res)
      ? isReadonly
        ? // 防止无限循环
          readonly(res)
        : reactive(res)
      : res
  }
}

这段代码的重点在,track函数,看名字就知道是收集的意思,
传了三个参数,原始值,动作,键。

reactivity/src/effect.ts

export function track(target: object, type: OperationTypes, key?: unknown) {
  ...
  ...
  const effect = effectStack[effectStack.length - 1] //获取依赖对象
  ...
  ...
  let depsMap = targetMap.get(target)
  ...
  ...
  let dep = depsMap.get(key!) // 获取触发动作下的依赖集合
  ...
  ...
  if (!dep.has(effect)) {
    dep.add(effect) // 依赖对象保存依赖集合
    effect.deps.push(dep) // 把依赖集合也保存到依赖对象的dep属性下
    ...
    ...
  }
}

建立关系的主要逻辑:

  1. 依赖数据执行,触发对应响应式数据get函数
  2. get函数调用track,进行建立关系核心逻辑
  3. 通过全句数据的依赖栈,获取当前的依赖数据
  4. 通过全句数据的targetMap,获取当前响应式数据依赖集合
  5. 然后把依赖数据添加在响应式数据依赖集合对应分类动作集合
  6. 也把对应动作集合添加在依赖数据Dep属性下

响应式数据的状态改变时,如何通知依赖数据

响应式数据修改数据时,会触发代理事务中set

// reactivity/src/baseHandlers.ts

function set(
  target: object,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
  ...
  ...
  const hadKey = hasOwn(target, key)
  const result = Reflect.set(target, key, value, receiver) //更新值

  if (target === toRaw(receiver)) {
    ...
    ...
      if (!hadKey) {
        //新字段
        trigger(target, OperationTypes.ADD, key) //trigger触发add类型
      } else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key) //trigger触发set类型
      }
  }
  return result
}

这段代码的重点在,trigger函数,看名字就知道是通知的意思,
传了三个参数,原始值,动作,键。

reactivity/src/effect.ts

export function trigger(target, type, key?, extraInfo?) {
  const depsMap = targetMap.get(target) //获取依赖集合
  if (depsMap === void 0) {
    return
  }
  // 普通依赖和计算依赖分开存放到各自Set
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  if (type === OperationTypes.CLEAR) {// 如果是clear动作,通知所有依赖
    depsMap.forEach(dep => {
      addRunners(effects, computedRunners, dep)
    })
  } else {
    // 修改 | 增加 | 删除 都会有key
    if (key !== void 0) {
      addRunners(effects, computedRunners, depsMap.get(key)) //把动作集合进行分类存放到对应Set
    }
    // 如果是增加或者删除动作,通知length或者ITERATE_KEY动作下的依赖
    if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
      const iterationKey = isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }
  //遍历各自Set,通知依赖更新
  const run = (effect: ReactiveEffect) => {
    scheduleRun(effect, target, type, key, extraInfo)
  }
  computedRunners.forEach(run)
  effects.forEach(run)
}

④why
// 判断如果是普通依赖就放进 effects, 计算依赖就放进 computedRunners
function addRunners(
  effects: Set<ReactiveEffect>,
  computedRunners: Set<ReactiveEffect>,
  effectsToAdd: Set<ReactiveEffect> | undefined
) {
  if (effectsToAdd !== void 0) {
    effectsToAdd.forEach(effect => {
      if (effect.options.computed) {
        computedRunners.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }
}

function scheduleRun(
  effect: ReactiveEffect,
  target: object,
  type: OperationTypes,
  key: unknown,
  extraInfo?: DebuggerEventExtraInfo
) {
  ...
  ...
  if (effect.options.scheduler !== void 0) {
    effect.options.scheduler(effect) //如果有 scheduler 参数,使用 scheduler
  } else {
    effect() // 更新依赖
  }
}

通知依赖数据主要逻辑:

  1. 通过全句数据的targetMap,获取当前响应式数据依赖集合
  2. 创建effectscomputedRunners两个普通依赖和计算依赖的Set
  3. 通过addRunners函数,把依赖分类存放effectscomputedRunners两个Set中
  4. 遍历两个Set,执行并更新依赖数据

依赖数据收到通知如何做更新

响应式数据调用trigger函数后,会立刻执行依赖数据
依赖数据的执行过程和创建的逻辑大致相同,当然到了,存在差异才是这步最有价值的地方。

解答第③问题,为什么要清除当前依赖与所有响应式关系

// reactivity/src/effect.ts
export const effectStack: ReactiveEffect[] = []

function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  ...
  cleanup(effect) //清除与所有响应式对象建立的依赖关系 ③why
}

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
  }
}

因为响应式数据会存在被删除的情景,所以清除对应关系,便会释放内存以免造成内存泄漏,
而当前依赖数据无法感知具体被删除的响应式数据
但是被删除的响应式数据是无法触发get收集依赖数据
所以只好全都删除,然后与其在没有删除的响应式数据重建关系。


响应式数据的状态有多少种改变情况

所以改变情况的情况都写在operations文件中,其中需要注意的地方是
CLEAR动作会触发所有状态,SET、ADD、DELETE会附带通知ITERATE状态更新

// reactivity/src/operations.ts
export const enum OperationTypes {
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear',
  GET = 'get',
  HAS = 'has',
  ITERATE = 'iterate'
}