目录
一、ref和reactive的区别
1. ref
- 用途:创建一个响应式的值类型(如
number
、string
、boolean
)或引用类型(如对象、数组)。 - 语法:
const count = ref(0)
。 - 访问方式:通过
.value
访问值(如count.value
)。
2. reactive
- 用途:创建一个响应式的对象或数组(引用类型)。
- 语法:
const state = reactive({ count: 0 })
。 - 访问方式:直接访问属性(如
state.count
)。
3、核心区别
特性 | ref |
reactive |
---|---|---|
适用类型 | 任意类型(值类型或引用类型) | 仅对象或数组(引用类型) |
创建方式 | ref(initialValue) |
reactive(object) |
访问方式 | 通过 .value 访问(如 count.value ) |
直接访问属性(如 state.count ) |
响应式原理 | 使用 Object.defineProperty() 或 Proxy |
仅使用 Proxy |
深层响应式 | 引用类型(对象 / 数组)自动深层响应式 | 自动深层响应式 |
解构后响应性 | 解构后失去响应性,需使用 toRefs 保留 |
解构后仍保持响应性 |
性能 | 基本类型(值类型)更高效 | 对象 / 数组的操作更高效 |
4、原理差异
1. ref
的实现
- 本质是一个对象,包含
value
属性:class RefImpl { constructor(value) { this._value = value; // 原始值 this.value = value; // 响应式值 // 通过 getter/setter 拦截 value 的读写 } }
- 对于基本类型,使用
Object.defineProperty()
拦截value
的读写。 - 对于引用类型,内部调用
reactive()
转为深层响应式对象。
2. reactive
的实现
- 使用 ES6 的
Proxy
拦截整个对象的属性读写:function reactive(target) { return new Proxy(target, { get(target, key) { // 依赖收集 return target[key]; }, set(target, key, value) { // 触发更新 target[key] = value; return true; } }); }
- 自动深层响应式:访问嵌套对象时,递归创建 Proxy。
5、常见误区
ref
在模板中无需.value
:
Vue 3 模板会自动解包ref
,直接使用{{ count }}
即可。reactive
不能替代ref
:
reactive
无法处理基本类型(如number
),必须使用ref
。解构
reactive
的注意事项:
解构后属性的响应性依赖于原始对象,若重新赋值会失去响应性。
二、计算属性(Computed)
计算属性是 Vue3 中用于处理复杂数据逻辑的重要特性,基于响应式依赖进行缓存,只有当依赖的值发生变化时才会重新计算。
1. 基本用法
计算属性通过 computed
来定义,接受一个 getter 函数或包含 getter/setter 的对象:
<template>
<div>
<p>原值: {{ firstName }} {{ lastName }}</p>
<p>计算属性值: {{ fullName }}</p>
<p>计算属性(缓存演示): {{ cachedComputed }}</p>
<p>普通函数: {{ getFullName() }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const age = ref(30);
// 基础计算属性(getter 形式)
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value;
});
// 带缓存的计算属性演示
const cachedComputed = computed(() => {
console.log('计算属性被调用');
return age.value > 18 ? '成年人' : '未成年人';
});
// 普通函数(非响应式缓存)
const getFullName = () => {
console.log('普通函数被调用');
return firstName.value + ' ' + lastName.value;
};
</script>
2. 计算属性的 setter 用法
计算属性不仅可以读取,还可以通过对象形式定义 setter 实现双向绑定:
<template>
<div>
<input v-model="fullName" />
<p>名: {{ firstName }}</p>
<p>姓: {{ lastName }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('');
const lastName = ref('');
// 带 setter 的计算属性
const fullName = computed({
get() {
return firstName.value + ' ' + lastName.value;
},
set(value) {
const [first, last] = value.split(' ');
firstName.value = first;
lastName.value = last;
}
});
</script>
3. 计算属性的特点
- 响应式依赖:只在依赖的响应式数据变化时重新计算
- 缓存机制:避免重复计算,提升性能
- 简洁语法:相比函数调用更直观,适合处理模板中的复杂逻辑
三、Watch 的使用
Watch(监听器)用于监听数据变化,并在变化时执行回调函数,适用于需要在数据变化时触发副作用的场景。
1. 基础监听(watch 函数)
<template>
<div>
<input v-model="message" />
<p>输入内容: {{ message }}</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const message = ref('');
// 监听单个 ref 类型数据(注意ref对象在watch中不用加.value)
watch(message, (newValue, oldValue) => {
console.log('值变化了:', newValue, '旧值:', oldValue);
// 执行副作用操作
});
// 监听多个数据(数组形式)
const count = ref(0);
watch([message, count],
( [newcount, newmessage], [oldcount, oldmessage]) => {
console.log('message 或 count 变化了');
});
</script>
2. 深度监听(对象属性)
对于对象类型的数据,需要深度监听才能捕获属性变化:
<template>
<div>
<input v-model="user.name" />
<input v-model="user.age" />
</div>
</template>
<script setup>
import { ref, reactive, watch } from 'vue';
const user = reactive({
name: '张三',
age: 18
});
// 深度监听对象属性(方法一:开启 deep 选项)
watch(
() => user,
(newValue, oldValue) => {
console.log('用户信息变化了');
},
{ deep: true }
);
// 深度监听对象属性(方法二:直接监听属性)
watch(
() => user.name,
(newName) => {
console.log('用户名变化了:', newName);
}
);
</script>
注意当监听的是reactive对象时,watch的第一个参数 需要写为函数的形式。
当监听的是ref对象时,watch的第一个参数为普通的变量名的形式。
监听目标 | 第一个参数写法 | 是否需要 deep 选项 |
---|---|---|
ref 变量 | count |
否 |
ref 对象的单个属性(精确监听) | () => user.value.age |
否 |
reactive 对象的属性 | () => user.name |
否 |
整个 reactive 对象 | () => user |
是 |
多个数据源 | [count, () => user.age] |
部分情况需要 |
计算属性式的监听值 | () => obj.a * obj.b |
否 |
3. 立即执行监听(immediate 选项)
<template>
<div>
<p>当前状态: {{ status }}</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const status = ref('init');
// 组件挂载后立即执行一次监听回调
watch(
status,
(newStatus) => {
console.log('状态变化:', newStatus);
},
{ immediate: true }
);
</script>
4. watchEffect 自动追踪依赖
watchEffect
会自动追踪回调函数中的响应式依赖,适用于需要立即执行且自动追踪依赖的场景:
<template>
<div>
<input v-model="count" />
<p>计算结果: {{ result }}</p>
</div>
</template>
<script setup>
import { ref, watchEffect } from 'vue';
const count = ref(0);
let result = 0;
// 自动追踪 count 的变化
watchEffect(() => {
result = count.value * 2;
console.log('依赖变化,重新计算');
});
</script>
四、计算属性 vs Watch 的选择
场景 | 计算属性 | Watch |
---|---|---|
数据转换 | ✅(推荐) | ❌ |
异步操作 | ❌ | ✅(推荐) |
副作用(如 API 调用) | ❌ | ✅(推荐) |
多依赖组合 | ✅(推荐) | ✅ |
立即执行 | ❌ | ✅(通过 immediate 选项) |
五、生命周期API
阶段 | 选项式 API | 组合式 API | 触发时机 |
---|---|---|---|
创建阶段 | beforeCreate |
相当于setup | 实例创建之前,数据观测和事件配置尚未初始化。 |
created |
实例创建完成,数据观测和事件已配置,但 DOM 尚未生成。 | ||
挂载阶段 | beforeMount |
onBeforeMount |
模板编译完成,即将开始渲染 DOM 前。 |
mounted |
onMounted |
DOM 挂载完成后触发,可访问 DOM 元素。 | |
更新阶段 | beforeUpdate |
onBeforeUpdate |
数据更新导致组件重新渲染前,此时 DOM 尚未更新。 |
updated |
onUpdated |
组件更新完成,DOM 已同步更新后触发。 | |
卸载阶段 | beforeUnmount |
onBeforeUnmount |
组件即将卸载前,可用于清理定时器、事件监听等资源。 |
unmounted |
onUnmounted |
组件已卸载,所有子组件也已卸载完成。 | |
错误处理 | errorCaptured |
onErrorCaptured |
捕获到组件或子组件的错误时触发,可用于错误日志记录。 |
服务器渲染 | serverPrefetch |
onServerPrefetch |
(仅服务端渲染)组件在服务器端渲染前触发,用于数据预获取。 |
六、组合式API下的组件间数据传递
一、父传子:通过 props 传递数据
父组件通过 defineProps
声明并传递数据,子组件通过 defineProps
接收。
父组件:
<!-- Parent.vue -->
<template>
<Child :message="parentMessage" :count="parentCount" />
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const parentMessage = ref('Hello from parent');
const parentCount = ref(100);
</script>
子组件:
<!-- Child.vue -->
<template>
<div>
<p>Received message: {{ message }}</p>
<p>Received count: {{ count }}</p>
</div>
</template>
<script setup>
//import { defineProps } from 'vue'; 可以不引入
// 定义 props 类型和默认值
const props = defineProps({
message: String,//定义类型
count: {
type: Number,
default: 0
}
});
// 使用 props
console.log(props.message); // "Hello from parent"
</script>
二、子传父:通过 emits 触发事件
子组件通过 defineEmits
声明事件,通过 emit
触发,父组件监听事件并处理。
子组件:
<!-- Child.vue -->
<template>
<button @click="sendDataToParent">Send to Parent</button>
</template>
<script setup>
//import { defineEmits } from 'vue'; 可以不用引入
// 定义可触发的事件
const emit = defineEmits(['updateData', 'customEvent']);
// 触发事件并传递数据
const sendDataToParent = () => {
emit('updateData', 'Data from child');
emit('customEvent', { key: 'value' });
};
</script>
父组件:
<!-- Parent.vue -->
<template>
<Child @updateData="handleUpdate" @customEvent="handleCustom" />
</template>
<script setup>
import Child from './Child.vue';
// 处理子组件事件
const handleUpdate = (data) => {
console.log('Received from child:', data); // "Data from child"
};
const handleCustom = (payload) => {
console.log('Custom event payload:', payload); // { key: 'value' }
};
</script>
三、双向绑定:v-model 语法糖
通过 v-model
实现父子组件的双向数据流动,子组件通过 update:propName
事件更新父组件数据。
父组件:
<!-- Parent.vue -->
<template>
<!-- 默认 v-model -->
<Child v-model="parentValue" />
<!-- 自定义 prop 和事件名 -->
<Child v-model:title="parentTitle" />
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const parentValue = ref('Initial value');
const parentTitle = ref('Initial title');
</script>
子组件:
<!-- Child.vue -->
<template>
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
// 默认 v-model 对应 modelValue prop
const props = defineProps({
modelValue: String,
title: String
});
const emit = defineEmits(['update:modelValue', 'update:title']);
</script>
四、provide/inject:跨层级通信
父组件通过 provide
提供数据,任意层级的子组件通过 inject
获取数据,无需逐级传递。
祖先组件:
<!-- Ancestor.vue -->
<template>
<ChildComponent />
</template>
<script setup>
import { provide, ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 提供响应式数据
const sharedData = ref('Shared data from ancestor');
// 提供方法
const updateSharedData = (newValue) => {
sharedData.value = newValue;
};
// 注入 provide
provide('sharedData', sharedData);
provide('updateSharedData', updateSharedData);
</script>
任意层级子组件:
<!-- AnyChild.vue -->
<template>
<div>
<p>Shared data: {{ sharedData }}</p>
<button @click="updateData">Update Shared Data</button>
</div>
</template>
<script setup>
import { inject } from 'vue';
// 注入数据
const sharedData = inject('sharedData');
const updateSharedData = inject('updateSharedData');
const updateData = () => {
updateSharedData('New value from child');
};
</script>
如果想让子孙组件修改父组件中的参数,可以把方法写在父组件中,通过provide和inject把方法传递给子孙组件,子孙组件调用这个方法来修改参数
五、使用 $parent
和 $children
(不推荐)
通过 $parent
访问父组件实例,通过 $children
访问子组件实例。这种方式破坏了组件封装性,不推荐在大型项目中使用。
子组件:
<!-- Child.vue -->
<script setup>
import { getCurrentInstance } from 'vue';
const instance = getCurrentInstance();
// 访问父组件数据或方法
const parentData = instance.parent.data;
instance.parent.someMethod();
</script>
六、使用事件总线或状态管理
对于复杂场景,可使用第三方库(如 Pinia、Vuex)或自定义事件总线实现组件通信。
总结
通信方式 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
props | 父 → 子单向数据流 | 简单直观,类型安全 | 只能单向传递 |
emits | 子 → 父事件触发 | 语义明确,便于调试 | 多层级时需逐级传递 |
v-model | 双向数据绑定 | 语法简洁,代码量少 | 依赖特定事件名(update:propName) |
provide/inject | 跨层级数据共享 | 无需逐级传递,支持响应式 | 依赖注入键,调试困难 |
\(parent/\)children | 直接访问组件实例(不推荐) | 快速获取实例 | 破坏封装性,耦合度高 |
在实际开发中,推荐优先使用 props 和 emits 实现单向数据流,复杂场景使用 provide/inject 或状态管理库。
七、组合式API下的模板引用
模板引用允许在 JavaScript 中直接访问 DOM 元素或组件实例,通常用于
- 手动操作 DOM(如聚焦、滚动)
- 调用子组件的方法
- 获取组件状态
一、组合式 API 中的模板引用
在组合式 API 中,模板引用通过 ref()
创建,并通过 v-bind
绑定到元素或组件上。
示例:获取 DOM 元素
<template>
<div>
<input ref="inputRef" type="text" />
<button @click="focusInput">聚焦输入框</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 创建模板引用
const inputRef = ref(null);
// 访问 DOM 元素
const focusInput = () => {
inputRef.value.focus(); // 调用 DOM 方法
};
// 在 mounted 钩子中访问 DOM
onMounted(() => {
console.log(inputRef.value); // <input type="text">
});
</script>
示例:获取组件实例
<template>
<ChildComponent ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 获取子组件实例
const childRef = ref(null);
const callChildMethod = () => {
childRef.value.someMethod(); // 调用子组件方法
};
</script>
二、使用 $refs
(选项式 API 风格)
在组合式 API 中,也可以通过 getCurrentInstance
访问 $refs
:
<template>
<div ref="container">容器</div>
</template>
<script setup>
import { getCurrentInstance, onMounted } from 'vue';
const instance = getCurrentInstance();
onMounted(() => {
console.log(instance.refs.container); // 访问 $refs
});
</script>
三、动态绑定引用
模板引用可以动态绑定到不同元素:
<template>
<div>
<button @click="changeRef">切换引用</button>
<div v-if="showA" ref="currentRef">元素 A</div>
<div v-else ref="currentRef">元素 B</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const currentRef = ref(null);
const showA = ref(true);
const changeRef = () => {
showA.value = !showA.value;
// 切换后,currentRef 会指向新的元素
};
</script>
四、组件引用与跨组件访问
在子组件中暴露方法供父组件调用:
<!-- ChildComponent.vue -->
<script setup>
//import { defineExpose } from 'vue'; 可以不引入
const count = ref(0);
const increment = () => {
count.value++;
};
// 暴露方法和属性给父组件
defineExpose({
increment,
count
});
</script>
<!-- ParentComponent.vue -->
<template>
<ChildComponent ref="childRef" />
<button @click="childRef.value.increment()">调用子组件方法</button>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const childRef = ref(null);
</script>
五、注意事项
引用值的延迟
模板引用在初始渲染时为null
,只有在组件挂载后才会指向实际元素。建议在onMounted
或之后访问。与响应式数据的区别
模板引用不是响应式的,其值变化不会触发重新渲染。组合式 API 与选项式 API 的区别
- 组合式 API:通过
ref()
创建引用变量。 - 选项式 API:通过
this.$refs
访问引用。
- 组合式 API:通过
函数式组件的引用
函数式组件需要显式接受ref
参数并通过forwardRef
转发。
总结
特性 | 说明 |
---|---|
创建引用 | 使用 ref(null) 创建,初始值为 null 。 |
绑定到元素 | 使用 ref="refName" 绑定到 DOM 元素或组件。 |
访问引用 | 通过 refName.value 访问实际元素或组件实例。 |
组件暴露 | 使用 defineExpose() 暴露子组件的方法和属性。 |
动态引用 | 支持动态绑定到不同元素,值会自动更新。 |