3-5 Vue3-核心源码讲解

166 阅读13分钟

原文链接(格式更好):《3-5 Vue3-核心源码讲解》

结构更新

Vue3 的源码采用 TS + monorepo 

为什么越来越多的项目选择 Monorepo? - 掘金

Vue3 的重大更新(breaking changes)

[科普文] Vue3 到底更新了什么?-腾讯云开发者社区-腾讯云

⭐️组合式 API

将 Vue2 的选项式更新为组合式

选项式:

<script>
  export default {
    data() {
      return {
        count: 1
      }
    },
    mounted() {
      this.count = 0
    },
    methods: {
      addCount() {
        this.count++
      }
    }
  }
</script>

缺点:

  1. 遵循语法写在特定区域:data、methods、computed、watch 等都是有固定语法的
  2. 当项目的负责度增加后,这些逻辑就会散落在代码的各处,不利于后期维护

组合式:

<script setup>
  import { ref, onMounted } from "vue"
  const count = ref(0)
  const addCount = () => {
    count.value++
  }
  onMounted(() => {
    count.value = 0
  })
</script>

优点:

  1. 不需要遵循在特点区域写,可以按照逻辑一行行书写,就跟传统的 JS 代码写法一致,可以将相同的逻辑放在一起

⭐️响应式原理

Vue2:全部基于Object.defineProperty()get set实现。通过对data里面的数据递归处理,才能为每个属性增加getter setter,这样会有更高的性能开销,并且对于运行时动态新增/删除的属性无法自动处理为响应式

Vue3:基础类型基于对象的get|set实现,复杂类型则基于Proxy 实现。

Proxy是 ES6 新增的 API,可以直接拦截对象上的所有操作,所以解决了vue2 中的运行时动态新增/删除的属性无法自动处理为响应式问题,并且减少了不必要的性能开销

其他

  1. Fragment允许组件返回多个根元素,减少层级
  2. slot插槽的增强与语法简化
  3. Suspense 组件异步内容加载组件,可以展示备用 UI
  4. Teleport 组件允许将元素渲染到 DOM 的任意位置
  5. 编译优化:优化了 VDOM 的对比算法
  6. TS 的支持
  7. tree-shaking 的支持
  8. 生命周期优化
  9. 等等...

初始化

Vue.createApp({
  template: `
    <div>
      <h1>你好呀</h1>
      <p>{{ msg }}</p>
      <p v-if="array.length">{{ array.length }}</p>
    </div>
  `,
  setup() {
    const msg = Vue.ref('hello, my children')
    const array = Vue.reactive([1, 2])

    setTimeout(() => {
      // debugger
      msg.value = 'hello, my children~~~~~~'
    }, 2000)

    return { msg, array }
  }
}).mount('#app')

初始化入口为:createApp 函数

初始化流程图:

响应式原理

Ref 原理

完整源码:vue3-ref.ts

核心源码解析:

export function isRef(r: any): r is Ref {
  return !!(r && r.__v_isRef === true)
}

// ref方法:里面调用 createRef 方法
export function ref(value?: unknown) {
  return createRef(value, false)
}

// createRef方法:
function createRef(rawValue: unknown, shallow: boolean) {
  // 当为 Ref 类型时表明是响应式了,所以直接返回
  if (isRef(rawValue)) {
    return rawValue
  }

  // 否则,调用 new RefImpl,并返回已处理为响应式的实例
  return new RefImpl(rawValue, shallow)
}

// RefImpl:最核心的代码
class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(
    value: T,
    public readonly __v_isShallow: boolean,
  ) {
    // ref 调用时,__v_isShallow 为 false,所以直接返回 value(ref 的传参)
    // this._rawValue = value
    this._rawValue = __v_isShallow ? value : toRaw(value)
    
    // ref 调用时,__v_isShallow 为 false,所以直接返回 value(ref 的传参)
    // this._value = value
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    // 在读取 实例.value 属性时触发:
    // 1. 收集依赖
    trackRefValue(this)
    // 2. 返回值
    return this._value
  }

  set value(newVal) {
    // 在设置 实例.value 属性时触发:
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      // 1. 设置 _rawValue 的值
      this._rawValue = newVal
      // 2. 设置 _value 的值
      this._value = useDirectValue ? newVal : toReactive(newVal)
      // 3. 收集依赖
      triggerRefValue(this, DirtyLevels.Dirty, newVal)
    }
  }
}

