前言

Vue 3已经正式发布,我们现有的项目基本都是基于Vue 2进行开发的,如何由Vue 2.x迁移到Vue 3.x官方文档已经提供了迁移指南,本文是对官方迁移指南的总结。

重大改变

Vue 3相较于Vue 2有许多Break Change的重大改变,是我们在迁移时需要重点关注的:

全局API

  • 变更为使用应用程序实例
  • 全局和内部API可以被tree shaking

模版指令

  • v-model用法的变更
  • v-for节点上key用法的变更
  • v-ifv-for优先级的变更
  • v-bind="object"合并策略的变更
  • v-for中的ref不再注册为ref数组

组件

  • 只能使用普通函数创建功能组件
  • functional属性及选项被遗弃
  • 使用defineAsyncComponent创建异步组件

渲染函数

  • 渲染函数的API变更
  • $scopedSlotsproperty已删除,所有插槽都通过$slots作为函数暴露
  • 自定义指令的API和组组件的生命周期保持一致
  • 某些过渡class被重命名
  • 组件的watch选项和实例的$watch方法不再支持点分隔字符串路径
  • 应用程序容器的innerHTML将替换为根组件模版

其他改变

  • destroyed生命周期被重命名为unmountedbeforeDestroy被重命名为beforeUnmount
  • 组件的默认属性不能访问this
  • data选项应使用声明为函数
  • mixindata与组件的data选项只进行简单的合并
  • attribute强制行为的变更
  • <template>在没有特殊的指令标记时,将被视为普通的元素,而不是渲染其内容

移除API

  • 按键修饰符
  • 移除$on$off$once实例方法
  • 移除过滤器,建议使用计算属性或方法
  • 移除inline-template属性
  • 移除$destroy实例方法

全局API——createApp

Vue 2.x有许多全局API和配置,这些API和配置可以全局改变Vue的行为。例如,要创建全局组件,可以使用Vue.component

从技术上讲,Vue 2没有“app”的概念,我们定义的应用程序只是通过new Vue()创建的根Vue实例。从同一个Vue构造函数创建的每个根实例共享相同的全局配置,因此:

  • 全局配置使得在测试期间很容易意外地污染其他测试用例;
  • 全局配置使得在同一页面上的多个“app”之间共享同一个 Vue 副本非常困难。

Vue 3.x中引入了一个新的全局API:createApp,调用createApp返回一个应用实例:

1
2
3
import { createApp } from 'vue'

const app = createApp({})

任何全局改变 Vue 行为的 API 现在都会移动到应用实例上,以下是当前全局 API 及其相应实例 API 的表:

2.x 全局 API 3.x 实例 API (app)
Vue.config app.config
Vue.config.productionTip 移除
Vue.config.ignoredElements app.config.isCustomElement
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use

使用createApp(/* options */)初始化后,应用实例app可用于挂载具有app.mount(domTarget)

Vue 3 应用程序实例还可以提供可由应用程序内的任何组件注入的依赖项:

1
2
3
4
5
6
7
8
9
10
11
12
// 在入口
app.provide('guide', 'Vue 3 Guide')

// 在子组件
export default {
inject: {
book: {
from: 'guide'
}
},
template: `<div>{{ book }}</div>`
}

在应用程序之间共享配置 (如组件或指令) 的一种方法是创建工厂功能,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createApp } from 'vue'
import Foo from './Foo.vue'
import Bar from './Bar.vue'

const createMyApp = options => {
const app = createApp(options)
app.directive('focus' /* ... */)

return app
}

createMyApp(Foo).mount('#foo')
createMyApp(Bar).mount('#bar')

全局 API Treeshaking

Vue 3中,全局和内部API都经过了重构,并考虑到了tree-shaking的支持。因此,全局API现在只能作为ES模块构建的命名导出进行访问。例如,我们想要手动操作DOM

1
2
3
4
5
import { nextTick } from 'vue'

nextTick(() => {
// 一些和DOM有关的东西
})

受影响的API如下:

  • Vue.nextTick
  • Vue.observable (用Vue.reactive替换)
  • Vue.version
  • Vue.compile
  • Vue.set
  • Vue.delete

除了公共API,许多内部组件/帮助器现在也被导出为命名导出,只有当编译器的输出是这些特性时,才允许编译器导入这些特性,例如以下模板:

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

被编译为类似于以下的内容:

1
2
3
4
5
import { h, Transition, withDirectives, vShow } from 'vue'

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

这实际上意味着只有在应用程序实际使用了Transition组件时才会导入它。换句话说,如果应用程序没有任何 Transition组件,那么支持此功能的代码将不会出现在最终的捆绑包中。

v-model

  • v-modelprop和事件默认名称已更改
  • v-bind.sync修饰符和组件的model选项已移除,可用v-model作为代替
  • 可以在同一个组件上使用多个v-model进行双向绑定
  • 可以自定义v-model 修饰符

