[Vue入門] リストレンダリング

v-for

配列に基づいて項目のリストをレンダリングするには、v-for ディレクティブを使用します。v-for ディレクティブでは、item in items という形式の特別な構文が必要になります。ここで、items は元のデータの配列を指し、item は反復処理の対象となっている配列要素のエイリアスを指します:

const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="item in items">
  {{ item.message }}
</li>

v-for のスコープ内では、テンプレート内の式から親スコープのすべてのプロパティにアクセスできます。さらに、v-for では以下のように現在の項目のインデックスを指す、2 つ目の省略可能なエイリアスもサポートされています:

const parentMessage = ref('Parent')
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="(item, index) in items">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

Parent – 0 – FooParent – 1 – Bar

プレイグラウンドで試す

v-for の変数のスコープは、次の JavaScript と同様です:

const parentMessage = 'Parent'
const items = [
  /* ... */
]

items.forEach((item, index) => {
  // ここからスコープの外の `parentMessage` にはアクセスできますが、
  // `item` と `index` はこの中でしか使用できません。
  console.log(parentMessage, item.message, index)
})

v-for の値が forEach のコールバック関数のシグネチャと一致している様子に注目してください。実際、関数の引数で分割代入を使用するときと同様に、v-for の item のエイリアスでも分割代入を使用することができます:

<li v-for="{ message } in items">
  {{ message }}
</li>

<!-- index のエイリアスを伴う場合 -->
<li v-for="({ message }, index) in items">
  {{ message }} {{ index }}
</li>

ネストされた v-for でも、スコープの挙動はネストされた関数と同様です。以下のように、それぞれの v-for のスコープでは親のスコープにアクセスできます:

<li v-for="item in items">
  <span v-for="childItem in item.children">
    {{ item.message }} {{ childItem }}
  </span>
</li>

区切り文字として in の代わりに of を使用して、JavaScript のイテレーター構文に近付けることもできます:

<div v-for="item of items"></div>

v-for をオブジェクトに適用する

v-for は、オブジェクトの各プロパティを反復処理するのにも使用できます。

const myObject = reactive({
  title: 'How to do lists in Vue',
  author: 'Jane Doe',
  publishedAt: '2016-04-10'
})
<ul>
  <li v-for="value in myObject">
    {{ value }}
  </li>
</ul>

以下のように 2 つ目のエイリアスを指定すると、プロパティの名前 (「キー」とも呼ばれる) を取り出すことができます:

<li v-for="(value, key) in myObject">
  {{ key }}: {{ value }}
</li>

さらに 3 つ目のエイリアスを追加すると、インデックスを取り出せます:

<li v-for="(value, key, index) in myObject">
  {{ index }}. {{ key }}: {{ value }}
</li>

プレイグラウンドで試す

注意

オブジェクトを反復処理する際の順序は Object.keys() による列挙に基づきますが、JavaScript エンジンの異なる実装間での一貫性は保証されていません。

v-for で範囲を使用する

v-for は、整数を取ることもできます。その場合、1...n のような範囲に従って、その回数だけテンプレートが繰り返されます。

<span v-for="n in 10">{{ n }}</span>

n の値は 0 ではなく 1 から始まることに注意してください。

<template> に v-for を適用する

テンプレートに v-if を適用する場合と同様に、 <template> タグに v-for を適用すると、複数の要素からなるブロックをレンダリングできます。例:

<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider" role="presentation"></li>
  </template>
</ul>

v-for と v-if を組み合わせる場合

注意

暗黙の優先順位があるため、v-if と v-for を同一の要素に対して使用することは推奨されません。詳しくはスタイルガイドを参照してください。

同じノードに両方が存在する場合、v-for よりも v-if のほうが優先順位が高くなります。これは、以下のように v-for のスコープにある変数には v-if の条件式からアクセスできないことを意味します:

<!--
"todo" というプロパティがインスタンスで未定義となるため、
エラーがスローされます。
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

この問題は、以下のようにラップ用の <template> タグを設けて、そこに v-for を移動することで解決できます (このほうがより明示的でもあります):

<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

key による状態管理

v-for でレンダリングされた要素のリストを Vue が更新するとき、デフォルトでは「その場での修繕」(in-place patch) という戦略が用いられます。データ項目の順序が変更された場合、Vue は項目の順序に合うように DOM 要素を移動させるのではなく、個々の要素をその位置のままで修正し、各インデックスでレンダリングされるべきものを反映させます。

このデフォルトのモードは効率性が高いものの、これが適すのは、リストのレンダリング出力が子コンポーネントの状態や一時的な DOM の状態 (フォームの入力値など) に依存しない場合に限られます

