Vue 3.2 より Effect Scope API
が追加されました。
こちらについての個人用メモです。
公式ドキュメントはこちら
一度これらのドキュメントに目を通すことをおすすめします。(特にRFC)
実際このメモもRFCの内容に近いです
Effect Scope の使用例
具体例を交えて説明していきます。
以下のような useWindowSize
関数を考えます。
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
を使います。
<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
関数を書き換えます
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
}
onUnmounted
が onScopeDispose
に変わっています。
これは スコープが廃棄されるときに呼ばれるコールバックです。
次にスコープを作るための関数wrapScope
と、それを利用した新しいウィンドウサイズ用の関数useWindowSizeEx
を定義します。
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
}
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()
が実行されスコープを廃棄します。
この関数が実行されることで useWindowSize
の onScopeDispose
が実行され、リスナーが解除されます。
利用するvue
コンポーネントは useWindowSize
のかわりに useWindowSizeEx
を使うだけで済みます。
こうすることで、リスナー登録は1回にしつつ、利用するコンポーネントがある時だけリスナーが存在し続けるという事ができるようになります。
おわりに
ここで紹介した利用例はほんの一例です。
Effect Scope
の利用場面はこれ以外にもたくさんあると思います。
これらのライブラリでも Effect Scope
は使われているので ソースを見ると使い方の参考になると思います。