vue3笔记(1)自用
目录
一、ref和reactive的区别
1. ref
2. reactive
3、核心区别
4、原理差异
1. ref 的实现
2. reactive 的实现
5、常见误区
二、计算属性(Computed)
1. 基本用法
2. 计算属性的 setter 用法
3. 计算属性的特点
三、Watch 的使用
1. 基础监听(watch 函数)
2. 深度监听(对象属性)
3. 立即执行监听(immediate 选项)
4. watchEffect 自动追踪依赖
四、计算属性 vs Watch 的选择
五、生命周期API
六、组合式API下的组件间数据传递
一、父传子:通过 props 传递数据
二、子传父:通过 emits 触发事件
三、双向绑定:v-model 语法糖
四、provide/inject:跨层级通信
五、使用 $parent 和 $children(不推荐)
六、使用事件总线或状态管理
总结
七、组合式API下的模板引用
一、组合式 API 中的模板引用
示例:获取 DOM 元素
示例:获取组件实例
二、使用 $refs(选项式 API 风格)
三、动态绑定引用
四、组件引用与跨组件访问
五、注意事项
总结
一、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() 暴露子组件的方法和属性。 |
动态引用 | 支持动态绑定到不同元素,值会自动更新。 |