组合式 API和选项式 API
组合式 API
基于逻辑功能组织代码,所有逻辑集中在 setup 函数中,相关功能的代码可以紧密组织在一起,易于维护和复用。
选项式 API
Vue2 使用选项式 API,使用一组选项( data、methods、computed、watch 等)来定义组件的状态、逻辑和行为。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。
优点: 结构清晰,每个选项都有其明确的职责,逻辑上比较直观。
缺点: 当组件逻辑复杂时,数据和逻辑会被分散在多个选项中,很难一眼看出它们之间的关系,代码会变得难以维护和理解。
两者区别
- 管理逻辑代码的方式
Option API:有既定规则,代码按照选项分区管理Composition API:较为弹性、自由,代码通常按照功能逻辑分区管理
- 响应式数据
Composition API形式下,需要利用reactive()和ref()定义 data 是否有响应性,在其他地方取 data 时,也要根据reactive()和ref()的规则进行取值和操作。Option API形式下,Vue 已经帮开发者做好了响应式,自动为data中的数据加上响应性,其他选项要取用data内的数据时,用this.变量名取得即可。
响应式系统
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式原理 | 使用 Object.defineProperty() | 使用 Proxy |
| 数据追踪 | 静态劫持(只能对已定义属性劫持) | 动态劫持(对任意访问属性劫持) |
| 对象扩展 | 新增属性无响应式,需要 Vue.set() | 任意新增/删除属性都是响应式 |
| 数组响应式 | 重写部分数组方法 | 原生支持,无需特殊处理 |
| 性能 | 对每个属性都进行递归劫持,性能较差 | 延迟劫持,性能更优 |
| API 支持 | 选项式 API 为主 | 同时支持组合式 API |
| 响应式 API | 少,如 Vue.set()、Vue.delete() | 丰富,如 reactive()、ref() |
Object.defineProperty()
定义
Object.defineProperty 是 ES5 引入的方法,用于直接在对象上定义新属性或修改现有属性的特性。
语法
Object.defineProperty(obj, prop, descriptor)
obj:目标对象。prop:要定义或修改的属性名。descriptor:属性描述符对象,包含以下可选配置:
-
value:属性的值。writable:是否可写,默认为false。enumerable:是否可枚举,默认为false。configurable:是否可配置(删除或修改特性),默认为false。get:属性的 getter 函数。set:属性的 setter 函数。
Vue2 的响应式原理
核心思想是:将数据对象的属性访问(读取)和修改(赋值)操作拦截下来,通过 getter 和 setter 实现数据监听。当数据发生变化时,setter 会通知依赖该数据的视图或其他逻辑进行更新,从而实现响应式。
具体实现步骤:
- 深度遍历对象
-
- Vue2 会对
_data对象( Vue 实例中存储实际数据的对象)进行深度遍历。 - 使用
for...in遍历对象的每个属性,递归处理嵌套对象,确保所有层级的属性都被拦截。
- Vue2 会对
- 通过 ****
Object.defineProperty****定义或修改属性
-
- 对每个属性,使用
Object.defineProperty定义或修改其特性。 - 为每个属性添加
getter和setter:
- 对每个属性,使用
-
-
getter:当访问属性时,触发getter,可以记录依赖(例如哪些组件或计算属性依赖于这个属性)。setter:当修改属性时,触发setter,通知依赖更新。
-
- 代理对象(
vm)
-
- Vue2 实例(
vm)会代理_data对象,使得开发者可以通过vm.name直接访问和修改_data.name。 - 实际上,
vm.name的访问和修改会被重定向到_data.name的getter和setter。
- Vue2 实例(
代码示例
function defineReactive(obj, key, val) {
// 递归处理嵌套对象
observe(val);
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可配置
get() { // 获取属性值
console.log(`读取属性: ${key}`);
return val;
},
set(newVal) { // 设置属性值
console.log(`设置属性: ${key} 为 ${newVal}`);
if (newVal !== val) {
val = newVal;
// 通知依赖更新
}
}
});
}
function observe(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
// 遍历对象的每个属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
defineReactive(obj, key, obj[key]);
}
}
}
// 模拟 Vue 实例
function Vue(options) {
this._data = options.data;
observe(this._data);
// 代理,使得可以通过 vm.key 访问 _data.key
for (let key in this._data) {
if (this._data.hasOwnProperty(key)) {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return this._data[key];
},
set(newVal) {
this._data[key] = newVal;
}
});
}
}
}
// 使用示例
const vm = new Vue({
data: {
name: 'Alice',
age: 25,
info: {
hobby: 'reading'
}
}
});
// 访问属性
console.log(vm.name); // 输出: 读取属性: name \n Alice
// 修改属性
vm.name = 'Bob'; // 输出: 设置属性: name 为 Bob
// 访问嵌套属性
console.log(vm.info.hobby); // 输出: 读取属性: hobby \n reading
vm.info.hobby = 'coding'; // 输出: 读取属性: hobby \n 设置属性: hobby 为 coding
局限性
- 无法监听数组的变化:Vue2 对数组的响应式处理是通过重写数组方法实现的,而不是通过
Object.defineProperty。 - 性能问题:深度遍历和递归定义
getter和setter可能会导致性能开销,尤其是在大型对象上。 - 新增属性无法监听:由于
Object.defineProperty只能在对象定义时拦截属性,无法监听动态添加的属性(Vue2 提供了$set方法来解决这个问题)。
Proxy
定义
Proxy 是 ES6 引入的对象,用于创建一个对象的代理,从而拦截并自定义对对象的基本操作
语法
const proxy = new Proxy(target, handler)
target:需要被代理的原始对象。handler:一个对象,包含用于拦截各种操作的陷阱(trap)函数。- 代理对象(
proxy):通过Proxy构造函数创建的实例,用于拦截对目标对象的操作。
常用陷阱
get(target, prop, receiver):拦截属性读取。set(target, prop, value, receiver):拦截属性赋值。has(target, prop):拦截in操作符。deleteProperty(target, prop):拦截delete操作。ownKeys(target):拦截Object.keys()、for...in等操作。apply(target, thisArg, args):拦截函数调用。construct(target, args, newTarget):拦截new操作。
优势
- 整体拦截:可以拦截对对象的整体操作,而不仅仅是单个属性。
- 动态代理:动态代理整个对象,无需提前遍历属性。可以在运行时动态修改代理行为。
- 灵活性:支持更多的操作类型,功能更强大。
基本工作原理
- 创建代理对象
const obj = { name: 'Alice', age: 25 };
const proxy = new Proxy(obj, {
get(target, prop, receiver) {
console.log(`Getting property: ${prop}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`Setting property: ${prop} to ${value}`);
return Reflect.set(target, prop, value, receiver);
}
});
target:目标对象(obj)。prop:被访问或修改的属性名。receiver:通常是代理对象本身(proxy),用于处理继承链上的拦截。
- 拦截操作:
- 当访问
proxy.name时,会触发get拦截器。 - 当修改
proxy.age = 30时,会触发set拦截器。
- 代理效果:
- 开发者可以在
get和set中插入自定义逻辑,实现属性监听、验证、格式化等功能。
const obj = {
user: {
name: 'Alice',
details: {
age: 25
}
}
};
const proxy = new Proxy(obj, {
get(target, prop, receiver) {
console.log(`Getting property: ${prop}`);
const value = Reflect.get(target, prop, receiver);
if (typeof value === 'object' && value !== null) {
return new Proxy(value, receiver); // 递归代理
}
return value;
},
set(target, prop, value, receiver) {
console.log(`Setting property: ${prop} to ${value}`);
return Reflect.set(target, prop, value, receiver);
},
deleteProperty(target, prop) {
console.log(`Deleting property: ${prop}`);
return delete target[prop];
}
});
// 访问属性
console.log(proxy.user.name); // 输出: Alice
proxy.user.details.age = 30; // 输出: Setting property: age to 30
delete proxy.user.name; // 输出: Deleting property: name
响应式数据
reactive
作用: 定义一个响应式对象
语法: let obj = reactive({})
返回值:一个Proxy的实例对象
注意点: reactive定义的响应式数据是深层次的
内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
ref
作用: 接受简单类型或者对象类型的数据传入并返回一个响应式的对象
语法: let xxx = ref(初始值)
本质: ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回,ref对象的value属性是响应式的。
注意:
- 基本类型的数据:响应式依然是靠
object.defineProperty()的get与 set 完成的。 - 对象类型的数据:内部“求助”了--
reactive函数
ref 对比 reactive
- 宏观角度看:
ref用来定义:基本类型数据、对象类型数据reactive用来定义:对象类型数据
- 区别:
ref创建的变量必须使用.valuereactive重新分配一个新对象,会失去响应式(可以使用Object.assign去整体替换)
- 使用原则:
- 若需要一个基本类型的响应式数据,必须使用
ref - 若需要一个响应式对象,层级不深,
ref、reactive都可以 - 若需要一个响应式对象,且层级较深,推荐使用
reactive
容易踩坑的点
reactive响应式失效的情况
- 替换整个响应式对象
问题:当直接用新的对象替换 reactive 创建的响应式对象时,会导致响应式失效。
import { reactive } from 'vue';
const state = reactive({ count: 0 });
// 错误:直接替换整个对象
state = { count: 1 }; // 此时响应式失效
原因:
reactive 创建的对象是一个 Proxy 对象,直接替换该对象会丢失其 Proxy 包装,替换后新对象的地址与原对象不同,Vue 无法再追踪新对象的变化。而ref 的响应式核心是.value属性,而不是变量本身。即使 .value 被替换为新对象,Vue 也会确保新对象是响应式的。
解决方案:
- 保持对原对象的引用,只修改其属性,而不是替换整个对象:
- 使用
Object.assign整体替换:
import { reactive } from 'vue';
const state = reactive({ count: 0, name: 'Alice' });
// 错误:直接替换整个对象(响应式失效)
// state = { count: 1, name: 'Bob' };
// 正确:使用 Object.assign 替换属性(响应式生效)
Object.assign(state, { count: 1, name: 'Bob' });
// 添加新属性
// ❗Object.assign不会自动将新属性转换为响应式属性。
Object.assign(state, { count: 1, newCount: 1 }); // 这里newCount属性不会是响应式的
- 解构赋值导致响应式丢失
问题:当对 reactive 对象进行解构赋值时,解构后的变量不再是响应式的。
原因:解构赋值会创建一个普通变量,而不是响应式引用。因此修改该变量不会触发 Vue 的响应式更新。
解决方案:直接使用 reactive 对象的属性,而不是解构赋值。如果需要解构,可以使用 toRefs 或 toRef
watch监听
作用: 监视数据的变化(和Vue2中的watch作用一致)
语法: watch(source, callback, options?)
特点:Vue3中的watch只能监视以下四种数据:
ref定义的数据reactive定义的数据- 函数返回一个值(
getter函数) - 一个包含上述内容的数组
情况一
监视ref定义的【基本类型】数据:直接写数据名即可,监视的是其value值的改变
<script setup>
import {ref,watch} from 'vue'
let sum = ref(0)
const stopWatch = watch(sum,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
if(newValue >= 10){
stopWatch()
}
})
</script>
情况二
监视ref定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。
注意:
- 若修改的是
ref定义的对象中的属性,newValue和oldValue都是新值,因为它们是同一个对象。内部属性改变,但是内存地址不变 - 若修改整个
ref定义的对象,newValue是新值,oldValue是旧值,因为不是同一个对象了。
<script setup name="Person">
import {ref,watch} from 'vue'
let person = ref({
name:'张三',
age:18
})
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
},{deep:true})
</script>
情况三
监视reactive定义的【对象类型】数据,默认开启了深度监视
注意:
- 修改
reactive定义的对象中的属性或者直接使用Object.assign()给整个对象复制,newValue和oldValue都是新值
<script setup name="Person">
import {reactive,watch} from 'vue'
// 数据
let person = reactive({
name:'张三',
age:18
})
let obj = reactive({
a:{
b:{
c:666
}
}
})
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
})
watch(obj,(newValue,oldValue)=>{
console.log('Obj变化了',newValue,oldValue)
})
</script>
情况四
监视ref或reactive定义的【对象类型】数据中的某个属性,注意点如下:
- 若该属性值不是【对象类型】,需要写成函数形式。
- 若该属性值是依然是【对象类型】,可直接写,也可写成函数,建议写成函数
结论:监视的要是对象里的属性,那么最好写函数式,注意点:对象监视的是地址值,如果需要关注对象内部,需要手动开启深度监视。
<script setup name="Person">
import {reactive,watch} from 'vue'
// 数据
let person = reactive({
name:'张三',
age:18,
car:{
c1:'奔驰',
c2:'宝马'
}
})
function changeCar(){
// person整个不可以改,但是改里面的东西是可以的
person.car = {c1:'雅迪',c2:'爱玛'}
}
// 监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式
// 此时新旧值是不一样的
/* watch(()=> person.name,(newValue,oldValue)=>{
console.log('person.name变化了',newValue,oldValue)
}) */
// 监视响应式对象中的某个属性,且该属性是对象类型的
watch(()=>person.car,(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
// 此时changeCar方法不会被监听到
watch(person.car,(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
</script>
情况五
监视上述的多个数据
<script setup name="Person">
import {reactive,watch} from 'vue'
// 数据
let person = reactive({
name:'张三',
age:18,
car:{
c1:'奔驰',
c2:'宝马'
}
})
watch([()=>person.name,person.car],(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
</script>
watchEffect
- 立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。
watch对比watchEffect
- 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch要明确指出监视的数据watchEffect不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。
<script setup>
import {ref,watch,watchEffect} from 'vue'
// 数据
let temp = ref(0)
let height = ref(0)
// 用watch实现,需要明确的指出要监视:temp、height
watch([temp,height],(value)=>{
const [newTemp,newHeight] = value
if(newTemp >= 50 || newHeight >= 20){
console.log('停止')
}
})
// 用watchEffect实现,不用
const stopWtach = watchEffect(()=>{
if(temp.value >= 50 || height.value >= 20){
console.log('停止')
}
// 水温达到100,或水位达到50,取消监视
if(temp.value === 100 || height.value === 50){
console.log('清理了')
stopWtach()
}
})
</script>
组件通信
父子通信
Props / Emits
父传子:props
最常用的通信方式是props了,父组件通过props方式将属性传递给子组件,子组件接受props并用于数据操作和页面渲染。
❗子组件不要直接修改父组件传递过来的props,保持自上而下单项数据流。
子传父: emit
- 子组件中通过
defineEmits([...emitName])可以定义一个或多个emit,然后通过调用emit函数向父组件发射时间,并携带参数。 - 父组件中通过
@事件名监听子组件发射的事件,并接收其传过来的值。
ref + expose 暴露子组件方法
- 原理:父组件通过
ref可以拿到组件的实例,defineExpose可以显式指定在<script setup>组件中要暴露出去的属性,它两一起配合使用,就能实现父子组件的通信。
示例代码
import { ref, defineExpose } from 'vue'
const count = ref(0)
const increment = () => { count.value++ }
defineExpose({ increment })
<ChildComponent ref="childRef" />
onst childRef = ref(null)
childRef.value.increment()
- 适用场景:父组件需要调用子组件中的方法(比如表单校验、清空操作等)
v-model 双向绑定(语法糖)
适用场景:表单输入组件或需要双向绑定的场景。v-model可以在组件上使用以实现双向绑定,vue内部会传递值和绑定事件
演进变化:
- Vue 2 中一个组件只能有一个 v-model,Vue 3 支持多个 v-model 绑定
- 默认 prop 名从 value 改为 modelValue
- 默认事件名从 input 改为 update:modelValue
<!-- 等价关系 -->
<MyComponent v-model="value" />
<!-- 等价于 -->
<MyComponent
:modelValue="value"
@update:modelValue="newValue => value = newValue"
/>
兄弟/跨层级通信
provide/inject
provide/inject是vue3提供的可以跨层级通信的方式,无需逐层传递 props
代码示例
<script setup>
import { provide, ref } from 'vue'
const count = ref(0)
provide('countKey', count) // 提供数据
</script>
<script setup>
import { inject } from 'vue'
const count = inject('countKey') // 注入数据
</script>
特点:
- 单向数据流:祖先提供数据,后代注入使用。
- 可修改(如果提供的是
ref或reactive对象) - 适合全局状态共享
mitt
mitt相当于vue2的事件总线
// emitter.js
import mitt from'mitt';
export default mitt();
- 使用示例
代码示例
<script setup>
import emitter from '@/utils/emitter'
emitter.on('update', (val) => {
console.log('update事件触发', val)
})
</script>
<script setup>
import emitter from '@/utils/emitter'
setTimeout(() => {
emitter.emit('update', 'hello')
}, 1000)
</script>
特点:
- 任意组件间通信,适合兄弟组件或跨层级组件。
- 需要手动管理事件监听和销毁
Pinia(状态管理)
Pinia 是 Vue 3 推荐的状态管理库 , 适用于全局状态共享。
Pinia 的核心概念比 Vuex 更简单:
state:存储数据(类似 Vuex 的state)。getters:计算属性(类似 Vuex 的getters)。actions:修改数据的方法(替代 Vuex 的mutations+actions)。- 没有
mutations:Pinia 直接使用actions修改state。
生命周期
Vue2的生命周期
创建阶段:beforeCreate、created
挂载阶段:beforeMount、mounted
更新阶段:beforeUpdate、updated
销毁阶段:beforeDestroy、destroyed
Vue3的生命周期
创建阶段:setup
挂载阶段:onBeforeMount、onMounted
更新阶段:onBeforeUpdate、onUpdated
卸载阶段:onBeforeUnmount、onUnmounted
主要变化:
beforeCreate和created在 Vue 3 中被setup()函数替代,setup()在beforeCreate之前执行- 命名变化
核心特性对比
| 特性 | Vue2 | Vue3 |
|---|---|---|
| 响应式系统 | 基于 Object.defineProperty | 基于 Proxy |
| 性能 | 较大虚拟DOM,较慢渲染 | 更小更快的虚拟DOM,优化渲染 |
| API风格 | Options API 为主 | Composition API + Options API |
| TypeScript支持 | 支持有限 | 更好的类型推断支持 |
| 打包大小 | 较大 | 更小的核心库(约10KB轻量) |
| 生命周期 | 传统生命周期钩子 | 新增 setup 和调整部分钩子 |