java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Vue3 effectScope API

Vue3源码解读effectScope API及实现原理

作者:PHM

这篇文章主要为大家介绍了Vue3源码解读effectScope API及实现原理,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

vue3新增effectScope相关的API

其官方的描述是创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理。并给出了示例:

const scope = effectScope()
scope.run(() => {
  const doubled = computed(() => counter.value * 2)
  watch(doubled, () => console.log(doubled.value))
  watchEffect(() => console.log('Count: ', doubled.value))
})
// 处理掉当前作用域内的所有 effect
scope.stop()

我们就从这个示例入手看看具体的源码实现:

effectScope

// packages/reactivity/src/effectScope.ts
export function effectScope(detached?: boolean) {
  // 返回EffectScope实例
  return new EffectScope(detached)
}

EffectScope

export class EffectScope {
  /**
   * @internal
   */
  private _active = true
  /**
   * @internal
   */
  effects: ReactiveEffect[] = []
  /**
   * @internal
   */
  cleanups: (() => void)[] = []
  /**
   * only assigned by undetached scope
   * @internal
   */
  parent: EffectScope | undefined
  /**
   * record undetached scopes
   * @internal
   */
  scopes: EffectScope[] | undefined
  /**
   * track a child scope's index in its parent's scopes array for optimized
   * // index作用:在父作用域数组中跟踪子作用域范围索引以进行优化。
   * removal
   * @internal
   */
  private index: number | undefined
  constructor(public detached = false) {
    // 记录当前scope为parent scope
    this.parent = activeEffectScope
    if (!detached && activeEffectScope) {
      this.index =
        (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
          this
        ) - 1
    }
  }
  get active() {
    return this._active
  }
  run<T>(fn: () => T): T | undefined {
    if (this._active) {
      const currentEffectScope = activeEffectScope
      try {
        activeEffectScope = this
        return fn()
      } finally {
        activeEffectScope = currentEffectScope
      }
    } else if (__DEV__) {
      warn(`cannot run an inactive effect scope.`)
    }
  }
  /**
   * This should only be called on non-detached scopes
   * 必须在非分离的作用域上调用
   * @internal
   */
  on() {
    activeEffectScope = this
  }
  /**
   * This should only be called on non-detached scopes
   * @internal
   */
  off() {
    activeEffectScope = this.parent
  }
  // stop方法
  stop(fromParent?: boolean) {
    if (this._active) {
      let i, l
      // stop effects
      for (i = 0, l = this.effects.length; i < l; i++) {
        this.effects[i].stop()
      }
      // 执行所有的cleanups
      for (i = 0, l = this.cleanups.length; i < l; i++) {
        this.cleanups[i]()
      }
      // 递归停止所有的子作用域
      if (this.scopes) {
        for (i = 0, l = this.scopes.length; i < l; i++) {
          this.scopes[i].stop(true)
        }
      }
      // nested scope, dereference from parent to avoid memory leaks
      if (!this.detached && this.parent && !fromParent) {
        // optimized O(1) removal
        const last = this.parent.scopes!.pop()
        if (last && last !== this) {
          this.parent.scopes![this.index!] = last
          last.index = this.index!
        }
      }
      this.parent = undefined
      this._active = false
    }
  }
}

在执行scope.run的时候会将this赋值到全局的activeEffectScope变量,然后执行传入函数。对于computed、watch、watchEffect(watchEffect是调用doWatch实现的,与watch实现响应式绑定的方式相同)这些API都会创建ReactiveEffect实例来建立响应式关系,而收集对应的响应式副作用就发生在ReactiveEffect创建的时候,我们来看一下ReactiveEffect的构造函数:

// ReactiveEffect的构造函数
constructor(
  public fn: () => T,
  public scheduler: EffectScheduler | null = null,
  scope?: EffectScope
) {
  // effect实例默认会被记录到指定scope中
  // 如果没有指定scope则会记录到全局activeEffectScope中
  recordEffectScope(this, scope)
}
// recordEffectScope实现
export function recordEffectScope(
  effect: ReactiveEffect,
  // scope默认值为activeEffectScope
  scope: EffectScope | undefined = activeEffectScope
) {
  if (scope && scope.active) {
    scope.effects.push(effect)
  }
}

可以看到如果我们没有传入scope参数,那么在执行recordEffectScope时就会有一个默认的参数为activeEffectScope,这个值不正是我们scope.run的时候赋值的吗!所以新创建的effect会被放到activeEffectScope.effects中,这就是响应式副作用的收集过程。
那么对于一起处理就比较简单了,只需要处理scope.effects即可

组件的scope

日常开发中其实并不需要我们关心组件副作用的收集和清除,因为这些操作是已经内置好的,我们来看一下源码中是怎么做的

组件实例中的scope

在组件实例创建的时候就已经new了一个属于自已的scope对象了:

const instance: ComponentInternalInstance = {
  ...
  // 初始化scope
  scope: new EffectScope(true /* detached */),
  ...
}

在我们执行setup之前,会调用setCurrentInstance,他会调用instance.scope.on,那么就会将activeEffectScope赋值为instance.scope,那么在setup中注册的computed、watch等就都会被收集到instance.scope.effects

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  // 组件对象
  const Component = instance.type as ComponentOptions
  ...
  // 2. call setup()
  const { setup } = Component
  if (setup) {
    // 创建setupContext
    const setupContext = (instance.setupContext =
      // setup参数个数判断 大于一个参数创建setupContext
      setup.length > 1 ? createSetupContext(instance) : null)
    // instance赋值给currentInstance
    // 设置当前实例为instance 为了在setup中可以通过getCurrentInstance获取到当前实例
    // 同时开启instance.scope.on()
    setCurrentInstance(instance)
    // 暂停tracking 暂停收集副作用函数
    pauseTracking()
    // 执行setup
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      // setup参数
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    // 重新开启副作用收集
    resetTracking()
    // currentInstance置为空 
    // activeEffectScope赋值为instance.scope.parent
    // 同时instance.scope.off()
    unsetCurrentInstance()
    ...
  } else {
    finishComponentSetup(instance, isSSR)
  }
}

对于选项式API的收集是同样的操作:

// support for 2.x options
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
  setCurrentInstance(instance)
  pauseTracking()
  // 处理options API
  applyOptions(instance)
  resetTracking()
  unsetCurrentInstance()
}

完成了收集那么对于清理就只需要在组件卸载的时候执行stop方法即可:

// packages/runtime-core/src/renderer.ts
const unmountComponent = (
  instance: ComponentInternalInstance,
  parentSuspense: SuspenseBoundary | null,
  doRemove?: boolean
) => {
  if (__DEV__ && instance.type.__hmrId) {
    unregisterHMR(instance)
  }
  const { bum, scope, update, subTree, um } = instance
  ...
  // stop effects in component scope
  // 副作用清除
  scope.stop()
  ...
}

以上就是Vue3源码解读effectScope API及实现原理的详细内容,更多关于Vue3 effectScope API的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文