Vue 3.x 源码初探——reactive原理

@xingbofeng 2019-11-03 13:37:27发表于 xingbofeng/xingbofeng.github.io JavaScriptVue

近期 Vue 官方正式开放了 3.x 的源码,目前处于Pre Alpha阶段,笔者出于兴趣,抽空对 Vue 3.x 源码的数据响应式部分做了简单阅读。本文通过分析 Vue 3.x 的 reactive API 的原理,可以更方便理解 Vue 3.x 比起 Vue 2.x 响应式原理的区别。

在 Vue 3.x 源码开放之前,笔者曾写过Vue Composition API 响应式包装对象原理, Vue 3.x 的 reactive API 的实现与之有类似,感兴趣的同学可以结合前文进行阅读。

阅读此文之前,如果对以下知识点不够了解,可以先了解以下知识点:

笔者之前也写过相关文章,也可以结合相关文章:

搭建Vue 3.x 运行环境

进入vue-next的项目仓库,我们可以把 Vue 3.x 项目代码都clone下来,可以看到,通过执行vue-next/scripts/build.js可以将 Vue 3.x 的代码使用 rollup 打包,生成一个名为vue.global.js,可供开发者引用。为了方便调试,我们执行vue-next/scripts/dev.js,此时开启 rollup 的 watch 模式,可以方便我们对源码进行调试、修改、输出。

在项目目录下新建一个test.html,引用构建在项目目录下的packages/vue/dist/vue.global.js,在项目目录下执行npm run dev,写一个最简单 Vue 3.x 的 demo ,用浏览器打开可以直接运行,利用这个 demo ,我们构建好了 Vue 3.x 基本的运行环境,下面可以开始进行源码的调试了。

<!DOCTYPE html>
<html>
<head>
    <title>vue-demo</title>
</head>
<body>
    <div id="app"></div>
    <script src="./packages/vue/dist/vue.global.js"></script>
    <script>
        const { createComponent, createApp, reactive, toRefs } = Vue;
        const component = createComponent({
            template: `
                <div>
                    {{ count }}
                    <button @click="addHandler">add</button>
                </div>
            `,
            setup(props) {
                const data = reactive({
                    count: 0,
                });
                const addHandler = () => {
                    data.count++;
                };
                return {
                    ...toRefs(data),
                    addHandler,
                };
            },
        });
        createApp().mount(component, document.querySelector('#app'));
    </script>
</body>
</html>

Reactive源码解析

打开vue-next/packages/reactivity/src/reactive.ts,首先可以找到reactive函数如下:

export function reactive(target: object) {
  // 如果是readonly对象的代理,那么这个对象是不可观察的,直接返回readonly对象的代理
  if (readonlyToRaw.has(target)) {
    return target
  }
  // 如果是readonly原始对象,那么这个对象也是不可观察的,直接返回readonly对象的代理,这里使用readonly调用,可以拿到readonly对象的代理
  if (readonlyValues.has(target)) {
    return readonly(target)
  }

  // 调用createReactiveObject创建reactive对象
  return createReactiveObject(
    target, // 目标对象
    rawToReactive, // 原始对象映射响应式对象的WeakMap
    reactiveToRaw, // 响应式对象映射原始对象的WeakMap
    mutableHandlers, // 响应式数据的代理handler,一般是Object和Array
    mutableCollectionHandlers // 响应式集合的代理handler,一般是Set、Map、WeakMap、WeakSet
  )
}

上面的代码很好理解,调用reactive,首先进行是否是 readonly 对象的判断,如果 target 对象是 readonly 对象或者通过调用Vue.readonly返回的代理对象,则是不可相应的,会直接返回 readonly 响应式代理对象。然后调用createReactiveObject创建响应式对象。

createReactiveObject传递的五个参数分别是:目标对象、原始对象映射响应式对象的WeakMap、响应式对象映射原始对象的WeakMap、响应式数据的代理handler,一般是Object和Array、响应式集合的代理handler,一般是Set、Map、WeakMap、WeakSet。我们可以翻到vue-next/packages/reactivity/src/reactive.ts最上方,可以看到定义了以下常量:

// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()

// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
const readonlyValues = new WeakSet<any>()
const nonReactiveValues = new WeakSet<any>()

const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])

可以看到在reactive中会预存以下四个WeakMaprawToReactivereactiveToRawrawToReadonlyreadonlyToRaw,分别是原始对象到响应式对象和 readonly 代理对象到原始对象的相互映射,另外定义了readonlyValuesnonReactiveValues,分别是 readonly 代理对象的集合与调用Vue.markNonReactive标记为不可相应对象的集合。collectionTypesSetMapWeakMapWeakSet的集合

用 WeakMap 来进行相互映射的原因是 WeakMap 的 key 是弱引用的。并且比起 Map , WeakMap 的赋值和搜索操作的算法复杂度均低于 Map ,具体原因可查阅相关文档

下面来看createReactiveObject

function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 如果不是对象,直接返回,开发环境下会给警告
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // 目标对象已经是可观察的,直接返回已创建的响应式Proxy,toProxy就是rawToReactive这个WeakMap,用于映射响应式Proxy
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // 目标对象已经是响应式Proxy,直接返回响应式Proxy,toRaw就是reactiveToRaw这个WeakMap,用于映射原始对象
  if (toRaw.has(target)) {
    return target
  }
  // 目标对象是不可观察的,直接返回目标对象
  if (!canObserve(target)) {
    return target
  }
  // 下面是创建响应式代理的核心逻辑
  // Set、Map、WeakMap、WeakSet的响应式对象handler与Object和Array的响应式对象handler不同
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  // 创建Proxy
  observed = new Proxy(target, handlers)
  // 更新rawToReactive和reactiveToRaw映射
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  // 看reactive的源码,targetMap的用处目前还不清楚,应该是作者预留的尚未完善的feature而准备的
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

看了上面的代码,我们知道createReactiveObject用于创建响应式代理对象:

  • 首先判断target是否是对象类型,如果不是对象,直接返回,开发环境下会给警告
  • 然后判断目标对象是否已经是可观察的,如果是,直接返回已创建的响应式Proxy,toProxy就是rawToReactive这个WeakMap,用于映射响应式Proxy
  • 然后判断目标对象是否已经是响应式Proxy,如果是,直接返回响应式Proxy,toRaw就是reactiveToRaw这个WeakMap,用于映射原始对象
  • 然后创建响应式代理,对于SetMapWeakMapWeakSet的响应式对象handler与ObjectArray的响应式对象handler不同,要分开处理
  • 最后更新rawToReactivereactiveToRaw映射

响应式代理陷阱

Object和Array的代理

下面的重心来到了分析mutableCollectionHandlersmutableHandlers,首先分析vue-next/packages/reactivity/src/baseHandlers.ts,这个handler用于创建Object类型和Array类型的响应式Proxy使用:

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

我们知道,最重要的就是代理get陷阱和set陷阱,首先来看get陷阱:

function createGetter(isReadonly: boolean) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // 通过Reflect拿到原始的get行为
    const res = Reflect.get(target, key, receiver)
    // 如果是内置方法,不需要另外进行代理
    if (isSymbol(key) && builtInSymbols.has(key)) {
      return res
    }
    // 如果是ref对象,代理到ref.value
    if (isRef(res)) {
      return res.value
    }
    // track用于收集依赖
    track(target, OperationTypes.GET, key)
    // 判断是嵌套对象,如果是嵌套对象,需要另外处理
    // 如果是基本类型,直接返回代理到的值
    return isObject(res)
      // 这里createGetter是创建响应式对象的,传入的isReadonly是false
      // 如果是嵌套对象的情况,通过递归调用reactive拿到结果
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}
  • get 陷阱首先通过Reflect.get,拿到原始的get行为
  • 然后判断如果是内置方法,不需要另外进行代理
  • 然后判断如果是ref对象,代理到ref.value
  • 然后通过track来收集依赖
  • 最后判断拿到的res结果是否是对象类型,如果是对象类型,再次调用reactive(res)来拿到结果,避免循环引用的情况

下面来看set陷阱:

