Vue(八):Vue2 和 Vue3 的区别

80 阅读6分钟

创建实例

// Vue 2
import Vue from 'vue'
// 创建一个新的 Vue 实例并挂载到 #app 容器
new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue 2!'
  },
  template: '<div>{{ message }}</div>'
})

// Vue 3
import { createApp } from 'vue'
// 创建一个新的 Vue 应用实例并挂载到 #app 容器
createApp({
  data() {
    return {
      message: 'Hello Vue 3!'
    }
  },
  template: '<div>{{ message }}</div>'
}).mount('#app')

创建路由

// Vue2 new Router
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

const router = new Router({
  routes: [
    { path: '/', component: Home },
    // ...其他路由配置
  ]
})

// Vue3 createRouter
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import Home from './views/Home.vue'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    // ...其他路由配置
  ]
})

生命周期

Vue2Vue3调用时间
beforeCreatesetupvue实例初始化之前调用
createdsetupvue实例初始化之后调用
beforeMountonBeforeMount挂载到DOM树之前调用
mountedonMounted挂载到DOM树之后调用
beforeUpdateonBeforeUpdate数据更新之前调用
updatedonUpdated数据更新之后调用
beforeDestroyonBeforeUnmountvue实例销毁之前调用
destroyedonUnmountedvue实例销毁之后调用

数据响应

Vue2 响应式数据原理

Vue2 的数据响应使用了 ES5 中的 Object.defineProperty(obj, prop, descriptor)。

Vue2 初始化实例时会使用 Object.defineProperty() 把 data 中声明的对象属性进行 getter/setter 转化,把这些对象变成响应式的,如果在该对象上新增一个属性,不会触发该对象的响应,这时就要通过用 $set()$forceUpdate() 之类的方法触发响应。比较笨重。

Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历。如果属性值是对象,还需要深度遍历。

优点:

  • 兼容性好,支持 IE9。

缺点:

  • 只能劫持对象的属性,因此需要对每个对象的每个属性进行遍历。

  • 无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。是通过重写数据的操作方法(push、unshift…)来对数组进行监听的。

  • 不能对 es6 新产生的 Map,Set 这些数据结构做出监听。

模拟vue2中用Object.defineProperty(obj, prop, descriptor)实现的数据响应:

let obj = {
  key:'cue'
  flags: {
    name: ['AAA', 'VVV', 'FFF']
  }
}
function observer(obj) {
  if (typeof obj == 'object') {
    for (let key in obj) {
      defineReactive(obj, key, obj[key])
    }
  }
}
function defineReactive(obj, key, value) {
  Object.defineProperty(obj, key, {
    get() {
      console.log('获取:' + key)
      return value
    },
    set(val) {
      observer(val)
      console.log(key + "-数据改变了")
      value = val
    }
  })
}
observer(obj)

Vue3 响应式数据原理

vue3 的数据响应使用了 ES6 中的新增特性 Proxy。

Proxy,翻译过来的意思是代理,用在这里表示由它来代理某些操作。其功能类似于设计模式中的代理模式。可以理解成,在目标对象之前架设一层拦截,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

简而言之,Proxy 可以劫持整个对象,并返回一个新的对象。

优点:

  • 可以直接监听对象而非属性;

  • 可以直接监听数组的变化;

  • 有多种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等是Object.defineProperty 不具备的;

  • 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改。

模拟 vue3 中用 Proxy 实现的数据响应:

let obj = {
  key:'cue'
  flags: {
    name: ['AAA', 'VVV', 'FFF']
  }
}
function observerProxy(obj) {
  const handler = {
    get(target, key, receiver) {
      console.log("获取:" + key);
      if (typeof target[key] === "object" && target[key] !== null) {
       // 如果是对象,就添加 proxy 拦截
        return new Proxy(target[key], handler);
      }
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      console.log("设置:" + key);
      return Reflect.set(target, key, value, receiver);
    },
  };
  return new Proxy(obj, handler);
}
let newObj = observerProxy(obj);

