vue3中的effect
函数是响应式的核心,它被称作副作用函数
,那么什么是副作用函数呢?我们来看一下。
vue中,数据target改变,可能会引起其他数据或者视图发生变化,那么,其他数据改变以及视图改变这些效果就是target改变的副作用。像watch、computed、这些都是会产生副作用的函数,它们的底层都是使用了effect。
我们先来看看effect
的原理。首先先来认识几个重要的全局变量。
targetMap
targetMap是一个WeakMap,存储了{target -> key -> dep}的关系。targetMap的key是需要做响应式处理的原始对象,targetMap的value是一个Map,Map的key是原始对象的属性,Map的value是每个属性关联的副作用函数Set。副作用函数就是我们需要追踪的依赖,也就是订阅者。
activeEffect
activeEffect保存当前正在执行的副作用函数,它是一个对象,effect的类型如下:
let activeEffect: ReactiveEffect | undefined
// effect类型
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(){}
stop() {}
}
shouldTrack
shouldTrack 变量用来标识是否开启依赖搜集,只有 shouldTrack 的值为 true 时,才进行依赖收集,即将副作用函数添加到依赖集合中。初始化值是true。
当执行run()时,会将shouldTrack设置为true,开启依赖收集。
effect
接下来看看副作用函数effect的实现原理。当依赖的数据变化的时候副作用函数便会触发,想当然的肯定会涉及到依赖收集收集与追踪,因此之后我们还会讲解一下track、trigger与effect的关系。
interface ReactiveEffectRunner<T = any> {
(): T
// ReactiveEffect就是上面提到的activeEffect的类型
effect: ReactiveEffect
}
function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
// 如果传入的fn本身就是effect,那么就直接执行 effect的副作用
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
// 将fn包装成effect
const _effect = new ReactiveEffect(fn)
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
}
// 不是延迟执行,直接执行副作用函数
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
fn
就是传给effect
的副作用,将fn
传给ReactiveEffect
包装一下,将其包装成标准的effect
类型。标准的effect
类型是ReactiveEffect
类,具有active、deps、deps、run()、stop()
这些属性方法。其中run
方法中就是执行传入的副作用函数fn
。
但是effect
函数返回的不是ReactiveEffect
类型,而是ReactiveEffectRunner
类型,接口ReactiveEffectRunner
中具有effect
属性,它是ReactiveEffect
类型,因此effect最终需要runner.effect = _effect,然后返回runner。
我们传入的副作用在effect的run()中执行。需要重点看一下run。
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指向当前执行的自身
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()
}
}
}
执行run
的时候,将全局activeEffect
指向自身,也就是当前执行的effect
,然后开启依赖收集标识位,执行副作用函数。
说到这,我们好像还没发现effect与track
和trigger
有什么关系。track、trigger
函数与effect
函数都是位于源码同一个文件下的,那么它们肯定是有关联的。接下来,我们看看track
的逻辑。
track(target: object, type: TrackOpTypes, key: unknown) {
if (shouldTrack && activeEffect) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
}
我们知道,在研究vue
的双向数据绑定时,需要进行依赖收集与依赖追踪。track
就是依赖收集的过程,当我们通过响应式API初始化数据或者模板中渲染一些变量的时候,就需要进行依赖收集,也就是track
。依赖,也就是我们的副作用函数,依赖收集的过程其实就是收集副作用函数的过程。而副作用,就是我们刚刚上文讲的effect
。这就关联上了。
刚刚研究effect
的时候,提到了一些全局变量,其中targetMap
存储了{target -> key -> dep}
的关系,我们依赖收集的过程,就是将副作用存储起来的过程。因此在track中,传入的参数target对应的就是targetMap
的key。先查看全局变量targetMap
中是否存在该target
,没有的话就初始化map赋值。同理,通过key来寻找最终的副作用Set,没有的话就初始化设置。至此,我们完善了{target -> key -> dep}
的关系,接下来看看 trackEffects
。
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack({
effect: activeEffect!,
...debuggerEventExtraInfo!
})
}
}
}
trackEffects
的逻辑其实就是将当前激活的activeEffect
放到dep
中,这样就完成了依赖收集过程。
trigger
过程就不多赘述了,就是根据{target -> key -> dep}
的关系一层一层的找到最后的dep
,然后执行其中的副作用函数即可。
说到这,可能还是有人不明白,如果不使用watch、computed
这些带有副作用的 api, track 、trigger
与effect
又有什么关系呢?那track
收集的副作用又是什么呢?
说到这里我想说,关于副作用,除了watch、computed
这些api中用户手动传入的fn算作副作用,页面渲染也是副作用。我们使用vue中双向数据绑定的特性,意味着当我们定义一个响应式变量,如果模板中使用了这个响应式变量,那么这个变量在模板中的渲染是响应式的,这个渲染是副作用,那么这个页面渲染就需要被收集到dep中。如果这个变量还是作为 watch中fn的内部出现的,那么这个变量的副作用又多了一个,还需要被额外收集。所以dep是一个Set的数据结构。当这个变量变化的时候,页面要重新渲染,watch也要重新计算,这就是trigger的过程。
当然这些针对的是响应式数据,如果我们仅仅定义一个静态数据,那么是不需要进行依赖收集的,它也不是响应式的。 说到这里应该可以明白,effect
与响应式密切相关了。
computed
讲完effect,我们不难知道computed、watch也是一种effect,只是参数不同而已。我们简单看一下computed。
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
// computed也是实例化effect
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
从computed的构造器中,我们可以看到computed也是实例化effect实现的,只是传的参数不同而已,我们暂且称作computedEffect。可以看到computedEffect的computed属性是true。
watch
注意:
watch虽然依赖于effect函数,但是在源码目录中却不属于reactivity目录,而是runtime-core目录下的。
const effect = new ReactiveEffect(getter, scheduler)
可以看到watch中同样使用ReactiveEffect创建副作用,只不过多传了scheduler参数。