今回はループのディレクティブである「v-for」について
Vueチュートリアルの問題を噛み砕きまくって
分かるレベルまで落とし込みました。
v-forは超簡単にいうと
ループ処理ができるディレクティブ
問題のコード(回答)
参考にしたサイト
VueチュートリアルSTEP7
こんな動作をします
1:new todoと書かれたテキストボックス内に記述
2:「Add ToDo」と書かれたボタンを押すと
3:初期値で入力されていた要素の下に追加される
4:xボタンを押すと消える
<script setup>
import { ref } from 'vue'
// give each todo a unique id
let id = 0
const newTodo = ref('')
const todos = ref([
{ id: id++, text: 'Learn HTML' },
{ id: id++, text: 'Learn JavaScript' },
{ id: id++, text: 'Learn Vue' }
])
function addTodo() {
todos.value.push({ id: id++, text: newTodo.value })
newTodo.value = ''
}
function removeTodo(todo) {
todos.value = todos.value.filter((t) => t !== todo)
}
</script>
<template>
<form @submit.prevent="addTodo">
<input v-model="newTodo" required placeholder="new todo">
<button>Add Todo</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
<button @click="removeTodo(todo)">X</button>
</li>
</ul>
</template>
①大前提 Vueの構造をまずおさらい
SFC(単一ファイルコンポーネント) を構成する3つのブロックがある
script template style
<script setup> </script> ロジック担当
頭脳である、JavaScript(ロジック)を記述するところ
・function(関数)の定義やロジックの記述
・データの定義(ref reactive)
・インポート機能 import {ref} from 'vue'のように
必要な機能を外部から読み込む
・自動公開 setup属性をつけることで内で(.value)無しで記述可能に
<template> </template> 構造担当
HTML構造を記述するところだが、
単なるHTMLではなく、
JavaScriptの状態(データ)と見た目を繋ぐため Vue独自の構文が使える
例:
宣言的レンダリング(マスタッシュ構文) {{ こんなの }}
属性バインディング(v-bind)
イベントリスナー(v-on)
双方向バインディング(v-model)
リストレンダリング(v-for)
<style> </style> デザイン担当
コンポーネントの見た目 デザイン(CSS)を記述するところ
ここにデザインがまとまってるので直感的に分かりやすい
・デザインの適用
template内の要素に対し色、サイズ、レイアウトなどの装飾を適用
・HTMLとの連携
で指定した class="title"に対し
.title { color:red } のようにスタイルを当てる
これら3つが連携することで、Vue.jsの最大の特徴である**
「リアクティビティ(データが変わると画面が自動で変わる仕組み)」**が実現
script でデータが更新される。
template がその変化を検知して、表示を自動で書き換える。
style が、その構造に常に正しいデザインを適用し続ける。
②script の中身を整理
ここでは初期値の設定やロジックを担当します。
<script setup> //ロジック(JS)部分はscriptタグで囲む
import { ref } from 'vue' //vueをref型にて定義する
let id = 0 // 初期値に0を入れる
const newTodo = ref('') // newTodoに ref''を入れる テキストボックス部分
const todos = ref([ // todosの中にref型の配列を入れる
{ id: id++, text: 'Learn HTML' }, // idは1ずつ足していき、それぞれのtextを追加
{ id: id++, text: 'Learn JavaScript' }, // ++で1追加という意味
{ id: id++, text: 'Learn Vue' }
])
// 下記別解説 変数 addTodoを定義 新しい予定を入れるためのテキストボックスの処理をする
function addTodo() {
todos.value.push({ id: id++, text: newTodo.value })
newTodo.value = ''
}
// 下記別解説変数 removeTodoを定義 Xボタンを押して消す処理関連
function removeTodo(todo) {
todos.value = todos.value.filter((t) => t !== todo)
}
</script>
function(変数)部分を深掘り
別解説① addTodoの中身
function addTodo() {
todos.value.push({ id: id++, text: newTodo.value })
newTodo.value = ''
}
テキストボックスに入力された文字を、リストの「一番後ろ」に
ガッチャンコするイメージ
【データの流れ】
入力: newTodo.value(テキストボックスの中身)を確認。
作成: 新しいToDoカード { id: id++, text: ... } を作成。
追加: todos.value.push() で、配列の末尾にそのカードを投げ入れる。
リセット: newTodo.value = '' で、入力欄を空っぽにする。
別解説② removeTodoの中身
function removeTodo(todo) {
todos.value = todos.value.filter((t) => t !== todo)
}
「削除」といっても、実際には元のリストから**「消したいやつ以外」を救い出して、
新しいリストを作り直す**という「ふるい(まさにフィルター)」にかけるイメージ。
【データの流れ】
指定: 消したい項目(todo)を受け取る。
選別: filter メソッドを使い、リストを1つずつチェックする(これが (t))。
判定: 「それは消したいやつですか?(t !== todo)」と聞き、
NO 等しくないと言ったものだけを合格させる。
更新: 合格者だけで作った「新しいリスト」を todos.value に上書きする。
なぜ「NO(等しくない)」と言うものを合格させるのか?
プログラムの t !== todo という条件は、
日本語に訳すと**「t と todo は別物ですか?」**という意味
別物である(YES):消したくない大事なデータだから残す
別物ではない=同じものである(NO):これが消したい対象だから、無視する。
Vueは、「必要なものだけでリストを作り直して、
古いリストと丸ごと入れ替える(todos.value = ...)」という手順を踏むことで、
**「あ、中身が変わった!画面を更新しなきゃ!」**という
リアクティビティのスイッチが入る仕組みになっています。
PHPの array_filter もこれと全く同じ「ふるいにかける」考え方だから
「移し替え」のイメージを持っておくと良い
なぜわざわざ!== の形をとるのか
わざわざ t !== todo じゃなくて
t == todoじゃダメなのか?と思い調べてみたところ、
以下の理由により採用されないことが判明。
1. filter を使う時点で elseに相当する処理がないため
filterメソッドは、配列の要素を一つずつ検査し、
条件が true(合格)になったものだけを新しい箱に入れる仕組み
現在の動き (t !== todo): 「削除したいやつ以外ですか?」
→ 「はい(true)」→ 新しい箱に入れる。
不合格のとき: 「削除したいやつ」だった場合、条件は false になり、
このとき else に相当する処理がないため、その要素はどこにも保存されず、
そのまま消えてなくなってしまう
つまり、filter を使っている時点で、「else(不合格者)は救わない」
というルールが最初から決まってしまっている。
2.if文で同様の処理をするとなると冗長になってしまうため
else を使わずに「削除」と同じ動きを再現する場合、
おおよそ二倍のコードの多さになる。
let 新しいリスト = [];
todos.value.forEach((t) => {
// 「削除したいやつ以外」なら保存する
if (t !== todo) {
新しいリスト.push(t);
}
// ここに else は書かない
});
このコードでは、if の条件に合わない(=削除したい対象である)ときは、
単に何の処理も行われずスルーされます。
elseをどうしても採用する例として、
合格者 不合格者それぞれに応じたメッセージを表示したりするなどの場合。
その場合はfilterではなく、 foreachなどと組み合わせる。
scriptまとめ
filter を使った削除処理において else がないのは、
**「不採用通知すら出さない」**仕組みのため
「filter という道具が合格者(true)のことしか見ない性質であり、
消したくないものに true という名札を貼るために !== を使っている」
もしどうしてもelse を使いたいのであれば、それは filter(選別)ではなく、
forEach(全件処理)などの別の道具を使う必要がある。
③template の中身を整理
<template>
<form @submit.prevent="addTodo">
<input v-model="newTodo" required placeholder="new todo">
<button>Add Todo</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
<button @click="removeTodo(todo)">X</button>
</li>
</ul>
</template>
v-model(form)部分
<form @submit.prevent="addTodo">
<input v-model="newTodo" required placeholder="new todo">
<button>Add Todo</button>
</form>
【データの流れ】
1:ユーザーが入力欄に文字を打つ(v-model でデータが溜まる)
2:「Add Todo」ボタンを押す(またはEnterキーを押す)。
.prevent を入れるおかげで画面は真っ白(リロード)にならず、
そのまま addTodo 関数が実行される
3:関数の中で、入力された文字をリストに追加し、入力欄を空にする
v-for(ループ処理)部分
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
<button @click="removeTodo(todo)">X</button>
</li>
</ul>
【データの流れ】
1: v-for="todo in todos" (リストの繰り返し)
これは、JavaScript側の todos 配列の中身を1つずつ取り出して
PHPでいう foreach ($todos as $todo) と全く同じで、
配列に3つのデータがあれば、
画面にも自動的に3行のリスト<li>の中身が作成される
2 :key="todo.id" (背番号の指定)
Vue.jsが「どの行がどのデータなのか」を正確に識別するための背番号のようなもの
なぜ必要?:
リストの順番が変わったり削除されたりした際、
Vueはこの id を目印にして、画面の更新を最小限の負荷で、かつ正確に行うようにするため
3. {{ todo.text }} (中身の表示)
todo オブジェクトの中にある text(「買い物に行く」などの文字)を
HTMLの中に動的に埋め込む
4. @click="removeTodo(todo)" (削除の引き金)
removeTodo 関数を呼び出す「きっかけ」
@click: ボタンがクリックされた瞬間に、右側の関数を実行
(todo): v-for で今まさに表示している
「その行のToDoデータ」を丸ごと関数に放り投げる
削除に関するロジック
1:ユーザーが特定の行(この場合は消したい行)の
「Xボタン」 を押す。(v-on(@)のとこで感知)
2:その行の todo データが removeTodo(todo) 関数に送られる
3:関数の中で filter が発動し、
送られてきた todo 以外のメンバーで新しいリストが作られる
4:todos.value がその新しいリストで上書きされる
5:Vueがデータの変化を検知し、v-for が画面を再描画して、
クリックした行が消える
④まとめ
今回学んだことを図解に書き起こしてみました。
勉強の参考になればと思います。




