[!attention] No Reproduction Without Permission
Made By Hanbin Yi
在 Vue 开发中,直接在方法中手动更新数据易导致逻辑分散、性能低下等问题,而计算属性、监听器(watch)以及组件间通信则是解决这些问题的核心技术。本文围绕 Vue 的计算属性、watch 监听器的使用场景与技巧展开,并结合分页组件实战案例,详细讲解父子组件通过 Props 和 $emit 实现数据传递的逻辑,帮助开发者掌握 Vue 进阶开发的关键知识点,提升代码的可维护性与性能。
Vue 计算属性(computed)
一般计算当中 我们通过这种方式 直接在方法中手动更新 price
可能导致 逻辑分散、代码冗余、易出错、维护成本高,且无缓存机制导致性能较差。

所以 我们使用vue 中 的计算属性
一、计算属性的核心定义
计算属性是 Vue 实例中基于已有响应式数据计算得出的属性,会自动跟踪依赖的响应式数据,当依赖数据变化时,计算属性会重新计算,且具备缓存特性。
二、代码中的计算属性应用分析
1. 基础使用方式
<template>
<!-- 直接像普通数据一样调用计算属性 -->
<p>总价格:{{ price }}</p>
</template>
<script>
export default {
data() {
return {
number: 0 // 依赖的响应式数据
}
},
computed: {
// 定义计算属性price
price() {
// 基于number计算,返回结果
return this.number * 8888;
}
}
}
</script>
price是计算属性,依赖data中的number;- 模板中直接通过
{{ price }}调用,无需加括号。
2. 与事件方法的联动
代码中通过 handAdd() 和 handReduce() 修改 number,触发计算属性更新:
methods: {
handAdd() {
this.number++; // number变化,price自动重新计算
},
handReduce() {
if (this.number > 0) {
this.number--; // number变化,price自动重新计算
}
}
}
- 点击 “+1”“-1” 按钮,
number改变,计算属性price会实时更新并渲染到页面。
三、计算属性的核心特性
1. 依赖响应式数据
- 计算属性的结果必须通过已有响应式数据(如 data 中的数据)计算得来;
- 案例中
price依赖number,无依赖则计算属性无意义。
2. 自动更新
- 当依赖的响应式数据发生变化时,计算属性会自动重新计算结果;
- 无需手动调用,Vue 内部自动监听依赖变化。
3. 缓存机制
- 计算属性会缓存计算结果,若依赖数据未变化,多次访问计算属性会直接返回缓存值,不会重复计算;
- 对比:若用方法实现总价计算,每次渲染模板都会重新执行方法,性能更低。
4. 默认只读
-
案例中的
price是默认的只读计算属性,无法直接赋值(如this.price = 10000会报错); -
若需可读写,需配置
get和set方法:computed: { price: { // 获取计算属性值 get() { return this.number * 8888; }, // 设置计算属性值(触发时修改依赖数据) set(value) { this.number = value / 8888; } } }此时可通过
this.price = 17776间接修改number的值。
四、计算属性 vs 方法(methods)
| 特性 | 计算属性 (computed) | 方法 (methods) |
|---|---|---|
| 调用方式 | 像属性一样直接用({{price}}) | 像函数一样调用({{getPrice()}}) |
| 缓存 | 有,依赖不变则复用结果 | 无,每次调用都重新执行 |
| 响应式更新 | 依赖变化自动更新 | 需手动调用才会更新结果 |
| 使用场景 | 数据处理、转换(依赖其他数据) | 处理业务逻辑、事件处理 |
五、计算属性的优势
- 简化模板:避免在模板中写入复杂计算逻辑(如
{{number * 8888}}),让模板更简洁; - 逻辑复用:多个地方需要使用同一计算结果时,只需定义一次计算属性;
- 性能优化:缓存机制减少不必要的重复计算,提升页面性能。
Vue 监视属性(Watch)
1. 什么是监视属性?
- 作用:监听数据变化,在数据变化时执行回调函数
- 场景:数据变化时需要做额外操作(如日志记录、异步请求、数据过滤等)
- 语法:
watch是一个对象,键是要监听的数据,值是回调函数
二、watch 的基本用法
watch: {
// 1. 要监视的目标:计算属性 price
price(newVal, oldVal) {
// 2. 当 price 变化时,执行的回调函数
console.log('新的总价:', newVal);
console.log('旧的总价:', oldVal);
// 3. 这里是具体的业务逻辑(副作用)
if (newVal >= 50000) {
this.postAge = 0; // 满50000免邮
} else {
this.postAge = 18; // 否则邮费18元
}
}
}
代码执行流程分解:
- 你点击
+1按钮。 methods中的handAdd()方法被调用,this.number增加 1。- 因为
computed属性price依赖于number,所以price会自动重新计算。 watch一直在 “盯着”price,它发现price的值变了。- 于是,
watch中定义的回调函数被触发,并传入两个参数:newVal:变化后的新值(例如8888)。oldVal:变化前的旧值(例如0)。
- 在回调函数内部,根据
newVal(新的总价)来更新this.postAge(邮费)。 - 因为
postAge是响应式数据,所以页面上显示的邮费会自动更新。
[!note]
这里是监听了computed属性 也可以监视其他的
三、watch 的高级用法
1. 深度监听 (deep: true)
当你要监视的目标是一个对象或数组时,watch 默认只会监视它的引用地址是否变化。如果你想监视对象内部属性的变化,就需要开启深度监听。
export default {
data() {
return {
user: {
name: '张三',
age: 25
}
};
},
watch: {
user: {
handler(newVal, oldVal) {
// 注意:对于对象,newVal 和 oldVal 是同一个引用,除非你替换了整个对象
console.log('用户信息发生了变化');
console.log('新名字:', newVal.name);
},
deep: true // 开启深度监听
}
},
methods: {
changeName() {
// 这样修改对象内部属性,deep watch 会触发
this.user.name = '李四';
},
replaceUser() {
// 这样替换整个对象,即使没有 deep: true 也会触发
this.user = { name: '王五', age: 30 };
}
}
}
注意:深度监听会消耗更多性能,因为它需要遍历对象的所有属性。只在确实需要时使用。
2. 立即执行 (immediate: true)
默认情况下,watch 只有在数据第一次变化时才会触发。如果你希望在组件初始化时就让 watch 执行一次,可以使用 immediate: true。
export default {
data() {
return {
number: 0
};
},
watch: {
number: {
handler(newVal) {
console.log('当前数字是:', newVal);
},
immediate: true // 组件一加载就立即执行一次回调函数
}
}
}
// 输出: 当前数字是: 0 (组件初始化时)
3. 使用字符串方法名
如果回调逻辑比较复杂,你可以把它定义在 methods 里,然后在 watch 中引用方法名。
export default {
data() {
return {
number: 0
};
},
methods: {
handleNumberChange(newVal, oldVal) {
console.log(`数字从 ${oldVal} 变成了 ${newVal}`);
// ... 其他复杂逻辑
}
},
watch: {
// 直接写方法名的字符串形式
number: 'handleNumberChange'
}
}
四、总结
watch 的核心就是 “观察变化,执行副作用”。
- 核心用途:数据变化时,做一件事(通常是有副作用的事,如发请求、操作 DOM 等)。
- 基本写法:
watch: { 数据: 回调函数 }。 - 常用技巧:
- 用
deep: true监听对象内部属性的变化。 - 用
immediate: true在组件初始化时立即执行回调。 - 回调函数可以接收
newVal(新值)和oldVal(旧值)两个参数。 - 复杂逻辑可以封装到
methods中,通过方法名字符串调用。
- 用
补充handle 讲解

1. 完整写法 (使用 handler)
当你需要为一个监听器添加额外的配置项(如 deep: true 或 immediate: true)时,你必须使用对象语法。在这个对象中,handler 属性是必填的,它定义了数据变化时要执行的回调函数。
watch: {
price: {
// 这个函数就是 handler
handler(newVal, oldVal) {
// 业务逻辑...
if (newVal >= 50000) {
this.postAge = 0;
} else {
this.postAge = 18;
}
},
// 其他配置项
immediate: true
}
}
2. 简写写法 (直接使用函数)
当你不需要任何额外配置,只需要在数据变化时执行一个函数时,Vue 允许你使用更简洁的语法。你可以直接将这个函数赋值给要监听的属性名。
watch: {
// 这里的整个函数,会被 Vue 自动当作 handler 来处理
price(newVal, oldVal) {
// 业务逻辑...
if (newVal >= 50000) {
this.postAge = 0;
} else {
this.postAge = 18;
}
}
}
总结
这两种写法在功能上是完全等价的。
- 使用
handler:是完整、通用的写法,适用于所有场景,尤其是当你需要deep或immediate等高级功能时。 - 不使用
handler:是一种语法糖或简写,让代码更简洁。它只能用于不需要任何额外配置的简单监听器。
组件间的通信
### 子组件向父组件通信($emit 方式)
核心逻辑:子组件通过 $emit 触发自定义事件并传递数据,父组件监听该事件以接收数据。
用法:
-
子组件通过
this.$emit('事件名', 数据)触发一个自定义事件。 -
父组件在使用子组件时,通过
@事件名自定义 事件,并在事件处理函数中接收数据。
步骤 1:子组件中定义并触发事件
<template>
<!-- 点击按钮触发发送数据的方法 -->
<button @click="sendMsg">点击发送数据到父组件</button>
</template>
<script>
export default {
data() {
return {
msg: '我是子组件的数据' // 子组件要传递的数据
}
},
methods: {
sendMsg() {
// 触发自定义事件(事件名:'omo2511',传递的数据:this.msg)
this.$emit('omo2511', this.msg)
}
}
}
</script>
步骤 2:父组件中监听事件并接收数据
<template>
<!-- 1. 引入子组件,并监听子组件触发的自定义事件 -->
<LessonComponent @omo2511="omoHandler" />
<!-- 展示接收到的子组件数据 -->
<div>{{ msg }}</div>
</template>
<script>
import LessonComponent from './components/LessonComponent.vue' // 引入子组件
export default {
components: {
LessonComponent // 注册子组件
},
data() {
return {
msg: '父组件 等待接收子组件数据中'
}
},
methods: {
// 2. 事件处理函数:接收子组件传递的数据
omoHandler(data) {
this.msg = data // 将子组件数据赋值给父组件的msg
}
}
}
</script>

流程总结
- 子组件通过
this.$emit('事件名', 数据)触发自定义事件,携带要传递的数据; - 父组件在使用子组件时,通过
@事件名="处理函数"监听该事件; - 父组件的处理函数接收子组件传递的数据,完成通信。
父组件向子组件通信(Props 方式)
核心逻辑:父组件通过在子组件标签上绑定属性(Props)的方式传递数据,子组件通过 props 选项声明接收这些数据。
场景:父组件通过 :属性名 传递数据,子组件用数组形式的 props 接收
步骤 1:父组件传递数据
<template>
<div>
<!-- 1. 子组件标签上通过 `:Message` 绑定父组件的 `msg` 数据 -->
<lesson2-component :Message="msg"></lesson2-component>
</div>
</template>
<script>
import Lesson2Component from './components/Lesson2Component.vue';
export default {
components: {
Lesson2Component // 注册子组件
},
data() {
return {
msg: '这是父组件传给子组件的' // 父组件要传递的数据
}
}
}
</script>
步骤 2:子组件接收并使用数据
<template>
<div>
这是 Lesson2Component 组件
<!-- 3. 模板中直接使用 props 接收的数据 -->
<div>准备接收父组件数据: {{ msg }}</div>
</div>
</template>
<script>
export default {
// 2. 用数组形式的 props 声明要接收的属性(名称需和父组件绑定的一致)
props: ['Message'],
data() {
return {
// 4. 在 data 中可通过 this.Message 访问 props 数据
msg: this.Message
}
}
}
</script>
名称匹配:子组件 props 数组中的名称(如 'Message'),必须和父组件绑定的属性名(如 :Message)完全一致。
流程总结
-
父组件:
- 在
<template>中,在子组件标签上使用:属性名="父组件数据"的语法传递数据。 - 确保已引入并注册了该子组件。
- 在
-
子组件:
- 在
<script>的export default中,通过props对象声明要接收的属性,可以进行类型检查、必填校验等。 - 在子组件的
<template>或<script>中,通过{{ 属性名 }}或this.属性名来访问和使用这些从父组件传递过来的数据。
- 在
重要原则:Props 是单向绑定的。子组件不应该直接修改 props 的值。如果子组件需要修改这些数据并同步给父组件,应该通过 子组件向父组件通信($emit 方式) 来通知父组件进行修改。
案例:自定义组件
分页组件 PageComponent.vue
<template>
<div>
<!-- 分页容器 -->
<ul class="pagination">
<!-- 上一页按钮:点击触发页码减少逻辑 -->
<li @click="addHandle()"><a href="#">«</a></li>
<!-- 页码列表:遍历总页数生成页码 -->
<li>
<a
href="#"
v-for="item in totalPages" <!-- 遍历计算出的总页数 -->
:key="item" <!-- 唯一标识,优化渲染 -->
:class="{active: pageData.currentPage == item}" <!-- 当前页高亮 -->
@click="changePage(item)" <!-- 点击切换到对应页码 -->
>
{{ item }}
</a>
</li>
<!-- 下一页按钮:点击触发页码增加逻辑 -->
<li @click="reduceHandle()"><a href="#">»</a></li>
</ul>
</div>
</template>
<script>
export default {
name: "PageComponent", // 组件名称
data() {
return {
// 接收父组件传入的分页数据(此处直接赋值,实际可简化为直接使用props)
pageData: this.pageData,
};
},
// 接收父组件传递的分页配置
props: ["pageData"],
// 计算属性:动态计算总页数
computed: {
totalPages() {
// 总页数 = 向上取整(总条数 / 每页条数),确保最后一页数据不遗漏
return Math.ceil(this.pageData.total / this.pageData.pageSize);
},
},
// 事件处理方法
methods: {
// 切换到指定页码
changePage(page) {
// 直接修改当前页(因props是引用类型,父组件数据会同步更新)
this.pageData.currentPage = page;
},
// 上一页:当前页大于1时,页码减1
addHandle() {
if (this.pageData.currentPage > 1) {
this.pageData.currentPage--;
}
},
// 下一页:当前页小于总页数时,页码加1
reduceHandle() {
if (this.pageData.currentPage < this.totalPages) {
this.pageData.currentPage++;
}
},
},
// 监听当前页变化,触发事件通知父组件
watch: {
"pageData.currentPage": {
handler() {
// 触发change事件,将新页码传递给父组件
this.$emit("change", this.pageData.currentPage);
},
},
},
};
</script>
<style>
/* 分页样式重置与基础布局 */
ul.pagination {
display: inline-block;
padding: 0;
margin: 0;
}
ul.pagination li {
display: inline; /* 横向排列 */
}
/* 页码链接样式 */
ul.pagination li a {
color: black;
float: left;
padding: 8px 16px;
text-decoration: none;
border-radius: 5px; /* 圆角美化 */
}
/* 当前页高亮样式 */
ul.pagination li a.active {
background-color: #4CAF50; /* 绿色主题 */
color: white;
border-radius: 5px;
}
/* 非当前页 hover 效果 */
ul.pagination li a:hover:not(.active) {
background-color: #ddd; /* 浅灰色背景 */
}
</style>
父组件 App.vue
<template>
<div>
<!-- 引入分页组件 -->
<PageComponent
:pageData="pageData" <!-- 传递分页配置数据 -->
@change="changeCurrentPage" <!-- 监听分页组件触发的change事件 -->
/>
</div>
</template>
<script>
// 导入分页组件
import PageComponent from './components/PageComponent.vue'
export default {
name: 'App',
components: {
PageComponent // 注册分页组件
},
data() {
return {
// 分页配置数据(父组件维护核心状态)
pageData: {
currentPage: 1, // 当前页码,默认1
total: 23, // 总数据条数
pageSize: 4, // 每页显示条数
}
}
},
methods: {
// 接收分页组件传递的新页码
changeCurrentPage(page) {
console.log("当前页修改成功" + page);
// (可选)父组件同步更新页码(因子组件已修改引用类型数据,此处可省略,但保留更清晰)
this.pageData.currentPage = page;
// 此处可添加实际业务逻辑:如根据新页码请求后端数据
// 示例:this.fetchData(page);
}
}
}
</script>
<style>
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
}
</style>
核心设计思路总结
(1)组件职责拆分
-
子组件(PageComponent.vue):负责分页 UI 渲染和基础交互逻辑
- 接收父组件传递的分页配置(总条数、每页条数、当前页)
- 动态计算总页数(通过 computed 属性)
- 实现页码切换、上一页 / 下一页点击逻辑
- 监听当前页变化,通过 $emit 触发事件通知父组件
-
父组件(App.vue):负责数据管理和业务逻辑
- 维护分页核心状态(pageData)
- 向子组件传递分页配置
- 监听子组件的 change 事件,接收新页码并执行后续业务(如请求数据)
(2)数据流转逻辑
- 父组件初始化分页数据(currentPage:1, total:23, pageSize:4)
- 子组件通过 props 接收数据,计算总页数(23/4=5.75 → 向上取整为 6 页)
- 用户点击页码 / 上一页 / 下一页:
- 子组件修改
pageData.currentPage(因是引用类型,父组件数据同步更新) - 子组件 watch 监听 currentPage 变化,触发 $emit (‘change’, 新页码)
- 子组件修改
- 父组件监听 change 事件,接收新页码,执行业务逻辑(如请求第 N 页数据)
(3)关键技术点
- Props 传递数据:父→子单向数据流(此处因是 Object 引用类型,子组件修改会影响父组件,需注意数据一致性)
- Computed 计算总页数:动态响应 total 和 pageSize 变化,自动更新总页数
- Watch 监听变化:捕捉当前页变化,通过事件通知父组件
- 事件驱动通信:子组件通过 $emit 触发事件,父组件通过 @change 监听,实现跨组件通信
292

被折叠的 条评论
为什么被折叠?



