Vue3初步了解及迁移指南

关于 Vue3 的重构背景,尤大是这样说的:

「Vue 新版本的理念成型于 2018 年末,当时 Vue 2 的代码库已经有两岁半了。比起通用软件的生命周期来这好像也没那么久,但在这段时期,前端世界已经今昔非比了

在我们更新(和重写)Vue 的主要版本时,主要考虑两点因素:首先是新的 JavaScript 语言特性在主流浏览器中的受支持水平;其次是当前代码库中随时间推移而逐渐暴露出来的一些设计和架构问题」

简要就是:

  • 利用新的语言特性(es6)

  • 解决架构问题

新特性和新功能

新特性

概览 Vue3 的新特性:

  • 速度更快:重写了虚拟Dom实现、编译模板的优化、更高效的组件初始化、undate 性能提高 1.32 倍、SSR 速度提高了 23 倍。

  • 体积减少:通过 webpack 的 tree-shaking 功能,可以将无用模块 “删除”,仅打包需要的模块。

  • 更易维护:组合式 Api,更灵活的逻辑组合与复用、更好的 Typescript 支持等。

  • 更接近原生:可以自定义渲染 API。

  • 更易使用:响应式 Api 暴露出来。

新功能

Vue 3 中需要关注的一些新功能包括:

  • 组合式 API *:将相同功能的变量进行一个集中式的管理,更容易维护代码;

  • script 的 setup 属性 * :SFC(单文件组件)组合 API 语法糖;

  • Teleport:将模板移动到 DOM 中 Vue app 之外的其他位置(比如,dialog 移到 body 下);

  • Fragments:SFC(单文件组件)<template> 下支持有多个根节点;

  • Emits 组件选项 **;

  • 创建自定义渲染器的 createRenderer API;

  • SFC(单文件组件)可状态驱动的 CSS 变量(即,<style> 可通过 v-bind 绑定变量)* ;

  • SFC(单文件组件)中的 <style scoped>,可以包括全局样式或只针对槽位内容的样式;

  • Suspense(实验性)。

带一个星号(*),表示现在在 Vue 2.7 中也支持;

带两个星号(**),表示在 Vue 2.7 中支持,但仅用于类型推断。

新的框架级建议

Vue 3 的支持库已经过重大更新,以下是新默认建议摘要:

  • 支持 Vue 3 的新版本路由器、开发工具和测试工具

  • 构建工具链:Vue CLI -> Vite

  • 状态管理:Vuex -> Pinia

  • IDE 支持:Vetur -> Volar

  • 新的命令行 TypeScript 支持:vue-tsc

  • SSG:VuePress -> VitePress

  • JSX:@vue/babel-preset-jsx->@vue/babel-plugin-jsx

Vue2 和 Vue3 区别

响应式原理

Vue2 响应式原理基础是 Object.defineProperty;Vue3 响应式原理基础是 Proxy。

  • Vue2:当一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

  • Vue3:当把一个普通的 JavaScript 对象作为 data 选项传给应用或组件实例的时候,Vue 会使用带有 getter 和 setter 的处理程序遍历其所有 property 并将其转换为 Proxy (opens new window)。这是 ES6 仅有的特性,但是在 Vue 3 版本也使用了 Object.defineProperty 来支持 IE 浏览器。两者具有相同的 Surface API,但是 Proxy 版本更精简,同时提升了性能。

defineProperty 的缺点:

  • 检测不到对象属性的添加和删除;

  • 数组 API 方法无法监听到;

  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题。

注意: Object.defineOProperty 是可以监听数组已有元素,Vue2 没有提供的原因是性能问题。

proxy 优势:

  • 可以劫持整个对象,并返回一个新对象,操作新的对象达到响应式目的;

  • 可以直接监听数组的变化(push、shift、splice),包括 length 的变化;

  • Map、Set、WeakMap、WeakSet 的支持。

注意: Proxy 不兼容 IE,也没有 polyfill、defineProperty 能支持到 IE9。

组合式API

Vue2 是 选项式 API(Option API),一个逻辑会散乱在文件不同位置(data、props、computed、watch、生命周期函数等),导致代码的可读性变差,需要上下来回跳转文件位置。Vue3 组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起。

除了增强了代码的可读性、内聚性,组合式 API 还提供了较为完美的逻辑复用性方案:

<!-- main.vue -->
<template>
  <span>mouse position {{x}} {{y}}</span>
</template>

