手摸手带你阅读Vue3源码之Reactive 上

前言

你是否曾想过,Vue3 中的 reactive 究竟是如何在幕后管理数据和视图更新的?本文将带你深入挖掘 Vue3 响应式系统的精髓,从源码中解开 reactive 的工作原理,帮助你在实际开发中更好地运用这一强大的功能。

学习 reactive 源码,你可以获得以下技能和知识:

  1. 深入理解 JavaScript 中的 ProxyReflect,掌握它们的使用和优势。
  2. 学会如何在响应式系统中追踪依赖,触发视图更新,实现 发布订阅模式
  3. 了解 Vue 如何处理深度嵌套的对象,并支持浅响应和深响应模式。
  4. 学到性能优化的技巧,如何高效地管理依赖和更新。

基础步骤

1. 拉取 Vue3 最新源码并运行

首先,拉取 Vue3 的源码并安装依赖:

arduino 复制代码
git clone git@github.com:vuejs/core.git
​
// 安装依赖
pnpm install
// 运行 运行完成后会有提示
// built: packages\vue\dist\vue.global.js
pnpm run dev
2. 在源码目录 packages/reactivity 下创建 Demo

在源码目录下创建一个简单的 HTML 页面,引入 vue.global.js,并测试 reactive

js 复制代码
<!DOCTYPE html>
<html lang="en">
​
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../vue/dist/vue.global.js"></script>
</head>
​
<body>
    <div id="app"></div>
    <script>
        const { reactive } = Vue
​
        const obj = reactive({
            name: 'JiangJiang'
        })
​
        setTimeout(() => {
            obj.name = '帅哥-JiangJiang'
        }, 2000)
    </script>
</body>
​
</html>
3. 断点创建reactive对象位置以及改变值的位置,快速过一遍,看看用到了源码中的哪些方法

核心方法、类

下面是创建 reactive 对象以及修改时所用到的关键方法和类,我们逐个解析,先了解完用到的方法,再去进行断点调试加深了解

1.reactive创建对象阶段

js 复制代码
// packages\reactivity\src\reactive.ts
​
export function reactive<T extends object>(target: T): Reactive<T>
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,
  )
}
  • 这里定义了reactive方法,先判断 target 是否为只读,只读属性则直接返回
  • 否则return调用createReactiveObject方法

2.createReactiveObject函数

js 复制代码
function createReactiveObject(
  target: Target,  // 目标对象
  isReadonly: boolean,  // 是否是只读模式
  baseHandlers: ProxyHandler<any>,  // 基础对象的 Proxy 处理器
  collectionHandlers: ProxyHandler<any>,  // 集合类型的 Proxy 处理器 如 Map、Set、WeakMap
  proxyMap: WeakMap<Target, any>  // 存储 Proxy 对象的 Map,防止重复创建
) {
  // 1. 如果目标不是对象,则直接返回原值
  if (!isObject(target)) {
    if (__DEV__) {
      warn(
        `value cannot be made ${isReadonly ? 'readonly' : 'reactive'}: ${String(
          target,
        )}`  
      )
    }
    return target  
  }
​
  // 2. 如果目标已经是 Proxy 对象,直接返回
  if (
    target[ReactiveFlags.RAW] &&  
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])  
  ) {
    return target  
  }
​
  // 3. 判断目标对象类型
  const targetType = getTargetType(target)  // 获取目标对象的类型(普通对象或集合)
  if (targetType === TargetType.INVALID) {  // 如果是无效类型(如函数或原始类型),直接返回
    return target
  }
​
  // 4. 如果目标对象已经有对应的 Proxy,直接返回现有的 Proxy
  const existingProxy = proxyMap.get(target)  
  if (existingProxy) {
    return existingProxy  
  }
​
  // 5. 创建新的 Proxy 实例
  const proxy = new Proxy(
    target,  
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers  // 根据对象类型选择不同的 Proxy 处理器
  )
​
  // 6. 将新的 Proxy 缓存到 proxyMap
  proxyMap.set(target, proxy)  
​
  return proxy  
}
createReactiveObject函数的主要目的是:
  • 检查目标对象是否有效:只有对象可以变成响应式,如果传入的是非对象类型的值(比如原始值),函数会返回原值。
  • 避免重复创建响应式对象 :如果目标对象已经是一个 Proxy,则直接返回它。
  • 为目标对象创建 Proxy :根据不同的目标类型(普通对象或集合对象),为其创建一个新的 Proxy 实例。