Vue 2中,开发者使用v-model指令必须使用名为valueprop。如果开发者出于不同的目的需要使用其他的prop,他们就不得不使用v-bind.sync

Vue 3中,双向数据绑定的API已经标准化,减少了开发者在使用v-model指令时的混淆并且在使用 v-model指令时可以更加灵活。

3.x中,自定义组件上的v-model相当于传递了modelValue prop并接收抛出的 update:modelValue事件:

1
2
3
4
5
6
7
8
<ChildComponent v-model="pageTitle" />

<!-- 简写: -->

<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>

若需要更改model名称,而可以将一个argument传递给model

1
2
3
4
5
<ChildComponent v-model:title="pageTitle" />

<!-- 简写: -->

<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />

这也可以作为.sync修饰符的替代,而且允许我们在自定义组件上使用多个v-model

1
2
3
4
5
6
7
8
9
10
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />

<!-- 简写: -->

<ChildComponent
:title="pageTitle"
@update:title="pageTitle = $event"
:content="pageContent"
@update:content="pageContent = $event"
/>

key attribute

  • 对于v-if/v-else/v-else-if的各分支项key将不再是必须的,因为现在Vue会自动生成唯一的key
  • <template v-for>key应该设置在<template>标签上 ,而不是设置在它的子节点上

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

两者作用于同一个元素上时v-if会拥有比v-for更高的优先级。由于语法上存在歧义,建议避免在同一元素上同时使用两者。比起在模板层面管理相关逻辑,更好的办法是通过创建计算属性筛选出列表,并以此创建可见元素。

v-bind 合并行为

v-bind的绑定顺序会影响渲染结果,如果一个元素同时定义了v-bind="object"和一个相同的单独的property,那么声明绑定的顺序决定了它们如何合并。

1
2
3
4
5
6
7
8
9
<!-- template -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- result -->
<div id="blue"></div>

<!-- template -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- result -->
<div id="red"></div>

v-for的Ref数组

Vue 2中,在v-for里使用的ref attribute会用ref数组填充相应的$refs property。在Vue 3中,这样的用法将不再在$ref中自动创建数组。要从单个绑定获取多个ref,需要将ref绑定到一个更灵活的函数上:

1
<div v-for="item in list" :ref="setItemRef"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { ref, onBeforeUpdate, onUpdated } from 'vue'

export default {
setup() {
let itemRefs = []
const setItemRef = el => {
itemRefs.push(el)
}
onBeforeUpdate(() => {
itemRefs = []
})
onUpdated(() => {
console.log(itemRefs)
})
return {
itemRefs,
setItemRef
}
}
}

函数式组件

  • 3.x中,函数式组件的性能提升可以忽略不计,因此建议只使用有状态的组件
  • 函数式组件只能使用接收propscontext的普通函数创建
  • functional attribute在单文件组件 (SFC) <template>已被移除
  • { functional: true }选项在通过函数创建组件已被移除

Vue 3中,所有的函数式组件都是用普通函数创建的,换句话说,不需要定义{ functional: true }组件选项。此外,现在不是在render函数中隐式提供h,而是全局导入h

异步组件

Vue 2中异步组件是通过将组件定义为返回Promise的函数来创建的:

1
const asyncPage = () => import('./NextPage.vue')

或者,对于带有选项的更高阶的组件语法:

1
2
3
4
5
6
7
const asyncPage = {
component: () => import('./NextPage.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
}

Vue 3中,由于函数式组件被定义为纯函数,因此异步组件的定义需要通过将其包装在新的defineAsyncComponent助手方法中来显式地定义,同时component选项现在被重命名为loader,以便准确地传达不能直接提供组件定义的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'

// 不带选项的异步组件
const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))

// 带选项的异步组件
const asyncPageWithOptions = defineAsyncComponent({
loader: () => import('./NextPage.vue'),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})

渲染函数 API

Vue 3.x中,h是全局导入的,而不是作为参数自动传入渲染函数:

1
2
3
4
5
6
7
import { h } from 'vue'

export default {
render() {
return h('div')
}
}

统一 Slot

Vue 3.x中,插槽被定义为当前节点的子对象,当你需要以编程方式引用作用域slot时,它们现在被统一到$slots选项中。

自定义指令

Vue 3中,将自定义指令的API与组件的生命周期保持一致:

  • bind → beforeMount,指令绑定到元素后发生,只发生一次。
  • inserted → mounted,元素插入父 DOM 后发生。
  • beforeUpdate,在元素本身更新之前调用。
  • 移除update,改用updated
  • componentUpdated → updated,组件和子级被更新,调用这个钩子。
  • beforeUnmount,在卸载元素之前调用。
  • unbind -> unmounted,指令被移除,调用这个钩子。

过渡的 class 名更改

过渡类名v-enter修改为v-enter-from、过渡类名v-leave修改为v-leave-from,变得更加明确易读。

在 prop 的默认函数中访问 this

