本文主要记录我在学习vue3的reactivity部分的源码的学习路径。通过删减源码复杂的分支判断,一步步构建一个简单的vue应用,从而学习reactivity实现的逻辑。
通过图形理解大致的实现思路
- 首先
reactive
需要通过Proxy
提供代理对象访问属性的能力。 - 然后proxy拦截访问属性和改变属性,在访问属性时追踪执行函数与被代理对象的依赖关系,在修改属性时触发执行函数。
第二部分的实现很复杂,一上来先看源码对我的心智负担还是太重了,通过生动的图形能帮助我理解vue实现追踪被执行对象和执行函数大致的思路。
www.bilibili.com/video/BV1SZ...
- vue3使用
WeakMap
先建立被代理对象与具体依赖关系的映射关系,key
是被代理对象的引用,value
是用于存储每个属性依赖关系的Map
。 depsMap
负责记录被代理对象中每个属性和对应的执行函数的映射关系,key
是具体的属性,value
是dep
。dep
是所有相关的执行函数的Set
。
到此就可以带着初步的思路来构建一个简单的vue应用了。
构建vue应用
我应该怎么使用这个简易的vue?
首先先从具体使用开始,沿着reactive
和effect
这两个暴露出来的方法逐步深入。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<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
// reactive应该传入一个对象
const obj = reactive({
name: '张三'
})
// 可以调用多个 effect 方法,它们会随着name属性的改变自动触发
effect(() => {
document.querySelector('#p1').innerText = obj.name
})
effect(() => {
document.querySelector('#p2').innerText = obj.name
})
setTimeout(() => {
obj.name = '李四'
}, 2000);
</script>
</html>
对于reactive
方法,我希望直接传入一个对象。 对于effect
方法,我希望它能随着依赖的属性的改变而自动触发,而且可以调用多次不会互相扰乱。
reactiv
e方法的实现
ts
import { mutableHandlers } from './basehandlers'
/*
创建一个weakMap用于存储原对象和生成的proxy对象的映射
key: object
value: proxy
*/
export const proxyMap = new WeakMap<object, any>()
/* 创建reactive方法,返回创建响应性对象的调用值,将复杂实现隐藏在内部函数 */
export function reactive(target: object) {
return createReactiveObject(target, mutableHandlers, proxyMap)
}
/* 创建对象的proxy代理实例,最终会返回已经存在的proxy或新建的proxy */
function createReactiveObject(
target: object,
basehandlers: object,
proxyMap: WeakMap<object, any>
) {
// 在proxyMap中根据传入的对象寻找对应的proxy实例
const existProxy = proxyMap.get(target)
// 如果找到直接返回实例
if (existProxy) {
return existProxy
}
// 否则创建一个新的proxy
const proxy = new Proxy(target, basehandlers)
// 将对象和proxy的映射关系存放到WeakMap中
proxyMap.set(target, proxy)
return proxy
}
Proxy
reactive
方法在vue2与vue3中依靠不同的API实现。vue2中是利用Object.defineProperty()
通过给对象已知的属性逐一添加属性描述符从而拦截set和get操作实现监听对象的变化,而vue3中利用Proxy
实现了监听的改进,二者的具体差异可以参考下表。
vue2 | vue3 | |
---|---|---|
核心API | Object.defineProperty() |
Proxy |
新增属性 | 实现响应性是通过具体的对象的具体的属性来设置属性描述符添加setter和getter,如果新增属性是没有办法直接获取到这个属性的 | 通过Proxy生成一个实例代理对象,handler中的操作是添加到被代理对象的所有属性上的,因此新增属性不会影响响应性 |
修改对象属性 | 直接在原对象上进行修改 | 需要在代理对象上进行修改 |
Proxy
相比 Object.defineProperty()
最核心的改进就是可以直接代理整个对象,这样就在新增属性的时候就不需要额外的操作了。除此之外Proxy
也可以直接监听数组的变化,而Object.defineProperty()
需要对数组进行额外的处理。
mutableHandlers
方法负责getter
和setter
的逻辑
ts
import { track, trigger } from './effect'
// 生成setter
const get = createGetter()
function createGetter() {
return function get(target: object, key: string | symbol, receiver: object) {
// 利用Reflect得到返回值,receiver的指定保证在读取属性时指针始终在proxy对象内
const result = Reflect.get(target, key, receiver)
// 当被读取时收集该属性和effect的依赖关系,以便setter中触发effect效果
track(target, key)
return result
}
}
// 生成setter
const set = createSetter()
function createSetter() {
return function get(
target: object,
key: string | symbol,
value: unknown,
receiver: object
) {
// 利用Reflect设置新值
const result = Reflect.set(target, key, value, receiver)
// 触发依赖
trigger(target, key)
return result
}
}
export const mutableHandlers: ProxyHandler<object> = {
set,
get
}
作为Proxy
的第二个参数传入的mutableHandlers
可以在set
和get
拿到target
:传入的对象,key
:当前操作的属性,value
:set
操作时传入的新值和receiver
:当前的代理对象。
Reflect
Reflect
是JavaScript中的一个内置对象,它提供了一组静态方法,用于操作对象
通过Reflect提供的get和set方法而不是直接操作被代理对象,是因为通过传入receiver
参数可以改变this的指向为代理对象,保证对属性的所有操作都走代理对象的操作逻辑。
构建被代理对象和执行函数的依赖关系
effect
函数
ts
export function effect<T = any>(fn: () => T) {
// 生成 ReactiveEffect实例,并立即执行一次执行函数
const _effect = new ReactiveEffect(fn)
_effect.run()
}
let activeEffect: ReactiveEffect | undefined
export class ReactiveEffect<T = any> {
constructor(public fn: () => T) {}
run() {
// 为activeEffect赋值,然后触发执行函数
activeEffect = this
return this.fn()
}
}
effect
函数的作用是基于传入的执行函数生成一个ReactiveEffect实例,这样做的目的是将函数逻辑和执行操作分开,同时实例对象也便于数据存储。
targetMap的构建
ts
// targetMap 收集 响应性对象 和 某一个属性 和 属性对应的执行函数的Map 的WeakMap
/*
key: 响应性对象
value: Map
key: 指定的某一个属性
value: 当指定属性改变时需要执行的函数Set
*/
const targetMap = new WeakMap<object, KeyToDepMap>()