<script setup>
import { ref } from  vue 
import useMousePosition from  "./useMousePosition"
const {x, y} = useMousePosition()
</script>
// useMousePosition.js
import { ref, onMounted, onUnmounted } from  vue 
function useMousePosition() {
  let x = ref(0)
  let y = ref(0)
  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }

  onMounted(() => {
    window.addEventListener( mousemove , update)
  })
  onUnmounted(() => {
    window.removeEventListener( mousemove , update)
  })

  return {
    x,
    y
  }
}

多根节点

Vue 3 支持了多根节点组件,也就是fragment;Vue 2 中,编写页面的时候,需要去将组件包裹在 <div>中,否则报错警告。

<!-- Vue 2 -->
<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>
<!-- Vue 3 -->
<template>
  <header>...</header>
  <main>...</main>
  <footer>...</footer>
</template>

异步组件

Vue3 提供 Suspense组件,允许程序在等待异步组件时渲染兜底的内容,如 loading ,使用户体验更平滑。使用它,需在模板中声明,并包括两个命名插槽:default 和 fallback。Suspense 确保加载完异步内容时显示默认插槽,并将 fallback 插槽用作加载状态。

<tempalte>
   <suspense>
     <template #default>
       <todo-list />
     </template>
     <template #fallback>
       <div>
         Loading...
       </div>
     </template>
   </suspense>
</template>

注意: 若想在 setup 中调用异步请求,需在 setup 前加 async 关键字。这时,会受到警告 async setup() is used without a suspense boundary

解决方案: 在父页面调用当前组件外包裹一层 Suspense 组件。

Teleport

Vue3 提供 Teleport 组件可将部分 DOM 移动到 Vue app之外的位置。比如项目中常见的 Dialog 组件:

<button @click="dialogVisible = true">点击</button>
<teleport to="body">
   <div class="dialog" v-if="dialogVisible">
   </div>
</teleport>

生命周期的变化

整体来看,变化不大,只是名字大部分需要 + on,功能上类似。

使用上 Vue3 组合式 API 需要先引入;Vue2 选项 API 则可直接调用。

常用生命周期函数:

注意: setup 函数是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地去定义。除了这些钩子函数外,Vue3 还增加了 onRenderTracked 和 onRenderTriggered 函数。

性能优化

Vue3 相比于 Vue2 虚拟 DOM 上增加 patchFlag 字段,可帮助 diff 时区分静态节点,以及不同类型的动态节点。一定程度地减少节点本身及其属性的比对。

Vue3 的 cacheHandler 可在第一次渲染后缓存事件。

打包优化

Vue3 中针对全局和内部的 API 进行了重构,并考虑到 tree-shaking 的支持。因此,全局 API 现在只能作为 ES 模块构建的命名导出进行访问。

通过这一更改,只要模块绑定器支持 tree-shaking,则 Vue 应用程序中未使用的 api 将从最终的捆绑包中消除,获得最佳文件大小。

类似的,也有诸如 transition、v-model 等标签或者指令被命名导出。只有在程序真正使用才会被捆绑打包。

TypeScript 支持

Vue3 由 TS 重写,相对于 Vue2 有更好地 TypeScript 支持。

  • Vue2 Option API 中 option 是个简单对象,而 TS 是一种类型系统,面向对象的语法,不是特别匹配。

  • Vue2 需要 vue-class-component 强化 vue 原生组件,也需要 vue-property-decorator 增加更多结合Vue特性的装饰器,写法比较繁琐。

选项式 vs 组合式 API

Vue 的组件可以按两种不同的风格书写:选项式 API 和组合式 API。

选项式 API(Options API)

使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 data、methods 和 mounted。选项所定义的属性都会暴露在函数内部的 this 上,this 会指向当前的组件实例。

组合式 API(Composition API)

通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。这个 setup 属性是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。

为什么要有组合式 API?

  • 更好的逻辑复用:最基本的优势是它能够通过组合函数来实现更加简洁高效的逻辑复用。

  • 更灵活的代码组织:将同一个逻辑关注点相关的代码被归为了一组,无需再为了一个逻辑关注点在不同的选项块间来回滚动切换。

  • 更好的类型推导:更好的支持 TypeScript。

  • 更小的生产包体积:组合式 API 比等价情况下的选项式 API 更高效,对代码压缩也更友好。这是由于 <script setup> 形式书写的组件模板被编译为了一个内联函数,和 <script setup> 中的代码位于同一作用域,不再需要依赖 this 上下文对象访问属性。这对代码压缩更友好,因为本地变量的名字可以被压缩,但对象的属性名则不能。

