• vue3 effectScope解析


    什么是effectScope

    • 用于收集在其中所创建的副作用,并能对其进行统一的处理

    为什么会有effectScope

    对于@vue/reactivity相关的Api,比如ref、computed、reactive、effect、watch等,根据当前的执行环境是否在组件上下文中,有以下两种情况:

    • 在vue的setup中,那么在组件初始化的时候,它们在调用过程中产生的所谓的effect,会被自动收集且绑定到当前组件实例上,在组件卸载时(onUnmounted),effect也会随之卸载掉,这也是一些api提供了stopHandle,但不需要手动调用的原因
    // 组件实例被创建的时候也会创建一个scope
    export function createComponentInstance(...args) {
        // ...
        const instance = {
            // ...
            vnode,
            type,
            scope: new EffectScope(true /* detached */),
            // ...
        }
        return instance
    }
    // 组件卸载时会调用 stop 方法
    const unmountComponent = (...args) => {
        // ...
        scope.stop()
        // ...
    }
    
    // watchEffect 和 watch 返回的 stop 方法
    function doWatch() {
        // ...
        return () => {
            effect.stop()
            if (instance && instance.scope) {
                remove(instance.scope.effects!, effect)
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 但我们在组件外使用或者编写一个独立的包时,这会变得不一样,这种情况该如何取消响应式依赖呢
      • 需要开发者手动去消除依赖,对于依赖较多的场景,则会显得很麻烦,甚至会忘掉一些隐蔽性强的依赖造成数据泄露、状态不一致等问题
    //(参考 vue-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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 总结一下:对于现在版本的vue,将 @vue/reactivity 即响应式单独拆分出来了,意味着可以脱离vue环境使用,当不在组件中执行时,也就意味着失去了vue所带来的自动卸载effect的能力,所以开发者需要手动去管理这些effect:
      • 创建scope环境收集effect
      • 适当的时机去除effect,即stop,随之配套的还有 onScopeDispose 来监听 scope 的销毁、getCurrentScope() 获取当前活跃的 scope

    使用场景

    • 避免随着组件生命周期重复创建某些监听

    // 使用到的组件都会重复创建监听器
    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 }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 通过effecScope创建独立的scope
    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替换onUnmounted,意味着可以脱离组件使用
      onScopeDispose(() => {
        window.removeEventListener('mousemove', handler)
      })
    
      return { x, y }
    }
    
    function createSharedComposable(composable) {
      let subscribers = 0
      let state, scope
    
      const dispose = () => {
        // 通过闭包进行计数,当subscribers为0时,stop掉该scope
        // 如果在组件中使用,则onUnmounted就意味着subscribers-1
        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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 简易的状态管理

    // useGlobalState
    import { effectScope } from '@vue/composition-api'
    
    export default run => {
      let state
      const scope = effectScope(true)
      return () => {
        // 防止重复触发
        if (!state) {
          state = scope.run(run)
        }
        return state
      }
    }
    
    // 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 }
      }
    )
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    和useSyncExternalStore区别

    • effecScope 和 React 18的useSyncExternalStore都能做一个简易的状态管理,倒不如说二者都具有收集发布的作用
      • useSyncExternalStore 需要手动订阅,而 effecScope 帮你做了这件事
      • 个人觉得二者最大的区别在于各自框架实现响应式的细节不一样,但最上层订阅发布的思路都差不太多
  • 相关阅读:
    网站服务器是什么意思?它的用途有哪些?
    高仿互站网多套模板完整源码
    500左右的耳机哪款降噪最好?500左右降噪最好的耳机推荐
    求直角三角形第三点的坐标
    Java中的堆和栈
    数据结构刷题——图论
    IP-Guard如何判断Windows客户端是否安装成功?
    【Python深度学习】Python全栈体系(二十七)
    mysql基础_视图
    Mysql数据库对表的基本操作
  • 原文地址:https://blog.csdn.net/weixin_43294560/article/details/128199051