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

学无止境,谦卑而行.

目录
相关文章
|
14天前
|
JavaScript
vue使用iconfont图标
vue使用iconfont图标
82 1
|
25天前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
|
2月前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱前端的大一学生,专注于JavaScript与Vue,正向全栈进发。博客分享Vue学习心得、命令式与声明式编程对比、列表展示及计数器案例等。关注我,持续更新中!🎉🎉🎉
54 1
vue学习第一章
|
2月前
|
JavaScript 前端开发 索引
vue学习第三章
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中的v-bind指令,包括基本使用、动态绑定class及style等,希望能为你的前端学习之路提供帮助。持续关注,更多精彩内容即将呈现!🎉🎉🎉
49 1
|
2月前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
43 1
vue学习第四章
|
2月前
|
JavaScript 前端开发 算法
vue学习第7章(循环)
欢迎来到瑞雨溪的博客,一名热爱JavaScript和Vue的大一学生。本文介绍了Vue中的v-for指令,包括遍历数组和对象、使用key以及数组的响应式方法等内容,并附有综合练习实例。关注我,将持续更新更多优质文章!🎉🎉🎉
35 1
vue学习第7章(循环)
|
2月前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
44 1
vue学习第九章(v-model)
|
2月前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
55 1
vue学习第十章(组件开发)
|
2月前
|
JavaScript 前端开发
vue学习第十一章(组件开发2)
欢迎来到我的博客,我是瑞雨溪,一名自学前端两年半的大一学生,专注于JavaScript与Vue。本文介绍Vue中的插槽(slot)使用方法,包括基本插槽、具名插槽及作用域插槽,帮助你在组件开发中实现内容的灵活定制。如果你觉得有帮助,请关注我,持续更新中!🎉🎉🎉
28 1
vue学习第十一章(组件开发2)
|
2月前
|
监控 JavaScript 前端开发
vue学习第十二章(生命周期)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。本文深入探讨了Vue实例的生命周期,从初始化到销毁各阶段的关键钩子函数及其应用场景,帮助你更好地理解Vue的工作原理。如果你觉得有帮助,欢迎关注我,将持续分享更多优质内容!🎉🎉🎉
41 1
vue学习第十二章(生命周期)