Vue に各ノードを一意に追跡するためのヒントを与え、既存の要素を再利用して並べ替えを適用できるようにするには、以下のように各項目に一意の key 属性を指定する必要があります:

<div v-for="item in items" :key="item.id">
  <!-- 内容 -->
</div>

<template v-for> を例に取ると、key は以下のように <template> の中に置きます:

<template v-for="todo in todos" :key="todo.name">
  <li>{{ todo.name }}</li>
</template>

注意

ここでいう key は、v-bind でバインドされる特別な属性です。v-for をオブジェクトに適用するときのプロパティのキーの変数と混同しないように注意してください。

v-for の key 属性は、可能な場合は必ず指定することが推奨されます。ただし、反復処理する DOM の内容が単純なものである (つまりコンポーネントやステートフルな DOM 要素を含まない) 場合、またはパフォーマンス向上のために意図的にデフォルト動作を用いたい場合は、この限りではありません。

key のバインディングにはプリミティブ型の値、つまり文字列と数値が想定されます。v-for の key にオブジェクトを指定してはいけません。key 属性の詳しい使い方については、key API のドキュメントを参照してください。

v-for をコンポーネントに適用する#

このセクションは、コンポーネントについての知識があることを前提としています。読み飛ばして、後で戻ってくるのも大丈夫です。

通常の要素と同様に、コンポーネントにも v-for を直接適用することができます (key を指定するのを忘れないでください):

<my-component v-for="item in items" :key="item.id"></my-component>

ただし、これだけではデータが自動的にコンポーネントに渡されるようにはなりません。なぜなら、コンポーネントはそれ自身の独立したスコープを持つからです。コンポーネントに反復処理対象のデータを渡すには、以下のようにプロパティを併用する必要があります:

<my-component
  v-for="(item, index) in items"
  :item="item"
  :index="index"
  :key="item.id"
></my-component>

item が自動的に注入されないようになっている理由は、そうしてしまうと、コンポーネントが v-for の動作と密に結合してしまうためです。データの供給源を明示的に指定することにより、コンポーネントが別の場面でも再利用できるような作りになっています。

シンプルな ToDo リストのサンプルで、v-for でコンポーネントのリストをレンダリングするとき、各インスタンスに異なるデータをどのように渡せばよいかを確認できます。

配列の変更の検出

ミューテーションメソッド

Vue は監視対象の配列のミューテーションメソッドをラップして、これらのメソッドでビューの更新が一緒にトリガーされるようにしています。Vue がラップしているメソッドは次の通りです:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

配列の置き換え

ミューテーションメソッドはその名が示す通り、呼び出し元の配列を変化させるメソッドです。これに対し、filter()concat()slice() など、呼び出し元の配列を変化させないメソッドもあります。これらのメソッドは常に新しい配列を返します。ミューテーションしないメソッドを扱う場合は、以下のように、古い配列を新しい配列に置き換える必要があります:

// `item` は配列値の参照です
items.value = items.value.filter((item) => item.message.match(/Foo/))

このようにすると、Vue が既存の DOM を破棄してリスト全体を再レンダリングするように思えるかもしれませんが、幸いにもそのようなことはありません。Vue には DOM 要素を最大限に再利用するためのスマートな発見的アルゴリズムが実装されているため、既存の配列を、重複するオブジェクトが含まれる新しい配列に置き換える場合でも、非常に効率的な処理が行われます。

フィルタリング/並べ替えの結果を表示する

時には、配列の元のデータを実際に変更することやリセットすることなしに、フィルタリングや並べ替えを適用したバージョンを表示したいことがあります。そのような場合には、フィルタリングや並べ替えを適用した配列を返す算出プロパティを作成することができます。

例:

const numbers = ref([1, 2, 3, 4, 5])

const evenNumbers = computed(() => {
  return numbers.value.filter((n) => n % 2 === 0)
})
<li v-for="n in evenNumbers">{{ n }}</li>

算出プロパティが使えない場所 (例えばネストされた v-for ループの内側) では、以下のようにメソッドを使用できます:

const sets = ref([
  [1, 2, 3, 4, 5],
  [6, 7, 8, 9, 10]
])

function even(numbers) {
  return numbers.filter((number) => number % 2 === 0)
}
<ul v-for="numbers in sets">
  <li v-for="n in even(numbers)">{{ n }}</li>
</ul>

算出プロパティの中で reverse() と sort() を使用するときは注意してください!これら 2 つのメソッドには、算出プロパティのゲッターの中では避けるべき、元の配列を変更するという作用があります。以下のように、これらのメソッドを呼び出す前には元の配列のコピーを作成します:

- return numbers.reverse()
+ return [...numbers].reverse()