第四章节 响应式的reactive与effect实现【手摸手带你实现一个vue3】

112 阅读5分钟

大家好,我是作曲家种太阳,本次的专栏会带你一步步实现一个mini-vue3,每个小节都都回有一些测试,验证当前的一个逻辑,并且我已经把代码上传到github上了,可以根据每个章节去看对应的源码提交记录。

本章介绍循序渐进的介绍vue3的响应式系统的reactivity和effect的实现


1️⃣ 创建核心模块:reactive.ts

首先,我们在项目目录下创建响应式模块的入口文件:

packages/reactivity/src/reactive.ts

此文件中,我们先预留一个简单的 reactive 函数框架,后续再逐步完善:

import { mutableHandlers } from './baseHandlers'

export function reactive(target: object) {
  return new Proxy(target, mutableHandlers)
}

这里我们使用了 Proxy 来代理目标对象,并使用 mutableHandlers 作为拦截配置。


2️⃣ 创建响应式拦截配置:baseHandlers.ts

接下来创建 Proxy 的处理器配置:

packages/reactivity/src/baseHandlers.ts

我们先导出一个空的 ProxyHandler 对象,后续会逐步为其补充 getset 等核心逻辑。

/**
 * 响应式拦截器(待实现)
 */
export const mutableHandlers: ProxyHandler<object> = {}

3️⃣ 统一 reactivity 模块出口:index.ts

创建 index.ts 文件作为 reactivity 模块的统一出口:

// packages/reactivity/src/index.ts
export { reactive } from './reactive'

这样方便其他模块引入 reactive 方法。


4️⃣ 集成到 Vue 主入口

vue 主包中引入我们刚刚写好的响应式模块:

// packages/vue/src/index.ts
export { reactive } from '@vue/reactivity'

5️⃣ 编译打包项目

执行项目构建命令:

npm run build

这一步会将 reactive 方法打包到最终的 vue.js 中,供浏览器环境使用。


6️⃣ 创建 HTML 文件进行测试验证

新建测试文件用于验证 reactive 函数的输出:

<!-- packages/vue/examples/reactivity/reactive.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <script src="../../dist/vue.js"></script>
</head>
<body>
  <script>
    const { reactive } = Vue

    const obj = reactive({ name: '张三' })
    console.log(obj) // 👉 输出 Proxy 实例
  </script>
</body>
</html>

打开该 HTML 文件,在控制台即可看到打印出的 Proxy 实例。


mutableHandlers 的实现

mutableHandlers 是 Vue3 响应式系统中传给 Proxy 的处理器对象。它定义了数据的读取和写入行为,用于在 get 时进行依赖收集(track),在 set 时触发更新(trigger),是整个响应式运行机制的“桥梁”

import { track, trigger } from './effect'

/**
 * 响应性的 handler
 */
export const mutableHandlers: ProxyHandler<object> = {
  get: function get(target: object, key: string | symbol, receiver: object) {
    // 利用 Reflect 得到返回值
    const res = Reflect.get(target, key, receiver)
    // 收集依赖
    track(target, key)
    return res
  },
  set: function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ) {
    // 利用 Reflect.set 设置新值
    const result = Reflect.set(target, key, value, receiver)
    // 触发依赖
    trigger(target, key, value)
    return result
  }
}


 

Effect 的实现

在 reactive 创建了响应式对象之后,我们需要调用 effect(fn) 来注册副作用函数。这个函数内部会创建 ReactiveEffect 实例,并在第一次调用时自动执行传入的 fn,从而触发 getter 依赖收集。

文件路径: /packages/reactivity/src/effect.ts 注释的是computed相关的逻辑,本章节用不到

import { createDep, Dep } from './dep'
import {  isArray } from '@vue/shared'

export type EffectScheduler = (...args: any[]) => any
type KeyToDepMap = Map<any, Dep>
/**
 * 收集所有依赖的 WeakMap 实例:
 * 1. `key`:响应性对象
 * 2. `value`:`Map` 对象
 *    1. `key`:响应性对象的指定属性
 *    2. `value`:指定对象的指定属性的 执行函数
 */
const targetMap = new WeakMap<any, KeyToDepMap>()

/**
 * 单例的,当前的 effect
 */
export let activeEffect: ReactiveEffect | undefined

/**
 * 用于收集依赖的方法
 * @param target WeakMap 的 key
 * @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
 */