该选哪一个?

两种 API 风格都能够覆盖大部分的应用场景。它们只是同一个底层系统所提供的两套不同的接口。实际上,选项式 API 是在组合式 API 的基础上实现的!关于 Vue 的基础概念和知识在它们之间都是通用的。

选项式 API 以 “组件实例” 的概念为中心(即 this),对于有面向对象语言背景的用户来说,这通常与基于类的心智模型更为一致。同时,它将响应性相关的细节抽象出来,并强制按照选项来组织代码,从而对初学者而言更为友好。

组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题。这种形式更加自由,也需要你对 Vue 的响应式系统有更深的理解才能高效使用。相应的,它的灵活性也使得组织和重用逻辑的模式变得更加强大。

备注: 组合式 API 能够覆盖所有状态逻辑方面的需求,但按官方说法,并不会废弃选项式 API,也允许同时使用两种 API(在一个选项式 API 的组件中通过 setup() 选项来使用组合式 API)。

setup()

setup() 是 Vue 3 新增的函数,会在创建组件之前执行,有以下特性:

  • 由于在执行 setup 函数时尚未创建组件实例,因此在 setup 函数中没有 this,也就是说,除了 props 之外,无法访问组件中声明的任何属性——本地状态(data)、计算属性(computed)或方法(methods)。

  • 接收两个参数:props、context(包含三个组件的属性:attrs、slots、emit)。

  • props 参数是响应式的,当传入新的 prop 时,它将会被更新,所以不能使用 es6 解构,因为它会消除 prop 得响应性;如需解构 prop,可以通过使用 setup 函数中得 toRefs 来完成此操作。

    import { toRefs } from 'vue'
    setup(props) {
    	const { title } = toRefs(props)
    }
  • context 是一个普通的 JavaScript 对象,它不是响应式的,这意味着,可以安全地对 context 使用 ES6 解构。它暴露三个组件的 property:

    export default {
      setup(props, { attrs, slots, emit }) {
        // ...
      }
    }
  • 单文件组件使用时,可以返回一个对象,且可以在 <template> 中直接访问返回对象的属性。注意: setup 函数返回对象中的 refs,在模板中访问时是被自动解开的,因此不应在模板中使用 ref 对象的 .value

    <template>
      <div>{{ readersNumber }} {{ book.title }}</div>
    </template>
    <script>
      import { ref, reactive } from 'vue'
      export default {
        setup() {
          const readersNumber = ref(0)
          const book = reactive({ title: 'Vue 3 Guide' })
          return {
            readersNumber,
            book
          }
        }
      }
    </script>
  • setup 还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态。

  • setup 函数只能是同步的不能是异步的。

重大变化

全局 API

全局 Vue API 改为实例 API

Vue 2.x 有许多全局 API 和配置可以全局改变 Vue 的行为,这会引起以下问题:

  • 全局配置使得在测试过程中很容易意外污染其他测试用例。

  • 全局配置使得在同一页面上的多个应用程序之间,共享同一 Vue 组件(具有不同的全局配置)变得困难。

为了避免这些问题,Vue 3 中引入了 createApp API 创建 Vue 实例,将全局 API 调用改为实例方法的调用。

Vue 3 首先会调用 createApp 返回一个应用实例,然后在实例中调用对应方法:

<!--Vue 2-->
import Vue from 'vue'
<!-- Vue 3 -->
import { createApp } from 'vue'
const app = createApp({})

全局 API tree-shaking

像 webpack 和 Rollup(基于 Vite)等模块打包器支持 tree-shaking,tree-shaking 是 “删除死代码” 的术语。

不幸的是,由于在以前的 Vue 版本中代码的编写方式,像 Vue.nexttick() 这样的全局 Api 是不可删除的,不管实际中是否有使用,都会被包含在最终的包中

在 Vue 3 中,全局和内部 API 已经重新构建,并考虑到了 tree-shaking 支持。因此,全局 API 现在能作为 ES 模块构建的命名导出来访问。

import { nextTick } from 'vue'

通过此更改,如果模块打包器支持 tree-shaking,则未在 Vue 应用程序中使用的全局 API 将从最终捆绑包中删除,从而获得最佳文件大小。

