案例
通过以下这个案例来进行理解:
<!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="../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { reactive, effect } = Vue
const obj = reactive({
name: 'first',
age: 18
})
effect(() => {
document.querySelector('#app').innerHTML = obj.name
})
setTimeout(() => {
obj.name = 'last'
}, 2000)
</script>
</body>
</html>
首先引入 reactive 和 effect 两个函数,创建了一个响应式对象obj,并使用 effect 注册了一个副作用函数,该函数将 obj.name 的值渲染到页面上,2 秒后,我们修改了 obj.name 的值,页面也随之更新
createReactiveObject
方法
reactive
函数在packages/reactivity/src/reactive.ts
文件下,它的主要作用是将普通对象转换为响应式对象

reactive
函数实际执行的是 createReactiveObject
方法,该方法也在该文件下
- target:我们需要传入的对象
- isReadonly:要创建的代理是否为只可读
- baseHandlers 是对基本类型的劫持,即 [Object, Array]
- collectionHandlers 是对集合类型的劫持,即 [Set, Map, WeakMap, WeakSet]
- proxyMap:WeakMap 类型,用于缓存已经创建的代理对象
如果 target 不是对象类型,直接返回原值,只有对象类型的数据才能被响应式系统处理;如果 target 已经是 Proxy 类型,并且 isReadonly 为 false,直接返回目标对象本身,因为 Proxy 对象已经被处理过,无需重复处理

该函数实际做了 proxyMap
缓存处理,最终返回一个 proxy
实例对象 。这里主要关注new Proxy
这段代码,第一个参数target
为传进来的对象,即{ name: 'first', age: 18 }
,第二个baseHandlers
参数即传入的 mutableHandlers
对象 ,该对象定义在packages/reactivity/src/baseHandlers.ts
中:

该对象定义了 get、set 等方法,从而对传入的数据进行依赖收集和依赖触发 ,先看下结果后面再对这部分逻辑进行分析,reactive
函数执行完毕,obj
得到了一个 proxy
的实例对象 ,接下来执行effect
方法

effect
方法
该方法定义在packages/reactivity/src/effect.ts
文件中,它用于创建副作用函数,能够自动追踪其内部依赖的响应式数据变化,并在数据变更时重新执行 ,它是watch
、computed
等 API 的底层实现基础
(可以这样理解:副作用函数就像是一个"观察者",它观察着某些数据的变化,一旦数据变化,它就会采取行动)
fn 是需要被追踪的副作用函数,若 fn 已被 effect 包装过,则提取其原始函数。声明一个构造函数ReactiveEffect
的实例对象_effect
,执行构造函数中的run
方法

