vue3中effect函数到底是什么详解
作者:木木夏呀
前言
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参数。
总结
到此这篇关于vue3中effect函数到底是什么的文章就介绍到这了,更多相关vue3中effect函数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!