3.小讲下createReactiveObject函数中const targetType = getTargetType(target)

js 复制代码
function getTargetType(value: Target) {
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}
​
function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}
  • 这段代码主要用于确定目标对象的类型,并通过不同类型的判断返回相应的类型标识。它通过 getTargetType 函数来获取目标对象的类型,然后根据类型返回不同的响应式处理方式。
  • 在创建Proxy实例时,会根据targetType的值去决定创建的是基础对象的 Proxy 处理器还是集合类型的 Proxy 处理器

5.再去回顾下reactive方法

js 复制代码
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时进行了传参,因为我们定义的对象是普通对象,基于createReactiveObject函数里的创建proxy场景,可发现最终创建Proxy对象的处理器是mutableHandlers
js 复制代码
  const proxy = new Proxy(
    target,  
    // 根据对象类型选择不同的 Proxy 处理器
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )

6.mutableHandlers方法

这时候文件来到了baseHandlers.ts,根据源码我们可以发现mutableHandlers方法返回了MutableReactiveHandler类的一个实例

js 复制代码
// packages\reactivity\src\baseHandlers.ts
​
// 创建了个MutableReactiveHandler示例
export const mutableHandlers: ProxyHandler<object> =
  /*@__PURE__*/ new MutableReactiveHandler()

7.MutableReactiveHandler类

