LINEヤフー Tech Blog

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

Vue Fes Japan 2024 企業ブース企画 クイズの解答と解説

こんにちは、Webフロントエンドエンジニアの爲本、山本です。

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

LINEヤフーでは、「LYPプレミアム」や「LINE公式アカウント」「LINEギフト」など、さまざまなプロダクトでVue.jsを使用しています。さらに、LINEヤフーはVue.jsのゴールドスポンサーとして、月500ドルの支援を行なっています。この支援を通じて、Vue.jsコミュニティの成長と発展に貢献しています。今回のVue Fes Japan 2024 では、選択問題から記述問題まで、Vue.jsに関する問題を時間を区切って全部で6つ出題しました。ブースに来ていただいた方には、付箋を貼っていただくことで、クイズを通じて LINEヤフーのスタッフとコミュニケーションを図りました。付箋が貼られた問題用紙の写真

問 1

設問

設問それぞれのコードはすべて正常に動作します。 Vue における SFC の記述方法で推奨されている書き方はどれでしょうか?(複数選択可)

(1)

<template>
  <!-- ... -->
</template>
<script lang="ts">
/* ... */
</script>
<style scoped>
/* ... */
</style>

(2)

<script lang="ts">
/* ... */
</script>
<template>
  <!-- ... -->
</template>
<style scoped>
/* ... */
</style>

(3)

<script lang="ts">
/* ... */
</script>
<style scoped>
/* ... */
</style>
<template>
  <!-- ... -->
</template>

(4)

<template>
  <!-- ... -->
</template>
<style scoped>
/* ... */
</style>
<script lang="ts">
/* ... */
</script>

解答

(1)、(2)

解説

Vue.js におけるシングルファイルコンポーネント(SFC)の書き方では、style セクションが最後にあることが重要です。これさえ守られていれば、template と script の順序については、どちらも推奨される形となっています。Vue 2.x の時代には、多くのプロジェクトで template が最初に書かれることが一般的でした。しかし、Vue 3.2 では script setup 構文が多く使われるようになり、Vue.js の公式ドキュメントでも script を最初に置いています。また、ファイルによって template が最初のものと、script が最初のものとが混在すると、管理が難しくなります。そのため、チーム内で統一したルールを決めることが重要です。筆者としては、エンジニアとデザイナーがそれぞれの作業を効率よく行えるように、script, template, style の順序で記述することをおすすめしています。この順番は、エンジニア/デザイナーの作業分担が明確で、エンジニアが主に script と template を担当し、デザイナーが template と style を担当するようなケースに適しています。この配置により、それぞれの作業エリアが隣接するため、従来の template, script, style の順序よりも、共同作業やコードの理解が向上します。

問 2

設問

下記コードはどの順番で実行されるでしょうか? また、その理由を記述してください。

  • (1)1、4、5、2、3
  • (2)1、5、4、2、3
  • (3)1、2、3、4、5
  • (4)1、4、2、5、3
<script setup lang="ts">
import { nextTick, onMounted } from "vue";

(async () => {
  console.log(1);
  await new Promise((resolve) =>
    setTimeout(() => {
      console.log(2);

      return resolve("ok");
    })
  ).then((_v) => {
    console.log(3);
  });
})();

nextTick(() => {
  console.log(4);
});

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

解答

答え:(2)理由:(例)nextTick は非同期処理でありマイクロタスクとして実行されるため、onMounted が先に実行され、nextTick に渡されたコールバック関数が実行されます。

こちらの問題は Vue.js のライフサイクルと非同期が絡む問題です。アプリ開発の中で、API からデータを取得し、それをインタラクティブに画面に反映するシーンは多いかと思います。この問題はその際にライフサイクルが関与するときの処理の実行順序を問います。なお、実行理由はいろいろな切り口で存在するため、スポンサーブース内では一部の実行処理部分の理由だけでも記載いただきました。

解説

最初に実行されるのは、即時関数内の console.log(1)です。これにより、まず 1 が出力されます。次に、Vue の onMounted フックが実行されます。onMounted はコンポーネントがマウントされた後に実行されるため、この時点で console.log(5)が呼び出されます。その結果、次に 5 が出力されます。その後、nextTick に登録されたコールバックが実行されます。nextTick はマイクロタスクとして実行されるため、通常の非同期タスクよりも優先されます。したがって、4 が出力されます。続いて、setTimeout 内のコールバック関数が実行されます。これはマクロタスクとして非同期に処理されるため、先に述べた処理がすべて終わった後に実行され、2 が出力されます。最後に、Promise の then メソッドが呼び出されます。これは setTimeout の後に実行され、最後に 3 が出力されます。

問 3

設問

A と B のコードを使い分ける場合、それぞれどういったユースケースが適切でしょうか?

<script setup lang="ts">
import { ref } from "vue";

const inputTextA = ref("");
const inputTextB = ref("");
</script>
<template>
  <form @submit.prevent>
    A: this is {{ inputTextA }}<br />
    <input v-model="inputTextA" />
  </form>

  <form @submit.prevent>
    B: this is {{ inputTextB }}<br />
    <input :value="inputTextB" @input="(e) => (inputTextB = e.target.value)" />
  </form>
</template>

解答

A:(例)日本語入力時、v-model は確定するまでバインディングされないため、日本語に対して適切なイベントが期待できる。B:(例)IME の中間状態(未確定状態)であってもイベントが発火するため、IME の入力を適切に扱えない場合があり、例えば入力値に対してバリデーションを行う場合、意図しない入力に対して作用する可能性がある。

フォームコンポーネントを実装する場合、データのバインディングを行う際に v-model で行うか v-bind+@input で迷ったご経験はないでしょうか?本設問は特にフォームコンポーネントでのバリデーション処理を想定したものになってます。

解説

v-model を使用したバインディングは、フォームの入力データとコンポーネントの状態を簡単に同期させる方法です。v-model は、特に日本語などの IME(Input Method Editor)を用いる場合に強力です。IME による入力では、v-model はテキストが確定した後にだけデータを更新します。これにより、未確定の入力段階で誤ってバリデーションやロジックが働くのを防ぐことができるため、日本語入力の際の動作が重要なケースでは v-model が適切であると考えられます。一方、v-input のバインドによる管理は、入力データにリアルタイムで反応する必要があるユースケースに適しています。入力イベントを直接監視することで、入力が行われるたびにデータを即時に更新し、フィードバックやバリデーションをリアルタイムで行うことが可能です。

問 4

設問

ref/reactive でデータを管理した際に、それぞれの特徴や注意点、使い分けなどを教えてください。

解答

以下解答例です。

ref

  • (例 1)どの値でも管理することが可能。オブジェクトが定義された際は内部で reactive でラップされる
  • (例 2)公式では ref での管理を推奨されている
  • (例 3).value 参照により、そのデータがリアクティブかどうかの判別がしやすい

reactive

  • (例 1)オブジェクトそのもののが常に存在することを担保したいとき
  • (例 2)Options API のイメージで書ける
  • (例 3)プリミティブの管理ができず、必ずオブジェクトで包まなければならない
  • (例 4)値を更新する際は、オブジェクト全体を更新することができない。ただし、Object.Assign を使えば可能

解説

ref を使用したデータ管理は、Vue の公式ドキュメントでも推奨されている方法です。ref はすべての値がリアクティブであるかどうかを.value で明示できるため、コードの可読性を向上させます。一方、reactive はオブジェクト全体をリアクティブに管理したい場合に便利です。データが複雑なオブジェクトとして構造化されている場合、reactive を使うことで、各プロパティを個別に扱う手間を省けます。また、Options API に慣れている開発者にとっても、reactive の使用は親しみやすく、直感的に理解しやすいです。ただし、プリミティブ型のデータを直接管理できない、分割代入やオブジェクトの一括代入が難しいなどの制約がありますので、状況に応じた工夫が必要です。reactive と ref のどちらを使用するかは、Vue.js の主要なトピックの 1 つであるため、今回のクイズにも取り上げました。どちらが正しいというわけではなく、プロジェクトの規模や複雑さ、開発チームのスタイルによって異なる選択が考えられます。

問 5

設問

下記の画面は検索結果画面であり、検索した大量の結果を表示するページです。機能としては、入力した値で絞り込みが行えます。下記の画面の UX をよくするための施策を考えてください。※ 検索結果数は自由に設定していただいて構いません。

一例として、下記は変数 results に 100,000 個の検索結果が入っているケースを想定しています。

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