组合式 API

  • vue2:选项式 API (Options API)

  • vue3:选项式 API、组合式 API( 推荐)

组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。

组合式 API 最基本的优势是它使我们能够通过组合函数来实现更加简洁高效的逻辑复用。

核心概念:

1. setup() 函数

  • setup() 函数是组合式 API 的入口点。在组件被创建之前调用,且仅当组件是 <script setup>{ setup() } 语法时才能使用。

  • 它提供了两个参数:props 和 context(一个普通的 JavaScript 对象,包含 { attrs, slots, emit, expose })。

  • 在 setup() 中定义的响应式状态、计算属性、方法等,需要通过 return 暴露给模板或其他组合式 API 使用。

    • 如果使用 <script setup> 语法糖则不需要 return

2. 响应式引用(Ref)

  • 使用 ref() 创建一个响应式的基本数据类型对象。在 JavaScript 代码中访问时,需要通过 .value 来获取或修改它的值,在模板中则不需要使用 .value。

3. 响应式对象(Reactive)

[riˈæktɪv]

  • 使用 reactive() 创建一个响应式的对象或数组。与 ref() 不同,reactive() 创建的对象可以直接访问和修改其属性,无需 .value。
<template>
    <div>{{ count }} </div>
    <div>{{ state.age}} </div>
    <button @click="changeCount">修改count</button>
    <button @click="changeAge">修改Age</button>
</template>

<script setup>
import { ref, reactive } from 'vue';

const count = ref(0);
const state = reactive({
    name: 'Alice',
    age: 30
})

function changeCount() {
    count.value++;
}
function changeAge() {
    state.age++;
}
</script>

<template>
    <div>{{ count }} </div>
    <div>{{ state.age}} </div>
    <button @click="changeCount">修改count</button>
    <button @click="changeAge">修改Age</button>
</template>

<script>
import { ref, reactive } from 'vue';
export default {
  setup() {
    const count = ref(0);
    const state = reactive({
      name: 'Alice',
      age: 30
    });
    function changeCount() {
        count.value++;
    }
    function changeAge() {
        state.age++;
    }
    return {
      count,
      state,
      changeCount, // 将方法暴露出去
      changeAge // 将方法暴露出去
    };

  }
};
</script>

4. watch 和 watchEffect

watch

wathc 接收 3 个参数:

  1. 数据源,可以是:

    • 一个 ref

    • 一个计算属性

    • 一个 getter 函数(有返回值的函数)

    • 一个响应式对象

    • 以上类型的值组成的数组

    注意:不能直接侦听响应式对象的属性值,用一个返回该属性的 getter 函数,例如:

    const obj = reactive({ count: 0 })
    
    // 错误,不能直接侦听响应式对象的属性值
    watch(obj.count, (newVal) => {
      console.log(`count is: ${newVal}`)
    })
    
    // 提供一个 getter 函数
    watch(
      () => obj.count,
      (newVal) => {
        console.log(`count is: ${newVal}`)
      }
    )
    
  2. 回调函数:它又接收 3 个参数,分别是:

    • 新值

    • 旧值

    • 清理副作用的回调函数

  3. 配置选项,可以是:

    • immediate:在侦听器创建时立即触发回调。

    • deep:深度遍历,以便在深层级变更时触发回调。

    • flush:回调函数的触发时机。pre:默认,dom 更新前调用,post: dom 更新后调用,sync 同步调用。

    • onTrack / onTrigger:用于调试的钩子。在依赖收集和回调函数触发时被调用。

观察单个响应式引用

<script setup>  
import { ref, watch } from 'vue';  
  
const count = ref(0);  
  
watch(count, (newVal, oldVal) => {  
  console.log(`count changed from ${oldVal} to ${newVal}`);  
});  
</script>

观察多个响应式引用

<script setup>  
import { ref, watch } from 'vue';  
  