targetMap
负责建立被代理对象与具体依赖关系的映射关系,key
是被代理对象的引用,value
是用于存储每个属性依赖关系的Map
。使用WeakMap
的理由主要有两点:
key
可以是对象。key
是弱引用,如果key
对象没有其他的强引用时会被垃圾回收。
收集依赖和触发依赖
ts
// 根据 effects 生成 dep 实例
export const createDep = (effects?: ReactiveEffect[]): Dep => {
// 使用Set存储所有Effect效果,解决多个Effect同时存在的问题
const dep = new Set(effects) as Dep
return dep
}
/*
用于收集依赖的方法
*/
export function track(target: object, key: unknown) {
// 1
let depsMap = targetMap.get(target)
// 2
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 3
let dep = depsMap.get(key)
// 4
if (!dep) {
dep = createDep()
depsMap.set(key, dep)
}
// 5
trackEffects(dep)
}
export function trackEffects(dep: Dep) {
dep.add(activeEffect!)
}
/*
用于触发依赖的方法
*/
export function trigger(target: object, key: unknown) {
// 1
const depsMap = targetMap.get(target)
// 2
if (!depsMap) return
// 3
const dep = depsMap.get(key)
// 4
if (!dep) return
// 5
triggerEffects(dep)
}
// 通过将dep转化成数组依次触发run方法
export function triggerEffects(dep: Dep) {
// 通过 dep 获取执行函数组成的数组
const effects = isArray(dep) ? dep : [...dep]
// 一次触发
for (const effect of effects) {
triggerEffect(effect)
}
}
export function triggerEffect(effect: ReactiveEffect) {
effect.run()
}

收集依赖的主要步骤:
- 根据传入的对象查找
targetMap
中对应的depsMap
。 - 判断
depsMap
是否存在,不存在则新建一个Map
,并存入targetMap
中。 depsMap
对应的Set
可能存在多个effect
,需要根据key
获取指定的effect
。- 判断
dep
是否存在,如果不存在则新建一个dep
,同时存入depsMap
中。 - 向
dep
中添加effect
触发依赖的主要步骤:
- 根据传入的对象查找
targetMap
中对应的depsMap
。 - 如果不存在直接返回(表示set操作的是非响应式对象,不起作用)。
- 继续向下查找
depsMap
中对应key
的dep
。 - 如果
dep
不存在,表示没有执行函数,直接返回。 dep
存在,依次调用存储在Set中的各个执行函数。
到这里,这个简易vue库就实现了。整体上主要是两个模块,一个是基于Proxy
的API实现监听reactive
方法传入的对象,另一个就是在拦截get
操作时构建从对象到依赖映射再到执行函数的完整依赖关系,拦截set
操作时触发对应的依赖关系,从而调用所有相关的执行函数。