总结:通过核心代码的解析,可以发现调用ref(0)后,最终返回的是个对象,传入的值是放在.value上的,并且通过get|set 函数实现响应式

所以这也是为什么const count = ref(0)后,使用/设置时要用count.value = 1

但在<template>里面可以省略.value,因为Vue框架在<template>解析编译时,自动加上了value

Reactive 原理

完整源码:vue3-reactive.ts

核心源码解析:

const user = reactive({ name: 'xx' })

// reactive方法:调用 createReactiveObject 方法
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,
  )
}

// createReactiveObject:最核心的代码
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>,
) {
  // 边缘检测 --- start
  // 传参不为对象时,警告,原值返回
  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 specific value types can be observed.
  const targetType = getTargetType(target) // COMMON:Object|Array;COLLECTION:Map|WeekMap|Set|WeekSet
  if (targetType === TargetType.INVALID) {
    return target
  }
  // 边缘检测 --- end

  // 调用 new Proxy,进行数据代理
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers, // handler 函数
  )
  proxyMap.set(target, proxy)
  return proxy
}

总结:通过核心代码的解析,可以发现核心在于new Proxy,针对不同的复杂类型,使用不同的handler函数,针对性的处理get|set方法

依赖收集、触发流程与原理

当明白了数据的能被改为响应式后,则需要研究下数据变化后为什么,对应的页面/函数会执行呢?

这就涉及到依赖的收集与触发

关键词:

  • 副作用函数
    • 会产生副作用的函数:函数内使用/更改的函数外的变量的函数
const userInfo = { name: 'lisi' }

function getUserInfo() { // 副作用函数
  return userInfo.name // 引用了外部变量
}
  • 响应式数据
    • 数据发生变化时,能触发其他使用该数据的,这种数据就被称为响应式数据
conts obj = { text: 'hello!' }

function effect() {
  document.body.innerHTML = obj.text
}

obj.text = '你好'
// 当重新赋值'你好'后,如果该 effect 能自动重新执行,则 obj 就是响应式数据

实现思路(简易代码):

conts obj = { text: 'hello!' }

function effect() {
  document.body.innerHTML = obj.text
}

obj.text = '你好'

通过上述例子代码观察可知:

  • 当副作用函数执行时,可以发现会触发obj.text读取操作
  • 当修改obj.text时,会触发obj.text设置操作

那我们是不是可以在读取设置时进行拦截呢?ES6 的 Proxy可以做代理

读取时,把对应的副作用函数收集存起来

设置时,把收集的副作用函数拿出来执行

const bucket = new Set(); // 存储副作用函数的“桶”

// 原始数据
const obj = { text: "hello!" };

// 对原始数据的代理
const data = new Proxy(obj, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数放入“桶”
    bucket.add(effect);
    console.log("[ bucket ] >", bucket);
    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newValue) {
    // 设置属性值
    target[key] = newValue;
    // 将副作用函数拿出“桶”,并执行
    bucket.forEach((fn) => fn());
  },
});

function effect() {
  document.body.innerHTML = data.text;
}

effect(); // 执行副作用函数,触发读取操作

setTimeout(() => {
  data.text = "你好"; // 触发设置操作
}, 3000);

上面的代码就是一个简易的可运行的响应式原理(还存在很多设计问题)

完善的响应式

问题1:

副作用函数的命名被我们固定为effect了,真实情况可能是其他名字或匿名

解决:设计一个专门注册副作用函数的函数

let activeEffect = undefined // 全局变量:存储被注册的副作用函数

// 注册副作用函数的函数
function effect(fn) {
  // 存储传入的副作用函数
  activeEffect = fn

  // 执行该副作用函数
  activeEffect()
}

// 使用 effect
effect(
  // 一个匿名的副作用函数
  () => {
    document.body.innerHTML = data.text;
  }
)

问题2:

当给响应式数据设置一个新值时,也会触发副作用函数的执行

解决:将副作用的存储与响应式数据的属性关联起来,存储就不能再使用Set

// 副作用函数1
effect(
  // 一个匿名的副作用函数
  () => {
    document.body.innerHTML = data.text;
  }
)
观察上述副作用函数,可以得到一个关系:
data(target)
  -- text(key)
    	-- effect

// 副作用函数2
effect(
  // 一个匿名的副作用函数
  () => {
    // 使用了 text 与 name 的副作用函数
    document.body.innerHTML = data.text + data.name
  }
)

若再新增一个副作用函数,可以得到一个关系:
data(target)
  -- text(key)
    	-- effect1
			-- effect2
  -- name(key)
			-- effect2

整理一下可得这样一个数据结构:
{
  [target]: {
    [key]: [effect1, effect2, ...]
    // ...
  }
}

解决问题1、问题2后的完善代码如下:

let activeEffect = undefined; // 全局变量:存储被注册的副作用函数

// 注册副作用函数的函数
function effect(fn) {
  // 存储传入的副作用函数
  activeEffect = fn;

  // 执行该副作用函数
  activeEffect();
}

const bucket = new WeakMap(); // 存储副作用函数的
// bucket 的数据结构为:{
//   [target]: {
//     [key]: [effect1, effect2, ...]
//     // ...
//   }
// }

// 对 Proxy 的封装
const reactive = (_obj) => {
  // 对原始数据的代理
  return new Proxy(_obj, {
    // 拦截读取操作
    get(target, key) {
      if (activeEffect) {
        let depsMap = bucket.get(target);
        if (!depsMap) {
          depsMap = new Map();
          bucket.set(target, depsMap);
        }

        let deps = depsMap.get(key);

        if (!deps) deps = new Set();

        deps.add(activeEffect);
        depsMap.set(key, deps);
      }

      // 返回属性值
      return target[key];
    },
    // 拦截设置操作
    set(target, key, newValue) {
      // 设置属性值
      target[key] = newValue;

      let depsMap = bucket.get(target);
      if (!depsMap) return;

      let deps = depsMap.get(key);
      if (!deps) return;

      deps.forEach((fn) => fn());
    },
  });
};

const data = reactive({ text: "hello!", name: "张三" });

function myEffect1() {
  console.log("[ myEffect1() ] >");
  document.body.innerHTML = data.text;
}

function myEffect2() {
  console.log("[ myEffect2() ] >");
  document.body.innerHTML = data.text + data.name;
}

// 使用 effect
effect(myEffect1);
effect(myEffect2);

setTimeout(() => {
  console.log("[ setTimeout 3000 ] >");
  data.pp = "你好!"; // 触发设置操作
}, 3000);

setTimeout(() => {
  console.log("[ setTimeout 5000 ] >");
  data.text = "你好!"; // 触发设置操作
}, 5000);

其中的bucket的数据结构如下:

在将上述完善后的代码的reactive函数再次完善下,可以得到越来越接近于 Vue3 源码的代码:

// ...

 const bucket = new Map(); // 存储副作用函数的“桶”

// 依赖收集(追踪)
const track = (target, key) => {
  if (activeEffect) {
    let depsMap = bucket.get(target);
    if (!depsMap) {
      depsMap = new Map();
      bucket.set(target, depsMap);
    }

    let deps = depsMap.get(key);

    if (!deps) deps = new Set();

    deps.add(activeEffect);
    depsMap.set(key, deps);
  }
};

// 依赖触发
const trigger = (target, key) => {
  let depsMap = bucket.get(target);
  if (!depsMap) return;

  let deps = depsMap.get(key);
  if (!deps) return;

  deps.forEach((fn) => fn());
};

const reactive = (_obj) => {
  // 对原始数据的代理
  return new Proxy(_obj, {
    // 拦截读取操作
    get(target, key) {
      // 依赖收集(追踪)
      track(target, key);

      // 返回属性值
      return target[key];
    },
    // 拦截设置操作
    set(target, key, newValue) {
      // 设置属性值
      target[key] = newValue;

      // 依赖触发
      trigger(target, key);
    },
  });
};

