likes
comments
collection
share

Vue3源码解析之 reactive

作者站长头像
站长
· 阅读数 15

前言

我们知道 Vue3 中声明响应式是通过 reactiveref 这两个函数,下面我们通过案例先来看下 reactive 是如何实现的。

案例

首先引入 reactiveeffect 两个函数,之后声明 obj 响应式对象,接着又执行 effect 函数,该函数传入了一个匿名函数,最后两秒后又修改 obj.name 值。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      const { reactive, effect } = Vue
      
      const obj = reactive({
        name: 'jc',
        age: 18
      })
      
      effect(() => {
        document.querySelector('#app').innerHTML = obj.name
      })

      setTimeout(() => {
        obj.name = 'cc'
      }, 2000)
    </script>
  </body>
</html>

reactive 实现

reactive 函数在 packages/reactivity/src/reactive.ts 文件下:

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

reactive 函数实际执行的是 createReactiveObject 方法,而 target 参数就是我们传进来的对象。接着我们再看下 createReactiveObject 函数,该函数也在 packages/reactivity/src/reactive.ts 文件下:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 省略
  // target already has corresponding Proxy
  // 缓存中读取 存在则直接返回
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 省略
  // 这里的 baseHandlers 参数 就是传进来的 mutableHandlers
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 设置缓存
  proxyMap.set(target, proxy)
  // 返回 proxy 实例对象
  return proxy
}

createReactiveObject 函数实际做了 proxyMap 缓存处理,最终返回一个 proxy 实例对象。这里我们主要关注 new Proxy 这段代码,第一个参数 target 为传进来的对象,即 { name: 'jc', age: 18 },第二个 baseHandlers 参数即传入的 mutableHandlers 对象,该对象定义在 packages/reactivity/src/baseHandlers.ts 中:

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

该对象定义了 getset 等方法,从而对传入的数据进行依赖收集和依赖触发,我们先看下结果,回头再对这块逻辑分析:

Vue3源码解析之 reactive

至此,reactive 函数执行完毕,obj 得到了一个 proxy 的实例对象。接着又执行 effect 方法,该方法定义在 packages/reactivity/src/effect.ts 文件中:

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  // 创建 ReactiveEffect 实例
  const _effect = new ReactiveEffect(fn)
  // 省略
  if (!options || !options.lazy) {
   // 执行 ReactiveEffect 中的 run 方法
    _effect.run()
  }
  // 省略
}

该函数先声明一个构造函数 ReactiveEffect 的实例对象 _effect,然后执行构造函数中的 run 方法。我们先看下 ReactiveEffect 构造函数:

export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined
  // 省略

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope)
  }

  run() {
    if (!this.active) {
      return this.fn()
    }
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    try {
      this.parent = activeEffect
      activeEffect = this
      shouldTrack = true
      // 省略
      // 执行 fn 函数 即传入的匿名函数
      //  () => {
      //     document.querySelector('#app').innerHTML = obj.name
      //  }
      return this.fn()
    } finally {
      // 省略

      if (this.deferStop) {
        this.stop()
      }
    }
  }

  stop() {
    // stopped while running itself - defer the cleanup
    if (activeEffect === this) {
      this.deferStop = true
    } else if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

这里接收一个 fn 方法即传入的匿名函数,然后设置 activedeps 等属性:

Vue3源码解析之 reactive

之后执行 _effect.run(),即执行构造函数 ReactiveEffectrun 方法。我们需要关注 activeEffect = this,此时被赋值为:

Vue3源码解析之 reactive

然后执行 fn 函数,即执行传入的匿名函数,之后执行 document.querySelector('#app').innerHTML = obj.name 触发 objget 方法。

get 方法上述中被定义在 packages/reactivity/src/baseHandlers.ts 文件中:

const get = /*#__PURE__*/ createGetter()

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 省略

    const targetIsArray = isArray(target)

    // 省略
    // Reflect API
    // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
    // Reflect.get() 等同于 res = target[key]
    // Reflect 用来替代直接调用 Object 的方法
    const res = Reflect.get(target, key, receiver)

    // 省略

    if (!isReadonly) {
      // 核心,添加依赖收集
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - skip unwrap for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }

    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

get 方法实际触发的是 createGetter 函数,我们主要关注 track(target, TrackOpTypes.GET, key) 这段代码,它是对数据的依赖收集,也是 get 方法的核心。 track 函数被定义在 packages/reactivity/src/effect.ts 文件中:

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      // key: target 传入的对象 value: 创建 Map 对象
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      // key: 'name' value: Set 对象
      depsMap.set(key, (dep = createDep()))
    }

    const eventInfo = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined
    // dep 为 Set 对象
    // eventInfo = { effect: activeEffect(之前已设置), target(传入的对象), type('get'), key('name') }
    trackEffects(dep, eventInfo)
  }
}

