Vue计算属性详解+模板引用与生命周期:解锁响应式与DOM操作全技巧
在Vue响应式开发中,我们经常需要根据已有响应式数据,派生出新的状态——比如根据用户输入过滤列表、根据多个变量拼接名称、根据状态切换显示内容等。如果直接在模板中编写复杂逻辑,会让模板臃肿杂乱、难以维护;如果重复编写相同逻辑,又会导致代码冗余、性能损耗。Vue提供的computed(计算属性),正是为解决这些问题而生,它让响应式逻辑的编写更简洁、高效、可维护。今天就从计算属性的基础用法、核心特性、实战场景到避坑指南,再延伸到模板引用与生命周期钩子,全方位解锁Vue响应式开发与手动DOM操作的核心技巧,帮你彻底掌握这些高频开发工具。
为什么需要计算属性?模板逻辑的“减负神器”
在Vue模板中,我们可以直接编写简单的表达式,比如拼接字符串、判断布尔值、简单运算等,这能满足基础的展示需求。但当逻辑变得复杂时,模板表达式的弊端就会凸显出来。
痛点示例:臃肿的模板逻辑
假设我们有一个用户信息组件,需要根据用户的姓名、性别、年龄,动态显示一段描述文本,同时判断用户是否为成年,还要拼接用户的完整身份信息。如果直接在模板中编写逻辑,代码会是这样:
<script setup>
import { ref } from 'vue';
// 响应式用户数据
const user = ref({
firstName: '张',
lastName: '三',
gender: '男',
age: 22
});
</script>
<template>
<div class="user-info">
<!-- 模板中嵌套复杂逻辑,可读性差、难以维护 -->
<p>用户描述:{{ user.firstName + user.lastName }},{{ user.gender === '男' ? '先生' : '女士' }},{{ user.age >= 18 ? '已成年' : '未成年' }}</p>
<p>完整身份:{{ user.gender === '男' ? 'Mr.' : 'Ms.' }} {{ user.firstName + user.lastName }} ({{ user.age }}岁)</p>
</div>
</template>
这段代码虽然能实现需求,但存在两个明显问题:一是模板逻辑过于复杂,可读性差,后续修改时需要反复梳理表达式;二是存在重复逻辑(比如拼接姓名、判断性别),如果多处需要使用,会导致代码冗余,且修改时需要多处同步修改,容易出错。
解决方案:用计算属性简化逻辑
计算属性的核心作用,就是将复杂的派生逻辑抽离出来,单独定义,让模板只负责展示,逻辑负责计算。上面的示例,用计算属性重构后,代码会变得简洁清晰:
<script setup>
import { ref, computed } from 'vue';
const user = ref({
firstName: '张',
lastName: '三',
gender: '男',
age: 22
});
// 计算属性:拼接完整姓名
const fullName = computed(() => {
return user.value.firstName + user.value.lastName;
});
// 计算属性:判断用户称谓
const honorific = computed(() => {
return user.value.gender === '男' ? '先生' : '女士';
});
// 计算属性:判断是否成年
const isAdult = computed(() => {
return user.value.age >= 18;
});
// 计算属性:完整用户描述
const userDesc = computed(() => {
return `${fullName.value},${honorific.value},${isAdult.value ? '已成年' : '未成年'}`;
});
// 计算属性:完整身份信息
const userIdentity = computed(() => {
const title = user.value.gender === '男' ? 'Mr.' : 'Ms.';
return `${title} ${fullName.value} (${user.value.age}岁)`;
});
</script>
<template>
<div class="user-info">
<!-- 模板只负责展示,逻辑清晰易懂 -->
<p>用户描述:{{ userDesc }}</p>
<p>完整身份:{{ userIdentity }}</p>
</div>
</template>
重构后的代码,将所有派生逻辑抽离为独立的计算属性,模板变得简洁干净,且每个计算属性职责单一,后续修改时只需修改对应计算属性,无需改动模板,可维护性大幅提升。这就是计算属性的核心价值:分离逻辑与展示,简化模板,减少冗余。
计算属性基础用法:从定义到使用
Vue3中,计算属性通过computed()函数定义,该函数接收一个getter函数(核心逻辑),返回一个计算属性ref。和普通ref一样,我们可以通过.value访问其值,在模板中会自动解包,无需手动添加.value。
1. 基础定义:只读计算属性
最常用的计算属性是“只读”的,即只定义getter函数,用于根据依赖的响应式数据派生新值。其基本语法如下:
<script setup>
import { ref, computed } from 'vue';
// 源响应式数据
const num1 = ref(10);
const num2 = ref(20);
// 计算属性:派生新值(只读)
const sum = computed(() => {
// getter函数:逻辑计算,返回派生值
return num1.value + num2.value;
});
</script>
<template>
<div>
<p>num1: {{ num1 }}</p>
<p>num2: {{ num2 }}</p>
<p>两数之和:{{ sum }}</p> <!-- 模板中自动解包,无需.sum.value -->
</div>
</template>
关键点说明:
- 计算属性会自动追踪其依赖的响应式数据(如上例中的num1、num2),当依赖发生变化时,计算属性会自动重新计算;
- 计算属性的值会被缓存,只要依赖没有变化,多次访问计算属性,都会直接返回缓存的结果,不会重复执行getter函数;
- 只读计算属性不能直接修改,若尝试修改(如
sum.value = 50),会触发运行时警告。
2. 实战场景1:列表过滤(结合v-for)
在列表渲染中,过滤功能是高频需求,结合计算属性使用,能让过滤逻辑更清晰,且无需手动触发过滤方法。比如我们有一个商品列表,需要根据搜索关键词过滤商品:
<script setup>
import { ref, computed } from 'vue';
// 源响应式数据:商品列表、搜索关键词
const goods = ref([
{ id: 1, name: 'Vue实战教程', price: 99 },
{ id: 2, name: 'JavaScript进阶指南', price: 89 },
{ id: 3, name: '前端面试题库', price: 69 },
{ id: 4, name: 'Vue3组件封装实战', price: 129 }
]);
const searchKey = ref('');
// 计算属性:过滤商品列表
const filteredGoods = computed(() => {
// 转换为小写,实现不区分大小写过滤
const key = searchKey.value.toLowerCase();
// 根据关键词过滤商品
return goods.value.filter(good => {
return good.name.toLowerCase().includes(key);
});
});
</script>
<template>
<div class="goods-list">
<input
type="text"
v-model="searchKey"
placeholder="请输入商品名称搜索"
/>
<ul>
<!-- 渲染过滤后的列表 -->
<li v-for="good in filteredGoods" :key="good.id" class="goods-item">
商品名称:{{ good.name }} | 价格:¥{{ good.price }}
</li>
</ul>
</div>
</template>
<style>
.goods-list {
margin: 20px;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
}
.goods-item {
margin: 8px 0;
list-style: none;
padding: 6px;
background: #f8f9fa;
}
input {
padding: 6px;
width: 300px;
margin-bottom: 10px;
}
</style>
这个示例中,搜索关键词searchKey变化时,filteredGoods会自动重新计算,过滤出包含关键词的商品,无需手动调用过滤方法,实现了“数据驱动视图”的响应式效果。
3. 实战场景2:多依赖派生状态
计算属性可以依赖多个响应式数据,甚至可以依赖其他计算属性,实现更复杂的派生逻辑。比如一个购物车组件,需要根据商品数量和单价,计算商品小计、购物车总价,还要判断是否满足满减条件:
<script setup>
import { ref, computed } from 'vue';
// 源响应式数据:商品信息、数量
const product = ref({
name: 'Vue实战教程',
price: 99,
discount: 0.9 // 折扣
});
const quantity = ref(2);
const fullReduction = ref(200); // 满200减50
// 计算属性:商品小计(单价×数量×折扣)
const subTotal = computed(() => {
return (product.value.price * quantity.value * product.value.discount).toFixed(2);
});
// 计算属性:购物车总价(此处简化为单个商品小计)
const totalPrice = computed(() => {
return parseFloat(subTotal.value); // 依赖其他计算属性
});
// 计算属性:是否满足满减条件
const isFullReduction = computed(() => {
return totalPrice.value >= fullReduction.value; // 依赖其他计算属性
});
// 计算属性:最终支付金额
const finalPrice = computed(() => {
return isFullReduction.value ? (totalPrice.value - 50).toFixed(2) : totalPrice.value.toFixed(2);
});
</script>
<template>
<div class="cart-item">
<h3>{{ product.name }}</h3>
<p>单价:¥{{ product.price }}({{ product.discount * 10 }}折)</p>
<p>数量:<button @click="quantity--" :disabled="quantity <= 1">-</button> {{ quantity }} <button @click="quantity++">+</button></p>
<p>小计:¥{{ subTotal }}</p>
<p>总价:¥{{ totalPrice }}</p>
<p v-if="isFullReduction" style="color: #e53e3e;">满足满{{ fullReduction }}减50!</p>
<p>最终支付:¥{{ finalPrice }}</p>
</div>
</template>
这个示例中,finalPrice依赖isFullReduction,isFullReduction依赖totalPrice,totalPrice依赖subTotal,形成了依赖链。只要其中任何一个源数据(如quantity、product.price)发生变化,所有相关的计算属性都会自动重新计算,且每个计算属性都会缓存自己的结果,避免重复计算。
核心特性:计算属性缓存 vs 方法
很多初学者会有一个疑问:既然计算属性能实现的逻辑,用方法也能实现,为什么还要用计算属性?比如上面的求和逻辑,用方法编写如下:
<script setup>
import { ref } from 'vue';
const num1 = ref(10);
const num2 = ref(20);
// 方法:计算两数之和
function getSum() {
return num1.value + num2.value;
}
</script>
<template>
<p>两数之和:{{ getSum() }}</p>
</template>
从表面上看,计算属性和方法的效果完全一致,但二者的核心区别在于缓存机制,这也是计算属性的性能优势所在。
1. 计算属性缓存:依赖不变,结果不变
计算属性会缓存其计算结果,只有当它依赖的响应式数据发生变化时,才会重新执行getter函数,重新计算结果。如果依赖没有变化,无论多少次访问计算属性,都会直接返回缓存的结果,不会重复执行逻辑。
举个例子,假设我们有一个耗时的计算逻辑(比如循环一个巨大的数组):
<script setup>
import { ref, computed } from 'vue';
const list = ref(new Array(100000).fill(1)); // 模拟巨大数组
// 计算属性:耗时计算(求和)
const listSum = computed(() => {
console.log('计算属性重新计算'); // 仅在list变化时打印
return list.value.reduce((total, item) => total + item, 0);
});
// 方法:耗时计算(求和)
function getListSum() {
console.log('方法重新执行'); // 每次调用都会打印
return list.value.reduce((total, item) => total + item, 0);
}
</script>
<template>
<!-- 多次访问计算属性,仅第一次(或list变化时)执行逻辑 -->
<p>计算属性求和:{{ listSum }}</p>
<p>计算属性求和:{{ listSum }}</p>
<p>计算属性求和:{{ listSum }}</p>
<!-- 多次调用方法,每次都执行逻辑 -->
<p>方法求和:{{ getListSum() }}</p>
<p>方法求和:{{ getListSum() }}</p>
<p>方法求和:{{ getListSum() }}</p>
</template>
运行这段代码会发现:计算属性的console.log只打印一次(或list变化时打印),而方法的console.log会打印三次。这就是缓存的作用——避免重复执行耗时逻辑,提升页面性能。
2. 方法:无缓存,每次调用都执行
方法没有缓存机制,每次调用方法,都会重新执行方法内部的逻辑。即使方法依赖的响应式数据没有变化,只要调用方法,就会重复计算。
3. 如何选择:计算属性 vs 方法
根据二者的特性,我们可以明确选择场景:
- 使用计算属性:当逻辑依赖响应式数据,且需要多次访问结果,且逻辑耗时(如过滤、排序、复杂运算)时,优先使用计算属性,利用缓存提升性能;
- 使用方法:当逻辑不依赖响应式数据(如生成随机数、格式化时间),或需要每次调用都重新执行逻辑(如点击按钮触发的计算)时,使用方法。
注意:如果计算属性的逻辑非常简单(如简单的加减乘除),缓存的性能优势不明显,此时使用计算属性或方法均可,主要看代码可读性。
进阶用法:可写计算属性
默认情况下,计算属性是只读的,只能通过修改其依赖的响应式数据来改变计算结果,不能直接修改计算属性的值。但在某些特殊场景中,我们需要“反向修改”计算属性,进而修改其依赖的源数据,这时就可以使用“可写计算属性”——通过同时提供getter和setter函数来实现。
1. 可写计算属性的定义
可写计算属性的语法的是,给computed()函数传递一个对象,该对象包含get( getter函数,用于计算派生值)和set(setter函数,用于反向修改源数据)两个方法:
<script setup>
import { ref, computed } from 'vue';
// 源响应式数据: firstName 和 lastName
const firstName = ref('张');
const lastName = ref('三');
// 可写计算属性:fullName
const fullName = computed({
// getter:根据firstName和lastName,计算完整姓名
get() {
return firstName.value + lastName.value;
},
// setter:修改fullName时,反向修改firstName和lastName
set(newValue) {
// 假设 newValue 是 "李四",拆分出 firstName 和 lastName
const [newFirst, newLast] = newValue.split('');
firstName.value = newFirst;
lastName.value = newLast;
}
});
</script>
<template>
<div>
<p>完整姓名:{{ fullName }}</p>
<button @click="fullName = '李四'">修改为李四</button>
<p> firstName:{{ firstName }} | lastName:{{ lastName }}</p>
</div>
</template>
运行这段代码,点击按钮后,fullName被修改为“李四”,setter函数会被触发,将“李四”拆分为firstName(李)和lastName(四),进而修改源数据。此时,getter函数会重新计算,fullName的值也会同步更新。
2. 实战场景:表单双向绑定
可写计算属性常用于表单双向绑定场景,比如将一个输入框的值,同步到多个响应式数据中。例如,一个地址输入框,输入“省-市-区”格式的地址,自动拆分到province、city、district三个变量中:
<script setup>
import { ref, computed } from 'vue';
// 源响应式数据:省、市、区
const province = ref('');
const city = ref('');
const district = ref('');
// 可写计算属性:完整地址
const fullAddress = computed({
get() {
// 拼接完整地址
return [province.value, city.value, district.value].filter(Boolean).join('-');
},
set(newValue) {
// 拆分地址,反向修改源数据
const [prov, cit, dist] = newValue.split('-');
province.value = prov || '';
city.value = cit || '';
district.value = dist || '';
}
});
</script>
<template>
<div class="address-form">
<input
type="text"
v-model="fullAddress"
placeholder="请输入地址(格式:省-市-区)"
/>
<div class="address-detail">
<p>省:{{ province }}</p>
<p>市:{{ city }}</p>
<p>区:{{ district }}</p>
</div>
</template>
<style>
.address-form {
margin: 20px;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
}
.address-detail {
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
}
input {
padding: 6px;
width: 300px;
}
</style>
这个示例中,输入“北京市-海淀区-中关村”后,fullAddress的setter函数会拆分地址,将省、市、区分别赋值给province、city、district;反之,修改province、city、district中的任何一个,fullAddress也会自动拼接更新,实现了双向同步。
高级特性:获取计算属性的上一个值(Vue3.4+)
Vue3.4及以上版本,支持在计算属性的getter函数中,通过第一个参数获取上一次计算的结果(previous),这在需要“保留上一个有效状态”的场景中非常实用。比如,我们需要一个计算属性,当count的值小于等于5时,返回count的值;当count大于5时,保留上一次小于等于5的值:
<script setup>
import { ref, computed } from 'vue';
const count = ref(3);
// 计算属性:保留上一个有效值
const validCount = computed((previous) => {
// previous 是上一次计算的结果(初始值为undefined)
if (count.value <= 5) {
return count.value; // 小于等于5,返回当前值
}
return previous; // 大于5,返回上一次的值
});
</script>
<template>
<div>
<p>当前count:{{ count }}</p>
<p>有效count:{{ validCount }}</p>
<button @click="count++">count+1</button>
</div>
</template>
运行这段代码:
- 初始时count=3,validCount=3;
- 点击按钮,count依次变为4、5,validCount同步变为4、5;
- count变为6时,validCount仍为5(上一次的值);
- 继续点击,count变为7、8,validCount始终保持5。
这个特性也适用于可写计算属性,只需在getter函数中添加previous参数即可。
避坑指南:计算属性的常见错误与最佳实践
虽然计算属性用法简单,但在实际开发中,很多初学者会因为忽略其特性而踩坑。下面总结几个常见错误和最佳实践,帮你避开陷阱。
1. 错误1:getter函数有副作用
计算属性的getter函数应该只做“计算”,不应该有任何副作用——比如修改其他响应式数据、发送异步请求、操作DOM等。getter函数的职责是“根据依赖派生值”,副作用会导致代码逻辑混乱,且可能引发不可预期的问题。
<!-- 错误示例:getter函数有副作用(修改其他数据) -->
<script setup>
import { ref, computed } from 'vue';
const num = ref(10);
const doubleNum = ref(0);
// 错误:getter中修改doubleNum,产生副作用
const sum = computed(() => {
doubleNum.value = num.value * 2; // 副作用:修改了其他响应式数据
return num.value + doubleNum.value;
});
</script>
正确做法:将副作用逻辑抽离到方法中,或使用watch侦听器(后续会讲解),getter函数仅负责计算。
2. 错误2:直接修改计算属性的值
只读计算属性不能直接修改,若尝试修改,会触发运行时警告,且修改无效。即使是可写计算属性,也应该通过修改计算属性的值,间接修改其依赖的源数据,而不是直接修改依赖。
<!-- 错误示例:直接修改只读计算属性 -->
<script setup>
import { ref, computed } from 'vue';
const num1 = ref(10);
const num2 = ref(20);
const sum = computed(() => num1.value + num2.value);
// 错误:直接修改计算属性的值
function changeSum() {
sum.value = 50; // 触发警告,修改无效
}
</script>
正确做法:修改计算属性依赖的源数据(num1、num2),让计算属性自动重新计算。
3. 错误3:依赖非响应式数据
计算属性只能追踪响应式数据(ref、reactive包裹的数据)的变化,如果依赖非响应式数据(如普通变量),计算属性不会自动重新计算。
<!-- 错误示例:依赖非响应式数据 -->
<script setup>
import { computed } from 'vue';
// 非响应式变量
let num = 10;
// 计算属性依赖非响应式数据,num变化时,计算属性不会更新
const doubleNum = computed(() => num * 2);
// 错误:修改非响应式数据,计算属性不会重新计算
function changeNum() {
num = 20;
console.log(doubleNum.value); // 输出20,而非40
}
</script>
正确做法:将非响应式数据改为响应式数据(用ref或reactive包裹)。
4. 最佳实践总结
- 单一职责:每个计算属性只负责一个逻辑,避免一个计算属性包含复杂的多步逻辑,提升可读性和可维护性;
- 无副作用:getter函数仅做计算,不修改其他数据、不发送请求、不操作DOM;
- 优先缓存:对于耗时逻辑、多次访问的派生值,优先使用计算属性,利用缓存提升性能;
- 慎用可写:可写计算属性仅在需要反向修改源数据的场景使用,大部分场景使用只读计算属性即可;
- 依赖响应式:确保计算属性的依赖都是响应式数据,否则无法触发自动更新。
延伸:模板引用与生命周期钩子——手动操作DOM的正确姿势
前面我们讲解的计算属性,核心是处理“响应式数据的派生”,全程由Vue自动管理DOM更新,无需我们手动干预。但在实际开发中,难免会遇到需要手动操作DOM的场景——比如获取DOM元素的尺寸、手动触发DOM事件、集成第三方DOM库(如图表、富文本)等。这时,Vue提供的模板引用和生命周期钩子,就是我们手动操作DOM的“最佳搭档”。
一、模板引用:获取模板中的DOM元素
模板引用(Template Ref)的核心作用,是让我们在Vue组件中,通过ref属性获取模板中指定的DOM元素或子组件实例。它的使用非常简单,分为两步:
1. 模板中标记ref属性
在模板中的DOM元素上,添加ref属性,并指定一个唯一的名称(相当于给这个DOM元素起一个“ID”),用于后续访问:
<template>
<!-- 给p标签添加ref,名称为pElementRef -->
<p ref="pElementRef">Hello, Vue!</p>
<!-- 给输入框添加ref,名称为inputRef -->
<input ref="inputRef" type="text" placeholder="请输入内容" />
</template>
2. 脚本中声明同名ref
在的ref变量,并初始化为null。这个变量将用于存储对应的DOM元素:
<script setup>
import { ref } from 'vue';
// 声明与模板ref同名的ref变量,初始值为null
const pElementRef = ref(null);
const inputRef = ref(null);
</script>
⚠️ 注意:脚本执行时,DOM元素还未挂载到页面上,所以初始值必须设为null。只有当组件挂载完成后,这个ref变量的.value才会指向对应的DOM元素,此时才能进行DOM操作。
二、生命周期钩子:在合适的时机操作DOM
Vue组件从创建到销毁,会经历一系列固定的阶段,比如“初始化数据”“编译模板”“挂载DOM”“更新DOM”“销毁组件”等。这些阶段被称为组件生命周期,而生命周期钩子,就是允许我们在组件特定生命周期阶段执行自定义代码的函数。
对于手动操作DOM来说,最常用的生命周期钩子是onMounted——它会在组件完成初始渲染、DOM元素挂载到页面后触发,此时模板引用已经可以访问,是执行DOM操作的最佳时机。
1. 基本使用:onMounted钩子
首先导入onMounted钩子,然后注册一个回调函数,在回调函数中访问模板引用,执行DOM操作:
<script setup>
import { ref, onMounted } from 'vue';
// 声明模板引用
const pElementRef = ref(null);
const inputRef = ref(null);
// 注册onMounted钩子,组件挂载后执行
onMounted(() => {
// 此时DOM已挂载,pElementRef.value指向对应的p标签
console.log('p标签DOM元素:', pElementRef.value);
// 执行DOM操作:修改p标签的文本内容和样式
pElementRef.value.textContent = '组件挂载完成,已修改文本';
pElementRef.value.style.color = '#e53e3e';
pElementRef.value.style.fontSize = '18px';
// 操作input标签:自动聚焦
inputRef.value.focus();
});
</script>
<template>
<p ref="pElementRef">Hello, Vue!</p>
<input ref="inputRef" type="text" placeholder="请输入内容" />
</template>
运行这段代码,组件挂载后会自动执行onMounted中的回调:p标签的文本和样式被修改,输入框自动聚焦,完美实现了手动DOM操作。
2. 其他常用生命周期钩子
除了onMounted,还有两个常用的生命周期钩子,可根据场景灵活使用:
- onUpdated:组件更新(比如响应式数据变化导致DOM重新渲染)后触发。适合在DOM更新完成后,再次调整DOM(如重新计算DOM尺寸)。
- onUnmounted:组件销毁前触发。适合清理手动创建的DOM资源(如移除事件监听、销毁第三方DOM库实例),避免内存泄漏。
示例:使用onUpdated和onUnmounted
<script setup>
import { ref, onMounted, onUpdated, onUnmounted } from 'vue';
const count = ref(0);
const pElementRef = ref(null);
let resizeListener = null;
onMounted(() => {
// 挂载后添加窗口大小变化监听
resizeListener = () => {
console.log('窗口尺寸变化:', window.innerWidth);
};
window.addEventListener('resize', resizeListener);
});
onUpdated(() => {
// 组件更新(count变化导致DOM更新)后,修改p标签颜色
if (pElementRef.value) {
pElementRef.value.style.color = count.value % 2 === 0 ? '#e53e3e' : '#48bb78';
}
});
onUnmounted(() => {
// 组件销毁前,移除事件监听,清理资源
window.removeEventListener('resize', resizeListener);
});
</script>
<template>
<p ref="pElementRef">count: {{ count }}</p>
<button @click="count++">count+1</button>
</template>
三、模板引用的进阶用法
1. 引用子组件
模板引用不仅可以获取普通DOM元素,还可以获取子组件实例,从而调用子组件的方法或访问子组件的响应式数据。
示例:父组件引用子组件
<!-- 子组件 Child.vue -->
<script setup>
import { ref } from 'vue';
const childCount = ref(0);
// 子组件的方法
const incrementChild = () => {
childCount.value++;
};
// 暴露子组件的方法和数据,供父组件访问
defineExpose({
childCount,
incrementChild
});
</script>
<template>
<p>子组件count: {{ childCount }}</p>
</template>
<!-- 父组件 Parent.vue -->
<script setup>
import { ref, onMounted } from 'vue';
import Child from './Child.vue';
// 声明引用子组件的ref
const childRef = ref(null);
onMounted(() => {
// 访问子组件的响应式数据
console.log('子组件初始count:', childRef.value.childCount);
// 调用子组件的方法
childRef.value.incrementChild();
console.log('调用子组件方法后count:', childRef.value.childCount);
});
</script>
<template>
<Child ref="childRef" />
</template>
⚠️ 注意:子组件中必须通过defineExpose,将需要被父组件访问的方法和数据暴露出来,否则父组件无法通过模板引用访问。
2. 动态模板引用(v-for中使用)
当在v-for中使用模板引用时,需要将ref绑定为一个函数,用于收集所有遍历产生的DOM元素(或子组件):
<script setup>
import { ref, onMounted } from 'vue';
// 用数组存储所有li标签的DOM元素
const listRefs = ref([]);
const list = ref(['Vue', 'React', 'Angular']);
// 绑定ref函数,收集DOM元素
const setListRef = (el) => {
if (el) {
listRefs.value.push(el);
}
};
onMounted(() => {
// 访问所有li标签的DOM元素
console.log('所有li元素:', listRefs.value);
// 给第一个li标签修改样式
listRefs.value[0].style.color = '#e53e3e';
});
</script>
<template>
<ul>
<li v-for="item in list" :key="item" :ref="setListRef">
{{ item }}
</li>
</ul>
</template>
四、模板引用与生命周期的注意事项
- 模板引用只能在组件挂载后访问(onMounted及之后的钩子),在挂载前(如setup直接执行时)访问,其.value始终为null;
- 生命周期钩子必须同步注册(不能在setTimeout、Promise.then等异步操作中注册),否则Vue无法关联到当前组件实例,钩子不会生效;
- 模板引用不是响应式的,不能在模板中用它进行数据绑定(如{{ pElementRef.value.textContent }}),仅用于脚本中手动操作DOM;
- 组件销毁时,务必清理手动创建的DOM资源(如事件监听、第三方实例),避免内存泄漏,这一步通常在onUnmounted中完成。
总结:Vue响应式与DOM操作的完整体系
本文我们从计算属性入手,讲解了其核心用法、缓存特性、进阶技巧与避坑指南,又延伸到模板引用与生命周期钩子,形成了Vue开发中“响应式数据处理+手动DOM操作”的完整体系:
- 计算属性:负责响应式数据的派生,分离逻辑与展示,利用缓存提升性能,适用于无需手动操作DOM的场景;
- 模板引用:负责获取模板中的DOM元素或子组件实例,是手动操作DOM的入口;
- 生命周期钩子:负责在组件合适的阶段(如挂载后、更新后、销毁前)执行代码,确保DOM操作的时机正确,避免报错和内存泄漏。
常见应用场景补充:
- 列表过滤、排序:结合v-for,实现响应式过滤排序,无需手动触发;
- 多数据派生:根据多个响应式数据,生成新的状态(如拼接、运算、判断);
- 表单双向绑定:通过可写计算属性,实现输入值与多个源数据的双向同步;
- 复杂逻辑抽离:将模板中的复杂表达式,抽离为计算属性,简化模板;
- 手动DOM操作:通过模板引用获取DOM元素,结合onMounted等钩子,实现聚焦、修改样式、集成第三方库等需求。
掌握这些知识点,能让你在Vue开发中灵活处理响应式数据与DOM操作,避开冗余代码和性能问题,尤其是在中大型项目中,合理运用计算属性、模板引用和生命周期钩子,能显著提升代码的可维护性和页面性能。后续我们还会讲解与计算属性、生命周期互补的“侦听器(watch)”,解锁更灵活的响应式开发技巧。