const count = ref(0);  
const name = ref('Vue 3');  
  
watch([count, name], (newVal, oldVal) => {  
  console.log(`count or name changed`);  
  console.log(`count: ${newVal[0]} -> ${oldVal[0]}`);  
  console.log(`name: ${newVal[1]} -> ${oldVal[1]}`);  
});  
</script>

观察响应式对象

<script setup>  
import { reactive, watch } from 'vue';  
  
const state = reactive({ count: 0, name: 'Vue 3' });  
  
watch(state, (newVal, oldVal) => {  
  console.log('state changed');  
  console.log(newVal, oldVal);  
}, { deep: true }); // 使用 deep 选项深度观察  
</script>

停止观察

watch 函数返回一个停止观察的函数,可以调用它来停止触发回调。

<script setup>  
import { ref, watch } from 'vue';  
  
const count = ref(0);  
  
const stopWatch = watch(count, (newVal, oldVal) => {  
  console.log(`count changed from ${oldVal} to ${newVal}`);  
});  

// 假设在某个条件下停止观察  
if (/* some condition */) {  
  stopWatch();  
}  
</script>

watchEffect

watchEffect 适用于你不需要访问变化前后的值时,只需要在依赖变化时执行某些操作。

<script setup>  
import { ref, watchEffect } from 'vue';  
  
const count = ref(0);  
  
watchEffect(() => {  
  console.log(`count is: ${count.value}`);  
});  
</script>

区别

  1. 使用方式和监听目标

    • watch:

      • 需要明确告诉 watch 要监听哪个响应式数据。

      • 当这个数据变化时,watch 会执行一个你提供的回调函数。

      • 示例:watch(count, (newVal, oldVal) => { /* ... */ })

    • watchEffect:

      • 不需要告诉 watchEffect 要监听哪个数据,它会自动检测。

      • 只需要提供一个函数,这个函数内部使用了哪些响应式数据,watchEffect 就会自动监听这些数据。

      • 示例:watchEffect(() => { /* 使用了 count,watchEffect 会自动监听 count */ })

  2. 执行时机

    • watch:

      • 默认情况下,watch 是惰性的,只有数据实际变化时才会执行回调函数。

      • 如果你想在绑定时立即执行一次,可以设置 immediate: true

    • watchEffect:

      • 组件一加载,watchEffect 就会立即执行一次,以获取初始状态。

      • 之后,只有当它监听的数据变化时,才会再次执行。

  3. 监听深度

    • watchEffect:

      • 默认就是深度监听,如果你监听了一个对象或数组,它内部的变化也能检测到。
    • watch:

      • 默认只监听数据的引用变化,即整个对象或数组被替换时才会触发。

      • 如果要监听对象内部的变化,需要设置 deep: true

  4. 获取变化前后的值

    • watchEffect:

      • 只能获取到变化后的值,无法直接获取变化前的值。
    • watch:

      • 可以同时获取到变化前后的值,通过回调函数的参数传递。
  5. 适用场景

    • watchEffect:

      • 当你不需要知道具体哪些数据变化,或者副作用函数依赖于多个响应式数据时,使用 watchEffect 更方便。
    • watch:

      • 当你需要细粒度控制响应式数据的变化,或者需要访问数据变化前后的值时,使用 watch 更合适。

Fragments 片段

[fræɡˈments]

  • Vue2.x中,不支持多根节点组件

  • Vue3.x 中,组件可以包含多个根节点

Teleport 组件

<Teleport> Vue3 新组件,它允许将组件的内容传送(teleport)到当前组件之外的目标位置。

实际主要作用是将组件中 <Teleport> 包裹的内容传送到页面的根元素或其他地方,从而让 <Teleport> 中的内容不受组件样式的影响,同时又可以受组件状态控制,多使用在全局弹框中。

<button @click="open = true">Open Modal</button>

<Teleport to="body">
  <div v-if="open" class="modal">
    <p>Hello from the modal!</p>
    <button @click="open = false">Close</button>
  </div>
