vue3-effectScope源码解析

714 阅读5分钟

阅读准备

本文使用的vue版本为3.2.26。在阅读 effectScope 源码之前,我们需要知道它的特性,可以通过阅读单例测试源码或者是阅读官网的 API了解特性,推荐阅读单例,了解特性在后面阅读时才能更好理解。

  在vue3中可以使用effectScope函数创建一个统一管理effect的对象。注入到这个对象的effect会被记录到effects属性中,当执行stop方法时,被记录的所有effect都会停止监听。既然是管理effect自然也就包括computeddeferredComputed,因为他们内部是通过effect实现的,具体参考之前文章vue3-computed源码解析

  effectScope也会记录子effectScope,当停止监听时子级也会同时停止。这个api很少人使用,我们稍微简单看看使用方式:

import { effectScope, reactive } from 'vue'

let dummy, doubled
const counter = reactive({ num: 0 })

const scope = effectScope()
scope.run(() => {
  effect(() => (dummy = counter.num))
  // 子级scope
  effectScope().run(() => {
    effect(() => (doubled = counter.num * 2))
  })
})

// 收集到的effect
console.log(scope.effects.length)   // 1
// 收集到的子级scope
console.log(scope.scopes!.length)   // 1

console.log(dummy)    // 0
console.log(doubled)  // 0
counter.num = 7

console.log(dummy)    // 7
console.log(doubled)  // 14

// 停止所有监听,子级都会停止
scope.stop()

counter.num = 6
console.log(dummy)    // 7
console.log(doubled)  // 14

  effectScope有一个可选参数为boolean,当传入true时表示阻断与父级的联系,阻断后这个scope对象将不会与父级关联,成为独立的scope。父级的stop也不会影响到它。比如:

import { effectScope, reactive } from 'vue'

let dummy, doubled
const counter = reactive({ num: 0 })

const scope = effectScope()
scope.run(() => {
  effect(() => (dummy = counter.num))
  // 阻断父级收集
  effectScope(true).run(() => {
    effect(() => (doubled = counter.num * 2))
  })
})

console.log(scope.effects.length)   // 1
console.log(scope.scopes)   // undefined

console.log(dummy)    // 0
console.log(doubled)  // 0
counter.num = 7

console.log(dummy)    // 7
console.log(doubled)  // 14

// 停止所有监听,子级被阻断
scope.stop()

counter.num = 6
console.log(dummy)    // 7
console.log(doubled)  // 12

effectScope

  现在我们了解effectScope的作用与特性了,接下来我们看看它是怎么实现的

// 当前正在执行的scope
let activeEffectScope: EffectScope | undefined
// 可能轮询调用,记录栈
const effectScopeStack: EffectScope[] = []

export class EffectScope {
  // 是否被停止了
  active = true
  // 记录的effects
  effects: ReactiveEffect[] = []
  // 用户注入的清除函数
  cleanups: (() => void)[] = []
  // 父级
  parent: EffectScope | undefined
  // 子级scopes
  scopes: EffectScope[] | undefined
  
  // 索引,方便删除,记住当前scope在父级中的位置
  private index: number | undefined

  // 是否需要阻断
  constructor(detached = false) {
    // 如果不阻断,则要记录父级关系
    if (!detached && activeEffectScope) {
      // 当前正在使用的effect作用域作为父级
      this.parent = activeEffectScope
      // 记住当前作用域在父级中的位置
      this.index =
        (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
          this
        ) - 1
    }
  }

  // 用户执行操作
  run<T>(fn: () => T): T | undefined {
    if (this.active) {
      try {
        // 打开effect收集
        this.on()
        // 执行用户方法
        return fn()
      } finally {
        // 关闭收集
        this.off()
      }
    } else if (__DEV__) {
      warn(`cannot run an inactive effect scope.`)
    }
  }

  // 开启收集
  on() {
    if (this.active) {
      // 将当前scope入栈,并设置为当前effect
      effectScopeStack.push(this)
      activeEffectScope = this
    }
  }

  // 关闭收集
  off() {
    if (this.active) {
      // 出栈,并恢复正在使用的effectScope
      effectScopeStack.pop()
      activeEffectScope = effectScopeStack[effectScopeStack.length - 1]
    }
  }

  // 统一停止收集到的effect监听,传入是否是父级删除标志
  stop(fromParent?: boolean) {
    if (this.active) {
      // 停止所有effect
      this.effects.forEach(e => e.stop())
      // 执行用户注册的清除函数
      this.cleanups.forEach(cleanup => cleanup())

      // 停止所有子级scope,并传入是父级停止
      if (this.scopes) {
        this.scopes.forEach(e => e.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
    }
  }
}

// 创建effect作用域对象
export function effectScope(detached?: boolean) {
  return new EffectScope(detached)
}

  effectScope函数只是创建EffectScope对象并抛出。这个类里面的关联实现并不多,相对比较简单。

  scope记录关联的effect与子级scope是在run方法实现上。用户传入操作函数即可记录函数中创建的子级scopeeffect。他们是如何实现的呢,在执行前会通过on函数开启收集,记录当前scope,并让scope入栈,然后再执行操作函数。执行完毕执行off关闭收集,恢复上一次当前scope

  scope实例化时会判断当前是否需要阻断,如果不阻断则在记录当前scope与实例化的scope父子关系。并记录实例化的scope的父级存储在parent,以及自身在父级scopes的位置,方便管理。这一步就收集到了子级scope

recordEffectScope

  在之前vue3-effect源码解析一章有看到,实例化effect时会执行recordEffectScope(this, scope),这个方法用来记录effectscope的关系的,我们看看它是如何实现的

// 记录effect作用域
export function recordEffectScope(
  effect: ReactiveEffect,
  scope?: EffectScope | null
) {
  // 如果没有指定则默认是当前激活的scope
  scope = scope || activeEffectScope
  if (scope && scope.active) {
    // 记录
    scope.effects.push(effect)
  }
}

  当effect实例化时会判断是否有明确指定scope,如果没有则使用当前scope作为他的作用域。然后记录到effects属性上,这一步就收集到了关联的effect

stop

  scope.stop会停止自身收集到的effect监听,然后再调用子级stop函数停止子级的effect监听,这样相关联的所有effect都被停止监听。

  scope.stop有一个参数标记当前是自身停止的还是父级调用停止收集的,如果是自身调用则解除自身与父级的关联,父级调用的则不需要解除与父级的关联。也就是说停止关联只会影响父级的scopes,自身并不影响。并通过active属性将scope标识为已停止。

onScopeDispose

  scope通过onScopeDispose方法来注册用户的当stop时的回调,并收集在scopecleanups属性上。注意onScopeDispose只能在scope.run的操作函数注册。下面我们看看源码:

// 注册stop scope时的回调
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.`
    )
  }
}

  非常简单只是判断当前是否存在当前scope,如果存在则记录到cleanups属性上,当scope.stop是就执行它们。

getCurrentScope

  vue还提供getCurrentScope方法来访问当前scope

// 获取当前作用域
export function getCurrentScope() {
  return activeEffectScope
}

  到这里effectScope就讲完了,emm,vue3-reactivity源码解析系列也更新完了。后面有时间我再开个专题vue3-runtime-core源码解析。完结撒花。

上一章:vue3-computed源码解析