Vue 响应式高阶 API - effectScope

effectScope 是 Vue 3.2.0 引入的新 API,属于响应式系统的高阶内容。从字面上理解,它就是 effect 作用域,用于收集在其中所创建的副作用,并能对其进行统一的处理。

除非是开发独立的库,我们几乎不会用到 effectScope。尽管如此,了解 effectScope 对于我们理解 Vue 3 源码或是其它开源库(比如 VueUse)还是很有必要的。

effectScope 最有价值的官方文档是这篇 RFC,另外就是 Vue 3 源码中的测试用例 effectScope.spec.ts

看下 effectScope 的基本用法。

复制代码
// 在 scope 中创建的 effect computed watch watchEffect 都会被收集起来
const scope = effectScope()

scope.run(() => {
  const doubled = computed(() => counter.value * 2)

  watch(doubled, () => console.log(doubled.value))

  watchEffect(() => console.log('Count: ', doubled.value))
})

// 处理 scope 中所有的副作用
scope.stop()

首先通过 effectScope 创建 scope 的本质其实是下面这样。

复制代码
export function effectScope(detached?: boolean) {
  return new EffectScope(detached)
}

对 new 操作进行了一次包装。注意这里有一个可选的 detached 参数,表示该作用域是否是独立的(是否存在父级作用域)。

然后通过 run 方法将入参函数中的副作用限定在当前作用域内,最后通过 stop 方法清除当前作用域内的所有副作用。

上面这个过程在组件 setup 内同样存在。组件实例被创建的同时会创建一个独立的 scope。

复制代码
export function createComponentInstance(...args) {
    // ...
    const instance = {
        // ...
        vnode,
        type,
        scope: new EffectScope(true /* detached */),
        // ...
    }
    return instance
}

当前实例中的副作用都会被收集到 scope 的 effects 中。在组件卸载时这些副作用会自动清除。

复制代码
const unmountComponent = (...args) => {
    // ...
    scope.stop()
    // ...
}

整个过程都是 Vue 内部处理的,我们不需要关心副作用的收集和清除。

一旦我们脱离组件一切就没这么简单了,为了实现副作用的收集和清除,我们可能就需要像下面这样来处理。

复制代码
<script setup>
import { ref, computed, watch, watchEffect, stop } from "vue";
// 收集副作用
let disposables = [];

const counter = ref(0);

const doubled = computed(() => counter.value * 2);

const stopWatch1 = watchEffect(() => {
  console.log(`counter: ${counter.value}`);
});

disposables.push(stopWatch1);

const stopWatch2 = watch(doubled, () => {
  console.log(doubled.value);
});

disposables.push(stopWatch2);

// 清除副作用
disposables.forEach((f) => f());
disposables = [];
</script>

<template>
  <button type="button" @click="counter++">counter is {{ counter }}</button>
</template>

<style scoped></style>

这段代码来自 rfc,最开始是有一些疑惑的,watchEffect 和 watch 竟然有返回值,并且是一个函数,执行该函数居然还能清除副作用。想搞清楚为什么,就只能通过源码寻找答案了。

复制代码
// watchEffect 和 watch 内部都是基于 doWatch 实现的
function doWatch() {
    // ...
    return () => {
        effect.stop()
        if (instance && instance.scope) {
            remove(instance.scope.effects!, effect)
        }
    }
}

可以看到,返回的函数内部调用了 effect 上的 stop 方法从而达到了清除副作用的目的。

很明显,这种维护方式是很繁琐的,特别是在一些巨型的组合函数中,我们收集的副作用很容易遗漏不全,可能导致内存泄漏和其它未知的问题。

基于此,Vue 3.2.0 版本将副作用自动收集和处理的逻辑抽象成了通用的 API 并且可以在组件外使用。

除了前面提到的 effectScope(),Vue 还为我们提供了 getCurrentScope() 用于获取当前的作用域以及 onScopeDispose() 来注册副作用清除的回调函数,这些 API 的基本使用可参考官方文档。

下面会对 API 使用上的细节做一些说明。

scope.stop() 的执行会清除当前作用域及其子作用域(递归地)的全部副作用。

复制代码
function stop() {
    // 清除当前作用域的副作用
    for (i = 0, l = this.effects.length; i < l; i++) {
        this.effects[i].stop()
    }
    // 清除子作用域的副作用
    if (this.scopes) {
        for (i = 0, l = this.scopes.length; i < l; i++) {
            this.scopes[i].stop(true)
        }
    }
}

这里要注意,如果子作用域是独立的(detached = true),它是不会被父作用域收集的,自然地,在父作用域清除副作用时是不会清除该独立子作用域中的副作用的。

scope.run() 可以重复执行,对副作用的收集和清除会进行合并。

复制代码
let dummy, doubled
let dummy1 = 0
const counter = reactive({ num: 0 })

const scope = effectScope()
scope.run(() => {
    effect(() => (dummy = counter.num))
    onScopeDispose(() => (dummy1 += 1))
    onScopeDispose(() => (dummy1 += 2))
})
// scope.effects.length = 1

scope.run(() => {
    effect(() => (doubled = counter.num * 2))
    onScopeDispose(() => (dummy1 += 4))
})
// scope.effects.length = 2

counter.num = 7
// dummy = 7
// dummy1 = 0
// doubled = 14

scope.stop()
// dummy1 = 7
相关推荐
幸运黒锦鲤9 小时前
npm 扩展Vite、Element-plus 、Windcss、Vue Router
前端·npm·node.js
IT_陈寒9 小时前
Java性能优化:3个90%开发者都忽略的高效技巧,让你的应用提速50%!
前端·人工智能·后端
^O^ ^O^9 小时前
pc端pdf预览
前端·javascript·pdf
艾小码9 小时前
还在纠结用v-if还是v-show?看完这篇彻底搞懂Vue渲染机制!
javascript·vue.js
徐同保9 小时前
js class定义类,私有属性,类继承,子类访问父类的方法,重写父类的方法
前端·javascript·vue.js
SUPER526614 小时前
FastApi项目启动失败 got an unexpected keyword argument ‘loop_factory‘
java·服务器·前端
sanx1814 小时前
专业电竞体育数据与系统解决方案
前端·数据库·apache·数据库开发·时序数据库
你的人类朋友17 小时前
【Node】认识一下Node.js 中的 VM 模块
前端·后端·node.js
Cosolar17 小时前
FunASR 前端语音识别代码解析
前端·面试·github