function set(
  target: object,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
  // 首先拿到原始值oldValue
  value = toRaw(value)
  const oldValue = (target as any)[key]
  // 如果原始值是ref对象,新赋值不是ref对象,直接修改ref包装对象的value属性
  if (isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
  // 原始对象里是否有新赋值的这个key
  const hadKey = hasOwn(target, key)
  // 通过Reflect拿到原始的set行为
  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)) {
    /* istanbul ignore else */
    if (__DEV__) {
      const extraInfo = { oldValue, newValue: value }
      // 没有这个key,则是添加属性
      // 否则是给原始属性赋值
      // trigger 用于通知deps,通知依赖这一状态的对象更新
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key, extraInfo)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key, extraInfo)
      }
    } else {
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key)
      }
    }
  }
  return result
}
  • set 陷阱首先拿到原始值oldValue
  • 然后进行判断,如果原始值是ref对象,新赋值不是ref对象,直接修改ref包装对象的value属性
  • 然后通过Reflect拿到原始的set行为,如果原始对象里是否有新赋值的这个key,没有这个key,则是添加属性,否则是给原始属性赋值
  • 进行对应的修改和添加属性操作,通过调用trigger通知deps更新,通知依赖这一状态的对象更新

Set、Map、WeakMap、WeakSet的代理

分析了mutableHandlers,下面来分析mutableCollectionHandlers,打开vue-next/packages/reactivity/src/collectionHandlers.ts,这个handler用于创建SetMapWeakMapWeakSet的响应式Proxy使用:

// 需要监听的方法调用
const mutableInstrumentations: Record<string, Function> = {
  get(this: MapTypes, key: unknown) {
    return get(this, key, toReactive)
  },
  get size(this: IterableCollections) {
    return size(this)
  },
  has,
  add,
  set,
  delete: deleteEntry,
  clear,
  forEach: createForEach(false)
}

// ...


function createInstrumentationGetter(
  instrumentations: Record<string, Function>
) {
  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) =>
    // 如果是`get`、`has`、`add`、`set`、`delete`、`clear`、`forEach`的方法调用,或者是获取`size`,那么改为调用mutableInstrumentations里的相关方法
    Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
}

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(mutableInstrumentations)
}

看上面的代码,我们看到mutableCollectionHandlers只有一个get陷阱,这是为什么呢?因为对于SetMapWeakMapWeakSet的内部机制的限制,其修改、删除属性的操作通过setadddelete等方法来完成,是不能通过Proxy设置set陷阱来监听的,类似于 Vue 2.x 数组的变异方法的实现,通过监听get陷阱里的gethasaddsetdeleteclearforEach的方法调用,并拦截这个方法调用来实现响应式。

关于为什么SetMapWeakMapWeakSet不能做到响应式,笔者在why-is-set-incompatible-with-proxy找到了答案。

那么我们理解了因为Proxy对于SetMapWeakMapWeakSet的限制,与 Vue 2.x 的变异方法类似,通过拦截gethasaddsetdeleteclearforEach的方法调用来监听SetMapWeakMapWeakSet数据类型的修改。看gethasaddsetdeleteclearforEach等方法就轻松多了,这些方法与对象类型的get陷阱、hasset等陷阱handler类似,笔者在这里不做过多讲述。

小结

本文是笔者处于继续对 Vue 3.x 相关动态的关注,首先,笔者讲述了如何搭建一个最简单的 Vue 3.x 代码的运行和调试环境,然后对 Vue 3.x 响应式核心原理进行解析,比起 Vue 2.x , Vue 3.x 对于响应式方面全面拥抱了 Proxy API,通过代理初始对象默认行为来实现响应式;reactive内部利用WeakMap的弱引用性质和快速索引的特性,使用WeakMap保存了响应式代理和原始对象, readonly 代理和原始对象的互相映射;最后,笔者分析了响应式代理的相关陷阱方法,可以知道对于对象和数组类型,是通过响应式代理的相关陷阱方法实现原始对象响应式,而对于SetMapWeakMapWeakSet类型,因为受到Proxy的限制,Vue 3.x 使用了劫持gethasaddsetdeleteclearforEach等方法调用来实现响应式原理。