9

Vue 3.x 响应式原理——reactive源码分析

 3 years ago
source link: https://zhuanlan.zhihu.com/p/89940326
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Vue 3.x 响应式原理——reactive源码分析

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK