文本插值
{{ 变量/表达式 }}(支持算术运算、三元判断等简单逻辑)
<script setup lang="ts">
const name = "枫枫"
const age = 12
const gender = true
const list = [1, 2,3]
const obj = {name: name}
</script>
<template>
<div>
<div>name: {{ name }}</div>
<div>age: {{ age }}</div>
<div>age: {{ age + 18 }}</div>
<div>gender: {{ gender }}</div>
<div>gender: {{ gender ? '男' : '女' }}</div>
<div>list: {{ list }}</div>
<div>obj: {{ obj }}</div>
<div>obj.name: {{ obj.name }}</div>
</div>
</template>
动态绑定
v-bind(简写:,绑定属性 / 样式 / 类名)
<script setup lang="ts">
const name = "枫枫"
const className = "active"
const classObj = {
menu: true,
active: true
}
const styleObj = {
fontSize: "20px",
color: "blue"
}
</script>
<template>
<div>
动态绑定
<div v-bind:name="name">动态绑定name</div>
<div v-bind:class="className">动态绑定class</div>
<div :class="className">动态绑定class</div>
<div :class="classObj">动态绑定class obj</div>
<div :style="styleObj">动态绑定style</div>
</div>
</template>
<style scoped>
.active {
color: red;
}
</style>
条件渲染
v-if/v-else(DOM 增删)、v-show(CSS 显示隐藏)及二者区别
<script setup lang="ts">
const gender = false
const age = 12
</script>
<template>
<div>
条件渲染
<div>
v-if v-else
<span v-if="gender">男</span>
<span v-else>女</span>
<br>
v-if v-else-if v-else
<span v-if="age >= 35">被优化</span>
<span v-else-if="age >= 18">已成年</span>
<span v-else>未成年</span>
</div>
<div>
v-show
<div>
<span v-show="gender">男</span>
</div>
</div>
</div>
</template>
<style scoped>
.active {
color: red;
}
</style>
列表渲染
v-for(遍历数组 / 对象,key属性的必要性)
<script setup lang="ts">
const array = [
"tom", "jerry", "张三", "李四"
]
const obj = {
name: "张三",
age: 21,
addr: "湖南长沙"
}
</script>
<template>
<div>
列表渲染
item in array
<ul>
<li v-for="item in array">{{ item }}</li>
</ul>
(item, index) in array
<ul>
<li v-for="(item, index) in array">{{ index }}.{{ item }}</li>
</ul>
item in obj
<ul>
<li v-for="item in obj">{{ item }}</li>
</ul>
(value, key) in obj
<ul>
<li v-for="(value, key) in obj">{{ key }}: {{ value }}</li>
</ul>
(value, key, index) in obj
<ul>
<li v-for="(value, key, index) in obj">{{ index }}.{{ key }}: {{ value }}</li>
</ul>
</div>
</template>
事件绑定
v-on(简写@,绑定点击 / 输入等事件)
<script setup lang="ts">
function clickHandler(e: Event){
console.log("点击事件", e)
}
function clickHandler1(name: string, e: Event){
console.log("点击事件1", name)
}
function alertHandler(msg: string){
alert(msg)
}
function enterHandler(e: KeyboardEvent){
const target = e.target as HTMLInputElement
const value = target.value
console.log("enter键按下了", value)
}
function inputHandler(e: InputEvent){
console.log("键盘输入中", e.data)
}
function focusHandler(){
console.log("触发焦点了")
}
function blurHandler(){
console.log("失去焦点了")
}
</script>
<template>
<div>
事件绑定
<div>点击事件 <button v-on:click="clickHandler">点我</button></div>
<div>点击事件 <button @click="clickHandler">点我</button></div>
<div>点击事件 <button @click="clickHandler1('枫枫', $event)">点我</button></div>
<div>双击事件 <button @dblclick="alertHandler('双击')">双击</button></div>
<div>右键 <button @click.right="alertHandler('右键')">右键</button></div>
<div>中键 <button @click.middle="alertHandler('中键')">中键</button></div>
<div>enter键 <input @keydown.enter="enterHandler" placeholder="enter键"></div>
<div>input输入 <input @input="inputHandler" placeholder="input输入"></div>
<div>input触发焦点 <input @focus="focusHandler" placeholder="input触发焦点"></div>
<div>input失去焦点 <input @blur="blurHandler" placeholder="input失去焦点"></div>
</div>
</template>
事件处理与修饰符
- 事件绑定:事件函数定义、参数传递(如
@click="handleBtn(1)") - 事件修饰符:
.stop(阻止冒泡)、.prevent(阻止默认行为)、.once(仅触发一次) - 按键修饰符:
@keyup.enter(回车键触发)等表单场景常用修饰符
.prevent
比如a标签点击默认会跳转,可以使用.prevent阻止默认的跳转
<script setup lang="ts">
function clickHandler(e: PointerEvent) {
console.log(e.target)
}
</script>
<template>
<div>
事件委托
<a href="https://www.fengfengzhidao.com" @click.prevent="clickHandler">枫枫知道</a>
</div>
</template>
.once
事件只触发一次
<script setup lang="ts">
function clickHandler(e: PointerEvent) {
console.log(e.target)
}
</script>
<template>
<div>
事件委托
<button @click.once="clickHandler">只有第一次点击才有效</button>
</div>
</template>
事件冒泡
事件冒泡和事件委托在实际开发中遇到的挺多的
需要单独讲一下

