likes
comments
collection
share

vue3源码之旅-effectScope

作者站长头像
站长
· 阅读数 46

前言

effectScope作用是捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理,与effectcomputedwatch都有关联,不熟悉这部分的同学参考之前的源码分析

vue3源码之旅-effect

vue3源码之旅-computed

vue3源码之旅-watch

上篇文章了解了effectScope如何使用,这次一起了解effectScope是如何设计的(换了电脑忘记之前vuedown的是哪个版本的了,目前这个版本是3.2.39,和之前有些许差异)

源码

import { ReactiveEffect } from './effect'
import { warn } from './warning'

let activeEffectScope: EffectScope | undefined

export class EffectScope {
  active = true
  effects: ReactiveEffect[] = []
  cleanups: (() => void)[] = []
  parent: EffectScope | undefined
  scopes: EffectScope[] | undefined
  private index: number | undefined

  constructor(detached = false) {
    if (!detached && activeEffectScope) {
      this.parent = activeEffectScope
      this.index =
        (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
          this
        ) - 1
    }
  }

  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.`)
    }
  }

  on() {
    activeEffectScope = this
  }

  off() {
    activeEffectScope = this.parent
  }

  stop(fromParent?: boolean) {
    if (this.active) {
      let i, l
      for (i = 0, l = this.effects.length; i < l; i++) {
        this.effects[i].stop()
      }
      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)
        }
      }
      if (this.parent && !fromParent) {
        const last = this.parent.scopes!.pop()
        if (last && last !== this) {
          this.parent.scopes![this.index!] = last
          last.index = this.index!
        }
      }
      this.active = false
    }
  }
}

export function effectScope(detached?: boolean) {
  return new EffectScope(detached)
}

export function recordEffectScope(
  effect: ReactiveEffect,
  scope: EffectScope | undefined = activeEffectScope
) {
  if (scope && scope.active) {
    scope.effects.push(effect)
  }
}

export function getCurrentScope() {
  return activeEffectScope
}

export function onScopeDispose(fn: () => void) {
  if (activeEffectScope) {
    activeEffectScope.cleanups.push(fn)
  } else if (__DEV__) {
    warn(
      `onScopeDispose() is called when there is no active effect scope` +
        ` to be associated with.`
    )
  }
}

源码并不复杂,主要是记录effect子父级关系、在strop时清除相关副作用 & 执行用户下一步动作

constructor

  1. detachedfalse表示子父级关联,若为true则断开子父级关联
  2. activeEffectScope存在表示在此之前已经执行过,存在父级

所以两者均满足条件时,记录parent、将当前EffectScope加入父级并且记录当前EffectScope在父级中的位置index

run

  1. 执行run前需根据active判断当前EffectScope是否处于激活状态
  2. 暂存上个activeEffectScopecurrentEffectScope
  3. 执行fn阶段保证activeEffectScope为当前EffectScope,避免后续若执行onScopeDisposeactiveEffectScope错乱
  4. 执行完毕finally阶段重新将activeEffectScope赋值为上一个EffectScope,保证初始化阶段子父级关系正确

执行阶段通过activeEffectScope的巧妙,既保证了当前阶段的正常执行,又可以让后续的EffectScope能正确记录子父级关系

stop

  1. 为防止重复执行,同样需要判断当前EffectScope是否处于激活状态
  2. 停止在recordEffectScope阶段记录的effect,即调用effect中的stop,新版本中effect引入了recordEffectScope记录effect
  3. 执行onScopeDispose阶段cleanups收集用户定义的动作
  4. 停止scopes收集的子集EffectScope
  5. 取消父级对对当前EffectScope引用,若移除的最后一个不是当前EffectScope,将父级中当前索引位置值赋值为last,改变last中记录的index

effectScope

返回EffectScope实例

recordEffectScope

添加当前effecteffect中会调用recordEffectScope收集effectwatchcomputed均通过effect实现,这里也呼应了官网对EffectScope的解释:捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理

getCurrentScope

获取当前EffectScope

onScopeDispose

这里会收集用户stop需要执行的动作:

activeEffectScope.cleanups.push(fn)

当执行stop时触发用户动作,run阶段的操作保证了activeEffectScope为当前EffectScope,不会发生混乱

结语

之前的源码分析时还没有这部分,所以在effect中没有看到EffectScope的影子,但大逻辑应该不影响,不熟悉的依然可以参考那部分源码分析