在属性的默认函数中无法访问this,替代方案为:

  • 把组件接收到的原始 prop作为参数传递给默认函数;
  • 注入API可以在默认函数中使用:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import { inject } from 'vue'

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

Data 选项

data组件选项声明不再接收纯JavaScript object,只接受返回objectfunction。当合并来自mixinextend的多个data返回值时,是浅层次合并的而不是深层次合并的(只合并根级属性)。

attribute 强制行为

删除枚举attribute的内部概念,并将这些attribute视为普通的非布尔attribute

2.x 和 3.x 行为的比较

Attributes v-bind value 2.x v-bind value 3.x HTML 输出
2.x “枚举attribute” i.e. contenteditable, draggable and spellcheck. undefined, false undefined, null 移除
2.x “枚举attribute” i.e. contenteditable, draggable and spellcheck. true, ‘true’, ‘’, 1, ‘foo’ true, ‘true’ “true”
2.x “枚举attribute” i.e. contenteditable, draggable and spellcheck. null, ‘false’ false, ‘false’ “false”
其他非布尔attribute eg. aria-checked, tabindex, alt, etc. undefined, null, false undefined, null 移除
其他非布尔attribute eg. aria-checked, tabindex, alt, etc. ‘false’ false, ‘false’ “false”

按键修饰符

  • 不再支持使用数字 (即键码) 作为v-on修饰符
  • 不再支持config.keyCodes

建议对任何要用作修饰符的键使用 kebab-cased (短横线) 大小写名称:

1
2
<!-- Vue 3 在 v-on 上使用 按键修饰符 -->
<input v-on:keyup.delete="confirmDelete" />

事件 API

$on$off$once实例方法已被移除,应用实例不再实现事件触发接口,$emit仍然是现有API的一部分,因为它用于触发由父组件以声明方式附加的事件处理程序。

过滤器

Vue 3.x中,filters已删除,不再受支持。建议用方法调用或计算属性替换它们。

内联模板 Attribute

Vue 2.x中,为子组件提供了inline-template attribute,以便将其内部内容用作模板,而不是将其作为分发内容:

1
2
3
4
5
6
<my-component inline-template>
<div>
<p>它们被编译为组件自己的模板</p>
<p>不是父级所包含的内容。</p>
</div>
</my-component>

Vue 3.x中不再支持此功能,所有模板都直接写在HTML页面中。

  • 使用<script>标签:

    1
    2
    3
    <script type="text/html" id="my-comp-template">
    <div>{{ hello }}</div>
    </script>
    1
    2
    3
    4
    const MyComp = {
    template: '#my-comp-template'
    // ...
    }
  • 默认 Slot

    1
    2
    3
    <my-comp v-slot="{ childState }">
    {{ parentMsg }} {{ childState }}
    </my-comp>
    1
    2
    3
    4
    5
    6
    7
    <!--
    在子模板中,在传递时渲染默认slot
    在必要的private状态下。
    -->
    <template>
    <slot :childState="childState" />
    </template>

片段

Vue 3中,组件正式支持多根节点组件,即片段。

自定义元素

如果我们想添加在Vue外部定义的自定义元素 (例如使用Web组件API),我们需要“指示”Vue将其视为自定义元素。以下面的模板为例:

1
<plastic-button></plastic-button>

Vue 3.0中,此检查在模板编译期间执行指示编译器将<plastic-button>视为自定义元素:

  • 如果使用生成步骤:将isCustomElement传递给Vue模板编译器,如果使用vue-loader,则应通过 vue-loadercompilerOptions选项传递:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // webpack 中的配置
    rules: [
    {
    test: /\.vue$/,
    use: 'vue-loader',
    options: {
    compilerOptions: {
    isCustomElement: tag => tag === 'plastic-button'
    }
    }
    }
    // ...
    ]
  • 如果使用动态模板编译,请通过app.config.isCustomElement传递:

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

自定义元素规范提供了一种将自定义元素用作自定义内置模板的方法,方法是向内置元素添加is属性。在Vue 3.0中,我们仅将Vueis属性的特殊处理限制到<component>tag。

  • 在保留的<component>tag上使用时,它的行为将与Vue 2.x中完全相同;

  • 在普通组件上使用时,它的行为将类似于普通 prop:

    1
    <foo is="bar" />
    • 2.x 行为:渲染bar组件。
    • 3.x 行为:通过isprop渲染foo组件。
  • 在普通元素上使用时,它将作为is选项传递给createElement调用,并作为原生attribute渲染,这支持使用自定义的内置元素。

    1
    <button is="plastic-button">点击我!</button>
    • 2.x 行为:渲染 plastic-button 组件。
    • 3.x 行为:通过回调渲染原生的 button。
      1
      document.createElement('button', { is: 'plastic-button' })

Vue 3.x中引入了一个新的指令v-is,用于DOM内模板解析解决方案。v-is指令像一个动态的2.x :is绑定。

参考文档

https://v3.cn.vuejs.org/guide/migration/introduction.html#%E6%A6%82%E8%A7%88