所谓的事件冒泡很简单,当内部的button按钮点击之后,会将点击事件向上传递
classB的div先会收到点击事件,然后是classA的div收到点击事件
<script setup lang="ts">
function clickHandler(e: Event, name: string){
console.log(e.target, name)
}
</script>
<template>
<div>
事件冒泡
<div class="A" @click="clickHandler($event, 'A')">
A
<div class="B" @click="clickHandler($event, 'B')">
B
<button @click="clickHandler($event, 'Button')">button</button>
</div>
</div>
</div>
</template>
<style scoped>
.A {
background-color: #ee4a4a;
padding: 20px;
}
.B {
background-color: #10d291;
padding: 20px;
}
</style>
可以使用事件修饰符 .stop 阻止事件冒泡
<template>
<div>
事件冒泡
<div class="A" @click.stop="clickHandler($event, 'A')">
A
<div class="B" @click.stop="clickHandler($event, 'B')">
B
<button @click.stop="clickHandler($event, 'Button')">button</button>
</div>
</div>
</div>
</template>
这样,点击button,就只有button的点击事件触发了
.self
作用是仅当事件触发源是当前元素自身时,才执行事件处理函数
<template>
<div>
事件冒泡
<div class="A" @click="clickHandler($event, 'A')">
A
<div class="B" @click.self="clickHandler($event, 'B')">
B
<button @click="clickHandler($event, 'Button')">button</button>
</div>
</div>
</div>
</template>
比如,给B绑定了.self,那么点击button的时候,由于事件冒泡,本来B是要收到事件的,但是因为.self的存在,把它给过滤了,然后A收到事件冒泡,没有.self,所以它收到了事件冒泡
点击B的时候,.self本身是不会阻止事件冒泡的,所以A也会收到事件
事件修饰符是可以连用的
<div class="A" @click="clickHandler($event, 'A')">
A
<div class="B" @click.stop.self="clickHandler($event, 'B')">
B
<button @click="clickHandler($event, 'Button')">button</button>
</div>
</div>
但是这个顺序是有讲究的,以B为例
@click.stop.self:点击 button → 事件冒泡到 B →.stop先触发(阻断冒泡到 A)→ 再判断.self(触发源是 button≠B,所以 B 的事件不执行)→ 最终:A 无响应,B 无响应,只有 button 自己执行。@click.self.stop:点击 button → 事件冒泡到 B → 先判断.self(触发源是 button≠B,B 的事件直接不执行)→.stop因 “事件未执行” 也不会触发 → 事件继续冒泡到 A → 最终:button 执行,B 无响应,A 执行。
事件委托
现在有一个列表,列表下面有很多元素,现在需要给每个元素都绑定点击事件