const query = ref("");
const results = ref(
  Array.from({ length: 100000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`,
    description: `Description for item ${i}`,
    detail: {
      title: `title for detail ${i}`,
      description: `Description for detail ${i}`,
      more: {
        title: `title for more ${i}`,
        description: `Description for more ${i}`,
      },
    },
  }))
);

const filteredItems = computed(() => {
  if (query.value === "") {
    return results.value;
  }
  return results.value.filter(
    (item) =>
      item.name.toLowerCase().includes(query.value.toLowerCase()) ||
      item.description.toLowerCase().includes(query.value.toLowerCase())
  );
});
</script>

<template>
  <div>
    <h1>検索結果の絞り込み</h1>
    <input
      type="text"
      v-model="query"
      placeholder="検索語句を入力してください"
    />
    <ul>
      <li v-for="item in filteredItems" :key="item.id">
        <strong>{{ item.name }}</strong
        >: {{ item.description }}
      </li>
    </ul>
  </div>
</template>

解答

  • (例 1)ref ではなく shallowRef/shallowReactive の利用を検討
  • (例 2)仮想スクロールの検討

解説

ref と reactive はデフォルトで、すべてのプロパティを監視対象にします。そのため、ネストされたデータ構造を持つ大規模なデータが多数存在すると、依存関係の更新によるオーバーヘッドが発生することがあります。

shallowRef と shallowReactive を使用すると、Vue のリアクティブシステムのパフォーマンスを向上させることが期待できます。これらは、オブジェクトの最上位レベルのプロパティのみをリアクティブに管理し、ネストされたプロパティの変更はリアクティブにしません。この特性により、パフォーマンスが向上します。

ただし、使用する際はリアクティビティが必要な部分と不要な部分を慎重に見極めることが重要です。詳細は、Vue.js の公式ドキュメントをご参照ください。

仮想スクロールは、大量のデータを効率的に表示するための技術で、パフォーマンスを向上させます。一度にすべてのデータを DOM にレンダリングするのではなく、ユーザーのビューポートに入る要素のみを動的にマウントし、不要になった要素はアンマウントします。これにより、ブラウザのレンダリングやメモリ消費が軽減されます。関心がある方は、Vue.js の公式ドキュメントで紹介されている便利なライブラリを参考にしてください。

問 6

設問

下記は withDefaults を用いて default 値が設定された prop を持つ SFC です。

<script setup lang="ts">
import { computed } from "vue";

const props = withDefaults(
  defineProps<{
    name?: string;
    count: number;
  }>(),
  {
    name: "AppWithDefaults",
  }
);

const double = computed(() => props.count * 2);
</script>

<template>
  <h2>{{ props.name }}</h2>
  <p>count: {{ count }}</p>
  <p>double: {{ double }}</p>
</template>

Vue 3.5 からはより簡略的な方法で props を宣言することができるようになりました。

const { count, name = "AppDestructure" } = defineProps<{
  name?: string;
  count: number;
}>();

const double = computed(() => count * 2);

上記 count のように、reactive なオブジェクトを分割代入してもなお、reactivity を維持していて computed による計算が行えるのはなぜでしょうか?

解答

コンパイラーが分割代入していないコード (props.count) に置き換えるから。

解説

リアクティブなオブジェクトのプリミティブ型のプロパティをローカル変数に分割代入すると、リアクティブなつながりが失われることが「reactive() の制限」としてドキュメントで紹介されています。

参考: https://ja.vuejs.org/guide/essentials/reactivity-fundamentals#limitations-of-reactive

defineProps の戻り値もリアクティブなオブジェクトですが、コンパイラーが自動的に分割代入していないコードに置き換えるため、リアクティブなふるまいを維持することができます。

参考: https://blog.vuejs.org/posts/vue-3-5#reactive-props-destructure

// (省略)

const __sfc__ = /*@__PURE__*/ _defineComponent({
  __name: "AppDestructure",
  props: {
    name: { type: String, required: false, default: "AppDestructure" },
    count: { type: Number, required: true },
  },
  setup(__props, { expose: __expose }) {
    __expose();

    const double = computed(() => __props.count * 2);

    const __returned__ = { double };
    Object.defineProperty(__returned__, "__isScriptSetup", {
      enumerable: false,
      value: true,
    });
    return __returned__;
  },
});

// (省略)

defineProps における分割代入のメリットは vuejs/rfcs#502 で以下のように紹介されています。

  • JavaScript のネイティブのデフォルト値構文を使用することで、DX を改善する
  • props の使用方法の一貫性
    • 分割代入を使用しない: <template> では {{ foo }}<script> では props.foo で参照する
    • 分割代入を使用する: <template> では {{ foo }}<script> では foo で参照する

おわりに

本記事では、Vue Fes Japan 2024 で出題した全 6 問のクイズの紹介と解説を行いました。これらのクイズは、参加者の皆さんに楽しんでいただきながら、Vue.js に関して新たな見解を得られましたら嬉しいです。別の記事では弊社社員の登壇内容なども紹介しているので、こちらもぜひご覧ください。

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

最後までお読みいただき、ありがとうございました。今後のイベントや活動についての情報は、引き続き当ブログでお知らせしていきますので、どうぞお楽しみに!

Name:爲本 雄亮

Description:LINEヤフー株式会社 Webフロントエンドエンジニア。Yahoo!知恵袋で主にWebフロントエンドを開発しています。

Name:山本 竜也

Description:LINEヤフー株式会社 マーケティングソリューションカンパニー Webフロントエンドエンジニア。LINE公式アカウントのWebフロントエンドを開発しています。オタク。