从 0 搭建一个 mini-vue 项目(二):初见 reactivity 模块
前言
从本章开始我们将开始实现 Vue3 中的 reactivity 模块
接下来我们看一段代码:
<body>
<div id="app"></div>
</body>
<script>
// 从 Vue 中结构出 reactie、effect 方法
const { reactive, effect } = Vue
// 声明响应式数据 obj
const obj = reactive({
name: '张三'
})
// 调用 effect 方法
effect(() => {
document.querySelector('#app').innerText = obj.name
})
// 定时修改数据,视图发生变化
setTimeout(() => {
obj.name = '李四'
}, 2000)
</script>
上面的代码很简单大家应该也都会写,最终的效果就是视图中的 张三 在 2 秒钟之后变成了 李四。但是大家有没有想过这是为什么呢?让我们来从源码中一探究竟吧~
1. 源码阅读
重要提示:我这里的 vue 的版本是 3.2.27,并且本系列中用的都是这个版本
1.1 reactive 部分
- 我们直接到
vue的源码路径/packages/reactivity/src/reactive.ts中的第90行找到reactive方法,并打上断点。

- 发现
reactive其实直接返回了一个createReactiveObject,听名字就这个方法是在创建一个reactive对象,接着跳转进这个方法。

- 可以看到
createReactiveObject这个方法其实就是返回了一个Proxy对象。 - 有两个点可以提的是:一个点是这里维护了一个
proxyMap对象用来缓存之前已经创建过的响应式对象,而他是一个 WeakMap 类型; 另一个点是在源码第214行 创建proxy的baseHandler,它来自上面reactive方法中返回的mutableHandlers,而mutableHandlers导入自baseHandlers.ts文件,这个我们后面说。

至此 reactive 方法执行完成。总结 reactive 的逻辑:1. 创建了 proxy。 2.把 proxy 加到了 proxyMap 里面。3. 返回了 proxy
1.2 effect 部分
- 接着我们来到
effect方法,我们直接到vue的源码路径/packages/reactivity/src/effect.ts中的第170行找到effect方法,并打上断点。

- 可以发现
effect方法内其实就只是创建了一个ReactiveEffect对象,并且执行了一次它的run方法,再将run方法返回。我们直接跳到run看代码。

- 调试发现,
run方法里只做了上图框框圈出来的两件事。但是大家不要忘记,fn函数 中的代码为document.querySelector('#app').innerText = obj.name,obj是个proxy,obj.name会触发getter,所以接下来我们就会进入到mutableHandlers的get中, 而get为createGetter函数的调用返回值,所以我们直接跳到createGetter中 - 调试得知,
createGetter方法中最主要做了两件事,一是调用const res = Reflect.get(target, key, receiver),res此时是 张三, 然后将res返回。二是触发了track函数,这个函数是一个重点函数,track在此为跟踪的意思。接下来我们看看里面发生了什么。
9. 可以看到 track 里面主要做了两件事,一是为 targetMap 赋值,targetMap 的结构是一个 Map 套 Set 的结构(createDep 方法实际是返回了一个 Set);二是执行了 trackEffects 方法。我们来看一下这个方法里做了什么。

- 可以看到在
trackEffects函数内部,核心也是做了两件事情:一是为dep(targetMap[target][key] 得到的 Set 实例)添加了activeEffect,这个activeEffect第6步有讲,就一个ReactiveEffect对象,里面存了fn函数;二是为activeEffect函数的 静态属性deps,增加了一个值dep,即建立起了dep和activeEffect的联系.
至此,整个 track 的核心逻辑执行完成。我们可以把整个 track 的核心逻辑说成:收集了 activeEffect(即:fn)
- 最后在
createGetter函数中返回了res(即:张三)
至此,整个 effect 执行完成。总结 effect 的逻辑:1.生成 ReactiveEffect 实例。2.触发 fn 方法,从而激活 getter。3.建立了 targetMap 和 activeEffect 之间的联系
1.3 obj.name = xx 部分
- 接着我们继续调试程序,两秒钟之后,
setTimeout触发,会执行obj.name = '李四',从而触发proxy的set。所以接下来我们就会进入到mutableHandlers的set中, 而set为createSetter函数的调用返回值,所以我们直接跳到createSetter中
13. createSetter 中主要做是有:1.创建变量: oldValue = 张三。2.创建变量:value = 李四。3.执行 const result = Reflect.set(target, key, value, receiver),即:修改了 obj 的值为 “李四”。4.触发:trigger(target, TriggerOpTypes.SET, key, value, oldValue)。trigger 在这里为 触发 的意思,我们来看看 trigger 里面做了什么
14. trigger 主要做了上图中框起来的三件事,我们再来看看 triggerEffect 做了什么?

