Vueで親コンポーネントへdataの値を渡す方法

Vueで親コンポーネントへdataの値を渡す方法について備忘録としてこちらにまとめます。

Vueのインスタンスをグローバル登録した際にdataプロパティの値を親コンポーネントに渡す方法は、以下の2つになります。

1つ目

  <div id="app">
    <my-component v-bind:list="list"></my-component>
  </div>
Vue.component('my-component', {
  template: `
    <ul>
      <comp-child v-for="item in list" v-bind:key="item.id" v-bind:name="item.name" v-bind:hp="item.hp"></comp-child>
    </ul>`,
    props: ['list']
})

Vue.component('comp-child', {
  template: '<li>{{ name }} HP.{{ hp }}</li>',
  props: ['name', 'hp']
});

new Vue({
  el: '#app',
  data: {
    list: [
      { id: 1, name: 'スライム', hp: 100 },
      { id: 2, name: 'ゴブリン', hp: 200 },
      { id: 3, name: 'ドラゴン', hp: 500 }
    ]
  }
});

Vueインスタンスにdataプロパティが設定されているので、htmlのid="app"の中では、dataの値を使えるので、v-bindで属性を設定し、属性の値としてdataの値を渡す。
親コンポーネント(my-component)でpropsとして属性に設定した値を受け取る。

2つ目

  <div id="app">
  </div>
Vue.component('my-component', {
  template: `
    <ul>
      <comp-child v-for="item in list" v-bind:key="item.id" v-bind:name="item.name" v-bind:hp="item.hp"></comp-child>
    </ul>`,
    props: ['list']
})

Vue.component('comp-child', {
  template: '<li>{{ name }} HP.{{ hp }}</li>',
  props: ['name', 'hp']
});

new Vue({
  el: '#app',
  data: {
    list: [
      { id: 1, name: 'スライム', hp: 100 },
      { id: 2, name: 'ゴブリン', hp: 200 },
      { id: 3, name: 'ドラゴン', hp: 500 }
    ]
  },
  template: '<my-component :list="list"></my-component>'
});

template オプションを使って、my-component をルートコンポーネントとして指定する。

※ ルートコンポーネントとは、Vue.jsアプリケーション全体のエントリーポイントとなる最初のコンポーネントのことを指します。
このルートコンポーネントが、他のすべての子コンポーネントの親となる。
一般的には、Vueインスタンスに直接マウントされるコンポーネントのことを指す。
id="app"の部分にルートコンポーネントがマウントされる。

Vue.jsのカスタムディレクティブのフックの違い

Vue.jsのカスタムディレクティブのフックの違いについて備忘録として、こちらにまとめます。

Vue.jsのカスタムディレクティブのフックには、以下の6つがある。

  • bind
  • inserted
  • update
  • componentUpdated
  • unbind

それぞれのフックのタイミングは、以下のようになる。
bind: ディレクティブが初めて要素にバインドされたときに呼び出される。(ディレクティブが要素と紐づいた時)
inserted: ディレクティブが設定されている要素が親ノードに挿入されたときに呼び出さる。(この時点で要素は確実にDOMに存在します。)

実際の挙動

  <div id="app">
    <input v-example>
  </div>
new Vue({
  el: '#app',
  directives: {
    example: {
      // ディレクティブが初めてバインドされたときに呼び出される
      bind: function(el) {
        console.log('v-example bind');
        el.value = 'バインドされました';
      },
      // バインドされた要素が親ノードに挿入されたときに呼び出される
      inserted: function(el) {
        console.log('v-example inserted');
        el.focus();
      }
    }
  }
});

gyazo.com

ポイント

bindフックとinsertedフックの違いは、ディレクティブが要素に紐づいた時か、ディレクティブが設定されている要素が親ノードに挿入された時かの違い。
処理の順番として以下のようになる。
①要素とディレクティブが紐づく(このタイミングでは、Nodeツリーは、構築されていない。)
②Nodeツリーが構築される

