vue3–1

## Vue3

vue2 和vue3的区别

image-20260812214752459

Vue2的API设计是Options(配置)风格的。

Vue3的API设计是Composition(组合)风格的。

Options API 的弊端

Options类型的 API,数据、方法、计算属性等,是分散在:data、methods、computed中的,若想新增或者修改一个需求,就需要分别修改:data、methods、computed,不便于维护和复用。

选项式API,可以用包含多个选项的对象来描述组件的逻辑,如:data,methods,mounted等。

组合式API:分散式维护变成集中式维护

1. setup:是一个标识,告诉Vue需要进行一些处理,让我们可以更简洁的使用组合式API
2. ref():接收一个内部值,返回一个响应式的ref对象,此对象只有一个指向内部值的属性 value
3. onMounted():在组合式API中的钩子方法,注册一个回调函数,在组件挂载完成后执行

Composition API 的优势

可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。

image-20260812205316026

image-20260812205522163

起步案例 — 创建 Vue 应用

Vue3 使用 createApp 创建应用实例,通过 setup() 函数组织逻辑。

<!-- 挂载容器 -->
<div id="app">
    <!-- 双花括号文本插值 -->
    <div>{{ msg }}</div>
    <div>{{ name }}</div>
</div>

<script src="js/vue.global.js"></script>
<script>
    // 1. 从 Vue 全局对象中解构出 createApp
    const { createApp } = Vue;

    // 2. 创建 Vue 应用并挂载
    createApp({
        // setup() 是组合式 API 的入口函数
        setup() {
            // 在这里定义变量和函数
            return {
                // 返回的对象中的属性,可以在模板中直接使用
                msg: `hello world`,
                name: `张三`
            };
        }
    }).mount("#app");  // 3. 挂载到 #app 容器
</script>

Vue3 应用创建的三个步骤:

  1. createApp(options) — 创建应用实例(传入根组件选项)
  2. setup() — 组合式 API 入口,定义数据和方法
  3. .mount(selector) — 挂载到 DOM 容器

理解 setup() 的 return:setup() 中 return 的对象属性会自动暴露给模板。这里用到了 ES6 的对象简写语法(return { msg } = return { msg: msg })。

要点:

  • 一个页面可以有多个 Vue 应用实例(挂载到不同的容器)
  • setup() 在组件创建之前执行,此时 this 不可用
  • 模板中的 JavaScript 表达式只能访问 setup() 返回的数据

setup 概述

setup是Vue3中一个新的配置项,值是一个函数,它是 Composition API “表演的舞台”,组件中所用到的:数据、方法、计算属性、监视……等等,均配置在setup中。setup() 钩子是在组件中使用组合式 API 的入口。

特点如下:

1. setup函数返回的对象中的内容,可直接在模板中使用。
2. setup中访问this是undefined。
3. setup函数会在beforeCreate之前调用,它是“领先”所有钩子执行的。

我们可以使用响应式 API 来声明响应式的状态,在 setup() 函数中返回的对象会暴露给模板和组件实例。

其他的选项也可以通过组件实例来获取 setup() 暴露的属性

setup函数有一个语法糖,这个语法糖,可以让我们把setup独立出去

<script setup name="Person">

模板语法

Vue 模板中支持多种语法来渲染数据。

文本插值与 JavaScript 表达式

<!-- 基本文本插值 -->
<div>{{ score }}</div>
<!-- {{}} 中可以使用 JavaScript 表达式(单条语句) -->
<div>{{ score >= 60 ? '及格' : '不及格' }}</div>
<div>{{ name.toUpperCase() }}</div>
<div>{{ price * quantity }}</div>

<!-- ❌ 以下写法不允许(不是表达式,是语句) -->
<!-- <div>{{ if (score >= 60) { return '及格'; } }}</div> -->
<!-- <div>{{ let x = 10; }}</div> -->

原始 HTML — v-html

<!-- v-html 将字符串作为 HTML 渲染 -->
<div v-html="link"></div>
const link = "<a href='https://www.baidu.com/'>baidu</a>";

⚠️ 安全警告:v-html 存在 XSS(跨站脚本攻击)风险!永远不要对用户输入的内容使用 v-html,只用于你自己信任的内容。

原始 text — v-text

<!-- v-text 将字符串纯文本文字 渲染 -->
<div v-text ="link"></div>
const link = "<a href='https://www.baidu.com/'>baidu</a>";

属性绑定 — v-bind(单向绑定)

<!-- 完整写法 -->
<div v-bind:id="id">hello Vue</div>

<!-- 简写形式(只用冒号 : ) — 最常用 -->
<div :id="id">hello Vue</div>

<!-- 同名简写(Vue 3.4+)— 属性名和变量名相同时 -->
<!-- <div :id></div>  等价于 <div :id="id"></div> -->

