vue3之effectScope强大功能

3,647 阅读3分钟

前言

effectScope是个强大的api,官网原话:创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理,在RFC对其有更详细的解释,在vueuse库中更有对其的妙用

为什么要EffectScope

RFC中已经对其做了解释,就是为了方便处理副作用,手动收集、清理副作用如下:

收集

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)

清除

disposables.forEach((f) => f())
disposables = []

上述过程需人工进行,对于复杂的逻辑收集成本较高,忘记收集也可能造成内存泄漏等问题。所以设计EffectScope处理该问题,对于开发库的同学更友好

EffectScope使用

function effectScope(detached?: boolean): EffectScope

interface EffectScope {
  run<T>(fn: () => T): T | undefined // 如果作用域不活跃就为 undefined
  stop(): void
}

子父EffectScope

detached表示是否阻断和父级的联系,若为true则表示与父级断开关联,执行父级stop方法时会递归停止子集的监听,但子集detachedtrue时则不会停止,如下:

let nestedScope
let childScope

const parentScope = effectScope()

parentScope.run(() => {
  const doubled = computed(() => counter.value * 2)
  // 与父级断开链接
  nestedScope = effectScope(true /* detached */)
  nestedScope.run(() => {
    watch(doubled, () => console.log('nestedScope', doubled.value))
  })
  // 父级断开监听也会断开
  childScope = effectScope()
  childScope.run(() => {
    watch(doubled, () => console.log('childScope', doubled.value))
  })

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

// 停止parentScope、childScope监听
parentScope.stop()

// 停止nestedScope监听
nestedScope.stop()

onScopeDispose

停止监听时触发改函数onScopeDispose,作用类似onUnmounted

import { onScopeDispose } from 'vue'

const scope = effectScope()

scope.run(() => {
  onScopeDispose(() => {
    console.log('cleaned!')
  })
})
// 停止监听,触发onScopeDispose
scope.stop()

性能提升

对于特定场景使用EffectScope性能更好更合理

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

对于useMouse获取坐标如果多个组件使用则会添加多个监听,增加性能成本,这个时候使用EffectScope更友好

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)

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

  return { x, y }
}

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

const useSharedMouse = createSharedComposable(useMouse)

export default useSharedMouse

这样无论多少组件使用,只会进行一次监听,不使用时同时清除监听

灵活的状态管理

获取坐标demo已经可以表现出EffectScope状态管理的作用,我们可以模拟store完成一套更灵活的状态管理又或用于子父组件、兄弟组件、全局通信

创建函数统一管理状态

// useGlobalState
import { effectScope } from '@vue/composition-api'

export default run => {
  let isChange = false
  let state
  const scope = effectScope(true)
  return () => {
    // 防止重复触发
    if (!isChange) {
      state = scope.run(run)
      isChange = true
    }
    return state
  }
}

创建store

// store.js
import { computed, ref } from '@vue/composition-api'
import useGlobalState from './useGlobalState'

export default useGlobalState(
  () => {
    // state
    const count = ref(0)
    // getters
    const doubleCount = computed(() => count.value * 2)
    // actions
    function increment() {
      count.value++
    }
    return { count, doubleCount, increment }
  }
)

在不同组件使用

// A.vue
import useStore from '@/hooks/useStore'
const { count, doubleCount, increment } = useStore()
// B.vue
import useStore from '@/hooks/useStore'
const { count, doubleCount, increment } = useStore()

在A/B组件获取的countdoubleCount数据一致,所以effectScope同时提供新的的状态管理方式,某种场景可以替代vuex

若是为了完成数据持久化,加入缓存即可。vueuse已经提供这种状态管理方式createGlobalState,若能够满足需求,直接使用即可

结语

时刻准备着