update: ディレクティブが紐づいている要素が再レンダリングされるたびに呼び出される。子コンポーネントの更新は含まれない。
componentUpdated: ディレクティブが紐づいている要素が含まれるコンポーネントとその子コンポーネントが全て再レンダリングされた後に呼び出される。

実際の挙動

  <div id="app">
    <input v-example v-model="inputValue">
    <p>{{ inputValue }}</p>
  </div>
new Vue({
  el: '#app',
  data: {
    inputValue: 'テストです!'
  },
  directives: {
    example: {
      update: function(el) {
        console.log('v-example update');
      },
      componentUpdated: function(el) {
        console.log('v-example componentUpdated');
      }
    }
  }
});

gyazo.com

unbind: ディレクティブが紐づいている要素から取り除かれた時に 1 度だけ呼ばれる。

実際の挙動

  <div id="app">
    <button v-on:click="addInput">入力フィールドを追加</button>
    <button v-on:click="removeInput">入力フィールドを削除</button>
    <div v-for="(input, index) in inputs" :key="index">
      <input v-example>
    </div>
  </div>

gyazo.com

Vue.jsのwatchでのnewValとoldVal

Vue.jsのwatchでのnewValとoldValの参照について、備忘録としてまとめます。

watchの中で、変更後の値と古い値を参照できるが、参照する際に気をつけないと、古い値と新しい値が同期してしまう。
具体的なイメージを掴むために、以下に具体的なコードを示します。

[古い値と新しい値が同期してしまう。]

  <div id="app">
    <button v-on:click="change">チェンジ</button>
    <ul>
      <li v-for="item in list" :key="item">{{ item }}</li>
    </ul>
  </div>
new Vue({
  el: '#app',
  data: {
    list: [1, 2, 3]
  },
  methods: {
    change: function() {
      // 配列に新しい要素を追加
      this.list.push(this.list.length + 1);
    }
  },
  created: function() {
    this.$watch(
      'list',
      function(newVal, oldVal) {
        console.log('New length:', newVal.length);
        console.log('Old length:', oldVal.length);
      },
      {
        immediate: true
      }
    );
  }
});

ブラウザでは、最初に以下のように表示される。 gyazo.com

[チェンジ]ボタンをクリックすると以下のように表示される。 gyazo.com

※
[チェンジ]ボタンをクリックすると、watchで監視しているデータが変更されても、newValとoldValが同期してしまい同じ要素数が表示されてしまう。

以下に対処法の具体例を示します。

[古い値と新しい値が同期しないように、監視対象の配列をコピーする。]

  <div id="app">
    <button v-on:click="change">チェンジ</button>
    <ul>
      <li v-for="item in list" :key="item">{{ item }}</li>
    </ul>
  </div>
new Vue({
  el: '#app',
  data: {
    list: [1, 2, 3]
  },
  methods: {
    change: function() {
      // 配列に新しい要素を追加
      this.list.push(this.list.length + 1);
    }
  },
  created: function() {
    this.$watch(
      function() {
        return Object.assign([], this.list); // 配列のシャローコピーを返す
      },
      function(newVal, oldVal) {
        console.log('New length:', newVal.length);
        console.log('Old length:', oldVal.length);
      },
      {
        immediate: true
      }
    );
  }
});

Object.assign([], this.list)は、this.list配列をシャローコピー(浅いコピー)する。
createdにwatchの処理を入れることで、最初に監視対象に設定できる。

ブラウザでは、最初に以下のように表示される。 gyazo.com

[チェンジ]ボタンをクリックすると以下のように表示される。 gyazo.com

※
[チェンジ]ボタンをクリックすると、watchで監視しているデータが変更され、newValとoldValの要素数を取得できる。

[古い値と新しい値が同期しないように、対象のデータにプロパティを追加する。]

  <div id="app">
    <button v-on:click="change">チェンジ</button>
    <ul>
      <li v-for="item in list" :key="item">{{ item }}</li>
    </ul>
  </div>
