Vue——effectScope()

2024-08-15 11:21:48 浏览数 (2)

前言

主要是在Vue2.7.14源码中的初始化的时候有这么个东西,不搞清楚有点心里痒痒的,因为2.7.14本身就是一个衔接,所以里面会有一些从Vue3.0移植过来的东西,effectScope就是其一;

reactivity-effect-scope: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md

内容

机翻加调整,想阅读原文的可以访问上文链接;

  • Start Date: 2020-08-20
  • Target Major Version: 3.x
  • Reference Issues: (fill in existing related issues, if any)
  • Implementation PR: #2195

摘要

Introducing a new effectScope() API for @vue/reactivity. An EffectScope instance can automatically collect effects run within a synchronous function so that these effects can be disposed together at a later time.

为了 @vue/reactivity引入一个新的API effectScope()。 An EffectScope 实例可以自动的收集运行在同步函数中的副作用,以便以后一起处理这些副作用。

基本示例

代码语言:javascript复制
// effect, computed, watch, watchEffect created inside the scope will be collected

const scope = effectScope()

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

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

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

// to dispose all effects in the scope
scope.stop()

动机

In Vue's component setup(), effects will be collected and bound to the current instance. When the instance get unmounted, effects will be disposed automatically. This is a convenient and intuitive feature.

在Vue的组件"setup()"中,副作用将被收集并绑定到当前实例。当实例被卸载时,副作用将被自动释放。这是一个方便而且直观的功能。

However, when we are using them outside of components or as a standalone package, it's not that simple. For example, this might be what we need to do for disposing the effects of computed & watch

然而,当我们在组件之外或作为一个独立的包使用它们时,这并不是那么简单。例如,这可能是我们处理"computed"&"watch"副作用时需要做的事情。

代码语言:javascript复制
const disposables = []

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

disposables.push(() => stop(doubled.effect))

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

disposables.push(stopWatch1)

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

disposables.push(stopWatch2)

And to stop the effects: 为了阻止这些影响:

代码语言:javascript复制
disposables.forEach((f) => f())
disposables = []