Vue 2.x 中的这些全局 API 受此更改影响:

  • Vue.nextTick

  • Vue.version

  • Vue.observable(替换为 Vue.reactive)

  • Vue.compile(仅在完整版本中)

  • Vue.set(仅在兼容版本中)

  • Vue.delete(仅在兼容版本中)

除了公共 API 之外,许多内部组件/帮助程序现在也以命名导出的形式导出:

<transition>
  <div v-show="ok">hello</div>
</transition>

被编译成类似于以下内容:

import { h, Transition, withDirectives, vShow } from 'vue'
export function render() {
  return h(Transition, [withDirectives(h('div', 'hello'), [[vShow, this.ok]])])
}

模板指令

重写 v-model 指令

在 Vue 2.x 中,v-model 在组件上使用相当于:

<ChildComponent v-model="title" />
<!-- 相当于 -->
<ChildComponent :value="title" @input="title = $event" />

Vue 3 对 v-model 指令做以下修改:

  • 在自定义组件上使用时,v-model 指令在子组件的 prop 的默认属性名更改:value-> modelValue,事件默认名称更改:input -> update:modelValue;

  • v-model 可以替换 v-bind.sync 和组件的 model 选项,且 Vue 3 已删除 v-bind.sync 用法和组件的 model 选项;

  • 同一个组件可以绑定多个 v-model;

在 Vue 3 中,v-model 在组件上使用相当于:

<ChildComponent v-model="title" />
<!-- 相当于 -->
<ChildComponent :modelValue="title" @update:modelValue="title = $event" />
<!-- 自定义 mode 名称 -->
<ChildComponent v-model:title="title" />
<!-- 多个 v-model -->
<ChildComponent v-model:title="title" v-model:content="content" />
<!-- 自定义 v-model 修饰符 -->
<ChildComponent v-model.capitalize="value" />

key 使用变化

变化概述:

  • key 在 v-if/v-else/v-else-if 分支不再是必需的,因为 Vue 现在自动生成唯一的键;

  • 如果手动提供 key,那么在每个分支必须是唯一的;

  • <template v-for> 的 key 应该放在 <template> 标签上,而不是放在它的子标签上。

v-if 与 v-for 优先级变化

在 Vue 3.x 中,同一个元素上,v-if 将始终具有比 v-for 更高的优先级。

v-bind 合并行为

在 3.x 中,v-bind 的绑定顺序会影响渲染结果。

在 2.x 中,如果一个元素同时绑定 v-bind="object" 和定义了一个相同名称的单个属性,则该单个属性将始终覆盖 Object:

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

在 3x 中,如果一个元素同时绑定 v-bind="object" 和定义了一个相同名称的单个属性,则声明绑定的顺序决定了它们的合并方式,即后面的定义,会覆盖前面的:

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

移除 .native 修饰符

在 3.x 中,v-on 的 .native 修饰符已被移除。

在 2.x 中,默认情况下,传递给组件的 v-on 事件监听器,只能通过 this.$emit 来触发。如果要将监听器添加到子组件的根元素,则需要使用 .native 修饰符:

<my-component
  v-on:close="handleComponentEvent"
  v-on:click.native="handleNativeClickEvent"
/>

在 3.x 中,v-on 的 .native 修饰符已被移除。同时,新 emits 选项允许子组件定义需要触发哪些事件。

也就是说,Vue 3.x 会将所有未在子组件的 emits 选项 定义的监听器,作为子组件根元素的事件侦听器(除非子组件设置了 inheritAttrs: false 选项)。

<my-component
  v-on:close="handleComponentEvent"
  v-on:click="handleNativeClickEvent"
/>
<script>
  export default {
    emits: ['close']
  }
</script>

组件

函数式组件

在 Vue 2.x 中,函数式组件有两个主要用例:

  • 作为性能优化,因为它们的初始化速度比有状态组件快得多;

  • 返回多个根节点。

// Vue 2 函数式组件示例
export default {
  functional: true,
  props: ['level'],
  render(h, { props, data, children }) {
    return h(`h${props.level}`, data, children)
  }
}
// Vue 2 函数式组件示例使用 <template>
<template functional>
  <component
    :is="`h${props.level}`"
    v-bind="attrs"
    v-on="listeners"
  />
</template>
<script>
export default {
  props: ['level']
}
</script>

然而,在 Vue 3.x 中,有状态组件的性能已经提高到可以忽略不计的程度。此外,有状态组件现在还包括返回多个根节点的能力。