new Vue({
  el: '#app',
  data: {
    list: [1, 2, 3]
  },
  methods: {
    change: function() {
      // 配列に新しい要素を追加
      this.list.push(this.list.length + 1);
    }
  },
  created: function() {
    this.$watch(
      function() {
        return { value: this.list, length: this.list.length } // 配列のシャローコピーを返す
      },
      function(newVal, oldVal) {
        console.log('New length:', newVal.length);
        console.log('Old length:', oldVal.length);
      },
      {
        immediate: true
      }
    );
  }
});

対象のデータにプロパティを追加する。
createdにwatchの処理を入れることで、最初に監視対象に設定できる。

ブラウザでは、最初に以下のように表示される。 gyazo.com

[チェンジ]ボタンをクリックすると以下のように表示される。

gyazo.com

※
[チェンジ]ボタンをクリックすると、watchで監視しているデータが変更され、newValとoldValの要素数を取得できる。

Vue.jsのimmediateオプションとdeepオプションの挙動

Vue.jsのimmediateオプションの挙動について、備忘録としてまとめます。
immediateオプションをtrueにすると、初期化時に呼び出される。
以下に挙動を具体的にイメージするためのコードを記載します。

 <div id="app">
    <button v-on:click="change">チェンジ</button>
    <p>{{ watchTarget }}</p>
  </div>

[wachを定義(immediateオプションがfalse)した後で、データを変える]

new Vue({
  el: '#app',
  data: {
    width: 400,
    height: 600
  },
  methods: {
    change: function() {
      this.$watch(function() {
        [this.width, this.height]
      }, function() {
        console.log('テストです!');
      }, { deep: true, immediate: false });
      
      this.width = 400
      this.height = 400
    }
  },
  computed: {
    watchTarget: function() {
      return [this.width, this.height]
    }
  }
})

ブラウザでの初期表示は、以下のようになる。 gyazo.com

[チェンジ]ボタンをクリックした後のブラウザの表示は、以下のようになる。 gyazo.com

※ 処理の流れは、以下のようになっている。 ①[チェンジ]ボタンをクリックした時にchangeメソッドの中に処理が入る。

↓

②immediateオプションがfalseになってるので、console.log('テストです!');は、実行されない。(初回の1回目の時には、実行されない。)

↓

③watchで監視しているデータが変更されたので、changeメソッドが発火されconsole.log('テストです!');が実行される。

[データを変える処理の後で、wachを定義(immediateオプションがtrue)する]

new Vue({
  el: '#app',
  data: {
    width: 400,
    height: 600
  },
  methods: {
    change: function() {
      
      this.width = 400
      this.height = 400

      this.$watch(function() {
        [this.width, this.height]
      }, function() {
        console.log('テストです!');
      }, { deep: true, immediate: true });
    }
  },
  computed: {
    watchTarget: function() {
      return [this.width, this.height]
    }
  }
})

ブラウザでの初期表示は、以下のようになる。 gyazo.com

[チェンジ]ボタンをクリックした後のブラウザの表示は、以下のようになる。 gyazo.com

※ 処理の流れは、以下のようになっている。 ①[チェンジ]ボタンをクリックした時にchangeメソッドの中に処理が入る。

↓

②watchで監視しているデータが変更されたが、watchを定義しているのはデータ変更の処理の後なので、データ変更時の発火分のconsole.log('テストです!');が実行されない。

↓

③watchで監視しているデータが変更されたので、changeメソッドが発火されconsole.log('テストです!');が実行される。

[wachを定義(immediateオプションがtrue)した後で、データを変える]

new Vue({
  el: '#app',
  data: {
    width: 400,
    height: 600
  },
  methods: {
    change: function() {
      this.$watch(function() {
        [this.width, this.height]
      }, function() {
        console.log('テストです!');
      }, { deep: true, immediate: true });
      
      this.width = 400
      this.height = 400
    }
  },
  computed: {
    watchTarget: function() {
      return [this.width, this.height]
    }
  }
})

