📗 Vue 3 基礎語法:模板、資料綁定與事件
📌 Composition API vs Options API
Vue 3 提供了兩種寫法風格。本教程使用推薦的 Composition API。
記住:不管哪種 API,底層都是 JavaScript。Vue 只是幫你封裝了響應式系統。
// ❌ Options API(Vue 2 風格,仍支援但不推薦)
export default {
data() {
return { count: 0 }
},
methods: {
increment() { this.count++ }
}
}
// ✅ Composition API + <script setup>(Vue 3 推薦)
// 更接近原生 JavaScript 的寫法
import { ref } from 'vue'
const count = ref(0)
function increment() { count.value++ }
📌 模板語法:插值與指令
文字插值 {{ }}
<template>
<!-- 雙大括號:將 JS 表達式渲染為文字 -->
<p>你好,{{ name }}!</p>
<p>1 + 1 = {{ 1 + 1 }}</p>
<p>大寫:{{ name.toUpperCase() }}</p>
<!-- ⚠️ 只能用「表達式」,不能用「語句」 -->
<!-- ❌ {{ if (ok) { return 'yes' } }} -->
<!-- ✅ {{ ok ? 'yes' : 'no' }} -->
</template>
<script setup>
import { ref } from 'vue'
// ref() 是 Vue 的 API,不是原生 JS!
// 它把普通值包裝成「響應式物件」
const name = ref('小明')
</script>
屬性綁定 v-bind
<template>
<!-- v-bind 綁定 HTML 屬性 -->
<img v-bind:src="imageUrl" v-bind:alt="imageAlt" />
<!-- 簡寫:用冒號 : 代替 v-bind -->
<img :src="imageUrl" :alt="imageAlt" />
<!-- 動態 class 和 style -->
<div :class="{ active: isActive, 'text-danger': hasError }">
條件式 class
</div>
<div :style="{ color: textColor, fontSize: fontSize + 'px' }">
動態樣式
</div>
</template>
<script setup>
import { ref } from 'vue'
const imageUrl = ref('https://example.com/logo.png')
const imageAlt = ref('Logo')
const isActive = ref(true)
const hasError = ref(false)
const textColor = ref('blue')
const fontSize = ref(16)
</script>
事件處理 v-on / @
<template>
<!-- v-on 監聽事件 -->
<button v-on:click="handleClick">點我 (完整寫法)</button>
<!-- 簡寫:用 @ 代替 v-on -->
<button @click="count++">直接寫表達式:{{ count }}</button>
<button @click="handleClick">呼叫函式</button>
<!-- 帶參數 -->
<button @click="greet('小明')">打招呼</button>
<!-- 事件修飾符 -->
<form @submit.prevent="onSubmit">
<!-- .prevent = preventDefault() -->
<input @keyup.enter="onEnter" placeholder="按 Enter" />
<button type="submit">送出</button>
</form>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
// 這些就是普通的 JavaScript 函式
function handleClick() {
console.log('被點擊了!')
count.value++
}
function greet(name) {
alert(`你好,${name}!`)
}
function onSubmit() {
console.log('表單送出')
}
function onEnter() {
console.log('按了 Enter')
}
</script>
📌 響應式資料:ref() 和 reactive()
ref() — 包裝任意值
<script setup>
import { ref } from 'vue'
// ref() 包裝基本型別
const count = ref(0) // 數字
const name = ref('小明') // 字串
const isReady = ref(false) // 布林值
const items = ref([1, 2, 3]) // 陣列
const user = ref({ name: '小明', age: 25 }) // 物件
// ⚠️ 在 <script> 中存取要加 .value
console.log(count.value) // 0
count.value++ // 修改值
console.log(count.value) // 1
// ✅ 在 <template> 中不需要 .value(Vue 自動解包)
// <p>{{ count }}</p> ← 直接用,不用 count.value
</script>
reactive() — 包裝物件
<script setup>
import { reactive } from 'vue'
// reactive() 只能包裝物件或陣列
const state = reactive({
count: 0,
user: { name: '小明', age: 25 },
todos: []
})
// 不需要 .value!直接存取
state.count++
state.user.name = '小華'
state.todos.push('學 Vue')
// ⚠️ 不能解構!會失去響應性
// ❌ const { count } = state // count 不會是響應式的
// ✅ 用 toRefs 解構
import { toRefs } from 'vue'
const { count } = toRefs(state) // 現在 count 是 ref
</script>
ref vs reactive 怎麼選?
ref() → 適合基本型別(數字、字串、布林)
reactive() → 適合物件、表單資料等複合型別
推薦:統一用 ref(),因為更一致、不容易出錯
📌 計算屬性 computed
<template>
<div>
<p>原價:${{ price }}</p>
<p>折扣後:${{ discountedPrice }}</p>
<p>訊息:{{ statusMessage }}</p>
<input v-model="firstName" placeholder="名" />
<input v-model="lastName" placeholder="姓" />
<p>全名:{{ fullName }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const price = ref(1000)
const discount = ref(0.8)
// computed 會自動追蹤依賴的 ref
// 當 price 或 discount 改變時,自動重新計算
const discountedPrice = computed(() => {
return Math.round(price.value * discount.value)
})
// computed 有快取!只有依賴變化時才重新計算
// 比起 methods,效能更好
const statusMessage = computed(() => {
return price.value > 500 ? '高價商品' : '平價商品'
})
const firstName = ref('')
const lastName = ref('')
// 可讀可寫的 computed
const fullName = computed({
get: () => `${lastName.value}${firstName.value}`,
set: (val) => {
lastName.value = val[0] || ''
firstName.value = val.slice(1) || ''
}
})
</script>
📌 條件渲染與列表渲染
<template>
<!-- v-if / v-else-if / v-else:條件渲染 -->
<div v-if="score >= 90">🏆 優秀!</div>
<div v-else-if="score >= 60">✅ 及格</div>
<div v-else>❌ 不及格</div>
<!-- v-show:用 CSS display 切換(頻繁切換用這個) -->
<div v-show="isVisible">我可以被顯示/隱藏</div>
<!-- v-for:列表渲染 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }}. {{ item.name }} - ${{ item.price }}
</li>
</ul>
<!-- v-for 搭配物件 -->
<div v-for="(value, key) in userInfo" :key="key">
{{ key }}: {{ value }}
</div>
</template>
<script setup>
import { ref } from 'vue'
const score = ref(85)
const isVisible = ref(true)
const items = ref([
{ id: 1, name: '蘋果', price: 30 },
{ id: 2, name: '香蕉', price: 15 },
{ id: 3, name: '橘子', price: 25 }
])
const userInfo = ref({
name: '小明',
age: 25,
city: '台北'
})
</script>
📌 完整範例:計數器 + 待辦清單
<template>
<div class="app">
<h1>Vue 3 練習</h1>
<!-- 計數器 -->
<section>
<h2>計數器</h2>
<p>目前數字:{{ count }}</p>
<p>是否為偶數:{{ isEven ? '是' : '否' }}</p>
<button @click="count--">-1</button>
<button @click="count++">+1</button>
<button @click="count = 0">歸零</button>
</section>
<!-- 待辦清單 -->
<section>
<h2>待辦清單 ({{ remainingCount }} 項未完成)</h2>
<form @submit.prevent="addTodo">
<input v-model="newTodo" placeholder="輸入待辦事項..." />
<button type="submit" :disabled="!newTodo.trim()">新增</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id"
:class="{ done: todo.completed }">
<input type="checkbox" v-model="todo.completed" />
<span>{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">🗑️</button>
</li>
</ul>
<p v-if="todos.length === 0">還沒有待辦事項 🎉</p>
</section>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 計數器
const count = ref(0)
const isEven = computed(() => count.value % 2 === 0)
// 待辦清單
const newTodo = ref('')
const todos = ref([])
let nextId = 1
function addTodo() {
if (!newTodo.value.trim()) return
todos.value.push({
id: nextId++,
text: newTodo.value.trim(),
completed: false
})
newTodo.value = ''
}
function removeTodo(id) {
todos.value = todos.value.filter(t => t.id !== id)
}
const remainingCount = computed(() => {
return todos.value.filter(t => !t.completed).length
})
</script>
<style scoped>
.done span {
text-decoration: line-through;
color: #999;
}
</style>
💡 小提醒
ref()在 JS 中要用.value,在 template 中不用computed有快取,methods沒有——需要快取的用 computedv-if是真正移除 DOM,v-show只是 CSS 隱藏v-for一定要加:key,用唯一值(不要用 index)- 這些語法(
ref、computed、v-model)都是 Vue 封裝的 API,不是 JS 原生的!