持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情
前言
上篇讲了 reactive 方法的作用和源码,这篇我们讲讲副作用函数是什么。
副作用函数的作用
在 Vue3 中是如何追踪数据的变化呢?其中起到作用的就是副作用函数 effect 副作用是一个函数包裹器,在函数被调用前就启动跟踪,而 Vue3 在派发更新时就能准确的找到这些被收集起来的副作用函数,当数据发生更新时再次执行它。
使用
let foo
const counter = reactive({ num: 0 })
effect(() => (foo = counter.num))
// 此时 foo 应该是 0
counter.num = 7
// 此时 foo 应该是 7
现在创建了一个响应式对象 counter,然后创建了一个副作用函数,将 counter.num 赋值给 foo,这是foo会初始化为 0,而 foo 也会被 counter 收集为一个依赖,而 () => (foo = counter.num) 就是它的更新方法。
如果这个时候我对 counter.num 进行赋值 7,会触发 set 陷阱,在set中触发依赖,执行更新方法,也就是 () => (foo = counter.num) 从而使 foo 的值变为 7
effect
effect 文件中有2个全局变量很重要
// packages/reactivity/src/effect.ts
export type Dep = Set<ReactiveEffect> & TrackedMarkers
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
let activeEffect: ReactiveEffect | undefined
targetMap
targetMap 是一个非常重要的变量,它是 WeakMap 类型,从上面的类型就能看出,它是存储了一个 {target -> Key -> Dep} 的链接。
它的值是一个 KeyToDepMap 而 KeyToDepMap 是一个以依赖 Dep 为值的 Map 对象,我们一直在说的依赖收集就是在收集 Dep 类型的 Set 对象。
举一个例子:
targetMap: {
// key 是对象,value 是 depsMap
{age: 25} : {
// key 是对象里边的 key, value 是 dep
age: [ ...此处存储一个个依赖 ]
}
}
activeEffect
activeEffect 这个变量标记了当前正在执行的副作用,或者也可以理解为 effect 栈中的栈顶元素。 当一个副作用被压入栈时,会将这个副作用赋值给 activeEffect 变量,而当副作用中的函数执行完后该副作用会出栈,并将 activeEffect 赋值为栈的下一个元素。所以当栈中只有一个元素时,执行完出栈后,activeEffect 就会为 undefined。
整体流程
整个追踪的过程大致是这样的,比如我先有一个响应式对象 const target = {age: 25}。
- 当依赖收集的时候,会在 targetMap 中创建一个键值对,
targetMap.set(target, (depsMap = new Map())) - 然后再把 key 值保存到 depsMap 中
depsMap.set('age', new Set())这个 new Set() 保存的就是我们的依赖 - 当我现在需要派发更新的时候,就通过 target 和它的key age,找到对应的依赖 new Set(),然后在一个个的去执行更新方法。
ReactiveEffect
我们了解完大概的流程后,再看看 effect 的具体实现。
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
const _effect = new ReactiveEffect(fn)
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
- 首先通过 类
ReactiveEffect,传进去的一个方法 fn,从而创建一个 effect,这个fn就是我们开头例子中的() => (foo = counter.num) - 然后判断用户是否传了 options,如果 options.lazy 不为 true 时,就先执行一次 effect.run()
- 最后把 effect.run 绑定 this 到 effect,并返回 run 方法。
其实到这里就能够猜到一点,我们传进去的 fn 在 effect.run 中会被执行。
再来看下类 ReactiveEffect 的实现
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
computed?: ComputedRefImpl<T>
allowRecurse?: boolean
private deferStop?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
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
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined
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
}
}
}
到这里 effect 的属性和方法我们就都能看到了,在 run 方法中,设置了 activeEffect 为当前的 effect,并执行了 fn 方法。
每次我们派发更新时都会调用这个run方法,从而更新值。
小结
今天我们了解了 effect 的用法和作用,并了解到了依赖收集到派发更新的具体流程,了解了这些后,下一节我们就能详细看看如何进行依赖收集,和派发更新。