<!-- 绑定多个属性 — 用对象 -->
<div :id="attrobj.id" :class="attrobj.class" :title="attrobj.title">
    hello Vue
</div>

<!-- 动态属性名 — 方括号内写表达式 -->
<a v-bind:[link1.name]="link1.value">link</a>
<!-- 当 link1.name = 'href' 时,等价于 <a href="https://www.baidu.com/">link</a> -->
const id = 'div2';
const attrobj = {
    id: 'div3',
    class: 'c1',
    title: '这是一个div'
};
const link1 = {
    name: 'href',
    value: 'https://www.baidu.com/'
};

事件绑定 — v-on(单击事件)

<!-- 完整写法 -->
<button v-on:click="test">click</button>

<!-- 简写(@ 符号) — 最常用 -->
<button @click="test">click</button>

<!-- 动态事件名 -->
<button @[eventName]="handler">click</button>
function test() {
    console.log("---------");
}

要点总结:

指令 简写 用途
v-bind:属性="值" :属性="值" 动态绑定 HTML 属性
v-on:事件="方法" @事件="方法" 绑定事件处理器
v-html="值" 无 渲染原始 HTML(注意安全)
{{ 表达式 }} — 文本插值
v-text="值" 渲染纯文本

响应式基础 — ref 与 reactive

Vue3 提供两种创建响应式数据的方式,这是 Vue 最核心的概念。

ref — 适用于基本类型 + 引用类型

const { createApp, ref } = Vue;

createApp({
    setup() {
        // ref 包装一个值,使其变成响应式
        const count = ref(0);

        // 在 JS 中通过 .value 访问/修改
        function text() {
            count.value++;
        }

        return { count, text };
    }
}).mount("#app");
<!-- 在模板中不需要 .value,Vue 会自动解包 -->
<div>{{ count }}</div>
<button @click="count++">click</button>
<button @click="text()">click</button>

核心认知:ref() 返回一个对象 { value: 原始值 }。JS 中操作需要 .value,模板中自动解包。这是 Vue3 响应式系统的核心设计。

reactive — 适用于对象/数组类型

const { createApp, reactive } = Vue;

createApp({
    setup() {
        // reactive 将对象变成响应式代理
        const count1 = reactive({ count: 0 });

        // 直接修改属性,无需 .value
        function text1() {
            count1.count++;
        }

        // 表单数据绑定场景
        const student = reactive({
            name: '',
            age: '',
            address: ''
        });

        // 模拟从服务器获取数据
        function getStudentInfo() {
            // 发送 AJAX 请求从后台获取数据后赋值
            student.name = "zs";
            student.age = "10";
            student.address = "qd";
        }

        function setStudentInfo() {
            console.log(student);  // 直接使用,不需要 .value
        }

        return { count1, text1, student, getStudentInfo, setStudentInfo };
    }
}).mount("#app");
<!-- reactive 对象直接绑定到表单 -->
<div>
    <div>姓名 <input type="text" v-model="student.name" /></div>
    <div>年龄 <input type="text" v-model="student.age" /></div>
    <div>地址 <input type="text" v-model="student.address" /></div>
</div>
<button @click="getStudentInfo()">修改</button>
<button @click="setStudentInfo()">确认</button>

ref:套一层外壳,数据放 .value;reactive:无外壳,直接操作对象本身

ref vs reactive 对比

特性 ref reactive
适用类型 任意类型(基本 + 引用) 仅引用类型(对象/数组/Map/Set)
访问方式(JS) 需要 .value 直接访问属性
访问方式(模板) 自动解包,不需要 .value 直接访问属性
重新赋值 ✅ 可以(ref.value = newObj) Object.assign(person, {name:’80’,age: 4});
解构 不会丢失响应性(配合 toRefs) ❌ 直接解构会丢失响应性
原理 内部也是用 reactive 包装 Proxy 代理

选 ref 还是 reactive?

// 社区共识和官方推荐:优先使用 ref
// 原因1:ref 适用于任何类型,心智模型统一
const count = ref(0);
const user = ref({ name: 'zs' });

// 原因2:ref 可以安全地重新赋值
user.value = { name: 'ls' };  // ✅ 响应性保持

// reactive 重新赋值会丢失响应性
const state = reactive({ name: 'zs' });
// state = reactive({ name: 'ls' });  // ❌ 响应性丢失!

// 原因3:ref 解构更安全
const { name } = user.value;  // 明确知道这是取值操作

// reactive 适合的场景:管理一组相关状态(类似组件内部状态)
const formState = reactive({
    username: '',
    password: '',
    remember: false
});