export function track(target: object, key: unknown) {
  // 如果当前不存在执行函数,则直接 return
  if (!activeEffect) return
  // 尝试从 targetMap 中,根据 target 获取 map
  let depsMap = targetMap.get(target)
  // 如果获取到的 map 不存在,则生成新的 map 对象,并把该对象赋值给对应的 value
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 获取指定 key 的 dep
  let dep = depsMap.get(key)
  // 如果 dep 不存在,则生成一个新的 dep,并放入到 depsMap 中
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }

  trackEffects(dep)
}

/**
 * 利用 dep 依次跟踪指定 key 的所有 effect
 * @param dep
 */
export function trackEffects(dep: Dep) {
  dep.add(activeEffect!)
}

/**
 * 触发依赖的方法
 * @param target WeakMap 的 key
 * @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
 * @param newValue 指定 key 的最新值
 * @param oldValue 指定 key 的旧值
 */
export function trigger(
  target: object,
  key?: unknown
) {
  // 依据 target 获取存储的 map 实例
  const depsMap = targetMap.get(target)
  // 如果 map 不存在,则直接 return
  if (!depsMap) {
    return
  }
  // 依据指定的 key,获取 dep 实例
  let dep: Dep | undefined = depsMap.get(key)
  // dep 不存在则直接 return
  if (!dep) {
    return
  }
  // 触发 dep
  triggerEffects(dep)
}

/**
 * 依次触发 dep 中保存的依赖
 */
export function triggerEffects(dep: Dep) {
  // 把 dep 构建为一个数组
  const effects = isArray(dep) ? dep : [...dep]
  // 依次触发
  // for (const effect of effects) {
  // 	triggerEffect(effect)
  // }

  // 不在依次触发,而是先触发所有的计算属性依赖,再触发所有的非计算属性依赖
  for (const effect of effects) {
    // if (effect.computed) {
    //   triggerEffect(effect)
    // }
  }
  for (const effect of effects) {
    // if (!effect.computed) {
    //   triggerEffect(effect)
    // }
  }
}
/**
 * 触发指定的依赖
 */
export function triggerEffect(effect: ReactiveEffect) {
  if (effect.scheduler) {
    effect.scheduler()
  } else {
    effect.run()
  }
}

/**
 * effect 函数
 * @param fn 执行方法
 * @returns 以 ReactiveEffect 实例为 this 的执行函数
 */
export function effect<T = any>(fn: () => T) {
  // 生成 ReactiveEffect 实例
  const _effect = new ReactiveEffect(fn)
  // 执行 run 函数
  _effect.run()
}


/**
 * 响应性触发依赖时的执行类
 */
export class ReactiveEffect<T = any> {
  /**
   * 存在该属性,则表示当前的 effect 为计算属性的 effect
   */
  // computed?: ComputedRefImpl<T>

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null
  ) {}

  run() {
    // 为 activeEffect 赋值
    activeEffect = this

    // 执行 fn 函数
    return this.fn()
  }

  stop() {}
}

dep 文件

  • 在Track的步骤,可以一对多收集Effect函数,
  • 这里定义了一个类型别名 Dep,它本质上是一个 Set 集合,里面存储的是多个副作用函数(effect 实例)。
  • 一个响应式属性可能会被多个 effect 所依赖,所以我们用 Set 来防止重复依赖。

文件路径: /packages/reactivity/src/dep.ts


import { ReactiveEffect } from './effect'

export type Dep = Set<ReactiveEffect>

export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set<ReactiveEffect>(effects) as Dep
  return dep
}

创建 isArray 方法

文件路径: /packages/shared/src/index.ts


/**
 * 判断是否为一个数组
 */
export const isArray = Array.isArray

修改reactive-effect-dep.html文件,并且验证

<!DOCTYPE html>
<html lang="en">

<body>
<div id="app"></div>
</body>

<head>
  <meta charset="UTF-8">
  <script src="../../dist/vue.js"></script>
</head>
<script>
  const { reactive, effect } = Vue

  const obj = reactive({
    name: '张三'
  })
  console.log(obj.name); // 此时应该触发 track
  obj.name = '李四' // 此时应该触发 trigger
  effect(() => {
    document.querySelector('#app').innerText = obj.name
  })

  console.log(obj);
</script>

</html>

🧩 小结:Vue3 响应式系统第一阶段 —— reactive + effect

这一章节我们完成了 Vue3 响应式核心机制中的第一块拼图:reactiveeffect 的实现,并构建了最基础的依赖收集与触发机制。