因此,函数式组件有以下变化:

  • 在 3.x 中,函数式组件 2.x 的性能提升可以忽略不计,因此建议只使用有状态的组件;

  • 函数式组件只能使用接收 props 和 context(包含组件的 attrs、slots、和 emit 属性)的普通函数创建,也就是说,不需要定义 { functional: true } 组件选项;

  • 非兼容变更:SFC(单文件组件)的 template 标签上的 functional 属性已被移除;

  • 非兼容变更:通过函数创建组件的 { functional: true } 选项已被移除。

// Vue 3 函数式组件示例
import { h } from 'vue'
const DynamicHeading = (props, context) => {
  return h(`h${props.level}`, context.attrs, context.slots)
}
DynamicHeading.props = ['level']
export default DynamicHeading

在 SFC 上,删除 template 标签上的 functional 属性,并将 props 的所有引用重命名为 $props,将 attrs 重命名为 $attrs:

// Vue 3 函数式组件示例使用 <template>
<template>
  <component
    v-bind:is="`h${$props.level}`"
    v-bind="$attrs"
  />
</template>
<script>
export default {
  props: ['level']
}
</script>

异步组件

异步组件加载变化:

  • 新增异步组件的辅助方法:defineAsyncComponent;

  • 方法中的 component 选项重命名为 loader;

  • Loader 函数本身并不接收 resolve 和 reject 参数,且必须返回一个 Promise。

在 Vue 2.x 中,异步组件载入:

const asyncModal = () => import('./Modal.vue')
// 或者
const asyncModal = {
  component: () => import('./Modal.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
}

在 Vue 3.x 中,异步组件载入:

import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'

const asyncModal = defineAsyncComponent(() => import('./Modal.vue'))
// 或者
const asyncModalWithOptions = defineAsyncComponent({
  loader: () => import('./Modal.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})

此外,在 Vue 3.x 中,加载器函数不再接收 resolve 和 reject 参数,并且必须始终返回一个 Promise:

// Vue 2.x
const oldAsyncComponent = (resolve, reject) => {}

// Vue 3.x
const asyncComponent = defineAsyncComponent(
  () => new Promise((resolve, reject) => {})
)

emits 选项

Vue 3 现在提供了一个 emits 选项,类似于现有 props 选项。此选项可用于定义组件可以向其父级发出的事件。

在 Vue 2.x 中,可以定义组件接收的 props,但不能显示声明组件可以向父级发出哪些事件(默认所有事件都可以):

<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {
    props: ['text']
  }
</script>

在 Vue 3.x 中,与 props 类似,组件发出的事件可以使用 emits 选项定义:

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

注意: 由于移除了 .native 修饰符,这一点尤其重要。任何未声明的事件侦听器 emits 现在都将包含在组件的 $attrs 中,默认情况下将绑定到组件的根节点。

渲染函数

渲染函数 API

渲染函数 API 变化:

  • h 现在是全局导入的,而不是作为参数传递给渲染函数;

  • 渲染函数参数更改为在有状态组件和功能组件之间更加一致;

  • VNodes 现在有一个扁平的 props 结构。

在 Vue 2.x 中,render 函数会自动接收 h 函数(createElement 函数别名)作为参数:

export default {
  render(h) {
    return h('button-counter')
  }
}

在 3.x 中,h 现在是全局导入的,而不是作为参数自动传递:

import { h, resolveComponent } from 'vue'
export default {
  setup() {
    const ButtonCounter = resolveComponent('button-counter')
    return () => h(ButtonCounter)
  }
}

注意: 在 Vue 3.x 中,VNode 是上下文无关的,不能再使用字符串 ID 来隐式查找已注册的组件,因此,需要使用 resolveComponent 解析。

Slot 统一

Vue 3.x 统一了普通 slot 和作用域 slot:

  • this.$slots 现在将 slots 作为函数公开;

  • 非兼容:移除 this.$scopedSlots。

在 Vue 2.x 中,定义 slot 内容节点上的数据属性:

h(LayoutComponent, [
  h('div', { slot: 'header' }, this.header),
  h('div', { slot: 'content' }, this.content)
])
// 引用作用域插槽
this.$scopedSlots.header

在 Vue 3.x 中,插槽被定义为当前节点的子节点作为对象:

h(LayoutComponent, {}, {
  header: () => h('div', this.header),
  content: () => h('div', this.content)
})
// 引用作用域插槽
this.$slots.header()

$listeners 合并到 $attrs

Vue 3.x 删除了 $listeners 对象,事件监听器包含在 $attrs 对象。

在 Vue 2.x 中,使用 $attrs 对象接收传递给组件的属性,使用 $listeners 对象接收传递给组件的事件监听器。结合使用 inheritAttrs: false,可以将这些属性和监听器应用到组件内的其他元素,而不是组件的根元素:

<template>
  <label>
    <input type="text" v-bind="$attrs" v-on="$listeners" />
  </label>
</template>
<script>
  export default {
    inheritAttrs: false
  }
</script>

在 Vue 3.x 的虚拟 DOM 中,$listeners 对象已被删除。事件监听器只是前缀为 on 的属性,是 $attrs 对象的一部分:

<template>
  <label>
    <input type="text" v-bind="$attrs" />
  </label>
</template>
<script>
  export default {
    inheritAttrs: false
  }
</script>

如果此组件接收到一个 id 属性和一个 v-on:close 侦听器,则该 $attrs 对象现在将如下所示:

{
  id: '',
	onClose: () => {}
}

$attrs 包括 class 和 style

在 Vue 3.x 中,$attrs 包含传递给组件的所有属性,包括 class 和 style。

在 Vue 2.x 中,class 和 style 属性有一些特殊处理,使它们不包含在中 $attrs 对象。

如果使用时出现 inheritAttrs: false,所有 $attrs 对象内的属性不再自动添加到根元素中,而是由开发人员决定将它们添加到何处;而 class 和 style 不是 $attrs 的一部分,仍将应用于组件的根元素。

在 Vue 3.x 中,$attrs 包含所有属性,这样可以更轻松地将所有属性应用于不同的元素。

自定义元素交互

自定义元素交互变化:

  • 非兼容:自定义元素白名单现在在模板编译期间执行,应该通过编译器选项而不是运行时配置来配置;

  • 非兼容:特定 is 属性仅限于保留的 component 标签;

  • 新增 v-is 指令来支持 2.x 用例,其中在原生元素上使用了 v-is 来处理原生 HTML 解析限制。

自定义元素

<plastic-button></plastic-button>

在 Vue 2.x 中,如果想添加在 Vue 外部定义的自定义元素(如,使用 Web 组件 API),需要告之 Vue 将其视为自定义元素:

Vue.config.ignoredElements = ['plastic-button']

在 Vue 3.x 中,此检查在模板编译期间执行指示编译器将其视为自定义元素。

如果使用构建步骤,将 isCustomElement 选项传递给 Vue 模板编译器。

如果使用 vue-loader,通过 compilerOptions 选项传递:

// webpack.config.js
rules: [
  {
    test: /\.vue$/,
    use: 'vue-loader',
    options: {
      compilerOptions: {
        isCustomElement: tag => tag === 'plastic-button'
      }
    }
  }
]

如果使用动态模板编译,则:

const app = Vue.createApp({})
app.config.compilerOptions.isCustomElement = tag => tag === 'plastic-button'

注意: 运行时配置只影响运行时模板的编译——它不会影响预编译的模板。

自定义内置元素

自定义元素规范提供了一种通过将属性添加到内置元素,来将自定义元素用作自定义内置元素的方法:is。

<button is="plastic-button">Click Me!</button>

在浏览器普遍使用 is 属性之前, Vue 的特殊属性 is 是模拟原本的属性行为。 然而,在 Vue 2.x 时,is 被渲染为一个名为 plastic-button 的 Vue 组件。 这将阻止上面提到的自定义内置元素的原本使用。

Vue 3.x 只将 is 属性的这一特殊处理(渲染为 Vue 组件)用于 component 标签,而在其他组件上,is 就是一个普通的、包含在 $attr 对象的属性。

DOM 内模板解析

在 Vue 3.x 中,将 is 渲染为 Vue 组件的解决方案:

is 属性值加 vue: 前缀:

<table>
  <tr is="vue:blog-post-row"></tr>
</table>

使用新增的 v-is 指令:

<!-- 注意:必需加单引号 -->
<tr v-is="'blog-post-row'"></tr>

其他小改动

生命周期钩子:destroyed 重命名为 unmounted;beforeDestroy 重命名为 beforeUnmount

props 对象 default 函数不能再访问 this

在 Vue 3.x 中,props 选项默认值函数不再能访问 this。

替代方案:把组件接收到的原始 prop 作为参数传递给默认函数:

import { inject } from 'vue'
export default {
  props: {
    theme: {
      default (props) {
        // `props` 是传递给组件的原始值。
        // 在任何类型/默认强制转换之前
        // 也可以使用 `inject` 来访问注入的 property
        return inject('theme', 'default-theme')
      }
    }
  }
}

自定义指令变化

  • 指令的钩子函数已经重新命名,以便更好地与组件的生命周期保持一致。

  • 钩子函数的传参中,binding.expression 已删除。如,在 Vue 2.x 中,v-my-directive="1 + 1",binding.expression 为 "1 + 1"。

在 Vue 2.x 中,自定义指令使用以下钩子创建:

  • bind:指令绑定到元素后触发(只触发一次)。

  • inserted:元素插入父 DOM 后触发。

  • update:当元素更新,但子元素尚未更新时,将调用此钩子。

  • componentUpdated:一旦组件和子级被更新,就会调用这个钩子。

  • unbind:一旦指令被移除,就会调用这个钩子。也只调用一次。

Vue.directive('highlight', {
  bind(el, binding, vnode) {
    el.style.background = binding.value
  }
})

在 Vue 3 中,钩子函数已经重新命名:

  • bind(重命名):beforeMount

  • inserted(重命名):mounted

  • beforeUpdate(新增):元素本身更新之前调用的。

  • componentUpdated(重命名):updated

  • update(移除):多余的,请改用 updated

  • beforeUnmount(新增):在卸载元素之前调用。

  • unbind(重命名):unmounted

const MyDirective = {
  beforeMount(el, binding, vnode, prevVnode) {},
  mounted() {},
  beforeUpdate() {}, // 新
  updated() {},
  beforeUnmount() {}, // 新
  unmounted() {}
}

data 选项

在 Vue 3.x 中:

  • 非兼容:data 选项不再接收纯 JavaScript 对象,必需是 function 声明。

  • 非兼容:当合并来自 mixins 或 extends 的多个 data 返回值时,并现在是浅合并而不是深合并(仅合并根级属性)。

属性强制行为

这是一个低级别的内部 API 更改,不会影响大多数开发人员。

主要变化:

  • 删除枚举属性的内部概念,将这些属性视为普通的非布尔属性;

  • 非兼容:如果值为布尔值,则不再删除属性 false。相反,它设置为 attr="false",要删除该属性,请使用 null 或 undefined。

transition 的 class 名更改

在 Vue 3.x 中,transition 的 class 名变化:v-enter 重命名为 v-enter-from;v-leave 重命名为 v-leave-from。

TransitionGroup 默认不再渲染根元素

transition-group 标签默认情况下不再渲染根元素,但仍然可以使用该 tag 属性创建一个。

在 Vue 2.x 中,transition-group 标签像其他自定义组件一样,需要一个根元素,默认情况下是 <span>,但可以通过 tag 属性进行自定义。

<transition-group tag="ul">
  <li v-for="item in items" :key="item">
    {{ item }}
  </li>
</transition-group>

在 Vue 3 中,因为已经支持片段,transition-group 标签默认情况下不再呈现一个根元素:

<transition-group>
  <li v-for="item in items" :key="item">
    {{ item }}
  </li>
</transition-group>

watch 数组

在 Vue 3 中,watch 数组时,回调只会在数组被替换时触发。如果需要数组子元素值变化时触发,则必须指定 deep 选项。

template 标签渲染

在 Vue 3 中,template 标签在没有特殊指令(v-if/else-if/else、v-for、v-slot)时,被视为普通元素,相当于原生 template 元素,而不是渲染其内部内容。

挂载 Vue 实例,不会替换被挂载的元素

在 Vue 2.x 中,当挂载一个具有 template 的应用程序时,渲染的内容会替换挂载的元素;

在 Vue 3.x 中,渲染的应用程序是被附加到挂载元素的子元素,替换该元素的 innerHTML。

VNode 生命周期事件

在 Vue 2 中,可以使用事件来监听组件生命周期中的关键阶段。这些事件的名称以前缀 hook: 开头,后跟相应生命周期挂钩的名称。

在 Vue 3 中,此前缀已更改为 vue:,此外,这些事件现在可用于 HTML 元素和组件。

在 Vue 2 中,生命周期事件前缀为 hook:

<template>
  <child-component @hook:updated="onUpdated">
</template>

在 Vue 3 中,生命周期事件前缀为 vue:

<template>
  <child-component @vue:updated="onUpdated">
</template>

移除的 API

实例方法:$on、$off、$once

在 Vue 3.x 中,删除了实例的 $on、$off、$once 方法。替换方案:将监听器将 props 参数传给组件。

实例方法:$destroy

用户不应该再手动管理单个 Vue 组件的生命周期。

全局和实例:$set、$delete

基于 proxy 的检测变更,不再需要它们。

实例属性:$children

Vue 3.x 删除了实例的 $children 属性。如果需要访问子组件实例,建议使用 refs。

v-on 的 keyCode 修饰符

在 Vue 3.x 中:

  • 不兼容:v-on: 不再支持使用数字(即 keyCodes)作为修饰符;

  • 不兼容:不再支持全局的 config.keyCodes。

在 Vue 2.x 中,keyCodes 支持数字和配置:

Vue.config.keyCodes = {
  f1: 112
}
<input v-on:keyup.112="showHelpText" />
<input v-on:keyup.f1="showHelpText" />

在 Vue 3.x 中,由于 KeyboardEvent.keyCode 已弃用,因此不再支持。建议对要用作修饰符的任何键使用 kebab-case 名称:

<input v-on:keyup.page-down="nextPage">
<!-- 匹配 q 和 Q -->
<input v-on:keypress.q="quit">

注意: 语法限制会阻止匹配某些字符,如," ' / = > .,对于这些字符,应该改为在监听器内部进行检查。

Filters

Vue 3.x 移除了 filters 选项。建议用 computed 的属性或 methods 的方法替换它们。

如果使用的是全局注册的过滤器,则可以通过 globalProperties 定义方法,可用于所有组件:

const app = createApp({})
app.config.globalProperties.$filters = {
  currencyUSD(value) {
    return '$' + value
  }
}

内联模板属性

Vue 3.x 移除了对内联模板功能的支持。

在 Vue 2.x 中,Vue 提供了 inline-template 子组件的属性,以将其内部内容用作其模板,而不是将其视为分布式内容(可渲染的组件)。

在 Vue 3.x 中的替代方案:

<script type="text/html" id="my-comp-template">
  <div>{{ hello }}</div>
</script>
const MyComp = {
  template: '#my-comp-template'
}

propsData 选项

Vue 3.x 移除了propsData 选项。

propsData 用于在创建 Vue 实例期间将值传递给 props。

const Comp = Vue.extend({
  props: ['username'],
  template: '<div>{{ username }}</div>'
})
new Comp({
  propsData: {
    username: 'Evan'
  }
})

在 Vue 3.x 中,使用 createApp 的第二个参数:

const app = createApp(
  {
    props: ['username'],
    template: '<div>{{ username }}</div>'
  },
  { username: 'Evan' }
)

Vue 3 代码示例

<template>
  <a-modal
    v-model:visible="reactData.visible"
    title="标题"
    @cancel="handleCancel"
    @ok="handleOk"
  >
    <a-form
      ref="refForm"
      :model="reactData.formData"
      :rules="formRules"
      :label-col="{ span: 4 }"
      :wrapper-col="{ span: 20 }"
      autocomplete="off"
    >
      ...
    </a-form>
  </a-modal>
</template>
<script setup lang="ts">
import { ref, defineEmits, defineProps, reactive, watch, computed } from 'vue';
import type { FormInstance } from 'ant-design-vue';

const formRules = {}
const refForm = ref<FormInstance>()

// props
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
    default: false
  }
})
// emits
const emits = defineEmits(['update:visible'])

// data
const reactData = reactive({
  loading: false,
  visible: false,
  formData: {
    keyword: '',
    birthMonth: ''
  }
})

// watch
watch(
  () => props.visible,
  (newValue, oldValue) => {
    reactData.visible = newValue
  }
)
// computed
let isPasswordSame = computed(() => {
  return reactData.formData.password === reactData.formData.newPassword
})

// methods
function handleCancel () {}
function handleOk () {}
</script>

常见问题

Vue3中,怎么定义 Vue2 的 created 生命周期函数?

Vue 3 没有 beforeCreated、created 生命周期钩子,而 setup() 函数处于生命周期 beforeCreated 和 created 两个钩子函数之前,因此,相关逻辑可以直接放在 setup() 函数中。

参考资料

Vue3 Migration Guide

Vue3 迁移指南(中文)

vue3 - Breaking Changes

vue3 - New Framework-level Recommendations

vue3 区别于 vue2 的“与众不同”

最后更新于