大家好,我是作曲家种太阳,本次的专栏会带你一步步实现一个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 对象,后续会逐步为其补充 get、set 等核心逻辑。
/**
* 响应式拦截器(待实现)
*/
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 响应式核心机制中的第一块拼图:reactive 与 effect 的实现,并构建了最基础的依赖收集与触发机制。