文字记录vue团队成员讲解effectScope

零 Vue.js教程评论80字数 5020阅读16分44秒阅读模式

文字记录vue团队成员讲解effectScope

前言

说起 effectScope,我第一次见到还是在vueuse的源码中,还不太理解,大佬当时也讲解了很多:

Pasted image 20240624221224.png就在刚刚大佬也是出了期视频小小的讲解了一下,本文章结合大佬的讲解视频,以及源码来对视频进行总结文章源自灵鲨社区-https://www.0s52.com/bcjc/vue-jsjc/16134.html

开始

先看一个案例: 使用 effect 得方式写一个累加器,并使用一个暂停按钮,实现暂停后,累加器再点击加减无效文章源自灵鲨社区-https://www.0s52.com/bcjc/vue-jsjc/16134.html

html

复制代码
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.4.29/vue.global.min.js"></script>
    <style>
      #app {
        margin: 100px 300px;
      }
      button {
        background: #1a1a1a;
        color: #fff;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div>counter: <span class="counter"></span></div>
      <div>doubled: <span class="doubled"></span></div>
      <button class="increment">+</button>
      <button class="decrement">-</button>
      <button class="stop">停止</button>
    </div>

    <script>
      const { watch, watchEffect, ref, effect, effectScope, computed } = Vue
      // effectScope
      const counterEl = document.querySelector('.counter')
      const doubledEl = document.querySelector('.doubled')
      const incrementBtn = document.querySelector('.increment')
      const decrementBtn = document.querySelector('.decrement')
      const stopBtn = document.querySelector('.stop')
      const counter = ref(0)
      const cleanups = []
      
      watchEffect(() => {
        counterEl.textContent = counter.value
      })

      watch(counter, () => {
        console.log("使用 watch,监听counter变化")
      })

      effect(() => {
        doubledEl.textContent = counter.value * 2
      })

      incrementBtn.addEventListener('click', () => {
        counter.value++
      })

      decrementBtn.addEventListener('click', () => {
        counter.value--
      })

      stopBtn.addEventListener('click', () => {
        // 点击停止,让累加器不在变化
      })

    </script>
  </body>
</html>

这是分别使用了 watchEffect 和 effect 来改变html上的值,这时候在这个代码实现停止功能的话,需要把每个effect清除掉文章源自灵鲨社区-https://www.0s52.com/bcjc/vue-jsjc/16134.html

js

复制代码
      const stopWatchEffect = watchEffect(() => {
        counterEl.textContent = counter.value
      })

      const stopWatch = watch(counter, () => {
        console.log("使用 watch,监听counter变化")
      })

      const {effect: effectInstance} = effect(() => {
        doubledEl.textContent = counter.value * 2
      })
      
      cleanups.push(stopWatchEffect, stopWatch, effectInstance.stop.bind(effectInstance))

      incrementBtn.addEventListener('click', () => {
        counter.value++
      })

      decrementBtn.addEventListener('click', () => {
        counter.value--
      })

      stopBtn.addEventListener('click', () => {
        // 点击停止,让累加器不在变化
        cleanups.forEach(clean => clean())
      })

这样清除后,再去点击增减的时候,就没有依赖执行了文章源自灵鲨社区-https://www.0s52.com/bcjc/vue-jsjc/16134.html

接下来使用 effectScope 来实现这个需求文章源自灵鲨社区-https://www.0s52.com/bcjc/vue-jsjc/16134.html

effectScope 源码

在使用它实现这个需求前,需要知道它到底是啥,直接翻源码看下文章源自灵鲨社区-https://www.0s52.com/bcjc/vue-jsjc/16134.html

ts

复制代码
export class EffectScope {
  /**
   * @internal
   */
  private _active = true
  /**
   * @internal
   */
  effects: ReactiveEffect[] = []
  /**
   * @internal
   */
  cleanups: (() => void)[] = []

  /**
   * only assigned by undetached scope
   * @internal
   */
  parent: EffectScope | undefined
  /**
   * record undetached scopes
   * @internal
   */
  scopes: EffectScope[] | undefined
  /**
   * track a child scope's index in its parent's scopes array for optimized
   * removal
   * @internal
   */
  private index: number | undefined

  constructor(public detached = false) {
    this.parent = activeEffectScope
    if (!detached && activeEffectScope) {
      this.index =
        (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
          this,
        ) - 1
    }
  }

  get active() {
    return this._active
  }

  run<T>(fn: () => T): T | undefined {
    if (this._active) {
      const currentEffectScope = activeEffectScope
      try {
        activeEffectScope = this
        return fn()
      } finally {
        activeEffectScope = currentEffectScope
      }
    } else if (__DEV__) {
      warn(`cannot run an inactive effect scope.`)
    }
  }

  /**
   * This should only be called on non-detached scopes
   * @internal
   */
  on() {
    activeEffectScope = this
  }

  /**
   * This should only be called on non-detached scopes
   * @internal
   */
  off() {
    activeEffectScope = this.parent
  }

  stop(fromParent?: boolean) {
    if (this._active) {
      let i, l
      for (i = 0, l = this.effects.length; i < l; i++) {
        this.effects[i].stop()
      }
      for (i = 0, l = this.cleanups.length; i < l; i++) {
        this.cleanups[i]()
      }
      if (this.scopes) {
        for (i = 0, l = this.scopes.length; i < l; i++) {
          this.scopes[i].stop(true)
        }
      }
      // nested scope, dereference from parent to avoid memory leaks
      if (!this.detached && this.parent && !fromParent) {
        // optimized O(1) removal
        const last = this.parent.scopes!.pop()
        if (last && last !== this) {
          this.parent.scopes![this.index!] = last
          last.index = this.index!
        }
      }
      this.parent = undefined
      this._active = false
    }
  }
}

export function effectScope(detached?: boolean) {
  return new EffectScope(detached)
}

可以看到 effectScope就是一个工厂函数,返回了一个 EffectScope的实例,虽然这个 class EffectScope 看起来很长,如果一步一步看,其实也并没有多少东西,大致这个实例就是有 run,off,on,stop方法,以及一些参数,暂时不用管,可以看到 stop 方法中 cleanups 部分,将这个数组中的函数依次执行,这和我们第一次实现暂停一模一样,就是收集了所有 effect 的 stop 放在这个数组中,需要暂停时,执行一遍即可。文章源自灵鲨社区-https://www.0s52.com/bcjc/vue-jsjc/16134.html

结合大佬给我讲解的记录来看,他会有嵌套的 scope,所以在 stop函数中,也全部执行了一次 scope 的 stop 函数,递归清除,也可以嵌套 effect,所以在收集的effect也是执行了stop,大致就了解这些吧,源码比较详细,可以自己多试试。文章源自灵鲨社区-https://www.0s52.com/bcjc/vue-jsjc/16134.html

使用effectScope

effectScope一定了解后,开始使用effectScope来实现这个需求文章源自灵鲨社区-https://www.0s52.com/bcjc/vue-jsjc/16134.html

js

复制代码
      const scope = effectScope()

      scope.run(() => {
        watchEffect(() => {
            counterEl.textContent = counter.value
        })

        watch(counter, () => {
            console.log("使用 watch,监听counter变化")
        })

        effect(() => {
            doubledEl.textContent = counter.value * 2
        })
      })
      

      incrementBtn.addEventListener('click', () => {
        counter.value++
      })

      decrementBtn.addEventListener('click', () => {
        counter.value--
      })

      stopBtn.addEventListener('click', () => {
        // 点击停止,让累加器不在变化
        scope.stop()
      })

在 run 函数中,会自动收集 effect,scope,stop函数等,收集到这个scope中,执行stop函数的时候,会执行内部的每个effect.stop,cleanups收集的stop还有递归执行scope.stop。文章源自灵鲨社区-https://www.0s52.com/bcjc/vue-jsjc/16134.html

在 vueuse中大量使用了effectScope,可以挑个看下

effectScope在vueuse的运用

ts

复制代码
export function createGlobalState<Fn extends AnyFn>(
  stateFactory: Fn,
): Fn {
  let initialized = false
  let state: any
  const scope = effectScope(true)

  return ((...args: any[]) => {
    if (!initialized) {
      state = scope.run(() => stateFactory(...args))!
      initialized = true
    }
    return state
  }) as Fn
}

创建了一个单独的 scope,初始化时收集依赖,并返回了参数函数的返回值。具体使用就不过多说明了,可以去vueuse中查看相关代码。

总结

  1. effectScope 在平时写代码中基本不会使用到,只有封装一个库等,提供给别人使用的工具,就回经常使用到。
  2. effectScope 就是提供了一个给用户对上下文进行简单操作的方式。

零
  • 转载请务必保留本文链接:https://www.0s52.com/bcjc/vue-jsjc/16134.html
    本社区资源仅供用于学习和交流,请勿用于商业用途
    未经允许不得进行转载/复制/分享

发表评论