LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

Vue Fes Japan 2025 クイズ解説

こんにちは、Webフロントエンドエンジニアの水迫、芝山、福田です。

先日開催された「Vue Fes Japan 2025」にて、LINEヤフー株式会社は昨年に続きゴールドスポンサーとして参加しました。LINEヤフー企業ブースでは、Vue.js に関するクイズを実施しました。この記事では、今回出題した全5問について解説します。

実際に使用したクイズ用のボードの画像

問 1

設問

次のうち、computed と watch の違いとして正しいものを選んでください。(1つ選択)

(1)computed は計算結果をキャッシュするが、watch はキャッシュしない
(2)watch は常に同期的に実行される
(3)computed は副作用を許容するが、watch は許容されない
(4)両者は全く同じ挙動で置き換え可能

解答

(1)

解説

Vue.js の computed(算出プロパティ)は、依存しているリアクティブな値が変化した時だけ再評価され、結果がキャッシュされます。詳細は公式ドキュメント(computed)に記載されています。

一方で watch は、特定の値の変化を監視して副作用を伴う処理を実行するための API です。watch の動作については公式ドキュメント(watch)が参考になります。watch は値の変化ごとにコールバックが実行されますが、結果のキャッシュは行われません。また、複数の変更が短期間で発生した場合は Vue の内部でバッチ処理されるようになっています。

さらに、computed の getter 内で副作用を発生させることは推奨されていません。副作用的な処理を行いたい場合は watch を使うことがベストプラクティスとされています。

問 2

設問

count をインクリメントした時にリアクティブな変更になるものを選んでください。(複数選択可)

(1)

const state = reactive({ count: 0 });
let { count } = state;
count++;

(2)

const state = reactive({ nested: { count: 1 } });
const { nested } = state;
nested.count++;

(3)

const state = reactive({ count: 0 });
const { count } = toRefs(state);
count.value++;

(4)

const number = ref(1);
const state = { count: number.value * 2 };
state.count++;

(5)

const state = { nested: reactive({ count: 1 }) };
state.nested.count++;

解答

(2)、(3)、(5)

解説

reactive はオブジェクトを Proxy 化し、これを経由したプロパティへのアクセスや代入を検知することでリアクティビティを実現しています。Proxy のプロパティへのアクセスや代入を起点に追跡されるので、Proxy からプリミティブな値を取り出し別の変数に代入するとその変数は追跡できなくなります。

(1)は分割代入された count がプリミティブ値であるため追跡できません
(2)は reactive がネストされたオブジェクトもリアクティブに変換するため、nested はリアクティブ化された Proxy であり追跡できます。
(3)はリアクティビティを維持しながらプリミティブな値を分割代入するためにtoRefs を利用してref化しています
(4)は state がプレーンなオブジェクトであり state.count はプリミティブ値なので追跡されることはありません。
(5)は state.nested がリアクティブ化された Proxy であるため、nested.count の変更は検知できます。

問 3

設問

次の(1)〜(5)の実装のうち、Vue 公式で推奨されていないものを選んでください。(複数選択可)

<script setup>
import { ref, computed } from "vue";

const fruits = ref([
  { id: 1, name: "Orange", inStock: false },
  { id: 2, name: "Banana", inStock: true },
  { id: 3, name: "Apple", inStock: true },
  { id: 4, name: "Grape", inStock: false }
]);

const stockedFruits = computed(() =>
  fruits.value.filter(fruit => fruit.inStock)
);

const sortedFruits = computed(() =>
  fruits.value.sort((a, b) => a.name - b.name)
);
</script>

<template>
  <!-- (1) -->
  <label v-for="fruit in fruits">
    <input type="checkbox" />{{ fruit.name }}
  </label>

  <!-- (2) -->
  <ul>
    <li v-for="fruit in fruits" v-if="fruit.inStock">{{ fruit.name }}</li>
  </ul>

  <!-- (3) -->
  <ul>
    <li v-for="fruit in fruits" :key="fruit.id">{{ fruit.name }}</li>
  </ul>

  <!-- (4) -->
  <ul>
    <li v-for="fruit in stockedFruits" :key="fruit.id">{{ fruit.name }}</li>
  </ul>

  <!-- (5) -->
  <ul>
    <li v-for="fruit in sortedFruits" :key="fruit.id">{{ fruit.name }}</li>
  </ul>
</template>

解答

(1)、(2)、(5)

解説

(1)は v-for に key が指定されていません。key の設定は公式ドキュメントでも推奨されています。key が無いと DOM 再利用時に状態が誤って引き継がれる可能性があります。
(2)は v-for と v-if を同じ要素に併用しています。これは公式では非推奨とされています。v-ifv-for より先に評価されるため、意図した動作にならない場合があります。詳細は公式ドキュメント「リストレンダリング」に記載されています。
(5)は computed 内で破壊的メソッド sort() を使用しています。computed の getter は副作用を持たない純粋な関数であるべきとされています。説明は公式ドキュメント(computed)に記載されています。sort() は元配列を書き換えてしまうため非推奨です。改善例として、[...fruits.value].sort((a, b) => a.name.localeCompare(b.name)) のようにスプレッド構文でコピーしてからソートする方法があります。

問 4

設問

次のコンポーネントをマウントしてボタンを1回クリックした時、コンソールには何が出力されるでしょうか?

<script setup>
import { ref, onMounted, onUpdated } from "vue";
import Child from "./Child.vue";

const score = ref(0);

onUpdated(() => {
  console.log("Parent onUpdated");
});

onMounted(() => {
  score.value++;
});

const someOperation = () => {
  // some Operations...
};

const increment = async () => {
  score.value++;
  someOperation();
  score.value++;
  await Promise.resolve();
  score.value++;
};
</script>

