Vue——effectScope(六)

简介: Vue——effectScope()

前言

主要是在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 实例可以自动的收集运行在同步函数中的副作用,以便以后一起处理这些副作用。

基本示例

// 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"副作用时需要做的事情。

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:

为了阻止这些影响:

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
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:

创建一个作用域:

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":

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"方法同样可以转发一致性函数的返回值:

console.log(scope.run(() => 1)) // 1

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

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

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.

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

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.

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

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()"。

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()来获取当前作用域。

import { getCurrentScope } from 'vue'
getCurrentScope() // EffectScope | undefined

使用示例

Example A: 共享组合

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

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

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":

- 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:

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

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":

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: 临时作用域

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添加到实例绑定。这使得以下声明不等效

// 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"直接收集效果,不再需要计算重写。

// 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

学无止境,谦卑而行.

目录
相关文章
|
22天前
|
数据采集 监控 JavaScript
在 Vue 项目中使用预渲染技术
【10月更文挑战第23天】在 Vue 项目中使用预渲染技术是提升 SEO 效果的有效途径之一。通过选择合适的预渲染工具,正确配置和运行预渲染操作,结合其他 SEO 策略,可以实现更好的搜索引擎优化效果。同时,需要不断地监控和优化预渲染效果,以适应不断变化的搜索引擎环境和用户需求。
|
8天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
8天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
8天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
8天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
7天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
9天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
7天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
9天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
14天前
|
JavaScript
Vue基础知识总结 4:vue组件化开发
Vue基础知识总结 4:vue组件化开发