LoginSignup
4
3

More than 1 year has passed since last update.

Vue3.2 で追加された effectScope の使用例のメモ

Posted at

Vue 3.2 より Effect Scope API が追加されました。

こちらについての個人用メモです。

公式ドキュメントはこちら

一度これらのドキュメントに目を通すことをおすすめします。(特にRFC)

実際このメモもRFCの内容に近いです

Effect Scope の使用例

具体例を交えて説明していきます。

以下のような useWindowSize 関数を考えます。

useWindowSize.js
import { ref, readonly, onUnmounted } from "vue";

/**
 * windowのリサイズを監視し、ウィンドウサイズを返す
 */
function useWindowSize() {
  const width = ref(0);
  const height = ref(0);

  const onResize = (ev) => {
    const t = ev.target;
    width.value = t.innerWidth;
    height.value = t.innerHeight;

  };

  // 初期値
  width.value = window.innerWidth;
  height.value = window.innerHeight;

  // リスナー登録
  window.addEventListener("resize", onResize);

  onUnmounted(() => {
    window.removeEventListener("resize", onResize);
  });

  return {
    width: readonly(width),
    height: readonly(height),
  };
}

export {
  useWindowSize
}

この関数は 実行時に ウィンドウの resize イベント監視を開始し、コンポーネント廃棄時に監視を終了するようにしています。

利用する側は以下のように 戻り値の width, height を使います。

QiitaSandbox.vue
<script>
import { useWindowSize } from "./useWindowSize";

export default {
  name: "QiitaSandbox",
  setup() {
    const size = useWindowSize();

    // size.height, size.width を使う

    // 以下省略
  },
};
</script>

問題点

上記のコードでも特に問題なく動きますが、気になる点があります。

useWindowSize 関数ですが 使用箇所それぞれで window にリスナー登録が行われます。
極端な話、とあるコンポーネントで useWindowSizeが使われており、そのコンポーネントが100個使われてると、100個のリスナーが登録されることになります。

これを回避する方法の例としては ルートに近いコンポーネントで useWindowSize を使い、サイズを Vuex などのストアで管理する方法も考えられます。
ただし、利用するコンポーネントが存在しない状況でもリッスンされ続けるという問題があります。

これらの問題を独自で解決する方法もありますが、Effect Scope API を使うと解決することができるようになります。

書き換えてみる

まず useWindowSize 関数を書き換えます

useWindowSize.js
import { ref, readonly, onScopeDispose } from "vue";

/**
 * windowのリサイズを監視し、ウィンドウサイズを返すuse
 */
function useWindowSize() {
  const width = ref(0);
  const height = ref(0);

  const onResize = (ev) => {
    const t = ev.target;
    width.value = t.innerWidth;
    height.value = t.innerHeight;

    console.log(`resize(${width.value}, ${height.value})`);
  };

  // 初期値
  width.value = window.innerWidth;
  height.value = window.innerHeight;

  // リスナー登録
  window.addEventListener("resize", onResize);

  // 解除はonScopeDispose で行う
  onScopeDispose(() => {
    window.removeEventListener("resize", onResize);
  });

  return {
    width: readonly(width),
    height: readonly(height),
  };
}


export {
  useWindowSize
}

onUnmountedonScopeDispose に変わっています。
これは スコープが廃棄されるときに呼ばれるコールバックです。

次にスコープを作るための関数wrapScopeと、それを利用した新しいウィンドウサイズ用の関数useWindowSizeExを定義します。

wrapScope.js
import { effectScope, onScopeDispose } from "vue";

function wrapScope(fn) {
  let count = 0;
  let state = null;
  let scope = null;

  const dispose = () => {
    count--;
    if (count <= 0) {
      // count が0 
      // つまり、使用中のものがなくなった時に実体を廃棄する。

      state = null;
      scope.stop();
      scope = null;
    }
  };

  return (...args) => {
    count++;
    if (state == null) {
      //trueにするとスコープの親子関係が切り離される
      scope = effectScope(true);
      state = fn(...args);
    }

    //おもな呼び出しタイミングはコンポーネント廃棄の時
    onScopeDispose(dispose);

    return state;
  };
}

export {
  wrapScope
}
useWindowSizeEx.js
import { wrapScope } from "./wrapScope";
import { useWindowSize } from "./useWindowSize";

// wrapScope でラップすることで、
// 関数呼び出し毎にグローバルリスナーが登録されるのではなく
// グローバルリスナーは一つのみ
// 参照がなくなればグローバルのリスナーも削除される
// ということができるようになる。
const useWindowSizeEx = wrapScope(useWindowSize);

export { useWindowSizeEx };

ポイントとなるのは wrapScope 関数でこれは関数を返す関数です。

これが生成した関数は 実行されるたびに count が+1されていきます。また0→1の時に effectScope でスコープが 作成されます。 effectScope の引数は true にする事で 現在のスコープとは切り離された独立したスコープになるようです。また0→1のタイミングでfn が実行されることで1回だけ リスナー登録がされるようになります。

onScopeDispose はそれを利用しているスコープが廃棄されたとき(典型的な例ではコンポーネント廃棄時)に実行されます。
この時に dispose が実行され countが-1されていきます。全ての利用箇所がなくなった時に scope.stop()が実行されスコープを廃棄します。
この関数が実行されることで useWindowSizeonScopeDispose が実行され、リスナーが解除されます。

利用するvue コンポーネントは useWindowSize のかわりに useWindowSizeEx を使うだけで済みます。

こうすることで、リスナー登録は1回にしつつ、利用するコンポーネントがある時だけリスナーが存在し続けるという事ができるようになります。

おわりに

ここで紹介した利用例はほんの一例です。
Effect Scope の利用場面はこれ以外にもたくさんあると思います。

これらのライブラリでも Effect Scope は使われているので ソースを見ると使い方の参考になると思います。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3