Skip to content

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 里要加 .valuereactive 直接用属性名(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 通信。项目实战里做的运维平台前端,就是这个套路套到具体业务上。