要点:

  • ref 的本质:ref(0) → reactive({ value: 0 })
  • 模板中的自动解包仅对顶层 ref 有效(嵌套在对象里的 ref 不会自动解包)
  • reactive 不能重新赋值整个对象,只能修改属性
  • 发展趋势:ref 越来越受推荐,Vue 官方也在推动”ref 优先”

toRefs 与 toRef

作用:将一个响应式对象中的每一个属性,转换为ref对象。

备注:toRefs与toRef功能一致,但toRefs可以批量转换。

import {ref,reactive,toRefs,toRef} from 'vue'
let person = reactive({name:'张三', age:18, gender:'男'})
// 通过toRefs将person对象中的n个属性批量取出,且依然保持响应式的能力
let {name,gender} =  toRefs(person)

计算属性(computed)

计算属性基于响应式依赖进行缓存,只在依赖变化时重新计算。这是性能优化的关键手段。

方法 vs 计算属性

const { createApp, ref, computed } = Vue;

createApp({
    setup() {
        const score = ref(90);
        const firstname = ref("bill");
        const familyname = ref("gates");
        const title = ref("ceo");

        // 普通方法 — 每次组件重新渲染都会重新执行(无缓存)
        function test() {
            console.log('test 方法执行了');
            if (score.value >= 80) return "优秀";
            else if (score.value >= 70) return "良好";
            else if (score.value >= 60) return "及格";
            else return "不及格";
        }

        // 计算属性 — 有缓存,依赖不变则不会重新计算
        const level = computed(() => {
            console.log('level 计算属性执行了');
            if (score.value >= 80) return "优秀";
            else if (score.value >= 70) return "良好";
            else if (score.value >= 60) return "及格";
            else return "不及格";
        });

        // 组合多个数据源的计算属性
        const allInfo = computed(() => {
            return `${title.value}: ${firstname.value} ${familyname.value}`;
        });

        return { score, level, allInfo, test };
    }
}).mount("#app");
<div>{{ score }}</div>
<!-- 计算属性可以绑定到属性上 -->
<div :title="level">{{ level }}</div>
<div>{{ allInfo }}</div>
<!-- 方法每次都要调用,模板中也要写括号  点击事件不用加括号-->
<div>{{ test() }}</div>

计算属性的缓存机制(图解说明)

score = 90  →  level 计算 →  "优秀"(缓存结果)
    ↓
模板渲染 3 次(其他数据变化导致的重新渲染)
    ↓
level 直接返回缓存 "优秀",不重新计算
    ↓
score = 75 →  依赖变了 →  level 重新计算 →  "良好"(更新缓存)

可写的计算属性

const fullName = computed({
    get() {
        return firstname.value + ' ' + familyname.value;
    },
    set(newValue) {
        const [firstname.value, familyname.value] = newValue.split(' ');
    }
});

fullName.value = 'Steve Jobs';  // 触发 setter
console.log(firstname.value);   // 'Steve'

何时用 computed、何时用 method?

场景 用 原因
基于已有数据计算一个值 computed 有缓存,性能更好
点击按钮执行一个操作 method 需要主动触发,不是取值
列表过滤、排序的结果 computed 依赖变化时才重新计算
提交表单 method 这是动作,不是衍生值

要点:

  • computed() 返回一个只读的 ref 对象(默认只读)
  • 计算属性有缓存:只有依赖的响应式数据变化时才重新计算
  • 普通方法每次组件重新渲染都会执行,没有缓存
  • 适用场景:依赖其他数据的衍生状态(如全名、等级、过滤后的列表)
  • 计算属性应该纯粹(无副作用),不要在计算属性里发请求或修改 DOM

条件渲染

Vue 提供 v-if 系列和 v-show 两种条件渲染方式。