js 复制代码
​
class MutableReactiveHandler extends BaseReactiveHandler {
  constructor(isShallow = false) {
    super(false, isShallow)
  }
​
  set(
    target: Record<string | symbol, unknown>,
    key: string | symbol,
    value: unknown,
    receiver: object,
  ): boolean {
    let oldValue = target[key]
    if (!this._isShallow) {
      const isOldValueReadonly = isReadonly(oldValue)
      if (!isShallow(value) && !isReadonly(value)) {
        oldValue = toRaw(oldValue)
        value = toRaw(value)
      }
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        if (isOldValueReadonly) {
          return false
        } else {
          oldValue.value = value
          return true
        }
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }
​
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(
      target,
      key,
      value,
      isRef(target) ? target : receiver,
    )
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
​
  deleteProperty(
    target: Record<string | symbol, unknown>,
    key: string | symbol,
  ): boolean {
    const hadKey = hasOwn(target, key)
    const oldValue = target[key]
    const result = Reflect.deleteProperty(target, key)
    if (result && hadKey) {
      trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
    }
    return result
  }
​
  has(target: Record<string | symbol, unknown>, key: string | symbol): boolean {
    const result = Reflect.has(target, key)
    if (!isSymbol(key) || !builtInSymbols.has(key)) {
      track(target, TrackOpTypes.HAS, key)
    }
    return result
  }
​
  ownKeys(target: Record<string | symbol, unknown>): (string | symbol)[] {
    track(
      target,
      TrackOpTypes.ITERATE,
      isArray(target) ? 'length' : ITERATE_KEY,
    )
    return Reflect.ownKeys(target)
  }
}
下面对里面的各个方法进行详解

8.set 方法

js 复制代码
set(
  target: Record<string | symbol, unknown>,  // 目标对象
  key: string | symbol,                      // 目标属性
  value: unknown,                            // 新的属性值
  receiver: object,                          // 接收者对象(通常是 Proxy)
): boolean {
  let oldValue = target[key]  // 获取当前属性的旧值
​
  if (!this._isShallow) {  // 如果不是浅响应,进行深层响应的处理
    const isOldValueReadonly = isReadonly(oldValue)  // 判断旧值是否是只读的
​
    if (!isShallow(value) && !isReadonly(value)) {  // 如果新值和旧值都不是浅层响应和只读
      oldValue = toRaw(oldValue) 
      value = toRaw(value) 
    }
​
    if (!isArray(target) && isRef(oldValue) && !isRef(value)) {  // 如果旧值是 ref 类型而新值不是 ref
      if (isOldValueReadonly) {  // 如果旧值是只读的,直接返回 false
        return false
      } else {
        oldValue.value = value  // 更新 ref 的值
        return true
      }
    }
  }
​
  // 判断目标对象是否已经存在该属性
  const hadKey = 
    isArray(target) && isIntegerKey(key)  // 如果是数组且 key 是有效索引
      ? Number(key) < target.length       // 判断索引是否小于数组的长度
      : hasOwn(target, key)  // 检查该属性是否存在于目标对象上
​
  // 使用 Reflect.set 执行属性赋值
  const result = Reflect.set(
    target,
    key,
    value,
    isRef(target) ? target : receiver,  // 如果目标对象是 ref,则传入目标对象,否则传入接收者
  )
​
  // 如果目标对象没有被代理(即没有包装成 Proxy)
  if (target === toRaw(receiver)) {
    if (!hadKey) {
      // 如果属性是新增的,触发 ADD 操作
      trigger(target, TriggerOpTypes.ADD, key, value)
    } else if (hasChanged(value, oldValue)) {
      // 如果属性值发生变化,触发 SET 操作
      trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    }
  }
  return result
}

拦截对象的属性修改操作,这个方法处理了深响应和浅响应的逻辑,确保在修改对象属性时能够更新视图。

9.deleteProperty 方法

js 复制代码
deleteProperty(
  target: Record<string | symbol, unknown>,  // 目标对象
  key: string | symbol,                      // 要删除的属性
): boolean {
  const hadKey = hasOwn(target, key)  // 检查目标对象是否包含该属性
  const oldValue = target[key]       // 获取删除前的旧值
  const result = Reflect.deleteProperty(target, key)  // 执行删除操作
​
  if (result && hadKey) {
    // 如果删除操作成功且属性确实存在
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

拦截对象属性的删除操作。当某个属性被删除时,需要进行依赖更新。

10.has 方法

js 复制代码
has(target: Record<string | symbol, unknown>, key: string | symbol): boolean {
  const result = Reflect.has(target, key)  // 判断目标对象是否包含该属性
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    // 如果 key 不是内建的 Symbol,则进行依赖追踪
    track(target, TrackOpTypes.HAS, key) 
  }
  return result  // 返回 Reflect.has 的结果
}

拦截对象的 has 操作(如 key in target)。在检查目标对象是否包含某个属性时,进行依赖追踪。

11.ownKeys 方法

js 复制代码
ownKeys(target: Record<string | symbol, unknown>): (string | symbol)[] {
  track(
    target,
    TrackOpTypes.ITERATE,
    isArray(target) ? 'length' : ITERATE_KEY,  // 如果是数组,则跟踪 'length',否则跟踪 ITERATE_KEY
  )
  return Reflect.ownKeys(target)  // 获取目标对象的所有属性键
}

拦截对象的 ownKeys 操作(如 Object.keys(target))。当获取对象的属性键时,进行依赖追踪。

总结与思考

通过上述代码,我们了解了 Vue3 如何通过 ProxyReflect 实现响应式数据的追踪与更新。reactive 方法通过创建代理对象,能够监听对象属性的变化,并自动触发视图更新。

下篇文章我们将对MutableReactiveHandler类所用到的toRaw、track、trigger方法进行讲解,捋清vue3发布订阅模式的思路,以及正式在浏览器中断点调试创建reactive对象位置以及改变值的流程

相关推荐
fengfeng N26 分钟前
AxiosError: Network Error
前端·https·axios·跨域换源
StarPlatinum230 分钟前
CSS实现一张简易的贺卡
前端·css
Stestack38 分钟前
Python 给 Excel 写入数据的四种方法
前端·python·excel
SRC_BLUE_1741 分钟前
[Web 安全] PHP 反序列化漏洞 —— PHP 序列化 & 反序列化
前端·安全·web安全·php
IT猿手1 小时前
智能优化算法:雪橇犬优化算法(Sled Dog Optimizer,SDO)求解23个经典函数测试集,MATLAB
开发语言·前端·人工智能·算法·机器学习·matlab
扫地僧0091 小时前
Java 面试题及答案整理,最新面试题
java·jvm·算法·面试
Cheese%%Fate1 小时前
【C++】面试常问八股
c++·面试
windyrain2 小时前
基于 Ant Design Pro 实现表格分页与筛选参数的持久化
前端·javascript·react.js
懒人村杂货铺2 小时前
父子组件事件冒泡和捕获的顺序
前端·javascript·react.js
小刘不知道叫啥2 小时前
React 源码揭秘 | 更新队列
前端·react.js·前端框架