如果这样写,就很浪费性能了
<template>
<div>
事件委托
<div class="list">
<div class="item" v-for="item in 20" @click="clickHandler($event, item)">{{ item }}</div>
</div>
</div>
</template>
可以使用事件委托,给父元素绑定事件,通过e.target判断是否是子元素点击
<script setup lang="ts">
function clickHandler(e: PointerEvent) {
const target = e.target as HTMLDivElement
if (target.classList.contains("item")){
console.log(target.dataset["item"])
}
}
</script>
<template>
<div>
事件委托
<div class="list" @click="clickHandler($event)">
<div class="item" v-for="item in 20" :data-item="item">{{ item }}</div>
</div>
</div>
</template>
<style scoped>
.list {
background-color: #ee4a4a;
padding: 20px;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-row-gap: 20px;
grid-column-gap: 20px;
.item {
background-color: #084bc7;
width: 40px;
height: 40px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
}
}
</style>
双向数据绑定
在输入框这些需要与用户进行交互的地方,使用双向数据绑定可以非常方便的绑定和获取数据
<script setup lang="ts">
import {ref} from "vue";
const msg = ref("")
</script>
<template>
<div>
双向数据绑定
<div>
<input v-model="msg" placeholder="输入内容">
</div>
<div>
你输入的内容 {{ msg }}
</div>
</div>
</template>
这里引入了 ref和v-model
ref(""):创建一个响应式数据容器(存储 msg 的值),保证数据变化时视图自动更新 ;v-model="msg":把这个响应式数据和输入框做双向绑定,实现「用户输入 → msg 自动更新」「msg 手动修改 → 输入框内容自动更新」。
很多输入组件都可以使用v-model
<script setup lang="ts">
import {reactive} from "vue";
const data = reactive({
text: "",
age: 18,
gender: true,
like: [],
})
</script>
<template>
<div>
双向数据绑定
<div>
<input v-model="data.text" placeholder="输入内容">
<br>
<input v-model.number="data.age" type="number" placeholder="输入年龄">
<br>
选择性别
<input type="radio" v-model="data.gender" :value="true"> 男
<input type="radio" v-model="data.gender" :value="false"> 女
<br>
选择爱好
<input type="checkbox" v-model="data.like" value="羽毛球"> 羽毛球
<input type="checkbox" v-model="data.like" value="篮球"> 篮球
<input type="checkbox" v-model="data.like" value="乒乓球"> 乒乓球
<input type="checkbox" v-model="data.like" value="足球"> 足球
</div>
<div>
你输入的内容
<div>text: {{ data.text }}</div>
<div>age: {{ data.age }}</div>
<div>gender: {{ data.gender }}</div>
<div>like: {{ data.like }}</div>
</div>
</div>
</template>
这里我又引入了reactive,一般用reactive给对象设置响应式
双向数据绑定原理
v-model 本质是语法糖,如下
<script setup lang="ts">
import {ref} from "vue";
const msg = ref("");
// 手动监听输入事件,更新 msg
const handleInput = (e: Event) => {
// TS 类型断言:确保 e.target 是 HTMLInputElement
const target = e.target as HTMLInputElement;
msg.value = target.value;
};
</script>
<template>
<div>
手动实现双向绑定
<div>
<!-- 等价于 v-model:value 绑定 + input 事件监听 -->
<input :value="msg" @input="handleInput" placeholder="输入内容">
</div>
<div>
你输入的内容 {{ msg }}
</div>
</div>
</template>
计算属性
模板语法痛点:模板中允许写简单表达式(如 {{ a + b }}),但复杂逻辑会导致模板臃肿、可读性差。
<template>
<div>已完成任务数:{{ list.filter(item => item.done).length }}</div>
</template>
<script setup>
import { ref } from 'vue'
const list = ref([{ done: true }, { done: false }, { done: true }])
const firstName = ref('张')
const lastName = ref('三')
const price = ref(100)
const count = ref(2)
const discount = ref(0.1)
</script>
计算属性 —— 专门封装「基于响应式数据派生的逻辑」,让模板回归 “展示” 本质。
<template>
<div>已完成任务数:{{ doneCount }}</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 响应式数据
const list = ref([{ done: true }, { done: false }, { done: true }])
// 计算属性
const doneCount = computed(() => {
return list.value.filter(item => item.done).length
})
</script>
计算属性核心特性:缓存—— 只有依赖的响应式数据(如 list.value)变化时,计算属性才会重新计算,否则复用上次结果(对比 methods 无缓存)。
<script setup>
import {ref} from "vue";
import {computed} from "vue";
const list = ref([{done: true}, {done: false}, {done: true}])
const doneCount = computed(()=>{
console.log("doneCount")
return list.value.filter((item)=>item.done).length
})
function doneCount2() {
console.log("doneCount2")
return list.value.filter((item)=>item.done).length
}
</script>
<template>
<div>
任务完成数量: {{ list.filter((item)=>item.done).length }}
<br>
任务完成数量(计算属性) : {{ doneCount }} - {{ doneCount }}
<br>
任务完成数量(函数) : {{ doneCount2() }} - {{ doneCount2() }}
</div>
</template>
带参数的计算属性
通过返回函数的方式就好了,但是这种方式会失去计算属性缓存的功能
<script setup>
import { ref, computed } from 'vue'
const list = ref([1, 2, 3, 4])
// 看似“带参数”的计算属性:实际是计算属性返回一个函数
const getDouble = computed(() => {
return (num) => { // 内层函数接收参数
console.log('执行计算逻辑')
return num * 2
}
})
</script>
<template>
<!-- 每次调用都会执行内层函数,失去缓存 -->
<div>{{ getDouble(2) }}</div>
<div>{{ getDouble(2) }}</div>
</template>
可写的计算属性
默认的计算属性是只读的(仅包含 get 方法),而可写计算属性通过 set 方法定义 “修改逻辑”:当你直接给计算属性赋值时,Vue 会调用 set 方法,并将赋值的内容作为参数传入,你可以在 set 中处理数据更新,最终同步到响应式数据源。
<template>
<div>
<p>全名:{{ fullName }}</p>
<input v-model="fullName" placeholder="输入全名" />
</div>
</template>
<script setup>
import { ref, computed } from "vue";
// 响应式数据源
const firstName = ref("张");
const lastName = ref("三");
// 可写的计算属性
const fullName = computed({
// 读取逻辑
get() {
return firstName.value + lastName.value;
},
// 修改逻辑
set(newValue) {
const [first, last] = newValue.split(" ");
firstName.value = first || "";
lastName.value = last || "";
},
});
</script>
监听器
- 基本语法:
watch(监听目标, (新值, 旧值) => { 执行逻辑 }, 配置项); - 清理函数:返回一个函数,会在「下一次回调执行前 / 监听器停止时」执行,用于清理定时器、取消请求等(解决异步竞态问题);
- 触发时机:只有监听目标变化时才执行(首次渲染不执行,可通过配置项修改)。
监听单个ref数据
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
// 监听 keyword 变化:参数1=监听目标,参数2=回调函数(新值、旧值)
watch(keyword, (newVal, oldVal) => {
console.log('关键词从', oldVal, '变成', newVal)
})
</script>
监听多个数据
<script setup>
import { ref, watch } from 'vue'
const name = ref('张三')
const age = ref(18)
// 监听多个目标:数组形式
watch([name, age], ([newName, newAge], [oldName, oldAge]) => {
console.log('姓名变化:', oldName → newName)
console.log('年龄变化:', oldAge → newAge)
// 比如:姓名/年龄变化时,同步更新用户信息
})
</script>
监听reactive对象
<script setup>
import { reactive, watch } from 'vue'
const user = reactive({ name: '张三', info: { age: 18 } })
// 监听 reactive 对象:默认深度监听(无需配置 deep: true)
watch(user, (newVal, oldVal) => {
console.log('user 任意属性变化', newVal)
})
// 监听 reactive 对象的单个属性(需用函数返回)
watch(() => user.info.age, (newAge) => {
console.log('年龄变化:', newAge)
})
</script>
watch核心配置项
| 配置项 | 作用 | 适用场景 |
|---|---|---|
| immediate | 首次渲染立即执行回调 | 初始化时就执行监听逻辑(如默认加载数据) |
| deep | 深度监听对象 / 数组的嵌套属性变化 | 监听 ref ({})、ref ([]) 时 |
| flush | 回调执行时机:pre(默认)/post/ 同步 | post = 需要操作更新后的 DOM |
watchEffect隐式监听
watchEffect 是「隐式监听」—— 无需指定监听目标,自动收集回调中用到的响应式数据作为依赖,代码更简洁,但可读性稍弱。
const we = ref("")
watchEffect(()=>{
console.log("we:", we.value)
})
watch
可以暂停、恢复监听
stop是永久停止监听
const handler = watchEffect(()=>{
console.log("we:", we.value)
})
<button @click="handler.pause">暂停监听</button>
<button @click="handler.resume">恢复监听</button>