v-if / v-else-if / v-else`

<div id="app">
    <!-- 单条件 — 为 false 时不渲染(DOM 中不存在该元素) -->
    <div v-if="score >= 60">及格</div>

    <!-- 二选一 — 两个中只有一个存在于 DOM -->
    <div v-if="score >= 60">及格</div>
    <div v-else>不及格</div>

    <!-- 多条件 — 类似 if / else if / else 链 -->
    <div v-if="score >= 80">优秀</div>
    <div v-else-if="score >= 70">良好</div>
    <div v-else-if="score >= 60">及格</div>
    <div v-else>不及格</div>
</div>

关键规则:v-if / v-else-if / v-else 必须连续使用,中间不能插入其他元素。

<template> 分组

当需要对多个元素统一判断时,用 <template> 包裹(不产生额外 DOM 节点):

<template v-if="score >= 80">
    <span>优秀</span>
    <span>很棒!</span>
</template>
<template v-else-if="score >= 70">
    <span>良好</span>
    <span>不错!</span>
</template>

渲染结果中不会出现 <template> 标签,只有 <span> 元素。

v-show

<!-- v-show 通过 CSS display 属性切换显示,元素始终保留在 DOM 中 -->
<div v-show="score >= 60">及格</div>
<!-- 当条件为 false 时,等价于 <div style="display: none;">及格</div> -->

v-if vs v-show 对比

特性 v-if v-show
渲染方式 条件为 false 时,元素完全不渲染(不存在于 DOM 树) 元素始终渲染,通过 display: none 切换
初始渲染开销 低(false 时不渲染) 高(始终渲染)
切换开销 高(切换时销毁/重建子组件和监听器) 低(仅切换一个 CSS 属性)
适用场景 条件很少改变时(如用户登录状态、权限判断) 需要频繁切换时(如 Tab 页、展开/折叠)
与 <template> 配合 ✅ 可以 ❌ 不可以(v-show 不能用 template)

选择建议:

  • 运行时条件很少改变 → v-if
  • 需要非常频繁地切换 → v-show
  • 条件大概率不会为 true → v-if(避免不必要的渲染)

列表渲染(v-for)

v-for 用于遍历数组和对象,生成列表内容。

遍历数组

<table border="10px">
    <tr>
        <th>index</th>
        <th>Id</th>
        <th>name</th>
        <th>age</th>
        <th>addr</th>
    </tr>
    <!-- 基本遍历 -->
    <tr v-for="student in studentList">
        <td></td>
        <td>{{ student.id }}</td>
        <td>{{ student.name }}</td>
        <td>{{ student.age }}</td>
        <td>{{ student.addr }}</td>
    </tr>
    <hr>
    <!-- 带索引遍历,推荐加 :key 以提升 Vue 的 diff 算法效率 -->
    <tr v-for="(student, index) in studentList" :key="student.id">
        <td>{{ index }}</td>
        <td>{{ student.id }}</td>
        <td>{{ student.name }}</td>
        <td>{{ student.age }}</td>
        <td>{{ student.addr }}</td>
    </tr>
</table>
const studentList = reactive([
    { id: 1, name: 'zs', age: 10, addr: 'qd' },
    { id: 2, name: 'zs', age: 10, addr: 'qd' },
    { id: 3, name: 'zs', age: 10, addr: 'qd' },
    { id: 4, name: 'zs', age: 10, addr: 'qd' }
]);

遍历对象

<!-- (value, key) in object -->
<div v-for="(v, k) in student">{{ k }} - {{ v }}</div>
<!-- 输出:
  id - 1
  name - zs
  age - 10
  addr - qd
-->

使用范围值

<!-- v-for 也可以接受整数,从 1 开始计数 -->
<div v-for="n in 5">{{ n }}</div>
<!-- 输出: 1 2 3 4 5 -->

v-for 与 v-if 结合

当需要筛选列表中的某些项时,二者结合使用。但不建议在同一元素上同时使用(Vue3 中 v-if 优先级高于 v-for,容易混淆)。

推荐做法 — 用 <template> 包裹分层:

<template v-for="(student, index) in studentList" :key="student.id">
    <!-- 过滤掉 id 为 3 的学生 -->
    <tr v-if="student.id != 3">
        <td>{{ index }}</td>
        <td>{{ student.id }}</td>
        <td>{{ student.name }}</td>
        <td>{{ student.age }}</td>
        <td>{{ student.addr }}</td>
    </tr>
</template>

更好的做法 — 用计算属性替代模板内筛选:

const filteredStudents = computed(() =>
    studentList.filter(s => s.id !== 3)
);

:key 的重要性

<!-- ✅ 正确:使用唯一 ID 作为 key -->
<tr v-for="item in list" :key="item.id">...</tr>

<!-- ⚠️ 迫不得已才用 index(列表不会重新排序/增删时) -->
<tr v-for="(item, index) in list" :key="index">...</tr>

为什么 key 很重要? Vue 的虚拟 DOM diff 算法依赖 key 来识别每个节点。使用唯一 ID 可以确保节点被正确地复用/移动/删除,避免不必要的 DOM 操作。使用 index 作为 key 在列表增删时可能导致渲染错误。

要点:

  • v-for 的语法:item in list 或 (item, index) in list
  • :key 建议用唯一值(如 id),避免用 index
  • 不在同一元素上同时使用 v-if 和 v-for
  • 遍历对象:(value, key) in obj
  • 范围值:n in 5 从 1 开始

事件处理

Vue 通过 v-on(简写 @)绑定事件处理器,支持丰富的修饰符。

事件处理器类型

<div id="app">
    <!-- 1. 内联事件处理器 — 直接写 JS 代码 -->
    <button @click="console.log('++++'); console.log('+++++');">click</button>

    <!-- 2. 内联调用方法 — 带括号调用(可传参) -->
    <button @click="printSth()">click</button>

    <!-- 3. 方法事件处理器 — 只写方法名(推荐,Vue 自动传入事件对象) -->
    <button @click="printSth">click</button>
</div>
function printSth() {
    console.log("++++++");
}

方法处理器 vs 内联处理器:

  • 方法处理器(@click="fn"):Vue 会自动传入原生事件对象作为参数
  • 内联处理器(@click="fn()"):需要手动传 $event 才能获取事件对象

访问事件对象

<!-- 在内联事件处理器中使用 $event 传递事件对象 -->
<form action="https://www.baidu.com" @submit="test($event)">
    <button type="submit">提交</button>
</form>
function test(event) {
    // 阻止浏览器的默认行为(如表单提交后的页面跳转)
    event.preventDefault();
    console.log("test....");
}

注意:在方法处理器 @click="fn" 中,Vue 自动将事件对象作为第一个参数传入。在 v-on 中,处理函数应尽量保持纯粹,副作用统一管理。

事件修饰符

事件修饰符是 Vue 对原生 DOM 事件的封装,让事件处理的逻辑更清晰。

修饰符 作用 对应原生 JS
.prevent 阻止事件的默认行为 event.preventDefault()
.stop 阻止事件冒泡(阻止向父元素传递) event.stopPropagation()
.self 事件只有在元素自身触发时才执行(子元素触发不执行) event.target === event.currentTarget
.once 事件只触发一次,之后自动移除监听 { once: true }
.capture 使用捕获模式(从外到内) { capture: true }
.passive 不阻止默认行为(与 .prevent 互斥),提升滚动性能 { passive: true }
<!-- prevent 示例 — 阻止表单默认提交 -->
<form @submit.prevent="onSubmit">...</form>

<!-- 修饰符链式调用 -->
<a @click.stop.prevent="doSomething">...</a>

<!-- 多层嵌套演示 self 和 once -->
<div style="width: 300px; height: 300px; background-color: blue;"
     @click.self="console.log('外层 — 只有点蓝色区域才触发')">
    <div style="width: 200px; height: 200px; background-color: yellow;"
         @click.self="console.log('中层 — 只有点黄色区域才触发')">
        <div style="width: 100px; height: 100px; background-color: red;"
             @click.once="console.log('内层 — 只触发一次')">
        </div>
    </div>
</div>

按键修饰符

<!-- 键盘按键修饰符 -->
<input @keyup.enter="handleSubmit" />       <!-- 回车键弹起 -->
<input @keyup.right="handler" />            <!-- 右方向键 -->
<input @keyup.esc="handler" />              <!-- Esc 键 -->
<input @keyup.delete="handler" />           <!-- Delete / Backspace -->

<!-- 系统修饰键 -->
<input @keyup.ctrl.enter="handler" />       <!-- Ctrl + Enter -->
<input @keyup.shift.a="handler" />          <!-- Shift + A -->

<!-- .exact — 精确匹配(不配合其他键) -->
<input @keyup.a.exact="handler" />          <!-- 只按 a 键,不配合 Ctrl/Shift 等 -->

<!-- 数字键 -->
<input @keyup.0="handler" />                <!-- 数字键 0 -->

<!-- 鼠标修饰符 -->
<button @click.left="handler">左键</button>
<button @click.right="handler">右键</button>
<button @click.middle="handler">中键</button>

修饰符可以链式调用:@click.stop.prevent="handler"(先阻止冒泡,再阻止默认行为)。

要点:

  • 方法处理器优于内联处理器(更清晰、可复用)
  • 修饰符让模板中的事件逻辑一目了然
  • .self 场景:当父元素有点击事件,但不想被子元素的点击触发时
  • .prevent 和 .passive 不能同时使用

表单输入绑定(v-model)

v-model 实现表单元素与响应式数据的双向绑定——数据变化更新视图,视图变化更新数据。

各种表单元素的 v-model

const username = ref('');
const hobby = ref(['爬山']);       // 复选框组用数组
const gender = ref('f');           // 单选框用字符串
const city = ref('beijing');       // 下拉选择用字符串
const text = ref('');              // 文本域用字符串
<!-- 文本输入框 -->
<input type="text" v-model="username" />

<!-- 复选框 — 绑定数组时,选中的 value 会自动加入数组 -->
<input type="checkbox" v-model="hobby" value="爬山" />爬山
<input type="checkbox" v-model="hobby" value="游泳" />游泳
<input type="checkbox" v-model="hobby" value="学习" />学习
<!-- 选中结果:hobby = ['爬山', '学习'] -->

<!-- 单选框 — 同一组用同一个 v-model -->
<input type="radio" value="m" v-model="gender" />男
<input type="radio" value="f" v-model="gender" />女

<!-- 下拉选择框 -->
<select v-model="city">
    <option value="beijing">北京市</option>
    <option value="tianjin">天津市</option>
    <option value="shanghai">上海市</option>
    <option value="wuhan">武汉市</option>
</select>

<!-- 文本域 -->
<textarea v-model="text"></textarea>

<!-- 显示当前值 -->
<div>{{ hobby }}</div>
<div>{{ city }}</div>

v-model 的本质

<!-- v-model 本质上等于以下两个指令的组合 -->
<input
    :value="text"
    @input="text = $event.target.value"
/>

<!-- 所以 v-model 是 v-bind:value + v-on:input 的语法糖 -->

v-model` 修饰符

修饰符 作用 示例
.lazy 改为 change 事件后同步(而非 input) v-model.lazy="msg"
.number 自动将输入转为数值类型 v-model.number="age"
.trim 自动过滤首尾空白字符 v-model.trim="name"
<!-- .lazy — 失去焦点时才更新(而非每次按键) -->
<input v-model.lazy="msg" />

<!-- .number — 输入自动转数字(parseFloat 失败则保留原值) -->
<input v-model.number="age" type="number" />

<!-- .trim — 自动去掉首尾空格 -->
<input v-model.trim="name" />

表单数据获取的完整流程

// 1. 定义响应式数据
const student = reactive({
    name: '',
    age: '',
    address: ''
});

// 2. 从服务器获取数据,自动填充到表单
function getStudentInfo() {
    // 模拟 AJAX 请求
    student.name = "zs";
    student.age = "10";
    student.address = "qd";
}

// 3. 提交表单时收集数据
function setStudentInfo() {
    console.log(student);  // { name: 'zs', age: '10', address: 'qd' }
    // 发送 AJAX 请求保存到服务器
}

要点:

  • v-model 是 :value + @input 的语法糖
  • 复选框绑定数组时,选中的 value 会自动加入数组
  • 选中状态由响应式数据决定,checked/selected 属性只在初始渲染时生效,之后以 v-model 为准
  • .number 在输入非数字时保留原始字符串,不会报错

侦听器(watch)

watch 用于监听响应式数据的变化,并执行副作用(如发请求、操作 DOM、写 localStorage 等)。与计算属性不同,watch 不返回值,只执行操作。

基本用法

<template>
    <div class="person">
        <h1>情况一:监视【ref】定义的【基本类型】数据</h1>
        <h2>当前求和为:{{ sum }}</h2>
        <button @click="changeSum">点我sum+1</button>
    </div>
</template>

<script setup name="Person">
    import {ref, watch} from 'vue'
    // 数据
    let sum = ref(0)

    // 方法
    function changeSum() {
        sum.value += 1
    }

    // 监视,情况一:监视【ref】定义的【基本类型】数据
    // watch方法的第一个参数不需要写sum.value,直接写sum
    const stopWatch = watch(sum, (newValue, oldValue) => {
        console.log('sum变化了', newValue, oldValue)
        if (newValue >= 10) {
            stopWatch()
        }
    })
</script>
const { createApp, ref, watch } = Vue;

createApp({
    setup() {
        const username = ref('as');

        // 监听 username 变化
        watch(username, () => {
            console.log('username 变为:', username.value);
        });

        return { username };
    }
}).mount("#app");
<div>
    <input type="text" v-model="username" />
</div>

watch` 的监听多个对象

<template>
    <div class="person">
        <h1>情况二:监视【reactive】定义的【对象类型】数据</h1>
        <h2>姓名:{{ person.name }}</h2>
        <h2>年龄:{{ person.age }}</h2>
        <button @click="changeName">修改名字</button>
        <button @click="changeAge">修改年龄</button>
        <button @click="changePerson">修改整个人</button>
    </div>
</template>

<script setup name="Person">
    import {reactive, watch} from 'vue'
    // 数据
    let person = reactive({
        name: '张三',
        age: 18
    })

    // 方法
    function changeName() {
        person.name += '~'
    }

    function changeAge() {
        person.age += 1
    }

    function changePerson() {
        //reactive重新分配一个新对象,会失去响应式(可以使用Object.assign去整体替换)
        //person = {name: 'lisi', age: 23}
        //person = reactive({name: 'lisi', age: 23})
        Object.assign(person, {name: '李四', age: 80})
    }

    // 监视,情况二:监视【reactive】定义的【对象类型】数据
    //newVal、oldVal 在回调里指向同一个内存对象 打印的值相同
    watch(person, (newValue, oldValue) => {
        console.log('person变化了', newValue, oldValue)
    })

    // vue在watch监听时:在变异 (不是替换) 对象或数组时,旧值将与新值相同,
    // 因为它们的引用指向同一个对象/数组。Vue 不会保留变异之前值的副本。
    //将对象字符串化,指向的值不同
    let newPerson = computed(() => {
       return JSON.stringify(person);
    });
    watch(newPerson, (newValue, oldValue) => {
        console.log('person变化了', newValue, oldValue)
    })
</script>
// 1. 监听单个 ref
watch(username, (newVal, oldVal) => {
    console.log(`从 ${oldVal} 变为 ${newVal}`);
});

// 2. 监听 reactive 对象的某个属性(需要写成 getter 函数)
const student = reactive({ name: 'zs', age: 10 });
watch(
    () => student.name,           // getter 函数 — 返回要监听的属性
    (newVal, oldVal) => {
        console.log(`name 从 ${oldVal} 变为 ${newVal}`);
    }
);

// 3. 同时监听多个数据源
watch(
    [username, () => student.age],  // 数组 — 每个元素是一个数据源
    ([newName, newAge], [oldName, oldAge]) => {
        console.log(`name: ${oldName} → ${newName}`);
        console.log(`age: ${oldAge} → ${newAge}`);
    }
);

// 4. 深度监听整个 reactive 对象
watch(student, (newVal) => {
    console.log('student 内部发生了变化', newVal);
}, { deep: true });

// 5. 立即执行(创建时先执行一次回调)
watch(username, (val) => {
    // 发请求验证用户名
    checkUsernameAvailability(val);
}, { immediate: true });

// 6. 一次性监听(触发一次后自动停止)
watch(username, (val) => {
    console.log('只触发一次');
}, { once: true });

watchEffect` — 更简洁的副作用监听

watchEffect

const { watchEffect } = Vue;

// watchEffect 自动追踪回调中使用的所有响应式数据
watchEffect(() => {
  console.log('count 当前值:', count.value)
})

// 对比 watch:不需要显式声明依赖,代码更简洁
  • watch:需明确指定侦听源,可拿到新旧值;默认懒执行
  • watchEffect:自动收集依赖,立即执行一次
  • watch 深层次对象时需加 { deep: true } 选项

watch vs computed vs watchEffect`

特性 computed watch watchEffect
返回值 返回一个计算后的值 不返回值 不返回值
用途 计算衍生数据 执行副作用 执行副作用
依赖追踪 自动 需显式声明 自动
缓存 ✅ 有 ❌ 无 ❌ 无
立即执行 访问时 默认不执行(可配 immediate) ✅ 立即执行
典型场景 过滤列表、格式化文本 发请求、存 localStorage 调试、日志输出

要点:

  • watch 是懒执行的(默认创建时不执行回调),加 { immediate: true } 可立即执行
  • 监听 reactive 对象的属性时,需要用 getter 函数:() => obj.prop
  • 直接 watch 一个 reactive 对象会隐式开启深度监听(无法关闭)
  • 用 watch 做副作用(异步请求、DOM 操作),用 computed 做衍生数据
  • watchEffect 简单但不够精细(无法获取 oldValue,无法控制监听时机)

生命周期钩子

Vue3 的组合式 API 提供生命周期钩子函数,让你在组件生命周期的特定阶段执行代码。

基本用法

<script setup name="Person">
    import {ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted} from 'vue'

    // 数据
    let sum = ref(0)

    // 方法
    function add() {
        sum.value += 1
    }

    // 创建,不用写beforeCreate、created,对于vue3来说,使用setup替代了
    // <script setup> 中所有的代码都是setup函数,所以想输出创建,直接打印就行
    console.log('创建')

    // 挂载前
    onBeforeMount(() => {
        console.log('挂载前')
    })
    // 挂载完毕
    onMounted(() => {
        console.log('挂载完毕')
    })
    // 更新前
    onBeforeUpdate(() => {
        console.log('更新前')
    })
    // 更新完毕
    onUpdated(() => {
        console.log('更新完毕')
    })
    // 卸载前
    onBeforeUnmount(() => {
        console.log('卸载前')
    })
    // 卸载完毕
    onUnmounted(() => {
        console.log('卸载完毕')
    })
</script>

完整生命周期图

组件创建
    │
    ▼
setup() 执行 ← 组合式 API 入口,此时没有 this
    │
    ▼
onBeforeMount ← 挂载前,DOM 未生成
    │
    ▼
onMounted ← 挂载完成,DOM 已生成 ★ 发送请求的最佳时机
    │
    ▼
onBeforeUpdate ← 数据变化,DOM 更新前
    │
    ▼
onUpdated ← DOM 更新完成
    │
    ▼
onBeforeUnmount ← 卸载前 ★ 清理资源的最佳时机
    │
    ▼
onUnmounted ← 卸载完成

所有生命周期钩子及典型用途

钩子 时机 典型用途
setup() 组件创建时,最先执行 定义数据和方法
onBeforeMount 挂载前 很少使用
onMounted 挂载后 发 AJAX 请求、初始化第三方库、操作 DOM
onBeforeUpdate 数据变化后、DOM 更新前 获取更新前的 DOM 状态
onUpdated DOM 更新后 操作更新后的 DOM
onBeforeUnmount 卸载前 清理定时器、取消网络请求、移除事件监听
onUnmounted 卸载后 最终清理
onActivated KeepAlive 缓存组件激活时 刷新缓存组件的数据
onDeactivated KeepAlive 缓存组件停用时 暂停定时器等
onErrorCaptured 子组件错误被捕获时 错误日志上报

实际应用示例

setup() {
    let timer = null;

    onMounted(() => {
        // 组件挂载后:获取数据、启动轮询
        fetchUserData();
        timer = setInterval(() => {
            refreshData();
        }, 30000);  // 每30秒刷新一次
    });

    onBeforeUnmount(() => {
        // 组件卸载前:清理定时器,防止内存泄漏
        if (timer) {
            clearInterval(timer);
            timer = null;
        }
    });

    return {};
}

⚠️ 内存泄漏警告:如果在组件内创建了定时器、事件监听、WebSocket 连接等,一定要在 onBeforeUnmount 中清理,否则组件销毁后这些资源仍会运行。

要点:

  • onMounted 是最常用的钩子 — 发送 AJAX 请求获取数据
  • onBeforeUnmount 必须清理资源(定时器、订阅、事件监听)
  • 生命周期钩子在 setup() 中同步调用,Vue 自动将它们关联到当前组件实例
  • 生命周期钩子可以在同一组件中多次调用(它们会按顺序执行,不会互相覆盖)

ES6 核心语法速查表

概念 语法 说明
let let x = 10; 块级作用域,可修改
const const PI = 3.14; 块级作用域,不可重赋值,必须初始化
解构赋值 let [a, b] = arr; / let { name } = obj; 从数组/对象中快速提取值
模板字符串 ` hello ${name} ` 反引号 + ${} 插值
对象简写 { name, age } 等价于 { name: name, age: age }
方法简写 fn() {} 等价于 fn: function() {}
扩展运算符 { ...obj } / [...arr] 浅拷贝/合并
剩余运算符 { a, ...rest } = obj 收集剩余属性
箭头函数 x => x * 2 简洁函数,无自己的 this
默认参数 function fn(x = 10) {} 参数默认值

Vue3 核心 API 速查表

API / 指令 写法 说明
createApp() createApp({ setup() {} }).mount('#app') 创建并挂载 Vue 应用
ref() const x = ref(0) 声明响应式变量,JS 中用 .value 访问
reactive() const obj = reactive({}) 声明响应式对象,直接访问属性
computed() const v = computed(() => expr) 计算属性,有缓存
watch() watch(source, callback) 侦听数据变化,执行副作用
v-bind / : :id="id" 属性绑定
v-on / @ @click="fn" 事件绑定
v-model v-model="data" 双向数据绑定
v-if v-if="condition" 条件渲染(DOM 销毁/重建)
v-show v-show="condition" 条件切换(display:none)
v-for v-for="item in list" 列表渲染
v-html v-html="htmlStr" 渲染原始 HTML
onMounted onMounted(() => {}) 挂载后生命周期钩子

常见使用模式速查

// Vue3 组件的标准写法
const { createApp, ref, reactive, computed, watch, onMounted } = Vue;

createApp({
    setup() {
        // 1. 状态 — 响应式数据
        const count = ref(0);
        const user = reactive({ name: '', age: 0 });

        // 2. 计算属性 — 衍生数据
        const double = computed(() => count.value * 2);

        // 3. 方法 — 事件处理
        function increment() { count.value++; }

        // 4. 侦听器 — 副作用
        watch(count, (newVal) => { console.log(newVal); });

        // 5. 生命周期 — 初始化和清理
        onMounted(() => { /* 发请求 */ });

        // 6. 暴露给模板
        return { count, user, double, increment };
    }
}).mount("#app");

常见错误与避免方法

错误 原因 正确做法
模板中 count.value 忘记模板自动解包 ref 模板中用 count,JS 中用 count.value
解构 reactive 丢响应性 reactive 解构后是普通值 使用 toRefs() 或直接用 ref
reactive 重新赋值 proxy = newObj 断开响应链 用 ref 代替,或 Object.assign(proxy, newObj)
v-for 不加 :key 影响 diff 算法效率 始终加 :key="item.id"
v-if 和 v-for 同元素 优先级歧义 用 <template> 分层
忘记清理定时器 内存泄漏 在 onBeforeUnmount 中 clearInterval
v-html 渲染用户输入 XSS 安全风险 只用 {{ }} 或对内容做消毒处理
上一篇
下一篇