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

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

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

阅读此文以前,若是对如下知识点不够了解,能够先了解如下知识点:vue

笔者以前也写过相关文章,也能够结合相关文章:java

搭建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 模式,能够方便咱们对源码进行调试、修改、输出。react

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

<!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函数以下:github

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最上方,能够看到定义了如下常量:npm

// 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的集合api

用 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等方法调用来实现响应式原理。

相关文章
相关标签/搜索