// ...

问题3:

当使用过的属性不在使用时,已绑定的依赖项还会触发

effect(() => {
  document.body.innerHTML = obj.success ? obj.msg : 'error'
})
// 当 success 为 true 时,则使用 msg,那对应的依赖项为:
obj(target)
  -- success(key)
    	-- effect
  -- msg(key)
			-- effect
// 但当 success 为 false 时,则固定显示文本 'error',但 msg 已绑的依赖项未解除
// 则如果执行 obj.msg = 'xx',则还是会触发 effect 的执行,这完全是多余的

解决:给副作用函数增加一个属性,用于存储相关联的依赖项,在读取副作用时先断开联系,等真正执行副作用时会重新建立联系

let activeEffect = undefined

// 新增的:clearup 函数,用于断开联系
function clearup(effectFn) {
  effectFn.deps.forEach((deps) => {
    deps.delete(effectFn);
  });
  effectFn.deps = [];
}

// 注册副作用函数的函数
function effect(fn) {
  const effectFn = () => {

    clearup(effectFn); // 新增的:clearup 函数,用于断开联系
    
    // 当 effectFn 执行时,将其设置为当前激活的副作用函数
    activeEffect = effectFn;

    // 执行该副作用函数
    fn();
  };

  effectFn.deps = []; // 新增的:deps 数组,用于存储相关联的依赖项

  effectFn();
}

// 改造 track,用于收集副作用函数的关联依赖项
// 依赖收集(追踪)
const track = (target, key) => {
  if (activeEffect) {
    let depsMap = bucket.get(target);
    if (!depsMap) {
      depsMap = new Map();
      bucket.set(target, depsMap);
    }

    let deps = depsMap.get(key);

    if (!deps) deps = new Set();

    deps.add(activeEffect);
    depsMap.set(key, deps);

    activeEffect.deps.push(deps) // 新增的:用于收集该副作用函数的关联依赖项
  }
};

// 依赖触发
const trigger = (target, key) => {
  let depsMap = bucket.get(target);
  if (!depsMap) return;

  let deps = depsMap.get(key);
  if (!deps) return;

  const newDeps = new Set(deps); // 新增的:用于避免出现死循环
  newDeps.forEach((fn) => fn()); 
};

Vue3 源码内的依赖收集与触发

reactive为例,讲述下依赖收集、触发的完整流程

reactive 的核心代码:

function reactive() {
// 调用 new Proxy,进行数据代理
  const proxy = new Proxy(
    target,
    mutableHandlers, // handler 函数
  )
}

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

const enum TrackOpTypes {
  GET = 'get',
  HAS = 'has',
  ITERATE = 'iterate'
}

const enum TriggerOpTypes {
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear'
}

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // ...
    
    const res = Reflect.get(target, key, receiver)

    // ...
    
    // ⭐️ 依赖收集
    track(target, TrackOpTypes.GET, key)

    // ...

    return res
  }
}

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // ...
 
    const result = Reflect.set(target, key, value, receiver)
    
    // ...

    // ⭐️ 依赖触发
    trigger(target, TriggerOpTypes.SET, key, value, oldValue) 
    
    return result
  }
}

依赖收集

通过getter实现依赖的收集

// ⭐️ 依赖收集
track(target, TrackOpTypes.GET, key)
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
// targetMap = {
//   [target]: {
//     [key]: []
//   }
// }

let activeEffect = null

