Vue 函数定义、事件绑定与列表渲染精讲
一、函数定义的两种写法对比
这是一个折叠面板的完整示例,它包含了数据控制、事件绑定和条件渲染:
html
<template>
<div id="app">
<h3>案例:折叠面板</h3>
<div>
<div class="title">
<h4>芙蓉楼送辛渐</h4>
<span class="btn" @click="togglePanel">
{{ isExpanded ? '收起' : '展开' }}
</span>
</div>
<div class="container" v-show="isExpanded">
<p>寒雨连江夜入吴,</p>
<p>平明送客楚山孤。</p>
<p>洛阳亲友如相问,</p>
<p>一片冰心在玉壶。</p>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
// 控制面板展开/收起
const isExpanded = ref(true)
const togglePanel = () => {
isExpanded.value = !isExpanded.value
}
</script>
<style scoped>
body {
background-color: #ccc;
}
#app {
width: 400px;
margin: 20px auto;
background-color: #fff;
border: 4px solid blueviolet;
border-radius: 1em;
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.5);
padding: 1em 2em 2em;
}
#app h3 {
text-align: center;
}
.title {
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #ccc;
padding: 0 1em;
}
.title h4 {
line-height: 2;
margin: 0;
}
.container {
border: 1px solid #ccc;
padding: 0 1em;
}
.btn {
cursor: pointer;
}
</style>
在这个例子里,我们用 togglePanel 函数来切换面板的展开状态。定义这个函数时,我可以选择两种写法。
1. 箭头函数表达式
js
const togglePanel = () => {
isExpanded.value = !isExpanded.value
}
2. 普通函数声明
js
function togglePanel() {
isExpanded.value = !isExpanded.value
}
它们都能完成同样的工作,但在 Vue 3 的 <script setup> 环境下,有一些关键区别,我给大家整理成了一张表格:
| 特性 | 箭头函数 const fn = () => {} |
普通函数 function fn() {} |
|---|---|---|
| 提升(Hoisting) | 不会被提升,必须先定义后使用 | 会被提升,可以在定义前调用 |
this 绑定 |
词法绑定(继承外层作用域的 this) |
动态绑定(取决于调用方式) |
| 能否作为构造函数 | 不能使用 new |
可以使用 new |
arguments 对象 |
没有 | 有 |
| 简洁性 | 单语句时可省略 {} 和 return |
需要完整函数体 |
在 Vue <script setup> 中的推荐度 |
推荐(风格一致,避免 this 陷阱) |
也可用,但需注意提升导致隐式依赖 |
我的建议是 :在组合式 API 中我们根本不使用 this,所以箭头函数更简洁、更安全。除非你需要利用函数提升(比如两个函数互相递归调用),或者需要动态 this,否则一律使用箭头函数。记住口诀:箭头优先,除非需要提升或 this。
二、事件绑定中括号 () 的用法对比
html
<span class="btn" @click="togglePanel">
{{ isExpanded ? '收起' : '展开' }}
</span>
这里 @click="togglePanel" 没有加括号。但有时候你也会看到 @click="togglePanel()" 的写法。它们有什么区别?
1. 不加括号:@click="togglePanel"
- 执行时机:点击事件触发时才会调用
togglePanel。 - 传参:Vue 会自动把原生事件对象
$event作为第一个参数传进去(如果函数需要的话)。 - 典型用途:处理函数不需要额外参数时,直接写函数名即可。
2. 加括号:@click="togglePanel()"
- 执行时机:模板渲染时就会立即执行一次
togglePanel(),然后把它的返回值(如果返回值是函数)作为事件处理函数。这种用法非常少见,通常是错误的。 - 传参:不会自动传递
$event。如果你想传参,必须手动写成@click="togglePanel($event)"或@click="togglePanel('自定义参数')"。 - 典型用途:需要传递自定义参数时,例如
@click="deleteItem(item.id)"。这时我们必须加括号,Vue 会正确地在点击时调用函数并传入参数。
一句话总结:
- 无参 → 不加括号,Vue 自动调用并传入事件对象。
- 有参 → 加括号,手动传参;如果需要事件对象,用
$event。
口诀:无参不加括号,有参加括号且传参。
三、列表渲染与删除实战
来看一个更复杂的案例:品牌列表的展示与删除。下面是完整的代码:
html
<template>
<div id="app">
<table class="tb">
<thead>
<tr>
<th>编号</th>
<th>品牌名称</th>
<th>创立时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in list" :key="item.id">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.time }}</td>
<td>
<button @click="deleteItem(item.id)">删除</button>
</td>
</tr>
<tr v-if="list.length === 0">
<td colspan="4">没有数据咯~</td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
interface BrandRecord {
id: number
name: string
time: string
}
const list = ref<BrandRecord[]>([
{ id: 1, name: '奔驰', time: '2020-08-01' },
{ id: 2, name: '宝马', time: '2020-08-02' },
{ id: 3, name: '奥迪', time: '2020-08-03' },
])
// 删除方法
const deleteItem = (id: number) => {
list.value = list.value.filter((item) => item.id !== id)
}
</script>
<style scoped>
#app {
width: 600px;
margin: 10px auto;
}
.tb {
border-collapse: collapse;
width: 100%;
}
.tb th {
background-color: #0094ff;
color: white;
}
.tb td,
.tb th {
padding: 5px;
border: 1px solid black;
text-align: center;
}
.add {
padding: 5px;
border: 1px solid black;
margin-bottom: 10px;
}
</style>
下面逐步拆解这段代码的逻辑。
1. 模板部分:数据的展示与交互
html
<tbody>
<tr v-for="item in list" :key="item.id">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.time }}</td>
<td>
<button @click="deleteItem(item.id)">删除</button>
</td>
</tr>
<tr v-if="list.length === 0">
<td colspan="4">没有数据咯~</td>
</tr>
</tbody>
-
v-for="item in list"循环渲染
list数组中的每一个对象,item就是当前迭代的品牌对象。 -
:key="item.id"key是 Vue 用来标识每一个循环项的特殊属性,必须唯一且稳定。这里我们用品牌id,它永远不会重复,能帮助 Vue 高效地更新 DOM。例如删除第二行时,Vue 能精确移除对应的<tr>,而不是重新渲染整个表格。 -
插值表达式
{``{ item.id }}、{``{ item.name }}、{``{ item.time }}用来在表格中显示对应数据。 -
事件绑定
@click="deleteItem(item.id)"给删除按钮绑定了点击事件,并传入当前项的id。注意,因为我们传了参数,所以必须加括号。 -
条件渲染
v-if="list.length === 0"会在列表为空时显示"没有数据咯~"这一行,colspan="4"让它合并四列占满整行。
2. 脚本部分:数据定义与删除逻辑
ts
interface BrandRecord {
id: number
name: string
time: string
}
const list = ref<BrandRecord[]>([
{ id: 1, name: '奔驰', time: '2020-08-01' },
{ id: 2, name: '宝马', time: '2020-08-02' },
{ id: 3, name: '奥迪', time: '2020-08-03' },
])
const deleteItem = (id: number) => {
list.value = list.value.filter((item) => item.id !== id)
}
-
interface BrandRecord这是 TypeScript 的接口,定义了品牌对象必须包含
id、name、time三个属性。这样我们在写代码时就能得到类型检查和智能提示。 -
ref<BrandRecord[]>我们声明了一个响应式数组,并通过泛型约束了数组元素的结构。
-
deleteItem函数它接收一个
id参数,然后用filter方法返回一个新数组,其中包含了所有id不等于传入id的项。相当于把要删除的那一项过滤掉。jslist.value.filter((item) => item.id !== id)这里
(item) => item.id !== id是一个箭头函数,它判断每一个元素的id是否不等于要删除的id。如果条件为true,该元素就被保留;为false就被过滤掉。最后我们把新数组重新赋值给list.value,触发 Vue 的响应式更新,页面就会自动移除那一行。
3. 整体执行流程
- 用户点击"宝马"行的删除按钮。
- 触发
deleteItem(2)(因为宝马的 id 是 2)。 filter遍历数组:{ id:1 }→1 !== 2为true→ 保留{ id:2 }→2 !== 2为false→ 移除{ id:3 }→3 !== 2为true→ 保留
list.value更新为只包含奔驰和奥迪的新数组。- Vue 检测到数据变化,重新渲染表格,宝马行消失。如果所有数据都被删除,会显示"没有数据咯~"。
四、简单列表中的 key 策略:什么时候可以用 index
最后看一个生成随机数并支持删除的简单列表:
html
<template>
<div>
<ul>
<li v-for="person in personList">{{ person }}</li>
</ul>
<button @click="walk">走一走</button>
<hr />
<div>
<ul>
<li v-for="(item, index) in numList" :key="index">
{{ item }} <button @click="del(index)">删除</button>
</li>
</ul>
<button @click="add">生成</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const personList = ref<string[]>(['帅哥', '美女', '程序员'])
const walk = () => {
const first = personList.value[0]
if (first) {
personList.value = [...personList.value.slice(1), first]
}
}
let numList = ref<number[]>([])
const add = () => {
const num = Math.floor(Math.random() * 20 + 1)
numList.value.push(num)
}
const del = (index: number) => {
numList.value.splice(index, 1)
}
</script>
注意,这里在 v-for 中使用了 (item, index) in numList,并且 :key="index"。
在特定场景下,使用 index 是可以接受的。 原因如下:
- 列表没有复杂状态 :每个项只是显示一个数字和一个删除按钮,没有
<input>、<checkbox>等需要维持内部状态的元素。即使 Vue 因为 index 复用策略导致 DOM 元素错乱,也不会产生可见的 bug。 - 数据量很小:用户生成几个随机数后删除,重新渲染的开销可以忽略不计。
- 没有更好的唯一键 :数字可能重复,不能直接当 key;如果强行改造成
{ id, value }结构反而会增加复杂度。
但要清楚,一旦列表变复杂(比如加入了输入框、动画或可排序功能),就必须使用唯一的业务 id。用 index 时删除非末尾项会导致后面所有元素的 key 变化,Vue 会错误地复用 DOM,表现为输入框内容串位、勾选状态错乱等。
总结:
- 简单列表(纯展示 + 删除)且数据量小时,用 index 没问题,可以节省代码。
- 正式项目或列表可能演变为复杂交互时,请从一开始就设计唯一 id 作为 key。
五、总结
- 函数定义 :在 Vue 3 的
<script setup>中,优先使用箭头函数;除非需要提升或动态this。 - 事件绑定括号:无参不加括号,Vue 自动传事件对象;有参必须加括号,手动传参。
- 列表渲染与删除 :掌握
v-for、:key、v-if,用filter实现不可变删除。 - key 的选择:唯一 id 是最佳实践;简单场景下 index 也可以应急,但要知道它的局限性。
以上是本人学习过程中遇到的问题,边学变搜索的,如不满意,不喜勿喷哈