Especially when we have some long and complex composable code, it's laborious to manually collect all the effects. It's also easy to forget collecting them (or you don't have access to effects created in the composable functions) which might result in memory leakage and unexpected behavior.

特别是当我们有一些冗长而复杂的组合代码时,手动收集所有副作用是很费力的。 也很容易忘记收集它们(或者您无法访问在组合函数中创建的副作用),这可能会导致内存泄漏和意外行为。

This RFC is trying to abstract the component's setup() effect collecting and disposing feature into a more general API that can be reused outside of the component model.

此 RFC 尝试将组件"setup()"的副作用收集和处置功能抽象为可以在组件模型之外重用的更通用的 API。

It also provides the functionality to create "detached" effects from the component's setup() scope or user-defined scope. Resolving https://github.com/vuejs/vue-next/issues/1532.

它还提供了从组件的"setup()"范围或用户定义范围创建"分离"副作用的功能。解决https://github.com/vuejs/vue-next/issues/1532.

详细设计

新API摘要

effectScope(detached = false): EffectScope

代码语言:javascript复制
interface EffectScope {
  run<T>(fn: () => T): T | undefined // undefined if scope is inactive
  stop(): void
}

getCurrentScope(): EffectScope | undefined

onScopeDispose(fn: () => void): void

Basic Usage

Creating a scope: 创建一个作用域:

代码语言:javascript复制
const scope = effectScope()

A scope can run a function and will capture all effects created during the function's synchronous execution, including any API that creates effects internally, e.g. computed, watch and watchEffect:

作用域可以运行一个函数,并将捕获函数同步执行期间创建的所有副作用,包括在内部创建副作用的任何API,例如"computed"、"watch"和"watchEffect":

代码语言:javascript复制
scope.run(() => {
  const doubled = computed(() => counter.value * 2)

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

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

// the same scope can run multiple times
scope.run(() => {
  watch(counter, () => {
    /*...*/
  })
})

The run method also forwards the return value of the executed function:

这个"run"方法同样可以转发一致性函数的返回值:

代码语言:javascript复制
console.log(scope.run(() => 1)) // 1

When scope.stop() is called, it will stop all the captured effects and nested scopes recursively.

当调用scope.stop()的时候,他将递归的停止所有收集的副作用和嵌套的作用域

代码语言:javascript复制
scope.stop()
嵌套作用域

Nested scopes should also be collected by their parent scope. And when the parent scope gets disposed, all its descendant scopes will also be stopped.

嵌套作用域也应该由他的父作用域收集。当父作用域被释放的时候,该父作用域下的所有子作用域也应该被释放。

代码语言:javascript复制
const scope = effectScope()

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

  // not need to get the stop handler, it will be collected by the outer scope
  effectScope().run(() => {
    watch(doubled, () => console.log(doubled.value))
  })

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

// dispose all effects, including those in the nested scopes
scope.stop()
分离嵌套作用域

effectScope accepts an argument to be created in "detached" mode. A detached scope will not be collected by its parent scope.

effectScope接受在"分离"模式下创建的参数。分离的作用域不会被其父作用域收集。

This also makes usages like "lazy initialization" possible.

这样也使得延迟初始化用法成为了可能。

代码语言:javascript复制
let nestedScope

const parentScope = effectScope()

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

  // with the detected flag,
  // the scope will not be collected and disposed by the outer scope
  nestedScope = effectScope(true /* detached */)
  nestedScope.run(() => {
    watch(doubled, () => console.log(doubled.value))
  })

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

// disposes all effects, but not `nestedScope`
parentScope.stop()

// stop the nested scope only when appropriate
nestedScope.stop()
onScopeDispose

The global hook onScopeDispose() serves a similar functionality to onUnmounted(), but works for the current scope instead of the component instance. This could benefit composable functions to clean up their side effects along with its scope. Since setup() also creates a scope for the component, it will be equivalent to onUnmounted() when there is no explicit effect scope created.

全局钩子"onScopeDispose()"提供与"onUnmounted()"类似的功能,但适用于当前作用域而不是组件实例。 这将有利于组合函数清理其副作用及其作用域。由于"setup()"也为组件创建了一个作用域,因此当没有创建显式效果作用域时,它将等同于"onUnmounted()"。

代码语言:javascript复制
import { onScopeDispose } from 'vue'

const scope = effectScope()

scope.run(() => {
  onScopeDispose(() => {
    console.log('cleaned!')
  })
})

scope.stop() // logs 'cleaned!'
获取当前作用域

A new API getCurrentScope() is introduced to get the current scope.

引入一个新的APIgetCurrentScope()来获取当前作用域。

代码语言:javascript复制
import { getCurrentScope } from 'vue'

getCurrentScope() // EffectScope | undefined

使用示例

Example A: 共享组合

Some composables setup global side effects. For example the following useMouse() function:

一些组合会产生全局副作用,例如下面的useMouse() 函数:

代码语言:javascript复制
function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function handler(e) {
    x.value = e.x
    y.value = e.y
  }

  window.addEventListener('mousemove', handler)

  onUnmounted(() => {
    window.removeEventListener('mousemove', handler)
  })

  return { x, y }
}

If useMouse() is called in multiple components, each component will attach a mousemove listener and create its own copy of x and y refs. We should be able to make this more efficient by sharing the same set of listeners and refs across multiple components, but we cant because each onUnmounted call is coupled to a single component instance.

如果在多个组件中调用"useMouse()",则每个组件将附加一个"mousemove"侦听器,并创建自己的"x"和"y"引用副本。 我们应该能够通过在多个组件之间共享相同的侦听器和引用集来提高效率,但我们做不到,因为每个"onUnmounted"调用都耦合到单个组件实例。

We can achieve this using detached scope, and onScopeDispose. First, we need to replace onUnmounted with onScopeDispose:

我们可以使用分离作用域和"onScopeDispose"来实现这一点。首先,我们需要将"onUnmounted"替换为"onScopeDispose":

代码语言:javascript复制
- onUnmounted(() => {
  onScopeDispose(() => {
  window.removeEventListener('mousemove', handler)
})

This still works because a Vue component now also runs its setup() inside a scope, which will be disposed when the component is unmounted.

这仍然有效,因为Vue组件现在也在一个作用域内运行其"setup()",该作用域将在卸载组件时释放。

Then, we can create a utility function that manages parent scope subscriptions:

然后,我们可以创建一个管理父范围订阅的实用程序函数:

代码语言:javascript复制
function createSharedComposable(composable) {
  let subscribers = 0
  let state, scope

  const dispose = () => {
    if (scope && --subscribers <= 0) {
      scope.stop()
      state = scope = null
    }
  }

  return (...args) => {
    subscribers  
    if (!state) {
      scope = effectScope(true)
      state = scope.run(() => composable(...args))
    }
    onScopeDispose(dispose)
    return state
  }
}

Now we can create a shared version of useMouse:

现在,我们可以创建共享版本的"useMouse":

代码语言:javascript复制
const useSharedMouse = createSharedComposable(useMouse)

The new useSharedMouse composable will set up the listener only once no matter how many components are using it, and removes the listener when no component is using it anymore. In fact, the useMouse function should probably be a shared composable in the first place!

无论有多少组件在使用新的"useSharedMouse"组合器,它都只会设置一次侦听器,当没有组件在使用它时,它会删除侦听器。事实上,"useMouse"函数首先应该是一个共享的组合函数!

Example B: 临时作用域

代码语言:javascript复制
export default {
  setup() {
    const enabled = ref(false)
    let mouseState, mouseScope

    const dispose = () => {
      mouseScope && mouseScope.stop()
      mouseState = null
    }

    watch(
      enabled,
      () => {
        if (enabled.value) {
          mouseScope = effectScope()
          mouseState = mouseScope.run(() => useMouse())
        } else {
          dispose()
        }
      },
      { immediate: true }
    )

    onScopeDispose(dispose)
  },
}

In the example above, we would create and dispose some scopes on the fly, onScopeDispose allow useMouse to do the cleanup correctly while onUnmounted would never be called during this process.

在上面的示例中,我们将动态创建和释放一些作用域,"onScopeDispose"允许"useMouse"正确执行清理,而在此过程中永远不会调用"onUnmounted"

在vue core中受影响的使用

Currently in @vue/runtime-dom, we wrap the computed to add the instance binding. This makes the following statements NOT equivalent

目前在@vue/runtime-dom中,我们包装computed添加到实例绑定。这使得以下声明不等效

代码语言:javascript复制
// not the same
import { computed } from '@vue/reactivity'
import { computed } from 'vue'

This should not be an issue for most of the users, but for some libraries that would like to only rely on @vue/reactivity (for more flexible usages), this might be a pitfall and cause some unwanted side-effects.

对于大多数用户来说,这应该不是问题,但对于一些希望只依赖"@vue/reactive"(更灵活的使用)的库来说,这可能是一个缺陷,并导致一些不必要的副作用。

With this RFC, @vue/runtime-dom can use the effectScope to collect the effects directly and computed rewrapping will not be necessary anymore.

使用此RFC,"@vue/runtime dom"可以使用"effectScope"直接收集效果,不再需要计算重写。

代码语言:javascript复制
// with the RFC, `vue` simply redirect `computed` from `@vue/reactivity`
import { computed } from '@vue/reactivity'
import { computed } from 'vue'

缺点

  • It doesn't work well with async functions

不适用于异步函数

选择

M/A

采用策略

This is a new API and should not affect the existing code. It's also a low-level API only intended for advanced users and library authors.

这是一个新的 API,不应影响现有代码。它也是一个仅供高级用户和库作者使用的低级 API。

未解决的问题

None

0 人点赞