</Teleport>

<Teleport> 接收一个 to 属性来指定传送的目标。to 的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象。这段代码的作用就是告诉 Vue 把以下模板片段传送到 body 标签下。

emits 选项

Vue 2.x 中,可以定义一个组件可接收的 prop,但是无法声明它可以触发哪些事件。

Vue 3.x 中, 提供一个 emits 选项,和现有的 props 选项类似。这个选项可以用来定义一个组件可以向其父组件触发的事件。

<!--Vue2.x写法-->
<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {
    props: ['text']
  }
</script>
<!--Vue3.x写法-->
<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {
    props: ['text'],
    emits: ['accepted']
  }
</script>

Suspense 组件

Vue3 新增的异步组件,主要使用在组件懒加载中。

自带两个 slot 分别为 default、fallback,在等待异步组件时渲染,会加载 fallback 中的内容,直到异步组件加载完成,才会渲染 default

<template>  
  <Suspense>  
    <template #default>  
      <AsyncComponent />  
    </template>  
    <template #fallback>  
      <div>  
        <p>加载中...</p>  
      </div>  
    </template>  
  </Suspense>  
</template>  
  
<script setup>  
import { defineAsyncComponent } from 'vue';  
   
// defineAsyncComponent 是 Vue3 中用于注册异步组件的方法,通常与动态导入 import() 语法结合使用
const AsyncComponent = defineAsyncComponent(() =>  
   // AsyncComponent.vue 是个普通的 Vue 组件
  import('./components/AsyncComponent.vue') 
);  
</script>

当异步组件加载完成之前,页面会显示 div 中的内容"加载中..."

自定义双向数据绑定

在 Vue2 中,自定义双向数据绑定依赖于 v-model 指令的自定义用法,本质上是通过监听输入事件(input)并更新绑定的数据来实现的,所以会默认会传递 value 属性并监听 input 事件

<!-- CounterComponent.vue 自定义组件 子组件 -->
<template>  
  <div>  
    <button @click="increment">-</button>  
    <span>{{ value }}</span>  
    <button @click="decrement">+</button>  
  </div>  
</template>  
  
<script>  
export default {  
  props: ['value'],  
  methods: {  
    increment() {  
      this.$emit('input', this.value - 1);  
    },  
    decrement() {  
      this.$emit('input', this.value + 1);  
    }  
  }  
}  
</script>
<!-- 父组件 -->
<template>  
  <div>  
    <CounterComponent v-model="count" />  
    <p>Count in parent: {{ count }}</p>  
  </div>  
</template>  
  
<script>  
import CounterComponent from './CounterComponent.vue';  
  
export default {  
  components: {  
    CounterComponent  
  },  
  data() {  
    return {  
      count: 0  
    }  
  },
  watch: {
    count: {
      handler(newVal, oldVal) {
        console.log(newVal, oldVal)
      }
    }
  }
}  
</script>

在 Vue3 中,v-model 的自定义用法变得更加灵活。
通过 v-model:propName 指定绑定的 prop,其中 propName 是你希望在组件内部使用的 prop 名称,
通过 update:eventName 指定绑定的 event

<!-- CounterComponent.vue 自定义组件 子组件 -->
<template>  
  <div>  
    <button @click="decrement">-</button>  
    <span>{{ count }}</span>  
    <button @click="increment">+</button>  
  </div>  
</template>  
  
<script setup>  
import { defineProps, defineEmits } from 'vue';  
  
// 可以使用 count 而不是 value  
const props = defineProps({  
  count: Number  
});  
  
const emit = defineEmits(['update:count']);  
  
function increment() {  
  emit('update:count', props.count + 1);  
}  
  
function decrement() {  
  emit('update:count', props.count - 1);  
}  
</script>
<!-- 父组件 -->
<template>  
  <div>  
    <CounterComponent :count="count" @update:count="updateCount" />  
    <p>Count in parent: {{ count }}</p>  
  </div>  
