vue3中effect函数到底是什么详解

 更新时间:2023年06月07日 10:57:51   作者:木木夏呀  
Effect几乎是Vue3.0中最重要的一个功能了,计算属性监听函数都是基于effect实现的,下面这篇文章主要给大家介绍了关于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与tracktrigger有什么关系。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 、triggereffect又有什么关系呢?那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函数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

最新评论