-
可以看到
triggerEffect其实就是调用了run方法,这一次进入run方法,执行了一下步骤:1. 首先还是为activeEffect = this赋值。2.最后执行this.fn()即:effect时传入的匿名函数。3.至此,fn执行,意味着:document.querySelector('#app').innerText = 李四,页面将发生变化。 -
triggerEffect完成triggerEffects完成trigger完成setter回调完成
至此,整个 setter 执行完成。总结 setter:1.修改 obj 的值。2.触发 targetMap 下保存的 fn 函数
1.4 总结
到这里,我们在前言中的代码已经从源码层面上全部分析完了,我们现在总结一下:
reactive函数effect函数obj.name = xx表达式
这三块代码背后,vue 究竟都做了什么。虽然整个的过程比较复杂,但是如果我们简单来去看,其实内部的完成还是比较简单的:
- 创建
proxy - 收集
effect的依赖 - 触发收集的依赖
接下来,我们的实现,就将会围绕着这三个核心的理念进行。
2. 框架实现
2.1 构建 reactive 函数,获取 proxy 实例
- 创建
packages/reactivity/src/reactive.ts模块:
import { mutableHandlers } from './baseHandlers'
/**
* 响应性 Map 缓存对象
* key:target
* val:proxy
*/
export const reactiveMap = new WeakMap<object, any>()
/**
* 为复杂数据类型,创建响应性对象
* @param target 被代理对象
* @returns 代理对象
*/
export function reactive(target: object) {
return createReactiveObject(target, mutableHandlers, reactiveMap)
}
/**
* 创建响应性对象
* @param target 被代理对象
* @param baseHandlers handler
*/
function createReactiveObject(
target: object,
baseHandlers: ProxyHandler<any>,
proxyMap: WeakMap<object, any>
) {
// 如果该实例已经被代理,则直接读取即可
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 未被代理则生成 proxy 实例
const proxy = new Proxy(target, baseHandlers)
// 缓存代理对象
proxyMap.set(target, proxy)
return proxy
}
- 创建
packages/reactivity/src/baseHandlers.ts模块:
/**
* 响应性的 handler
*/
export const mutableHandlers: ProxyHandler<object> = {}
-
此时我们就已经构建好了一个基本的
reactive方法,接下来我们可以通过 测试案例 测试一下。 -
创建
packages/reactivity/src/index.ts模块,作为reactivity的入口模块
export { reactive } from './reactive'
- 在
packages/vue/src/index.ts中,导入reactive模块
export { reactive } from '@vue/reactivity'
-
执行
npm run build进行打包,生成vue.js -
创建
packages/vue/examples/reactivity/reactive.html文件,作为测试实例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="../../dist/vue.js"></script>
</head>
<script>
const { reactive } = Vue
const obj = reactive({
name: '张三'
})
console.log(obj)
</script>
</html>
- 运行到
Live Server可见打印了一个proxy对象实例
至此我们已经得到了一个基础的 reactive 函数
2.2 createGetter && createSetter
接下来我们需要创建对应的 get 和 set 监听:
/**
* 响应性的 handler
*/
export const mutableHandlers: ProxyHandler<object> = {
get,
set
}
getter
/**
* getter 回调方法
*/
const get = createGetter()
/**
* 创建 getter 回调方法
*/
function createGetter() {
return function get(target: object, key: string | symbol, receiver: object) {
// 利用 Reflect 得到返回值
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
return res
}
}
setter
/**
* setter 回调方法
*/
const set = createSetter()
/**
* 创建 setter 回调方法
*/
function createSetter() {
return 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
}
}
track && trigger
在 getter 和 setter 中分别调用了 track && trigger 方法,所以我们需要分别创建对应方法:
- 创建
packages/reactivity/src/effect.ts:
/**
* 用于收集依赖的方法
* @param target WeakMap 的 key
* @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
*/
export function track(target: object, key: unknown) {
console.log('track: 收集依赖')
}
/**
* 触发依赖的方法
* @param target WeakMap 的 key
* @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
* @param newValue 指定 key 的最新值
* @param oldValue 指定 key 的旧值
*/
export function trigger(target: object, key?: unknown, newValue?: unknown) {
console.log('trigger: 触发依赖')
}
至此我们就可以:
- 在
getter时,调用track收集依赖 - 在
setter时,调用trigger触发依赖
我们可以在两个方法中分别进行一下打印,看看是否可以成功回调。
测试
在 packages/vue/examples/reactivity/reactive.html 中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="../../dist/vue.js"></script>
</head>
<script>
const { reactive } = Vue
const obj = reactive({
name: '张三'
})
console.log(obj.name) // 此时应该触发 track
obj.name = '李四' // 此时应该触发 trigger
</script>
</html>
2.3 构建 effect 函数,生成 ReactiveEffect 实例
根据之前的测试实例我们知道,在创建好了 reactive 实例之后,接下来我们需要触发 effect:
- 在
packages/reactivity/src/effect.ts中,创建effect函数:
/**
* effect 函数
* @param fn 执行方法
* @returns 以 ReactiveEffect 实例为 this 的执行函数
*/
export function effect<T = any>(fn: () => T) {
// 生成 ReactiveEffect 实例
const _effect = new ReactiveEffect(fn)
// 执行 run 函数
_effect.run()
}
- 接下来我们来实现
ReactiveEffect的基础逻辑:
/**
* 单例的,当前的 effect
*/
export let activeEffect: ReactiveEffect | undefined
/**
* 响应性触发依赖时的执行类
*/
export class ReactiveEffect<T = any> {
constructor(public fn: () => T) {}
run() {
// 为 activeEffect 赋值
activeEffect = this
// 执行 fn 函数
return this.fn()
}
}
- 在
packages/reactivity/src/index.ts导出
export { effect } from './effect'
- 在
packages/vue/src/index.ts中 导出
export { reactive, effect } from '@vue/reactivity'
根据以上代码可知,最终 vue 会执行 effect 传入的 回调函数,即:
document.querySelector('#app').innerText = obj.name
那么此时,obj.name 的值,应该可以被渲染到 html 中。
所以,我们可以到测试实例中,完成一下测试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const { reactive, effect } = Vue
const obj = reactive({
name: '张三'
})
// 调用 effect 方法
effect(() => {
document.querySelector('#app').innerText = obj.name
})
</script>
</html>
此时,我们成功 渲染了数据到 html 中,那么接下来我们需要做的就是:当 obj.name 触发 setter 时,修改视图,以此就可实现 响应性数据变化。
所以,下面我们就需要分别处理 getter 和 setter 对应的情况了。
2.4 构建 track 依赖收集
在 packages/reactivity/src/effect.ts 写入如下代码:
type KeyToDepMap = Map<any, ReactiveEffect>
/**
* 收集所有依赖的 WeakMap 实例:
* 1. `key`:响应性对象
* 2. `value`:`Map` 对象
* 1. `key`:响应性对象的指定属性
* 2. `value`:指定对象的指定属性的 执行函数
*/
const targetMap = new WeakMap<any, KeyToDepMap>()
/**
* 用于收集依赖的方法
* @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()))
}
//为指定 map,指定key 设置回调函数
depsMap.set(key, activeEffect)
// 临时打印
console.log(targetMap)
}
此时运行测试函数,查看打印的 targetMap,可得以下数据:

2.5 构建 trigger 触发依赖
在 packages/reactivity/src/effect.ts 中
/**
* 触发依赖的方法
* @param target WeakMap 的 key
* @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
*/
export function trigger(target: object, key?: unknown) {
// 依据 target 获取存储的 map 实例
const depsMap = targetMap.get(target)
// 如果 map 不存在,则直接 return
if (!depsMap) {
return
}
// 依据 key,从 depsMap 中取出 value,该 value 是一个 ReactiveEffect 类型的数据
const effect = depsMap.get(key) as ReactiveEffect
// 如果 effect 不存在,则直接 return
if (!effect) {
return
}
// 执行 effect 中保存的 fn 函数
effect.fn()
}
此时,我们就可以在触发 setter 时,执行保存的 fn 函数了。
那么接下来我们实现对应的测试实例,在 packages/vue/examples/reactivity/reactive.html 中:
<script>
const { reactive, effect } = Vue
const obj = reactive({
name: '张三'
})
// 调用 effect 方法
effect(() => {
document.querySelector('#app').innerText = obj.name
})
setTimeout(() => {
obj.name = '李四'
}, 2000)
</script>
运行测试实例,等待两秒,发现 视图发生变化
那么,至此我们就已经完成了一个简单的 响应式依赖数据处理
2.6 构建 Dep 模块,处理一对多的依赖关系
在我们之前的实现中,还存在一个小的问题,那就是:每个响应性数据属性只能对应一个 effect 回调
现象:如果我们新增了一个 effect 函数,即:name 属性对应两个 DOM 的变化。更新渲染就会变无效。
原因:因为我们在构建 KeyToDepMap 对象时,它的 Value 只能是一个 ReactiveEffect,所以这就导致了 一个 key 只能对应一个有效的 effect 函数。
解决方法:将 value 变为一个 Set 类型。可以把它叫做 Dep ,通过 Dep 来保存 指定 key 的所有依赖
- 创建
packages/reactivity/src/dep.ts模块:
import { ReactiveEffect } from './effect'
export type Dep = Set<ReactiveEffect>
/**
* 依据 effects 生成 dep 实例
*/
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects) as Dep
return dep
}
- 在
packages/reactivity/src/effect.ts修改KeyToDepMap的泛型:
import { Dep } from './dep'
type KeyToDepMap = Map<any, Dep>
- 修改
track方法,处理Dep类型数据:
/**
* 用于收集依赖的方法
* @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!)
}
此时,我们已经把指定 key 的所有依赖全部保存到了 dep 函数中,那么接下来我们就可以在 trigger 函数中,依次读取 dep 中保存的依赖。
- 在
packages/reactivity/src/effect.ts中:
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 = Array.isArray(dep) ? dep : [...dep]
// 依次触发
for (const effect of effects) {
triggerEffect(effect)
}
}
/**
* 触发指定的依赖
*/
export function triggerEffect(effect: ReactiveEffect) {
effect.run()
}
至此,我们即可在 trigger 中依次触发 dep 中保存的依赖
测试
- 创建
packages/vue/examples/reactivity/reactive-dep.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
<p id="p1"></p>
<p id="p2"></p>
</div>
</body>
<script>
const { reactive, effect } = Vue
const obj = reactive({
name: '张三'
})
// 调用 effect 方法
effect(() => {
document.querySelector('#p1').innerText = obj.name
})
effect(() => {
document.querySelector('#p2').innerText = obj.name
})
setTimeout(() => {
obj.name = '李四'
}, 2000)
</script>
</html>
发现两个 p 标签中的内容最后都变成了 李四
3. 总结
在本章,我们初次了解了 reactivity 这个模块,并且在该模块中构建了 reactive 响应性函数。
对于 reactive 的响应性函数而言,我们知道它:
- 是通过
proxy的setter和getter来实现的数据监听 - 需要配合
effect函数进行使用 - 基于
WeakMap完成的依赖收集和处理 - 可以存在一对多的依赖关系
但同时 reactive 函数也存在一些不足,比如:
reactive只能对 复杂数据 类型进行使用reactive的响应性数据,不可以进行解构
因为 reactive 的不足,所以 vue 3 又为我们提供了 ref 函数构建响应性。
关于 ref 函数是如何实现的,就留到下一章去学习吧~
转载自:https://juejin.cn/post/7183223303031488572