ブラウザでの初期表示は、以下のようになる。 gyazo.com

[チェンジ]ボタンをクリックした後のブラウザの表示は、以下のようになる。

gyazo.com

※ 処理の流れは、以下のようになっている。 ①[チェンジ]ボタンをクリックした時にchangeメソッドの中に処理が入る。

↓

②immediateオプションがtrueになってるので、console.log('テストです!');が実行される。(初回の1回目の時に実行される。)

↓

③watchで監視しているデータが変更されたので、changeメソッドが発火されconsole.log('テストです!');が実行される。

[wachを定義(deepオプションがfalse)した後で、データを変える]

new Vue({
  el: '#app',
  data: {
    width: 400,
    height: 600
  },
  methods: {
    change: function() {
      this.$watch(function() {
        [this.width, this.height]
      }, function() {
        console.log('テストです!');
      }, { deep: false, immediate: false });
      
      this.width = 400
      this.height = 400
    }
  },
  computed: {
    watchTarget: function() {
      return [this.width, this.height]
    }
})

ブラウザでの初期表示は、以下のようになる。 gyazo.com

[チェンジ]ボタンをクリックした後のブラウザの表示は、以下のようになる。 gyazo.com

※ 処理の流れは、以下のようになっている。 ①[チェンジ]ボタンをクリックした時にchangeメソッドの中に処理が入る。

↓

②watchで監視しているデータが変更されたが、監視対象を配列にして複数にしており、deepオプションがfalseになってるので、console.log('テストです!');が実行されない。

Dockerでmysqlのイメージのバージョンを8.0から5.7.44に下げたことによるエラー

Dockerでmysqlのイメージのバージョンを8.0から5.7.44に下げたらmysqlのコンテナが立ち上がらなくなってしまった。 Docker Desktopを確認すると以下のような状態であった。

gyazo.com

Docker Desktopのmysqlコンテナのログを確認すると以下のようなエラーが出ていた。

2024-06-25 14:55:41 2024-06-25T05:55:41.376353Z 0 [ERROR] [FATAL] InnoDB: Table flags are 0 in the data dictionary but the flags in file ./ibdata1 are 0x4800!
2024-06-25 14:55:41 2024-06-25 14:55:41 0x7ffffbe5e880  InnoDB: Assertion failure in thread 140737419536512 in file ut0ut.cc line 921
2024-06-25 14:55:41 InnoDB: We intentionally generate a memory trap.
2024-06-25 14:55:41 InnoDB: Submit a detailed bug report to http://bugs.mysql.com.
2024-06-25 14:55:41 InnoDB: If you get repeated assertion failures or crashes, even
2024-06-25 14:55:41 InnoDB: immediately after the mysqld startup, there may be
2024-06-25 14:55:41 InnoDB: corruption in the InnoDB tablespace. Please refer to
2024-06-25 14:55:41 InnoDB: http://dev.mysql.com/doc/refman/5.7/en/forcing-innodb-recovery.html
2024-06-25 14:55:41 InnoDB: about forcing recovery.
2024-06-25 14:55:41 05:55:41 UTC - mysqld got signal 6 ;
2024-06-25 14:55:41 This could be because you hit a bug. It is also possible that this binary
2024-06-25 14:55:41 or one of the libraries it was linked against is corrupt, improperly built,
2024-06-25 14:55:41 or misconfigured. This error can also be caused by malfunctioning hardware.
2024-06-25 14:55:41 Attempting to collect some information that could help diagnose the problem.
2024-06-25 14:55:41 As this is a crash and something is definitely wrong, the information
2024-06-25 14:55:41 collection process might fail.

docker compose upを実行して、localhostにアクセスすると以下のような画面になった。

gyazo.com

dockerのイメージ、コンテナ、volumeを削除すると解決できた。 具体的なコマンドと操作手順は、以下のようになる。

①コンテナの削除

docker ps -aでコンテナの一覧を表示する

上記で出てきたコンテナを以下のコマンドで全て削除する docker rm コンテナID

②イメージの削除

docker imagesでイメージの一覧を表示する

上記で出てきたイメージを以下のコマンドで全て削除する

docker rmi イメージ名

③volumeの削除

docker volume lsでvolumeの一覧を表示する

上記で出てきたvolumeを以下のコマンドで全て削除する

docker volume rm VOLUMEのID

④buildのやり直し

docker-compose build --no-cache

⑤起動する

docker-compose up -d

mysqlのイメージのバージョンを8.0から5.7.44に下げたことで、InnoDBが8.0から4.7への移行をサポートしていなく今回のエラーになった可能性があります。

参考記事

docker-composeでmysqlのバージョン変えたら起動しなくなった #MySQL - Qiita

Dockerイメージ削除時に Error: No such image #Docker - Qiita

HTML属性とDOMプロパティの違い

Vueのv-bind:valueとv-bind:value.propの違いが分からず、 それを理解するのにHTML属性とDOMプロパティの違いを理解する必要があったので備忘録として、記載します。

[v-bind:value]
HTML属性のバインドを行う

[v-bind:value.prop]
HTML属性ではなくDOMプロパティのバインディングを行う

ここで、HTML属性とDOMプロパティは、何が違うのかと疑問に思った。 具体的なイメージで理解するために以下にサンプルコードを記載する。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>HTML Attributes vs DOM Properties</title>
</head>
<body>
  <input type="text" id="myInput" value="Hello">
  <button onclick="changeValue()">Change Value</button>
  <script>
    function changeValue() {
      var input = document.getElementById('myInput');
      
      // DOMプロパティを変更
      input.value = "Changed via DOM Property";
      
      // HTML属性を取得
      console.log(input.getAttribute('value')); // 出力: Hello
      
      // DOMプロパティを取得
      console.log(input.value); // 出力: Changed via DOM Property
      
      // HTML属性を変更
      input.setAttribute('value', 'Changed via HTML Attribute');
      
      // HTML属性を取得
      console.log(input.getAttribute('value')); // 出力: Changed via HTML Attribute
    }
  </script>
</body>
</html>

最初の表示では、以下のようになっている。(value="Hello"になっている) gyazo.com

[Change Value]ボタンをクリックすると以下のようになる。(value="Changed via HTML Attribute"になっている) これは、

input.setAttribute('value', 'Changed via HTML Attribute');

でHTML属性を変更しているため。 上記のようにHTML属性を変更する処理を記載しないと、画面表示は切り替わるがHTML属性は、切り替わらない。 DOMを変更するだけだと画面表示は切り替わるが、HTML属性は切り替わらない。

gyazo.com

[v-bind:value]と[v-bind:value.prop]の違いがこれで分かったが、 以下の流れになるので結果としてVueでは、同じ挙動となる。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>v-bind:value vs v-bind:value.prop Example</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.11/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <input type="text" v-bind:value="message">
    <input type="text" v-bind:value.prop="message">
    <p>{{ message }}</p>
    <button @click="updateMessage">Update Message</button>
  </div>

  <script>
    var app = new Vue({
      el: '#app',
      data: {
        message: 'Hello, Vue.js!'
      },
      methods: {
        updateMessage: function() {
          this.message = 'Updated message!';
        }
      }
    });
  </script>
</body>
</html>

①ページが読み込まれると、input要素のvalue属性は"Hello"に設定される。

②[Update Message]ボタンをクリックすると、updateMessageメソッドが呼び出され、messageデータプロパティが"Updated message!"に更新される。

③プロパティが更新されたことで、再レンダリングされる。  再レンダリングされる時に、HTML属性は、value="Changed via HTML Attribute"になっているので、DOMプロパティもvalue="Changed via HTML Attribute"になる。

ポイント

  • ページが表示される際に、HTML文書を読み込みHTMLの属性値に対応するDOMプロパティに初期値として設定される。
  • JavaScriptを使用してDOMプロパティを変更すると、その変更は即座にUIに反映されるが、HTML属性は更新されない。

Vueのライフサイクルフック

Vueを勉強している時にライフサイクルフックの違いで少し悩んだので、調べたことを備忘録として記載します。

Vueのライフサイクルフックには、主に以下の8種類があります。

  • beforeCreate
    説明: インスタンスの初期化直前に呼ばれます。この時点ではデータやメソッドはまだセットアップされていません。
    タイミング: インスタンスが作成される前。
  • created
    説明: インスタンスが作成された直後に呼ばれます。データやメソッドが利用可能です。
    タイミング: インスタンスが作成された後。
  • beforeMount
    説明: テンプレートがDOMにマウントされる直前に呼ばれます。 タイミング: DOMにテンプレートがマウントされる前。
  • mounted
    説明: テンプレートがDOMにマウントされた直後に呼ばれます。この時点でDOMノードにアクセスできます。 タイミング: DOMにテンプレートがマウントされた後。
  • beforeUpdate
    説明: データが変更され、再描画が始まる直前に呼ばれます。 タイミング: データが変更されたが、DOMが再描画される前。
  • updated
    説明: データの変更による再描画が完了した後に呼ばれます。 タイミング: DOMが再描画された後。
  • beforeDestroy
    説明: インスタンスが破棄される直前に呼ばれます。この時点でまだインスタンスは利用可能です。 タイミング: インスタンスが破棄される前。
  • destroyed
    説明: インスタンスが破棄された後に呼ばれます。この時点でインスタンスのすべてのディレクティブがバインディング解除され、リスナーも削除されています。 タイミング: インスタンスが破棄された後。

それぞれのライフサイクルフックのイメージを具体的に掴むために以下に具体例を示します。

[index.html]

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue.js App</title>
  <link href="main.css" rel="stylesheet">
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.11/dist/vue.js"></script>
</head>

<body>
  <div id="app">
    <p id="test">{{message}}</p>
    <button v-on:click="changeMessage">メッセージ変更</button>
  </div>

  <script src="main.js"></script>
</body>
</html>
[main.js]

var app = new Vue({
  el: '#app',
  data: {
    message: "初期値",
    show: "true"
  },
  methods: {
    changeMessage: function() {
      this.message = "変更後";
    }
  },

  beforeCreate() {
    console.log('beforeCreate:' + this.message);
  },
  created() {
    console.log('created:' + this.message);
  },
  beforeMount() {
    let dom = document.getElementById('test');
    console.log('beforeMount: ' + this.message);
    // beforeMountではDOM要素がまだ存在しないため、nullが返る
    console.log('beforeMount:', dom); 
  },
  mounted() {
    let dom = document.getElementById('test');
    console.log('mounted: ' + this.message);
    // mountedではDOM要素が存在するため、取得できる
    console.log('mounted:', dom); 
  },
  beforeUpdate() {
    let dom = document.getElementById('test')
    let innerText = dom.innerText
    console.log('beforeUpdate:' + innerText);
  },
  updated() {
    let dom = document.getElementById('test')
    let innerText = dom.innerText
    console.log('updated:' + innerText);
  }
})

最初にブラウザに表示され時点では、以下のようになっている。 gyazo.com

[メッセージ変更]ボタンをクリックすると、以下のようになっている。 gyazo.com

※
beforeDestroyとdestroyedに関しては、以下の注意欄に具体イメージを記載しています。

注意

beforeDestroyとdestroyedが呼ばれるタイミングに関して注意があります。 そもそもbeforeDestroyとdestroyedが呼ばれるタイミングとして、主に2つあります。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue Instance Destroy Example</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.11/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <p>{{ message }}</p>
    <button @click="destroyInstance">Destroy Instance</button>
  </div>

  <script>
    const app = new Vue({
      el: '#app',
      data: {
        message: 'Hello, Vue.js!'
      },
      methods: {
        destroyInstance() {
          this.$destroy();
        }
      },
      beforeDestroy() {
        console.log('beforeDestroy: インスタンスが破棄される直前');
      },
      destroyed() {
        console.log('destroyed: インスタンスが破棄された後');
      }
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue Component Destroy Example</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.11/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <button @click="toggleComponent">Toggle Component</button>
    <child-component v-if="showComponent"></child-component>
  </div>

  <script>
    Vue.component('child-component', {
      template: '<div>Child Component</div>',
      beforeDestroy() {
        console.log('Child component beforeDestroy: コンポーネントが破棄される直前');
      },
      destroyed() {
        console.log('Child component destroyed: コンポーネントが破棄された後');
      }
    });

    new Vue({
      el: '#app',
      data: {
        showComponent: true
      },
      methods: {
        toggleComponent() {
          this.showComponent = !this.showComponent;
        }
      }
    });
  </script>
</body>
</html>

私は、コンポーネントがDOMから削除される場合のパターンでハマってしまった。 v-ifでDOMを削除すれば、beforeDestroyとdestroyedが呼ばれると認識していた。 以下のようなコードがあった時に[Domを削除]ボタンをクリックするとbeforeDestroyとdestroyedが呼ばれると思っていたが、beforeDestroyとdestroyedが呼ばれなかった。

[index.html]

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue.js App</title>
  <link href="main.css" rel="stylesheet">
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.11/dist/vue.js"></script>
</head>

<body>
  <div id="app">
    <p v-if="show" id="test">{{message}}</p>
    <button v-on:click="destroyDom">Domを削除</button>
  </div>

  <script src="main.js"></script>
</body>
</html>
[main.js]

var app = new Vue({
  el: '#app',
  data: {
    message: "初期値",
    show: "true"
  },
  methods: {
    destroyDom: function() {
      this.show = false;
    }
  },

  beforeDestroy() {
    console.log('beforeDestroy: インスタンスが破棄される直前');
  },
  destroyed() {
    console.log('destroyed: インスタンスが破棄された後');
  }
})

正しい認識としては、ルートインスタンス(この場合#appにマウントされたインスタンス)に対してのv-ifでの非表示は、beforeDestroyとdestroyedは呼び出されません。 代わりに、子コンポーネントを使用してそのライフサイクルフックをトリガーします。 以下が実際の例になります。

[index.html]

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue.js App</title>
  <link href="main.css" rel="stylesheet">
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.11/dist/vue.js"></script>
</head>

<body>
  <div id="app">
    <button v-on:click="toggleComponent">コンポーネント切り替え</button>
    <child-component v-if="showComponent"></child-component>
  </div>

  <script src="main.js"></script>
</body>
</html>
[main.js]

Vue.component('child-component', {
  template: `
    <div>
      <p v-if="show" id="test">{{ message }}</p>
      <button v-on:click="changeMessage">メッセージ変更</button>
      <button v-on:click="destroyDom">DOMを削除</button>
    </div>
  `,
  data() {
    return {
      message: "初期値",
      show: true
    };
  },
  methods: {
    changeMessage() {
      this.message = "変更後";
    },
    destroyDom() {
      this.show = false;
    }
  },
  beforeDestroy() {
    console.log('beforeDestroy: インスタンスが破棄される直前');
  },
  destroyed() {
    console.log('destroyed: インスタンスが破棄された後');
  }
});

var app = new Vue({
  el: '#app',
  data: {
    showComponent: true
  },
  methods: {
    toggleComponent() {
      this.showComponent = !this.showComponent;
    }
  },
  beforeCreate() {
    console.log('beforeCreate');
  },
  created() {
    console.log('created');
  },
  beforeMount() {
    console.log('beforeMount');
  },
  mounted() {
    console.log('mounted');
  },
  beforeDestroy() {
    console.log('beforeDestroy: インスタンスが破棄される直前');
  },
  destroyed() {
    console.log('destroyed: インスタンスが破棄された後');
  }
});