阅读 1010

Vue 3.0 响应式方法解析

前一段时间,尤雨溪公布了Vue 3.0 的核心部分代码。我们趁着目前代码量还比较少,来赶紧学一下。

Vue3.0 仓库

目录结构

首先看一下 Vue3.0 的文件目录结构,所有的核心代码都在 packages 文件夹下:

reactivity :Vue3.0的数据响应式系统,我们肯定都听说 Vue3 摒弃了 Object.defineProperty,基于 Proxy 实现了一套响应式系统,就是这个文件夹下。

runtime-coreruntime-domruntime-test,这三个文件夹都是Vue 运行时相关的核心代码。

compiler-corecompiler-domcompiler-sfc,这三个文件夹是 Vue 实现的编译器相关代码。

server-renderer 是服务端渲染部分。

vue 文件夹是整个项目打包构建之后的出口文件。

reactivity

我们首先来阅读 reactivity 响应式系统相关的代码。 响应式系统分别有几个文件:: ref 、reactive 、baseHandlers、collectionHandlers、computed 、effect 、lock 、operations。

其中lock其中有两个控制锁的开关的方法,operations里分别枚举了几种数据操作类型。

reactive: 响应式系统的核心部分,Proxy 就在这里了。

ref:ref 其实提供了一套 Ref 类型,它的主要作用是让JS的基础数据类型也能成为响应式数据被监测。

computed: computed 不用多说,就是 Vue2 中我们已经熟悉的计算属性了。

baseHandlerscollectionHandlers,收集依赖和触发监听方法的 Handlers

effect:effect 里面包含了具体如何收集依赖和触发监听方法的处理逻辑。

reactive

今天主要分析的就是 reactive 文件,话不多说,我们直接上代码:

首先文件的一开始分别引入了一下外部代码:

import { isObject, toRawType } from '@vue/shared'
复制代码

isObject 判断数据是否是对象,toRawType 获取数据类型。都是工具方法。

import {
  mutableHandlers, // 可变数据的 Handlers
  readonlyHandlers, // 只读数据的 Handlers
  readonlyPropsHandlers  // 只读 Props 的 Handlers
} from './baseHandlers'

import {
  mutableCollectionHandlers, // 可变集合数据的 Handlers
  readonlyCollectionHandlers // 只读集合数据的 Handlers
} from './collectionHandlers'
复制代码

上面引入了针对三种数据的 handlers 处理。

import { ReactiveEffect } from './effect'
复制代码

从 effect 文件引入了一种数据类型。

import { UnwrapRef, Ref } from './ref'
复制代码

从 ref 文件引入了两种数据类型。

import { makeMap } from '@vue/shared'
复制代码

生成 map 的工具方法。

常量

export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<any, Dep>
export const targetMap = new WeakMap<any, KeyToDepMap>()
复制代码

定义了一个 targetMap 常量,为了减少内存开销,使用 WeakMap 数据类型,但是还不知道这个 targetMap 是用来做什么的。

const rawToReactive = new WeakMap<any, any>() // 原始数据 和 响应式数据的映射
const reactiveToRaw = new WeakMap<any, any>() // 响应式数据 和 原始数据的映射
const rawToReadonly = new WeakMap<any, any>() // 原始数据和只读的映射
const readonlyToRaw = new WeakMap<any, any>() // 只读数据和原始数据的映射
复制代码

这里定义了四个 WeakMap 的常量来进行数据的双向映射。

const readonlyValues = new WeakSet<any>() // 被用户标记为只读类型的数据
const nonReactiveValues = new WeakSet<any>() // 被用户标记为不能转换成响应式的的数据
// 几种集合数据类型
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
// 是否是可监测类型数据
const isObservableType = /*#__PURE__*/ makeMap(
  'Object,Array,Map,Set,WeakMap,WeakSet'
)
复制代码

核心方法

除了上面定义的一些常量,还有一些工具方法,基本很简单易懂,我们直接来看核心方法。 这里有三个方法,分别是 reactivereadonlyreadonlyProps 方法:

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  // 已经在只读map里,说明已经是响应式数据,直接 return
  if (readonlyToRaw.has(target)) {
    return target
  }
  // target is explicitly marked as readonly by user
  // 被用户标记为只读类型,调用只读方法
  if (readonlyValues.has(target)) {
    return readonly(target)
  }
  // 调用 创建响应式对象方法
  return createReactiveObject(
    target, // 目标对象
    rawToReactive, // 原始数据 map
    reactiveToRaw, // 响应式数据 map
    mutableHandlers, // 可变数据的处理
    mutableCollectionHandlers // 可变集合的处理
  )
}

export function readonly<T extends object>(
  target: T
): Readonly<UnwrapNestedRefs<T>> {
  // value is a mutable observable, retrieve its original and return
  // a readonly version.
  // 如果响应式 map 里有目标对象,从 map 里取出
  if (reactiveToRaw.has(target)) {
    target = reactiveToRaw.get(target)
  }
  // 调用 创建响应式对象方法
  return createReactiveObject(
    target,
    rawToReadonly,
    readonlyToRaw,
    readonlyHandlers,
    readonlyCollectionHandlers
  )
}

export function readonlyProps<T extends object>(
  target: T
): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
  return createReactiveObject(
    target,
    rawToReadonly,
    readonlyToRaw,
    readonlyPropsHandlers,
    readonlyCollectionHandlers
  )
}
复制代码

从以上可以看出,除了一些相应校验之外,主要调用了 createReactiveObject 方法。

function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 判断 target 是否是对象
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target already has corresponding Proxy
  // 原始数据 已经有 对应的 响应式数据
  // 直接返回 对应的 响应式数据
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // target is already a Proxy
  // 原始数据 已经在 toRaw 里,说明已经是响应式数据
  // 直接返回 target
  if (toRaw.has(target)) {
    return target
  }
  // only a whitelist of value types can be observed.
  // 目标数据不能被 监听
  if (!canObserve(target)) {
    return target
  }
  // 根据数据是否是集合找寻对应的 handlers
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers

  // 使用 new Proxy 监听,返回一个响应式数据
  observed = new Proxy(target, handlers)
  // 设置原始数据与响应式数据的双向映射
  toProxy.set(target, observed)
  toRaw.set(observed, target) 
  // 如果 targetMap 里没有 入参的原始数据,那么 set 进 targetMap 里
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}
复制代码

我们可以看到 createReactiveObject 方法来生成了一个响应式数据,但是具体的收集依赖和监听函数都在 handlers 里。但 handlers 里是怎么个操作逻辑呢,这个具体等之后的文章来分析。

另外 createReactiveObject 里用到了上面我们不知所以的 targetMap 常量,那么再回头来看一下。

export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<any, Dep>
export const targetMap = new WeakMap<any, KeyToDepMap>()
复制代码

targetMap 的 key 就是我们在 createReactiveObject 方法里传入的 target,而 value 是一个 KeyToDepMap 类型的数据,是一个 any 类型的值 和 Dep 的映射。 Dep 又是一个 ReactiveEffect 类型的 Set 集合。

可以看出来 targetMap 是一个三维的数据结构。 后续如果看到 effect 部分代码的话,我们会知道 KeyToDepMap 的 key,其实是 target 对象的各个属性,而 Dep 当然就是存对应的监听函数了。

总结

其实这部分的代码比较简单,主要是几个数据映射和一些校验判断,更复杂的逻辑还是在 handlerseffect。Vue3.0 完全是使用 TS 编写,我也是初学 TS,所以读起来还是有些吃力的,所以先解读这一部分的代码,后续再继续深入。