Vue 进阶:计算属性、监听器与组件通信实战解析

[!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 会报错);

  • 若需可读写,需配置 getset 方法:

    computed: {
      price: {
        // 获取计算属性值
        get() {
          return this.number * 8888;
        },
        // 设置计算属性值(触发时修改依赖数据)
        set(value) {
          this.number = value / 8888;
        }
      }
    }
    

    此时可通过 this.price = 17776 间接修改 number 的值。

四、计算属性 vs 方法(methods)

特性计算属性 (computed)方法 (methods)
调用方式像属性一样直接用({{price}}像函数一样调用({{getPrice()}}
缓存有,依赖不变则复用结果无,每次调用都重新执行
响应式更新依赖变化自动更新需手动调用才会更新结果
使用场景数据处理、转换(依赖其他数据)处理业务逻辑、事件处理

五、计算属性的优势

  1. 简化模板:避免在模板中写入复杂计算逻辑(如 {{number * 8888}}),让模板更简洁;
  2. 逻辑复用:多个地方需要使用同一计算结果时,只需定义一次计算属性;
  3. 性能优化:缓存机制减少不必要的重复计算,提升页面性能。

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. 你点击 +1 按钮。
  2. methods 中的 handAdd() 方法被调用,this.number 增加 1。
  3. 因为 computed 属性 price 依赖于 number,所以 price 会自动重新计算。
  4. watch 一直在 “盯着”price,它发现 price 的值变了。
  5. 于是,watch 中定义的回调函数被触发,并传入两个参数:
    • newVal:变化后的新值(例如 8888)。
    • oldVal:变化前的旧值(例如 0)。
  6. 在回调函数内部,根据 newVal(新的总价)来更新 this.postAge(邮费)。
  7. 因为 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: trueimmediate: 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:是完整、通用的写法,适用于所有场景,尤其是当你需要 deepimmediate 等高级功能时。
  • 不使用 handler:是一种语法糖简写,让代码更简洁。它只能用于不需要任何额外配置的简单监听器。

组件间的通信

### 子组件向父组件通信($emit 方式)

核心逻辑:子组件通过 $emit 触发自定义事件并传递数据,父组件监听该事件以接收数据。

用法:

  1. 子组件通过 this.$emit('事件名', 数据) 触发一个自定义事件。

  2. 父组件在使用子组件时,通过 @事件名自定义 事件,并在事件处理函数中接收数据。

步骤 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>

在这里插入图片描述

流程总结
  1. 子组件通过 this.$emit('事件名', 数据) 触发自定义事件,携带要传递的数据;
  2. 父组件在使用子组件时,通过 @事件名="处理函数" 监听该事件;
  3. 父组件的处理函数接收子组件传递的数据,完成通信。

父组件向子组件通信(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)完全一致。

流程总结
  1. 父组件

    • <template> 中,在子组件标签上使用 :属性名="父组件数据" 的语法传递数据。
    • 确保已引入并注册了该子组件。
  2. 子组件

    • <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)数据流转逻辑
  1. 父组件初始化分页数据(currentPage:1, total:23, pageSize:4)
  2. 子组件通过 props 接收数据,计算总页数(23/4=5.75 → 向上取整为 6 页)
  3. 用户点击页码 / 上一页 / 下一页:
    • 子组件修改 pageData.currentPage(因是引用类型,父组件数据同步更新)
    • 子组件 watch 监听 currentPage 变化,触发 $emit (‘change’, 新页码)
  4. 父组件监听 change 事件,接收新页码,执行业务逻辑(如请求第 N 页数据)
(3)关键技术点
  • Props 传递数据:父→子单向数据流(此处因是 Object 引用类型,子组件修改会影响父组件,需注意数据一致性)
  • Computed 计算总页数:动态响应 total 和 pageSize 变化,自动更新总页数
  • Watch 监听变化:捕捉当前页变化,通过事件通知父组件
  • 事件驱动通信:子组件通过 $emit 触发事件,父组件通过 @change 监听,实现跨组件通信
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值