</template>  
  
<script>  
import CounterComponent from './CounterComponent.vue';  
  
export default {  
  components: {  
    CounterComponent  
  },  
  data() {  
    return {  
      count: 0  
    }  
  },  
  methods: {  
    updateCount(newCount) {  
      this.count = newCount;  
    }  
  }  
}  
</script>

父组件中为什么使用 :count="count" 而不是 v-model="count"?

在 Vue3 中,当你不希望直接使用 v-model 默认的 value 和 input 命名约定时,你需要显式地传递 prop 和监听事件,而不是使用 v-model 指令的简写形式。

v-model 实际上是一个语法糖,它背后做了两件事情:

  1. 将 v-bind 用于 prop(默认是 value)。
  2. 将 v-on 用于事件(默认是 input),并将 value 赋给绑定的变量。

如果想要使用 v-model 可以这么改:

<template>    
  <div>    
    <button @click="decrement">-</button>    
    <span>{{ props.value }}</span>    
    <button @click="increment">+</button>    
  </div>    
</template>    
  
<script setup>    
import { defineProps, defineEmits } from 'vue'  
  
const props = defineProps({  
  value: Number  
})  
  
const emit = defineEmits(['update:modelValue']) // 对于 v-model,通常使用 update:modelValue  
  
function increment() {    
  emit('update:modelValue', props.value + 1);    
}  
  
function decrement() {    
  emit('update:modelValue', props.value - 1);    
}    
</script>
<!-- 父组件 -->
<template>    
  <div>    
    <CounterComponent v-model="count" />    
    <p>Count in parent: {{ count }}</p>    
  </div>    
</template>    
  
<script setup>    
import { ref, watch } from 'vue'  
import CounterComponent from './CounterComponent.vue';    
  
const count = ref(0)  
  
watch(count, (newVal, oldVal) => {  
  console.log(newVal, oldVal)  
})  
</script>

v-bind 合并行为

在 Vue2.x 中,如果一个元素同时定义了 v-bind="object" 和一个相同的独立 attribute,那么这个独立 attribute 总是会覆盖 object 中的绑定,较为死板。

<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 结果 -->
<div id="red"></div>

在Vue3.x 中,如果一个元素同时定义了 v-bind="object" 和一个相同的独立 attribute,那么绑定的声明顺序将决定它们如何被合并。这样开发者能够按照希望方式进行合并。

<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 结果 -->
<div id="blue"></div>

<!-- 模板 -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- 结果 -->
<div id="red"></div>

v-if 与 v-for 的优先级对比

Vue2.x 版本中在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用。

Vue3.x 版本中 v-if 总是优先于 v-for 生效。

插槽

  • Vue 2.6.0 版本以前使用 slot 和 slot-scope attribute。

  • Vue 2.6.0 版本以后引入了 v-slot,提供更好的支持 slot 和 slot-scope attribute 的 API 替代方案。

  • Vue 3.x 版本中废弃 slot 和 slot-scope 写法,统一使用 v-slot

listeners 合并到 attrs

在 Vue2.x 中,可以通过 this.$attrs 传递属性,通过 this.$listeners 传递方法。

在 Vue3.x 中,$listeners 被移除了,全部融入到了 $attrs 中。

虚拟 DOM 优化

Vue3 针对虚拟 DOM 做了以下改进:

  • 静态节点提升:将静态节点与动态节点分离,有效提高了渲染性能

  • 快速标记和补丁:采用了更加高效的标记和补丁方式,使得页面渲染更加快速和稳定

  • 更小的 bundle 大小:使用了 tree-shaking 和基于 ES2015 的模块系统,使得框架的 bundle 大小更加的小。

移除部分 API

  • 移除 $destroy 实例方法。
  • 移除 set 和 delete 函数
  • 移除过滤器 (filter),建议用方法调用或计算属性来替换。