ReactiveEffect
构造函数如下:
active
:表示该副作用是否活跃,默认为true
deps
:一个数组,用于存储该副作用函数所依赖的依赖项parent
:表示该副作用的父副作用,默认为undefined
run()执行流程:若active
为false
,直接执行fn
但不收集依赖。设置activeEffect = this
,标记当前活跃的副作用,运行 fn()
,触发响应式数据的 get
拦截器,完成依赖收集
stop():标记active = false
,若当前正在执行自身(activeEffect === this
),标记deferStop
,待执行结束后清理;调用cleanupEffect(this)
从所有依赖集合中移除当前副作用
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
// 省略
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
run() {
if (!this.active) {
return this.fn()
}
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
try {
this.parent = activeEffect
activeEffect = this
shouldTrack = true
// 省略
// 执行 fn 函数 即传入的匿名函数
// () => {
// document.querySelector('#app').innerHTML = obj.name
// }
return this.fn()
} finally {
// 省略
if (this.deferStop) {
this.stop()
}
}
}
stop() {
// stopped while running itself - defer the cleanup
if (activeEffect === this) {
this.deferStop = true
} else if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
这里接收一个 fn 方法即传入的匿名函数,然后设置 active、deps、parent 等属性:

之后执行_effect.run()
,即执行构造函数ReactiveEffect
的run
方法,需要关注activeEffect = this
,此时被赋值为:

然后执行fn
函数,即执行传入的匿名函数,之后执行document.querySelector('#app').innerHTML = obj.name
触发obj
的get
方法
get/set
get
方法上述中被定义在packages/reactivity/src/baseHandlers.ts
文件中,实际触发的是createGetter
函数,主要关注**track(target, TrackOpTypes.GET, key)
** 这段代码,它是对数据的依赖收集, 也是get
方法的核心
const get = /*#__PURE__*/ createGetter()
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 省略
const targetIsArray = isArray(target)
// 省略
// Reflect API
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
// Reflect.get() 等同于 res = target[key]
// Reflect 用来替代直接调用 Object 的方法
const res = Reflect.get(target, key, receiver)
// 省略
if (!isReadonly) {
// 核心,添加依赖收集
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
track
函数被定义在packages/reactivity/src/effect.ts
文件中,这里的targetMap
为WeakMap
对象,该对象是一个弱引用类型
以传入的对象{ name: 'first', age: 18}
作为key
,value
值为Map
对象,之后设置depsMap
,key
当前为name
,value
为Set
对象,最后执行trackEffects(dep, eventInfo)
:

trackEffects
是用于依赖收集的核心函数,将当前活跃的副作用函数 activeEffect
与依赖集合 dep
关联起来,从而实现当依赖数据变化时能够触发副作用函数的重新执行

此时再看下targetMap
对象数据:

这样就完成了数据的依赖收集,之后就可以通过指定对象指定属性获取到对应的 fn
方法 。而依赖收集本质上就是targetMap
和ReactiveEffect
之间的关联
createGetter
执行完毕返回对应的值,当前为 first:

两秒后执行obj.name = 'last'
,触发set
方法,该方法定义在packages/reactivity/src/baseHandlers.ts
中,主要关注trigger(target, TriggerOpTypes.SET, key, value, oldValue)
这行代码,键存在且新值与旧值不同,触发 set
类型的依赖更新

trigger方法
该方法被定义在packages/reactivity/src/effect.ts
文件中:
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// 根据传入的对象 获取 对应的 Map 对象
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
// 根据属性获取对应的 ReactiveEffect
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
// 省略
// 当前为 set 类型
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
根据指定对象获取到对应的Map
对象,此时depsMap
为:

之后再根据指定属性获取对应的ReactiveEffect
,再添加到deps
中,此时deps
为:

后面我们只需关注triggerEffects(deps[0], eventInfo)
这行代码, triggerEffects
函数也packages/reactivity/src/effect.ts
文件中:

可以看出triggerEffects
函数实际先获取到 effect
数组,遍历数组执行每个 effect.run()
,实际执行的是 fn
方法 ,该方法是最初依赖收集时传入的匿名函数,之后再次触发getter
方法,从而进行赋值,至此整个依赖触发完成

总结
1)reactive 函数与 createReactiveObject 方法
reactive
函数实际执行了createReactiveObject
方法,该方法主要创建了一个Proxy
实例对象,给代理对象添加了getter
和setter
行为
getter
和setter
方法定义在mutableHandlers
对象中,用于拦截对象属性的获取和设置操作
2)get 方法与依赖收集
get
方法实际执行了createGetter
方法,在这个过程中,track
函数被调用,用于进行依赖收集
依赖收集的过程是构建一个WeakMap
(targetMap
)对象,完成指定对象obj
的指定属性name
到effect
的依赖收集工作
3)effect 函数与 ReactiveEffect 实例
effect
函数实际创建了一个ReactiveEffect
实例,该构造函数接收一个fn
函数(即传进来的匿名函数),该回调函数必须暴露getter
行为
在ReactiveEffect
的run
函数中,给activeEffect
赋值,并执行fn
函数
4)依赖收集的具体过程
当访问obj.name
时,getter
被触发,激活track
方法,该方法构建WeakMap
(targetMap
)对象,记录下当前活跃的副作用函数与obj.name
的依赖关系
5)set 方法与依赖触发
set
方法实际执行了createSetter
方法,触发trigger
函数进行依赖触发
trigger
函数从之前targetMap
依赖收集的对象中获取,根据key
(即name
)获取到对应的副作用函数,然后执行fn
函数,从而完成一个依赖触发的过程
6)依赖触发的具体过程
当修改obj.name
时,setter
被触发,激活trigger
方法,它通知所有依赖于obj.name
的副作用函数重新执行,从而更新视图
reactive
缺陷:
- 解构后不支持响应性
- 不支持基本类型,只能是对象