function track(target, type, key) {
  let depsMap = targetMap.get(target)

  if(!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  let dep = depsMap.get(key)

  if(!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }

  trackEffects(dep)
}

function trackEffects(dep) {
  dep.add(activeEffect)
  activeEffect!.deps.push(dep)
}

依赖触发

通过setter实现依赖的收集

// ⭐️ 依赖触发
trigger(target, TriggerOpTypes.SET, key, value, oldValue) 
function trigger(target, type, key, value, oldValue) {
  let depsMap = targetMap.get(target)

  if(!depsMap) return

  let deps = depsMap.get(key)

  triggerEffects(deps)
}

function triggerEffects(deps) {
  for (const dep of deps) {
    dep()
  }
}

渲染流程

流程

大致跟 Vue2 一样的:编译 -> 运行时

  1. 编译
    1. <template>转为模板 AST 树(用来描述模板的)
    2. 模板 AST 树转换为JS AST 树(用来描述渲染函数的)
      1. 期间会打上patchFlag(值为 number),用于精确化标记每个节点,只要打上了patchFlag则一定是动态的节点;没有打上的就是静态节点
      2. 并且还会额外使用dynamicChildren数组来储存打标的节点,直接用该数据进行 diff
    1. 基于JS AST 树生成render字符串
    2. 最后基于render字符串生成render函数
  1. 运行时
    1. 运行实例的render函数,生成vnode
    2. 基于vnode进行挂载或 diff 后更新页面

源码

编译的主入口:Compile.ts,触发条件:.mount('#app')函数的调用,并完成首次页面的渲染

// ...

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
	// ...

  // ⭐️ <template> 转为模板 AST 树(用来描述模板的)
  const ast = isString(template) ? baseParse(template, options) : template

  // ...

  // ⭐️ 将模板 AST 树转换为 JS AST 树(用来描述渲染函数的)
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )

  // ⭐️ 基于 JS AST 树生成 render 字符串
  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}

// ...
// ⭐️ 基于 render 字符串生成 render 函数
const render = (
  __GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
) as RenderFunction

运行时的主入口:无固定,触发条件:某个响应的数据的改变

// 若为 ref() 的值的改变,触发页面的重新渲染

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      // ⭐️ 依赖变更通知函数
      triggerRefValue(this, newVal)
    }
  }
}

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref)
  if (ref.dep) {
    if (__DEV__) {
      triggerEffects(ref.dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: newVal
      })
    } else {
      triggerEffects(ref.dep)
    }
  }
}

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  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) {
      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 {
      effect.run()
    }
  }
}

// ...

effect.run()

// ...


patch(...)

补充知识

如何获取复杂数据的具体类型?

比如:

  • { a: 1 },期望返回类型为object
  • [{ a: 1 }],期望返回类型为array
  • const a = function () {},期望返回类型为function
const objectType = (obj: object): string => {
  const fullTypeString = Object.prootype.toString.call(obj) // '[object Array]'
  const typeString = fullTypeString.slice(8, -1) // Array
  return typeString.toLocaleLowerCase() // array
}

Map、WeakMap、Set、WeakSet

Map:类似于object的,采用键值对存储数据,键可以是任意类型的(基础/复杂类型都可以)

WeakMap:虚弱版的Map,键必须为复杂类型,弱引用自动垃圾回收,不支持遍历

Set:类似于array的,里面的值不允许重复,值是任意类型的(基础/复杂类型都可以),无法通过索引取值,只能循环取值

WeakSet:虚弱版的Set,值必须为复杂类型,弱引用自动垃圾回收,不支持遍历

Proxy

new Proxy(target, handle);

// target: 目标对象
// property: 属性名
// value: 新值
// receiver: 最初接收赋值的对象,通常是 proxy 本身

const handle = {
  get: function (target, property, receiver) {},
  set(target, property, value, receiver) {}
}

面试题

手写一份 Vue3 的响应式


let activeEffect = undefined

const effect = fn => {
  const effectFn = () => {
    activeEffect = effectFn
    fn()
  }
  
  effectFn()
}

const targetMap = new WeakMap()

// 依赖收集
const track = (target, key, receiver) => {
  let depsMap = targetMap.get(target)

  if(!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  let depMap = depsMap.get(key)
  
  if(!deps) deps = new Set()

  deps.add(activeEffect)

  depsMap.set(key, deps)
}

// 依赖触发
const trigger = (target, key, receiver) => {
  let depsMap = targetMap.get(target)

  if(!depsMap) return

  let depMap = depsMap.get(key)

	if(!depMap) return

	depMap.forEach(fn => fn())
}

const reactive = _obj => {
  return new Proxy(_obj, {
    get(target, key, receiver) {
      track(target, key, receiver)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue,receiver)
      trigger(target, key, receiver)
    }
  })
}

相关资料

📎Vue.js设计与实现.pdf