这里的 targetMapWeakMap 对象,该对象是一个弱引用类型,那什么是弱引用类型呢?举个例子:

let obj = {
    name: 'jc'
}
const map = new WeakMap() // new Map()
map.set(obj, 'cc')

obj = null

正常来说,对象为空,堆内存中数据没有指针指向就会被回收。但 map 数据依然存在,说明 Map 为强引用;设置 WeakMap,数据为空,则说明 WeakMap 为弱引用。准确地说,obj 不存在其他引用时, WeakMap 不会阻止垃圾回收,基于 obj 的引用将会被清除,这就证明 WeakMap 的弱引用特性。

回过来我们再看下 targetMap 对象,以传入的对象 { name: 'jc', age: 18} 作为 keyvalue 值为 Map 对象,之后设置 depsMapkey 当前为 namevalueSet 对象,具体可以看下 createDep 方法,最后执行 trackEffects(dep, eventInfo)

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // 省略

  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack({
        effect: activeEffect!,
        ...debuggerEventExtraInfo!
      })
    }
  }
}

此时我们再看下 targetMap 对象数据:

Vue3源码解析之 reactive

这样就完成了数据的依赖收集,之后就可以通过指定对象指定属性获取到对应的 fn 方法。而依赖收集本质上就是 targetMapReactiveEffect 之间的关联。

createGetter 执行完毕返回对应的值,当前为 jc

Vue3源码解析之 reactive

两秒后执行 obj.name = 'cc',触发 set 方法,该方法定义在 packages/reactivity/src/baseHandlers.ts 中:

const set = /*#__PURE__*/ createSetter()

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false
    }
    if (!shallow && !isReadonly(value)) {
      if (!isShallow(value)) {
        value = toRaw(value)
        oldValue = toRaw(oldValue)
      }
      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
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // Reflect.set() 等同于 obj[key] = value
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    // 触发 getter
    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
  }
}

我们只需关注 trigger(target, TriggerOpTypes.SET, key, value, oldValue) 这行代码,也是依赖触发的核心。trigger 方法被定义在 packages/reactivity/src/effect.ts 文件中:

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // 根据传入的对象 获取 对应的 Map 对象
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      // 根据属性获取对应的 ReactiveEffect
      deps.push(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      // 省略
      // 当前为 set 类型
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  const eventInfo = __DEV__
    ? { target, type, key, newValue, oldValue, oldTarget }
    : undefined

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

根据指定对象获取到对应的 Map 对象,此时 depsMap 为:

Vue3源码解析之 reactive

之后再根据指定属性获取对应的 ReactiveEffect,再添加到 deps 中,此时 deps 为:

Vue3源码解析之 reactive

后面我们只需关注 triggerEffects(deps[0], eventInfo) 这行代码, triggerEffects 函数也在packages/reactivity/src/effect.ts 文件中:

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  // 获取到 ReactiveEffect 数组
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      // 实际执行是每个 effect 的 run 方法
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      // 实际执行的是传入的匿名函数 fn 方法
      // 再次触发 getter 方法 从而进行赋值
      effect.run()
    }
  }
}

可以看出 triggerEffects 函数实际先获取到 effect 数组,之后遍历数组执行每个 effect.run(),实际执行的是 fn 方法,该方法是最初依赖收集时传入的匿名函数,之后再次触发 getter 方法,从而进行赋值,至此整个依赖触发完成。

Vue3源码解析之 reactive

总结

  1. reactive 函数实际执行了 createReactiveObject 方法。
  2. createReactiveObject 方法主要创建了一个 proxy 实例对象,给代理对象添加 gettersetter 行为,getset 方法主要在 mutableHandlers 对象中。
  3. get 方法实际执行了 createGetter 方法,该方法中 track 函数来进行依赖收集,而 set 方法实际执行了 createSetter 方法,该方法中 trigger 进行依赖触发。
  4. effect 函数实际创建了一个 ReactiveEffect 实例,该构造函数接收一个 fn 函数,等于传进来的匿名函数,该回调函数必须暴露 getter 行为。
  5. 另外该构造函数还做了两件事,第一是在 run 函数中给 avtiveEffect 赋值,第二是执行 fn 函数。
  6. 一旦 getter 触发,就会激活 track 方法,构建 WeakMaptargetMap 对象,从而完成指定对象指定属性到 effect 的依赖收集的工作。
  7. 此时已经完成了一个依赖收集,之后进行依赖触发 setter
  8. set 方法实际执行了 createSetter 方法,然后触发 trigger 函数进行依赖触发。
  9. trigger 函数中首先或从之前 targetMap 依赖收集的对象中获取,根据 key 获取到 effect,然后执行 fn 函数,从而完成一个依赖触发的过程。
  10. reactive 缺陷:一是解构后不支持响应性,二是不支持基本类型,只能是对象。

Vue3 源码实现

vue-next-mini

Vue3 源码解析系列