7. 副作用
Vue における副作用(Side Effect / Effect)とは、Vue の管理外で DOM を更新する処理や API との非同期通信等のデータ取得など、UI の構築以外の処理を指します。
7.1 watchEffect と onMounted
Vue で副作用を用いた処理を行うには、watchEffect 関数を利用します。watchEffect の書式は次の通りです。
watchEffect(コールバック関数);コールバック関数は、まずコンポーネントの作成時に一度実行され、その後はコールバック関数の中で使われている状態が変更されるたびに再実行されます1。
次の例は、カウンタのボタンがクリックされるごとにページのタイトルを更新するようなコンポーネントの例です。
Counter2.vue
<script setup lang="ts">import { ref, watchEffect } from "vue";
const count = ref<number>(0);
// count に変更があった場合にコールバック関数を実行するwatchEffect(() => { document.title = `クリック数:${count.value}回`;});
// ボタンクリック時のイベントハンドラconst incrementCount = () => { count.value++;};</script>
<template> <div> <button @click="incrementCount">{{ count }}</button> </div></template>watchEffect がコールバック関数の中で使われている count の変更を自動的に監視し、変更とともにコールバック関数を呼び出しています2。
一方、「状態の変更とは関係なく、マウント時に一度だけ」処理を実行したい場合には、onMounted 関数を使います。マウントとは、コンポーネントに対応する DOM ノードを作成し、既存の DOM ツリーに挿入して最終的な UI に出力する処理です。即ち、コールバック関数は UI が構築された後に呼び出されます。次のコンポーネントは、マウント時にのみアラートを表示するようなコンポーネントです。
Alert.vue
<script setup lang="ts">import { onMounted } from "vue";
// マウント時にコールバック関数を実行するonMounted(() => { alert("マウントされました!");});</script>
<template> <p>ダミーテキスト</p></template>onMounted のような、コンポーネントの一生(作成〜破棄)の特定のタイミングに処理を差し込む関数をライフサイクルフックと呼びます。仲間には、コンポーネントが破棄される直前に呼ばれる onUnmounted などもあります。
なお、watchEffect や onMounted は一つのコンポーネントの中で何度も使用することができます。そのため監視する状態によって処理を変えることができます。
7.2 クリーンアップ関数
watchEffect のコールバック関数は、引数として onCleanup という関数を受け取ることができます。onCleanup に渡した関数はクリーンアップ関数と呼ばれ、次にコールバック関数が実行される直前に実行されます(コンポーネントのアンマウント時など、監視が停止されるときにも実行されます)3。たとえば、状態の変更によってネットワークリクエストをやり直す際に、不要になった前回のリクエストをキャンセルするといった用途で使います。また、addEventListener などの関数を呼び出してイベントリスナを設置しているような場合、再実行のたびにイベントリスナが重複して登録されてしまうことがあります。このような状況を避けるためにも、クリーンアップ関数でイベントリスナを削除するなど、適切な処理を行う必要があります。
次の例で挙動を確認してみましょう。
Counter3.vue
<script setup lang="ts">import { ref, watchEffect } from "vue";
const count = ref<number>(0);
// count が変更された際に実行されるコールバック関数watchEffect((onCleanup) => { document.title = `クリック数:${count.value}回`;
onCleanup(() => { console.log("クリーンアップされました!"); });});
// ボタンクリック時のイベントハンドラconst incrementCount = () => { count.value++;};</script>
<template> <div> <button @click="incrementCount">{{ count }}</button> </div></template>開発者ツールのコンソールタブを開くと、ボタンをクリックするたびに クリーンアップされました! と表示されているはずです。
7.3 コンポーネントのレンダリングと描画
Vue のコンポーネントのレンダリングは次のタイミングで行われます。
- コンポーネントの初回レンダリング時
templateが参照している状態(ref・computed・props)に変化があったとき
Vue がレンダリングを行う目的は、変更前と変更後の仮想 DOM を構築して変更差分を検出することです。リアル DOM を構築する必要があるかどうかを検出し、変更差分がなければ DOM の更新は行われません。リアル DOM の構築の流れは以下の通りです。
- 変更前の仮想 DOM と変更後の仮想 DOM を用意する
- コンポーネントの状態が書き換えられる
- 書き換えられた状態を用いて仮想 DOM を再構築する
- 変更前と変更後の 仮想 DOM を比較し、差分を検出する
- 検出された差分のみリアル DOM に反映する
描画とは、レンダリングによって変更差分が見つかった場合に構築されたリアル DOM をブラウザに反映させることです。
Footnotes
-
正確には、初回の実行はコンポーネントが画面に挿入される(マウントされる)前です。React の
useEffect(マウント後に実行)とはタイミングが異なり、この時点では自分の DOM 要素にはまだアクセスできません。DOM が必要な処理は、後述のonMountedに書きましょう。 ↩ -
React の
useEffectでは、監視したい変数を「依存配列」として自分で列挙する必要がありました(React 編 7 章参照)。Vue のwatchEffectはコールバックの中で使われた状態を自動で追跡してくれるので、列挙は不要です。特定の状態だけを明示的に監視したい場合は、watch(監視対象, コールバック)という関数もあります。 ↩ -
Vue 3.5 以降では、コールバック関数の引数から
onCleanupを受け取る代わりに、vueから import したonWatcherCleanup関数を使うこともできます(こちらはwatchでも使えます)。 ↩