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

作者:PHM 时间:2023-12-11 19:28:49 

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

来源:https://juejin.cn/post/7215484775640088613

标签:Vue3,effectScope,API,源码解读
0
投稿

猜你喜欢

  • java日期格式化SimpleDateFormat的使用详解

    2023-08-25 03:22:15
  • Android 四种动画效果的调用实现代码

    2021-06-26 17:59:54
  • 通过实例解析spring环绕通知原理及用法

    2022-12-26 23:33:20
  • Android设置默认锁屏壁纸接口的方法

    2021-09-25 00:16:42
  • java poi导出图片到excel示例代码

    2023-10-30 00:13:17
  • Android实现滑动屏幕切换图片

    2022-04-26 23:14:42
  • C#无损高质量压缩图片代码

    2023-01-10 10:01:33
  • Java基础之Web服务器与Http详解

    2021-08-13 16:39:42
  • 详解Java编程中JavaMail API的使用

    2022-08-02 06:18:23
  • 基于Kubernetes实现前后端应用的金丝雀发布(两种方案)

    2023-01-07 02:32:27
  • Android 7.0以上版本实现应用内语言切换的方法

    2022-12-21 00:25:31
  • Java两种常用的随机数生成方式(小白总结)

    2023-02-16 16:54:19
  • 实现java简单的线程池

    2023-08-09 06:05:15
  • Java 基础语法中的逻辑控制

    2022-11-22 16:30:59
  • mybaties plus selectMaps和selectList的区别说明

    2021-07-15 11:18:29
  • Java8新特性:函数式编程

    2021-12-01 03:09:02
  • java实现统计字符串中字符及子字符串个数的方法示例

    2022-10-14 13:47:40
  • java判断字符串是否有逗号的方法

    2021-11-03 08:01:23
  • android手机获取gps和基站的经纬度地址实现代码

    2022-04-05 03:03:00
  • Flutter Widgets之标签类控件Chip详解

    2023-06-26 14:22:35
  • asp之家 软件编程 m.aspxhome.com