<template>
  <div>
    <p>Score: {{ score }}</p>
    <button @click="increment">score up</button>

    <!-- 子コンポーネント -->
    <Child label="Child" />
    <Child :label="score.toString()" />
  </div>
</template>

Child.vue

<script setup>
import { onUpdated } from "vue";

const { label } = defineProps({ label: String });
onUpdated(() => {
  console.log("Child onUpdated");
});
</script>

<template>
  <p>{{ label }}</p>
</template>

選択肢

(1)

Child onUpdated
Parent onUpdated
Child onUpdated
Parent onUpdated
Child onUpdated
Parent onUpdated

(2)

Child onUpdated
Child onUpdated
Parent onUpdated
Child onUpdated
Child onUpdated
Parent onUpdated
Child onUpdated
Child onUpdated
Parent onUpdated

(3)

Parent onUpdated
Child onUpdated
Parent onUpdated
Child onUpdated
Parent onUpdated
Child onUpdated

(4)

Parent onUpdated
Child onUpdated
Child onUpdated
Parent onUpdated
Child onUpdated
Child onUpdated

解答

(1)

解説

親子関係のコンポーネントがあり、それぞれの onUpdated フックでログが出力されています。score が更新された時のコンポーネントの更新処理について詳しく見ていきます。

まず、Child コンポーネントの更新処理についてです。Vue の更新最適化によってコンポーネントは受け取った props が変化しない場合更新されません。Child コンポーネントが2つ描画されていますが、props が変化するのは :label="score.toString()" の方のみです。そのため score が更新された時、「Child onUpdated」は1度しか出力されません。

次に、親子コンポーネントの updated フックの実行順序についてです。Vue のレンダリング処理は子コンポーネントから親コンポーネントの順で完了していきます。Vue の公式ドキュメントには親の updated フックが子の後に呼ばれることが記載されています。そのため、「Child onUpdated」「Parent onUpdated」の順で出力されます。

最後に、コンポーネントの更新の発生タイミングについてです。ボタンをクリックした時に実行される increment 関数では計3回 score が更新されています。Vue の DOM 更新は非同期的に行われるため、同期的に複数回値を更新しても1度しかレンダリングされません。計3回の score 更新のうち、最初の2回は同期的な更新なため1度のレンダリングにまとめられます。コンポーネントが更新されるのは、mounted フックで score が1になった時、increment 関数で score が3になった時、increment 関数の非同期処理で score が4になった時の計3回となります。

これらを踏まえて最終的な出力は「Child onUpdated」「Parent onUpdated」の順で3回ずつになります。

問 5

設問

<script setup>
import { nextTick, onMounted } from "vue";
import Child from "./Child.vue";

console.log("A");

onMounted(() => {
  console.log("B");

  nextTick(() => {
    console.log("C");
  });

  setTimeout(() => {
    console.log("D");
  });

  Promise.resolve().then(() => {
    console.log("E");
  });
});
</script>

<template>
  <Child />
</template>

Child.vue

<script setup>
import { onMounted } from "vue";

onMounted(() => {
  console.log("F");
});
</script>

<template>
  <p>Child</p>
</template>

選択肢

(1)AFBCED
(2)AFBECD
(3)ABCEDF
(4)ABCDEF

解答

(2)

解説

親コンポーネントの非同期処理(E, C, D)の実行順を考える上でタスクキュー/マイクロタスクキューへの理解が必要になります。

DOM のイベントリスナーや setTimeout のコールバック関数の実行はタスクキューによって実現されています。一方で Promise による非同期処理はマイクロタスクキューによって実現されています。両者とも登録された順に処理を実行しますが、1タスクだけ実行して次へ進むタスクキューに対して、マイクロタスクは登録されたすべての処理を実行してキューが空になるまで次のステップには進まないという違いがあります。

これを踏まえて実行順を見ていきます。まず A は setup フック実行時にその場で実行されます。次に F → B の順ですが、mounted フックは子から親の順で実行されるため、Child の F → 親の B の順でログが出力されます。

続いて E → C の順についてです。nextTick も内部的には Promise による制御なので、Promise.resolve().then とともにマイクロタスクキューに登録されて実行されます。ただし、Promise.resolve().then のコールバック関数はその場でマイクロタスクキューに追加されるのに対し、nextTick のコールバック関数は DOM の更新処理後にマイクロタスクキューに追加されます。そのため実行順は E → C になります。

最後に D ですが、setTimeout はタスクキューに追加されて実行されます。マイクロタスクキューが空になるまでタスクキューの処理には移らないという特性から、E と C の出力が終わった後の実行となります。

おわりに

本記事では、Vue Fes Japan 2025 で出題した全 5 問のクイズの紹介と解説を行いました。これらのクイズを通じて、Vue.js のリアクティビティやライフサイクルの理解が深まれば幸いです。

LINEヤフーには、DevRel(Developer Relations)チームと連携し、カンファレンスでのブース活動に積極的に関わっているエンジニアもいます。これからも、技術者間の知識共有や交流の場を広げるために、イベントやカンファレンスへの参加を通じた活動を続けていきます。

最後までお読みいただき、ありがとうございました。

水迫 幹

Name:水迫 幹

Description:LINEヤフー株式会社 フロントエンドエンジニア。PayPayほけんのEMを担当。ウイスキーにハマってます。

芝山 友章

Name:芝山 友章

Description:LINEヤフー株式会社 フロントエンドエンジニア。PayPayほけんの開発を担当。アイコンは芝ではなくバランです。

福田 圭

Name:福田 圭

Description:LINEヤフー株式会社 Webフロントエンジニア。PayPayほけんの開発を担当。観葉植物とコーヒーが好きです。