Appearance
Vue 进阶
Vue 基础篇介绍了组件、数据绑定、条件渲染、列表渲染和事件处理。这些已经能写简单页面了,但实际开发还有几块绕不开:表单怎么双向绑定、派生数据怎么自动算、组件什么时候创建和销毁。
一、表单双向绑定 v-model
表单是前后端交互的关键——用户在输入框里打字,数据要同步更新;提交时把数据发给后端。原生 JS 里要手动监听 input 事件、读 value、赋给变量。Vue 用 v-model 一步搞定:
vue
<template>
<form @submit.prevent="handleSubmit">
<div>
<label>标题</label>
<input v-model="form.title" placeholder="输入文章标题" />
</div>
<div>
<label>分类</label>
<select v-model="form.category">
<option value="tech">技术</option>
<option value="life">生活</option>
</select>
</div>
<div>
<label>状态</label>
<input type="checkbox" v-model="form.published" /> 已发布
</div>
<button type="submit">保存</button>
</form>
<!-- 实时预览 -->
<div class="preview">
<p>标题:{{ form.title }}</p>
<p>分类:{{ form.category }}</p>
<p>已发布:{{ form.published }}</p>
</div>
</template>
<script setup>
import { reactive } from "vue";
// reactive() 适合管理对象类型的数据(ref 适合单个值)
const form = reactive({
title: "",
category: "tech",
published: false,
});
function handleSubmit() {
// form 里已经有了所有表单数据,直接提交给后端
console.log("提交:", form);
// fetch("/api/articles", { method: "POST", body: JSON.stringify(form) });
}
</script>v-model 做了两件事:用户输入时自动更新数据,数据变了自动更新输入框。这就是双向绑定——数据 ↔ 输入框,哪个变另一个跟着变。
@submit.prevent 里的 .prevent 是事件修饰符,等价于 event.preventDefault(),阻止表单默认提交(默认提交会刷新页面)。Vue 里事件修饰符很常用,省掉手动调 preventDefault。
reactive 和 ref 怎么选
- 单个值(字符串、数字、布尔)用
ref - 对象或数组用
reactive
两者都是响应式的,区别在用法:ref 在 script 里要加 .value,reactive 直接用属性名(form.title 不用 form.value.title)。表单数据是对象,用 reactive 更自然。
二、计算属性
有些数据是从其他数据算出来的——比如根据状态筛选文章数、根据列表算总价。每次都手写函数调用太啰嗦,计算属性(computed)让它自动追踪依赖,依赖变了自动重算:
vue
<template>
<p>全部文章:{{ articles.length }} 篇</p>
<p>已发布:{{ publishedCount }} 篇</p>
<p>草稿:{{ draftCount }} 篇</p>
<ul>
<li v-for="article in publishedArticles" :key="article.id">
{{ article.title }}
</li>
</ul>
</template>
<script setup>
import { ref, computed } from "vue";
const articles = ref([
{ id: 1, title: "Python 入门", status: "published" },
{ id: 2, title: "Go 并发", status: "draft" },
{ id: 3, title: "Rust 安全", status: "published" },
]);
// computed 自动追踪 articles,articles 变了这里自动重算
const publishedArticles = computed(() =>
articles.value.filter((a) => a.status === "published")
);
const publishedCount = computed(() => publishedArticles.value.length);
const draftCount = computed(() =>
articles.value.filter((a) => a.status === "draft").length
);
</script>computed(() => ...) 接收一个函数,函数里用到的响应式数据就是依赖。依赖变了,计算属性自动重算;依赖没变,直接返回缓存结果——不用每次渲染都重新算。
计算属性跟普通函数的区别:函数每次渲染都重新执行,计算属性有缓存——依赖没变就不重算。列表数据没变时不重复 filter,性能更好。
三、侦听器 watch
计算属性是"根据数据算新值"。有时候数据变了要执行副作用——发请求、改 localStorage、操作其他变量。这种场景用 watch:
vue
<script setup>
import { ref, watch } from "vue";
const keyword = ref("");
// keyword 变了就执行,适合"数据变了要做点什么"的场景
watch(keyword, (newValue, oldValue) => {
console.log(`搜索关键词从 "${oldValue}" 变成 "${newValue}"`);
// 实际场景:debounce 后调后端搜索接口
// searchArticles(newValue);
});
</script>watch(被监听的变量, 回调函数)。变量变了回调就执行,参数是新值和旧值。
watch 跟 computed 怎么选:
- 算出新值给页面用 →
computed - 数据变了要执行操作(发请求、存本地、跳页面)→
watch
四、生命周期
组件从创建到销毁会经历几个阶段,叫生命周期。每个阶段有对应的钩子函数,在特定时机执行代码:
vue
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const articles = ref([]);
let timer = null;
// 组件挂载到页面后执行——DOM 已经存在,可以操作
onMounted(() => {
console.log("组件已挂载,开始加载数据");
// 典型场景:页面加载后调后端接口获取数据
loadArticles();
// 典型场景:启动定时器
timer = setInterval(() => {
console.log("定时刷新");
}, 5000);
});
// 组件销毁前执行——清理工作
onUnmounted(() => {
console.log("组件即将销毁,清理定时器");
clearInterval(timer); // 不清理会一直跑,内存泄漏
});
async function loadArticles() {
// const data = await request("/articles");
// articles.value = data;
articles.value = [
{ id: 1, title: "Python 入门" },
{ id: 2, title: "Go 并发" },
];
}
</script>最常用的两个:
onMounted:组件挂载到页面后触发。加载初始数据的标准位置——DOM 准备好了,可以发请求、可以操作元素onUnmounted:组件销毁前触发。清理工作的标准位置——停定时器、关 WebSocket、取消请求
定时器不清理是内存泄漏的常见来源。组件销毁了定时器还在跑,回调里可能引用了已销毁组件的数据,导致报错或内存涨。onUnmounted 里清掉就行。
五、父子组件实战
把前面几块合到一个完整的文章列表组件里——父组件管理数据,子组件展示卡片,表单添加新文章:
vue
<!-- App.vue -->
<template>
<div class="app">
<h2>文章管理</h2>
<!-- 添加文章的表单 -->
<ArticleForm @create="handleCreate" />
<!-- 文章列表 -->
<div class="list">
<ArticleCard
v-for="article in sortedArticles"
:key="article.id"
:title="article.title"
:status="article.status"
:date="article.date"
/>
</div>
<p>共 {{ articles.length }} 篇,已发布 {{ publishedCount }} 篇</p>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
import ArticleCard from "./components/ArticleCard.vue";
import ArticleForm from "./components/ArticleForm.vue";
const articles = ref([
{ id: 1, title: "Python 入门", status: "published", date: "2024-06-01" },
{ id: 2, title: "Go 并发", status: "draft", date: "2024-06-02" },
]);
// 计算属性:按日期倒序排列
const sortedArticles = computed(() =>
[...articles.value].sort((a, b) => b.date.localeCompare(a.date))
);
const publishedCount = computed(() =>
articles.value.filter((a) => a.status === "published").length
);
// 子组件 emit create 事件,父组件接收并添加到列表
function handleCreate(newArticle) {
articles.value.push({
id: Date.now(),
...newArticle,
});
}
</script>vue
<!-- components/ArticleForm.vue -->
<template>
<form @submit.prevent="submit">
<input v-model="form.title" placeholder="文章标题" required />
<select v-model="form.status">
<option value="draft">草稿</option>
<option value="published">发布</option>
</select>
<button type="submit">添加</button>
</form>
</template>
<script setup>
import { reactive } from "vue";
const emit = defineEmits(["create"]);
const form = reactive({
title: "",
status: "draft",
});
function submit() {
emit("create", { ...form, date: new Date().toISOString().slice(0, 10) });
form.title = ""; // 提交后清空标题
}
</script>vue
<!-- components/ArticleCard.vue -->
<template>
<div class="card">
<h3>{{ title }}</h3>
<span class="badge" :class="status">{{ statusLabel }}</span>
<span class="date">{{ date }}</span>
</div>
</template>
<script setup>
import { computed } from "vue";
const props = defineProps({
title: String,
status: String,
date: String,
});
// 把英文状态翻译成中文显示
const statusLabel = computed(() =>
props.status === "published" ? "已发布" : "草稿"
);
</script>
<style scoped>
.card {
border: 1px solid #e0e0e0;
padding: 12px 16px;
margin-bottom: 8px;
border-radius: 6px;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
background: #f0f0f0;
}
.published {
background: #d4edda;
color: #155724;
}
</style>这个例子用到了前面所有的知识:
- v-model 表单绑定(ArticleForm 的输入框和下拉框)
- computed 计算属性(排序、计数、状态标签翻译)
- props/emit 父子通信(App → ArticleCard 传展示数据,ArticleForm → App 报创建事件)
- v-for 列表渲染
- reactive 管理表单对象
这就是 Vue 开发页面的基本套路——数据用 ref/reactive 管,页面用模板渲染,交互用事件,组件间用 props/emit 通信。项目实战里做的运维平台前端,就是这个套路套到具体业务上。