Vue3新特性与Vue2对比

317 阅读13分钟

组合式 API和选项式 API

组合式 API

基于逻辑功能组织代码,所有逻辑集中在 setup 函数中,相关功能的代码可以紧密组织在一起,易于维护和复用。

选项式 API

Vue2 使用选项式 API,使用一组选项( datamethodscomputedwatch 等)来定义组件的状态、逻辑和行为。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。

优点: 结构清晰,每个选项都有其明确的职责,逻辑上比较直观。

缺点: 当组件逻辑复杂时,数据和逻辑会被分散在多个选项中,很难一眼看出它们之间的关系,代码会变得难以维护和理解。

两者区别

  1. 管理逻辑代码的方式
  • Option API:有既定规则,代码按照选项分区管理
  • Composition API:较为弹性、自由,代码通常按照功能逻辑分区管理
  1. 响应式数据
  • Composition API 形式下,需要利用 reactive()ref() 定义 data 是否有响应性,在其他地方取 data 时,也要根据 reactive()ref() 的规则进行取值和操作。
  • Option API 形式下,Vue 已经帮开发者做好了响应式,自动为 data 中的数据加上响应性,其他选项要取用 data 内的数据时,用 this.变量名 取得即可。

响应式系统

特性Vue 2Vue 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 会通知依赖该数据的视图或其他逻辑进行更新,从而实现响应式。

具体实现步骤:

  1. 深度遍历对象
    • Vue2 会对 _data 对象( Vue 实例中存储实际数据的对象)进行深度遍历。
    • 使用 for...in 遍历对象的每个属性,递归处理嵌套对象,确保所有层级的属性都被拦截。
  1. 通过 ****Object.defineProperty ****定义或修改属性
    • 对每个属性,使用 Object.defineProperty 定义或修改其特性。
    • 为每个属性添加 gettersetter
      • getter:当访问属性时,触发 getter,可以记录依赖(例如哪些组件或计算属性依赖于这个属性)。
      • setter:当修改属性时,触发 setter,通知依赖更新。
  1. 代理对象( vm
    • Vue2 实例(vm)会代理 _data 对象,使得开发者可以通过 vm.name 直接访问和修改 _data.name
    • 实际上,vm.name 的访问和修改会被重定向到 _data.namegettersetter

代码示例

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
  • 性能问题:深度遍历和递归定义 gettersetter 可能会导致性能开销,尤其是在大型对象上。
  • 新增属性无法监听:由于 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 操作。

优势

  • 整体拦截:可以拦截对对象的整体操作,而不仅仅是单个属性。
  • 动态代理:动态代理整个对象,无需提前遍历属性。可以在运行时动态修改代理行为。
  • 灵活性:支持更多的操作类型,功能更强大。

基本工作原理

  1. 创建代理对象
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),用于处理继承链上的拦截。
  1. 拦截操作
  • 当访问 proxy.name 时,会触发 get 拦截器。
  • 当修改 proxy.age = 30 时,会触发 set 拦截器。
  1. 代理效果
  • 开发者可以在 getset 中插入自定义逻辑,实现属性监听、验证、格式化等功能。
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

  1. 宏观角度看:
  • ref用来定义:基本类型数据对象类型数据
  • reactive用来定义:对象类型数据
  1. 区别:
  • ref创建的变量必须使用.value
  • reactive重新分配一个新对象,会失去响应式(可以使用Object.assign去整体替换)
  1. 使用原则:
  • 若需要一个基本类型的响应式数据,必须使用ref
  • 若需要一个响应式对象,层级不深,refreactive都可以
  • 若需要一个响应式对象,且层级较深,推荐使用reactive

容易踩坑的点

reactive响应式失效的情况

  1. 替换整个响应式对象

问题:当直接用新的对象替换 reactive 创建的响应式对象时,会导致响应式失效。

import { reactive } from 'vue';

const state = reactive({ count: 0 });

// 错误:直接替换整个对象
state = { count: 1 }; // 此时响应式失效

原因

reactive 创建的对象是一个 Proxy 对象,直接替换该对象会丢失其 Proxy 包装,替换后新对象的地址与原对象不同,Vue 无法再追踪新对象的变化。而ref 的响应式核心是.value属性,而不是变量本身。即使 .value 被替换为新对象,Vue 也会确保新对象是响应式的。

解决方案:

  1. 保持对原对象的引用,只修改其属性,而不是替换整个对象:
  2. 使用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属性不会是响应式的
  1. 解构赋值导致响应式丢失

问题:当对 reactive 对象进行解构赋值时,解构后的变量不再是响应式的。

原因:解构赋值会创建一个普通变量,而不是响应式引用。因此修改该变量不会触发 Vue 的响应式更新。

解决方案:直接使用 reactive 对象的属性,而不是解构赋值。如果需要解构,可以使用 toRefstoRef

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定义的对象中的属性,newValueoldValue 都是新值,因为它们是同一个对象。内部属性改变,但是内存地址不变
  • 若修改整个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()给整个对象复制,newValueoldValue 都是新值
<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>

情况四

监视refreactive定义的【对象类型】数据中的某个属性,注意点如下:

  1. 若该属性值不是【对象类型】,需要写成函数形式。
  2. 若该属性值是依然是【对象类型】,可直接写,也可写成函数,建议写成函数

结论:监视的要是对象里的属性,那么最好写函数式,注意点:对象监视的是地址值,如果需要关注对象内部,需要手动开启深度监视。

<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

  1. 立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。
  2. 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
  1. 子组件中通过defineEmits([...emitName])可以定义一个或多个emit,然后通过调用emit函数向父组件发射时间,并携带参数。
  2. 父组件中通过@事件名监听子组件发射的事件,并接收其传过来的值。

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/injectvue3提供的可以跨层级通信的方式,无需逐层传递 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>

特点

  • 单向数据流:祖先提供数据,后代注入使用。
  • 可修改(如果提供的是 refreactive 对象)
  • 适合全局状态共享

mitt

mitt相当于vue2的事件总线

// emitter.js
import mitt from'mitt';
export default mitt();
  1. 使用示例

代码示例

<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

生命周期

  1. Vue2的生命周期

创建阶段:beforeCreatecreated

挂载阶段:beforeMountmounted

更新阶段:beforeUpdateupdated

销毁阶段:beforeDestroydestroyed

  1. Vue3的生命周期

创建阶段:setup

挂载阶段:onBeforeMountonMounted

更新阶段:onBeforeUpdateonUpdated

卸载阶段:onBeforeUnmountonUnmounted

主要变化:

  • beforeCreate created在 Vue 3 中被 setup() 函数替代,setup() beforeCreate 之前执行
  • 命名变化

核心特性对比

特性Vue2Vue3
响应式系统基于 Object.defineProperty基于 Proxy
性能较大虚拟DOM,较慢渲染更小更快的虚拟DOM,优化渲染
API风格Options API 为主Composition API + Options API
TypeScript支持支持有限更好的类型推断支持
打包大小较大更小的核心库(约10KB轻量)
生命周期传统生命周期钩子新增 setup 和调整部分钩子