内容来源于Vue 3 迁移指南
Vue2 => Vue3
全局API
全局API应用实例
Vue2.x有许多全局API和配置,它们可以全局改变Vue的行为。例如,要注册全局组件,可以使用Vue.component 或者Vue.direcitve API。这些声明方式虽然很方便,但是也会导致一些问题。在Vue2中,我们通过new Vue 创建的根 Vue 实例。从同一个Vue 构造函数创建的每一个根实例共享相同的全局配置,这会导致两个问题,一是在测试期间,全局配置很容易意外地污染其他测试用例。二是全局配置使得同一个页面上的多个“应用”在全局配置不同时共享一个Vue副本非常困难。
一个全新的全局API:createApp
调用 createApp 返回一个应用实例,这是一个Vue3中的新概念。
import { createApp } from 'vue'
const app = createApp({})
如果是使用 CDN 引入的 Vue ,那么 createApp 将通过全局的 Vue 对象暴露。
import { createApp } = Vue
const app = createApp({})
Vue3 把 Vue2 中任何全局改变 Vue 行为的 API 放到了应用实例上,以下是 Vue2 全局API 及其相应的实例API 列表:
2.x 全局 API | 3.x 实例API(APP) |
---|---|
Vue.config | app.config |
Vue.config.productionTip | 移除 |
Vue.config.ignoreedElements | app.config.compilerOptions.isCustomElement |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use |
Vue.prototype | app.config.globalProperties |
Vue.extend | 移除 |
所有不会全局改变行为的全局API现在都是具名导出,详细见下一节内容。
改动后的用法如下所示,这里以 Vue.use 为例,其他的API改动用法详见官方文档(用法都大同小异)。
const app = createApp(MyApp)
app.use(VueRouter)
挂载 App 实例
使用 createApp(/* options */)
初始化后,应用实例 app
可通过 app.mount(domTarget)
挂载根组件实例:
import { createApp } from 'vue'
import MyApp from './MyApp.vue'
const app = createApp(MyApp)
app.component('button-counter', {
data: () => ({
count: 0
}),
template: '<button @click="count++">Clicked {{ count }} times.</button>'
})
app.directive('focus', {
mounted: (el) => el.focus()
})
app.mount('#app')
// 现在,所有通过 app.mount() 挂载的应用实例及其组件树,
// 将具有相同的 “button-counter” 组件和 “focus” 指令,
// 而不会污染全局环境
Provide/Inject
与在 2.x 根实例中使用 provide
选项类似,Vue 3 应用实例也提供了可被应用内任意组件注入的依赖项:
// 在入口中
app.provide('guide', 'Vue 3 Guide')
// 在子组件中
export default {
inject: {
book: {
from: 'guide'
}
},
template: `<div>{{ book }}</div>`
}
在编写插件时使用 provide
将尤其有用,可以替代 globalProperties
。
在应用之间共享配置
在应用之间共享配置 (如组件或指令) 的一种方法是创建工厂函数,如下所示:
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')
现在,Foo
和 Bar
实例及其后代中都可以使用 focus
指令。
全局 API Treeshaking
Vue.nextTick() 是一个直接暴露在单个 Vue 对象上的全局 API。实际上,实例方法 $nextTick() 只是一个方便的 Vue.nextTick() 包裹器,将其回调的 this 上下文自动绑定到当前实例上,以方便使用。
但是对于没用手动操作 DOM 的需求,也没有在应用中使用或测试异步组件的话。nextTick() 的代码就会变成死代码——也就是说,代码写了,但从未使用过。
如 webpack 和 Rollup (Vite 基于它) 这样的模块打包工具支持 tree-shaking,这是表达“消除死代码”的一个花哨术语。并且在之前版本的 VUE中,像 Vue.nextTick() 这样的全局 API 是不支持 tree-shake 的,不管它们实际上是否被使用了,都会被包含在最终的打包产物中。
3.x 语法
在 Vue3 中,全局和内部 API 都经过了重构,并考虑到 tree-shaking 的支持。因此,对于 ES 模块构建版本来说,全局 API 现在通过具名进行导出访问。例如,我们之前的代码片段现在应该如下所示:
import { nextTick } from 'vue'
nextTick(() => {
// 一些和 DOM 有关的东西
})
受影响的 API
Vue 2.x 中这些全局 API 受到此更改的影响:
- Vue.nextTick
- Vue.observable(用 Vue.reactive 替换)
- Vue.version
- Vue.compile(仅完整构建版本)
- Vue.set(仅兼容构建版本)
- Vue.delete(仅兼容构建版本)
内部帮助器
除了公共API,许多内部组件/帮助器现在也以具名的方式导出。这允许编译器只在代码被使用到时才引入并输出它。
例如 ,只有在实际使用了 transition 标签它才会导入 Transition 组件。换句话说,如果应用没有使用任何的 Transition 组件,那么用于支持此功能的代码将不会出现在最终打包的产物当中。
重要
以上仅适用于ES模块构建版本,用于支持 tree-shaking 的打包工具——UMD 构建仍然包括所有特性,并暴露 Vue全局变量上的所有内容(编译器将生成适当的输出以从该全局变量上使用 API ,而不是导入)
插件中的用法
如果你的插件依赖到了受影响的 Vue 2.x 全局 API,例如:
const plugin = {
install: Vue => {
Vue.nextTick(() => {
// ...
})
}
}
在 Vue3 中,必须显式导入:
import { nextTick } from 'vue'
const plugin = {
install: app => {
nextTick(() => {
// ...
})
}
}
如果使用了 webpack 这样的模块打包工具,这可能会导致 Vue 的源码输出打包到插件中。为了防止这种情况的发生,一种常见的做法是配置模块打包工具以将 Vue 从最终的打包产物中排除。 对于 webpack 来说,你可以使用 externals 配置选项:
// webpack.config.js
module.exports = {
/*...*/
externals: {
vue: 'Vue'
}
}
这将告诉 webpack 将 Vue 模块视为一个外部库,而不是将它打包进来。
如果你选择的模块打包工具恰好是 Rollup,你基本上可以直接获得相同的效果。因为默认情况下,Rollup 会将绝对的模块 id (在我们的例子中为’vue’)作为外部依赖项,而不是将它们包含在最终的打包产物张。但是在输出打包期间,它可能会抛出一个“将Vue 作为外部依赖”的警告,可使用 external 选项阻止该警告:
// rollup.config.js
export default {
/*...*/
external: ['vue']
}
模板指令
v-model
概览
我们先来简单看一下有哪些变化:
- 非兼容(不兼容Vue 2.x):用于自定义组件时,v-model prop 和事件默认名称以更改:
- prop:value -> modelValue
- 事件:input -> update:modelValue
- 非兼容:b-bind 的 .sync 修饰符和组件的 model 选项以移除,可在 v-model 上加一个参数代替;
- 现在可以在同一个组件上使用多个 v-model 绑定;
- 现在可以自定义 v-model 修饰符
介绍
在 Vue 2.0 之后,开发者使用 v-model 指令时必须使用名为 value 的 prop。如果开发者出于不同的目的需要使用其他的 prop,他们就不得不使用 v-bind.sync。此外,由于 v-model 和 value 之间这种硬编码关系的原因,产生了如何处理原生元素和自定义元素的问题。
在 Vue 2.2 中,我们可以使用 model 组件选项,允许组件自定义用于 v-model 的 prop 和事件。但是,这仍然只允许在组件上使用一个 v-model。
在 Vue 3.0 中,双向数据绑定的 API 已经标准化,以减少开发者在使用 v-model 指令时的混淆,并且更加灵活。
2.x 语法
在 2.x 中,在组件上使用 v-model 相当于绑定 value prop 并触发 input 事件:
<Childcomponent v-model="pageTitle" />
<!-- 是以下的简写: -->
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
如果想要更改 prop 或事件名称,则需要在 ChildComponent 组件中添加 model 选项:
<!-- ParentComponent.vue -->
<ChildComponent v-model="pageTitle" />
// ChildComponent.vue
export default {
model: {
prop: 'title',
event: 'change'
},
props: {
// 这将允许 `value` 属性用于其他用途
value: String,
// 使用 `title` 代替 `value` 作为 model 的 prop
title: {
type: String,
default: 'Default title'
}
}
}
所以,在这个例子中 v-model 是以下的简写:
<ChildComponent :title="pageTitle" @change="pageTitle = $event" />
使用 v-bind.sync
在某些情况下,我们可能需要对某一个 prop 进行“双向绑定”。这时我们需要使用 update:myPropName 抛出事件。例如:
this.$emit('update:title', newValue)
然后父组件可以在需要时监听该事件,并更新到本地的 data property。例如:
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
可以缩写为:
<ChildComponent :title.sync="pageTitle" />
3.x 语法
在 3.x 中,自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件:
<ChildComponent v-model="pageTitle" />
<!-- 是以下的简写: -->
<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>
v-model 参数
若需要 更改 model 的名称,现在我们可以为 v-model 传递一个参数,以作为组件内 model 选项替代
<ChildComponent v-model:title="pageTitle" />
<!-- 是以下的简写: -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
这也可以作为 .sync 修饰符的替代,而且允许我们在自定义组件上使用多个 v-model
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />
<!-- 是以下的简写: -->
<ChildComponent
:title="pageTitle"
@update:title="pageTitle = $event"
:content="pageContent"
@update:content="pageContent = $event"
/>
v-model 修饰符
除了像 .trim 这样的 2.x 硬编码的 v-model 修饰符外,现在 3.x 还支持自定义修饰符:
<ChildComponent v-model.capitalize="pageTitle" />
查阅关于组件上的自定义 v-model 修饰符的更多信息。
迁移策略
推荐:
-
检查你的代码库中所有使用 ,sync 的部分并将其替换为 v-model:
<ChildComponent :title.sync="pageTitle" /> <!-- 替换为 --> <ChildComponent v-model:title="pageTitle" />
-
对于所有不带参数的 v-model, 请确保分别将 prop 和event 命名更改为 modelValue 和 update:modelValue
<ChildComponent v-model="pageTitle" />
// ChildComponent.vue export default { props: { modelValue: String // 以前是`value:String` }, emits: ['update:modelValue'], methods: { changePageTitle(title) { this.$emit('update:modelValue', title) // 以前是 `this.$emit('input', title)` } } }
key 使用改变
key Attribute
概览
- 新增: 对于 v-if/v-else/v-else-if 的各分支项 key 将不再是必须的,因为现在 Vue 会自动生成唯一的 key。
- 非兼容:如果你手动提供 key,那么每个分支必须使用唯一的 key。你将不再能通过故意使用相同的 key 来强制重置分支。
- 非兼容: 的 key 应该设置在 标签上,而不是它的子节点上。
背景
特殊的 key attribute 被作为 Vue 的虚拟 DOM 算法的提示,以保持对节点身份的持续跟踪。这样Vue 就可以知道何时能够重用和修补现有节点,以及何时需要对它们重新排序或重新创建。
在条件分支中
Vue2.x 使用如下
<!-- Vue 2.x --> <div v-if="condition" key="yes">Yes</div> <div v-else key="no">No</div>
这样在 Vue3.x 中仍然能正常工作。但是不建议再这样使用,因为即使没有为条件分支提供key 时,也能自动生成唯一的 key。
<!-- Vue 2.x --> <div v-if="condition" key="a">Yes</div> <div v-else key="a">No</div> <!-- Vue 3.x (推荐方案:移除 key) --> <div v-if="condition">Yes</div> <div v-else>No</div> <!-- Vue 3.x (替代方案:确保 key 始终是唯一的) --> <div v-if="condition" key="a">Yes</div> <div v-else key="b">No</div>
结合
在 Vue 2.x 中, 标签不能拥有 key。不过,你可以为每个子节点分别设置 key。
<!-- Vue 2.x --> <template v-for="item in list"> <div :key="'heading-' + item.id">...</div> <span :key="'content-' + item.id">...</span> </template>
在 Vue3.x 中,key 则应该被设置在 标签上。
<!-- Vue 3.x --> <template v-for="item in list" :key="item.id"> <div>...</div> <span>...</span> </template>
类似地,当使用
<template v-for>
时如果存在使用v-if
的子节点,则key
应改为设置在<template>
标签上。 - 新增: 对于 v-if/v-else/v-else-if 的各分支项 key 将不再是必须的,因为现在 Vue 会自动生成唯一的 key。
v-if 与 v-for 的优先级对比
概览
- 非兼容: 两者作用于同一个元素上时,v-if 会比v-for 优先级更高。
2.x 语法
2.x 版本中在一个元素上同时使用 v-if 和 v-for 时,v-for 的优先级更高。
3.x 语法
3.x 版本中 v-if 的优先级高于 v-for
v-bind 合并行为
概览
- 不兼容:v-bind 的绑定顺序会影响渲染结果
介绍
在一个元素上动态绑定 attribute 时,同时使用 v-bind=“object” 语法和独立 attribute 是常见的场景。然而,这就引出了关于合并的优先级的问题。
2.x 语法
在2.x中,如果一个元素同时定义了 v-bind=“object” 和一个相同的独立的 attribute,那么这个独立的attribute 总是会覆盖 object 中的绑定。
<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 结果 -->
<div id="red"></div>
3.x 语法
在3.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-on.native 修饰符
概览
v-on的.native 修饰符已被移除
2.x 语法
默认情况下,传递给带有 v-on 的组件的时间监听器只能通过this.$emit 触发。要将原生的 DOM 监听器添加到子组件的根元素中,可以使用 .native 修饰符:
<my-component
v-on:close="handleComponentEvent"
v-on:click.native="handleNativeClickEvent"
/>
3.x 语法
v-on 的 .native 修饰符已被移除。同时新增的 emits 选项允许子组件定义真正会被触发的事件。
因此,对于子组件中未被定义为组件触发的所有事件监听器,Vue现在将它们作为原生事件监听器添加到子组件的根元素中(除非在子组件的选项中设置了 inheritAttrs: false)。
<my-component
v-on:close="handleComponentEvent"
v-on:click="handleNativeClickEvent"
/>
MyComponent.vue
<script>
export default {
emits: ['close']
}
</script>
组件
函数式组件
概览
- 2.x 中函数式组件带来的性能提升在 3.x 中已经可以忽略不计,因此建议只使用有状态的组件
- 函数式组件只能接收 props 和 context (即:slot、attrs、emit)的普通函数创建
- 非兼容:functional attribute 已从单文件组件(SFC)的中移除
- 非兼容:{ functional: true }选项已从通过函数创建的组件中移除
函数式组件是什么呢?若一个组件只用于展示数据,所有动态数据都从父组件传递进来(只有props),内部没有逻辑交互(没有methods方法、也没有mounted等任何生命周期处理函数),没有状态修改(无data),则推荐使用函数式组件来提升vue的性能。
使用方法:
- 在template标签上加上关键字functional
<template functional>
- 使用 props. 获取父组件传递进来的数据(函数也可以传入)
介绍
在Vue 2 中,函数式组件主要有两个应用场景:
- 作为性能优化,因为它们的初始化速度比有状态组件快得多
- 返回多个根节点
然而,在 Vue3 中,有状态的性能已经提高到它们之间的区别可以忽略不计的程度。此外,有状态组件现在也支持返回多个根节点。
因此,函数式组件剩下的唯一应用场景就是简单组件,比如创建动态标题的组件。否则,不建议使用。
2.x 语法
使用 组件,负责提供适当的标题(即:h1、h2等),在 2.x 中,这可以通过单文件组件编写:
// Vue 2 函数式组件示例
export default {
functional: true,
props: ['level'],
render(h, { props, data, children }) {
return h(`h${props.level}`, data, children)
}
}
3.x 语法
通过函数创建组件
现在,在Vue3 中,所有的函数式组件都是用普通函数创建的。换句话说,不需要定义 { function: true }组件选项。
它们将接收两个参数: props 和 context。context 参数是一个对象,包含组件的 attrs、slots 和 emit property。
此外,h 现在是全局导入的,而不是在render 函数中隐式提供。
以及前面提到的 组件为例,下面是它现在的样子。
import { h } from 'vue'
const DynamicHeading = (props, context) => {
return h(`h${props.level}`, context.attrs, context.slots)
}
DynamicHeading.props = ['level']
export default DynamicHeading
单文件组件(SFC)
在 3.x 中,有状态组件和函数式组件之间的性能差异已经大大减少,并且在大多数用例中是微不足道的。因此,在单文件组件上使用 functional 的开发者的迁移路径是删除该 attribute,并将 props 的所有引用重命名为 $props,以及将 attrs 重命名为 $attrs。
以之前的 为例,下面是它现在的样子。
<template>
<component
v-bind:is="`h${$props.level}`"
v-bind="$attrs"
/>
</template>
<script>
export default {
props: ['level']
}
</script>
主要区别在于:
- 从 中移除 functional attitude
- listeners 现在作为 $attrs 的一部分传递,可以将其删除
异步组件
概览
以下是对变化的总体概述:
- 新的 defineAsyncComponent 助手方法,用于显式地定义异步组件
- component 选项被重命名为 loader
- Loader 函数本身不再接收 resolve 和 reject 参数,且必须返回一个promise
介绍
以前,异步组件是通过将组件定义为返回 Promise 的函数来创建的,例如:
const asyncModal = () => import('./Modal.vue')
或者,对于带有选项的更高阶组件语法:
const asyncModal = {
component: () => import('./Modal.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
}
3.x 语法
现在,在 Vue3 中,由于函数式组件被定义为纯函数,因此异步组件需要通过将其包裹在新的 defineAsyncComponent 助手方法中来显式地定义:
import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'
// 不带选项的异步组件
const async = defineAsyncComponent(() => import('./Modal.vue'))
// 带选项的异步组件
const asyncModalWithOptions = defineAsyncComponent({
loader: () => import('./Modal.vue'), // 跟Vue2.x的区别
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})
注意
Vue Router 支持一个类似的机制来异步加载路由组件,也就是俗称的懒加载。尽管类似,但是这个功能和 Vue 所支持的异步组件是不同的。当用 Vue Router 配置路由组件时,你不应该使用
defineAsyncComponent
。你可以在 Vue Router 文档的懒加载路由章节阅读更多相关内容。
对 2.x 所做的另一个更改是, component 选项现在被重命名为 loader,以明确组件定义不能直接被提供
此外,与 2.x 不同,loader 函数不再接收 resolve
和 reject
参数,且必须始终返回 Promise。
// 2.x 版本
const oldAsyncComponent = (resolve, reject) => {
/* ... */
}
// 3.x 版本
const asyncComponent = defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
/* ... */
})
)
emits 选项
概述
Vue 3 现在提供一个 emits 选项,和现在 props 选项类似。这个选项可以用来定义一个组件可以向其父组件触发的事件。
2.x 的行为
在 Vue 2 中,你可以定义一个组件可接收的 prop,但是你无法声明它可以触发哪些事件。
3.x 的行为
和 prop 类似,现在可以通过 emits 选项来定义组件可触发的事件:
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text'],
emits: ['accepted']
}
</script>
该选项也可以接收一个对象,该对象允许开发者定义传入事件参数的验证器,和 props 定义里的验证器类似。
迁移策略
强烈建议使用 emits 记录每个组件所触发的所有事件。
这尤为重要,因为 Vue 3 移除了 .native 修饰符。任何未在 emits 中声明的事件监听器都会被算入组件的 $attrs,并将默认绑定到组件的根节点上。
示例
对于向其父组件透传原生事件的组件来说,这会导致有两个事件被触发:
<template>
<button v-on:click="$emit('click', $event)">OK</button>
</template>
<script>
export default {
emits: [] // 不声明事件
}
</script>
当一个父级组件拥有 click 事件的监听器时:
<my-button v-on:click="handleClick"></my-button>
该事件现在会被触发两次:
- 一次来自 $emit()
- 另一次来自应用在根元素上的原生事件监听器
现在你有两个选项:
- 正确地声明 click 事件。当你真的在 的事件处理器上加入一些逻辑时,这会很有用。
- 移除透传的事件,因为现在父组件可以很容易地监听原生事件,而不需要添加 .native 。适用于你只想透传这个事件。
渲染函数
渲染函数 API
概览
此更改不会影响 用户
以下是更改的简要总结:
- h 现在是全局导入,而不是作为参数传递给渲染函数
- 更改渲染函数参数,使其在有状态组件和函数组件的表现更加一致
- VNode 现在有一个扁平的 prop 结构
渲染函数参数
2.x 语法
在 2.x 中,render 函数会自动接收 h 函数(它是 createElement 的惯用别名)作为参数:
// Vue 2 渲染函数示例
export default {
render(h) {
return h('div')
}
}
3.x 语法
在 3.x 中,h 函数现在是全局导入的,而不是作为参数自动传递。
// Vue 3 渲染函数示例
import { h } from 'vue'
export default {
render() {
return h('div')
}
}
Vnode Prop 格式化
2.x 语法
在 2.x 中,domProps 包含 VNode prop 中的嵌套列表:
// 2.x
{
staticClass: 'button',
class: { 'is-outlined': isOutlined },
staticStyle: { color: '#34495E' },
style: { backgroundColor: buttonColor },
attrs: { id: 'submit' },
domProps: { innerHTML: '' },
on: { click: submitForm },
key: 'submit-button'
}
3.x 语法
在 3.x 中,整个 VNode prop 的结构都是扁平的。使用上面的例子,来看看它现在的样子。
// 3.x 语法
{
class: ['button', { 'is-outlined': isOutlined }],
style: [{ color: '#34495E' }, { backgroundColor: buttonColor }],
id: 'submit',
innerHTML: '',
onClick: submitForm,
key: 'submit-button'
}
注册组件
在 2.x 中,注册一个组件后,把组件名作为字符串传递给渲染函数的第一个参数,它可以正常地工作:
// 2.x
Vue.component('button-counter', {
data() {
return {
count: 0
}
},
template: `
<button @click="count++">
Clicked {{ count }} times.
</button>
`
})
export default {
render(h) {
return h('button-counter')
}
}
3.x 语法
在 3.x 中,由于 VNode 是上下文无关的,不能再用字符串 ID 隐式查找已注册组件。取而代之的是,需要使用一个导入的 resolveComponent 方法:
// 3.x
import { h, resolveComponent } from 'vue'
export default {
setup() {
const ButtonCounter = resolveComponent('button-counter')
return () => h(ButtonCounter)
}
}
插槽统一
概览
此更改统一了 3.x 中普通插槽和作用域插槽
以下是变化的变更总结:
- this.$slots 现在将插槽作为函数公开
- 非兼容: 移除 this.$scopedSlots
2.x 语法
当使用渲染函数,即 h 时,2.x 曾经在内容节点上定义 slot 数据 property。
// 2.x 语法
h(LayoutComponent, [
h('div', { slot: 'header' }, this.header),
h('div', { slot: 'content' }, this.content)
])
此外,可以使用以下语法引用作用域插槽:
// 2.x 语法
this.$scopedSlots.header
3.x 语法
在 3.x 中,插槽以对象的形式定义为当前节点的子节点:
// 3.x Syntax
h(LayoutComponent, {}, {
header: () => h('div', this.header),
content: () => h('div', this.content)
})
当你需要以编程方式引用作用域插槽时,它们现在被统一到 $slots
选项中了。
// 2.x 语法
this.$scopedSlots.header
// 3.x 语法
this.$slots.header()
移除 $listeners
概览
$listeners 对象在 Vue 3 中已经被移除。事件监听器现在是 $attrs 的一部分:
{
text: '这是一个 attribute',
onClose: () => console.log('close 事件被触发')
}
2.x 语法
在 Vue 2 中,你可以通过 this. a t t r s 访问传递给组件的 a t t r i b u t e , 以及通过 t h i s . attrs 访问传递给组件的 attribute,以及通过 this. attrs访问传递给组件的attribute,以及通过this.listeners 访问传递给组件的事件监听器。结合 inheritAttrs: false,开发者可以将这些 attribute 和监听器应用到根元素之外的其他元素:
<template>
<label>
<input type="text" v-bind="$attrs" v-on="$listeners" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
3.x 语法
在 Vue 3 的虚拟 DOM 中,事件监听器现在只是以 on 为前缀的 attribute,这样它就成为了 $attrs 对象的一部分,因此 $listeners 被移除了。
<template>
<label>
<input type="text" v-bind="$attrs" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
如果这个组件接受一个 id attribute 和一个 v-on:close 监听器,那么 $attrs 对象现在将如下所示:
{
id: 'my-input',
onClose: () => console.log('close 事件被触发')
}
$attrs 包含 class & sstyle
概览
$attrs 现在包含了所有传递给组件的 attribute,包括 class 和 style。
2.x 行为
Vue 2 的虚拟 DOM 实现对class 和 style attribute 有一些特殊处理。因此,与其它所有 attribute 不一样,它们没有被包含在 $attrs 中。
上述行为在使用 inheritAttrs: false 时会产生副作用:
-
$attrs 中的 attribute 将不再被自动添加到根元素中,而是由开发者决定在哪添加。
-
但是 class 和 style 不属于 $attrs ,它们仍然会被应用到组件的根元素中:
<template> <label> <input type="text" v-bind="$attrs" /> </label> </template> <script> export default { inheritAttrs: false } </script>
像这样使用时:
<my-component id="my-id" class="my-class"></my-component>
…将生成以下 HTML:
<label class="my-class"> <input type="text" id="my-id" /> </label>
3.x 行为
$attrs 包含了所有的 attribute,这使得把它们全部应用到另一个元素上变得更加容易了。现在上面的示例将生成以下 HTML:
<label> <input type="text" id="my-id" class="my-class" /> </label>
自定义元素
与自定义元素的互操作性
概览
- 非兼容:检测并确定哪些标签应该被视为自定义元素的过程,现在会在模板编译期间执行,且应该通过编译器选项而不是运行时配置来配置。
- 非兼容:特殊的 is attribute 的使用被严格限制在保留的 标签中。
- 新增:为了支持 2.x 在原生元素上使用 is 的用例来处理原生 HTML 解析限制,我们用 vue: 前缀来解析一个 Vue 组件。
自主定制元素
如果我们想在 Vue 外部定义添加自定义元素(例如使用 Web Component API),则需要“指示”Vue 将其视为自定义元素。让我们以下面的模板为例。
<plastic-button></plastic-button>
2.x 语法
在 Vue 2.x 中,通过 Vue.config.ignoredElements 将标签配置为自定义元素:
// 这将使 Vue 忽略在其外部定义的自定义元素 // (例如:使用 Web Components API) Vue.config.ignoredElements = ['plastic-button']
3.x 语法
在 Vue 3.0 中,此检查在模板编译期间执行。要指示编译器将 视为自定义元素:
- 如果使用构建步骤:给 Vue 模板编译器传入 isCustomElement 选项。如果使用了 vue-loader,则应通过 vue-loader 的 compilerOptions 选项传递:
// webpack 中的配置 rules: [ { test: /\.vue$/, use: 'vue-loader', options: { compilerOptions: { isCustomElement: tag => tag === 'plastic-button' } } } // ... ]
-
如果使用动态模板编译,请通过 app.config.compilerOptions.isCustomElement 传递:
const app = Vue.createApp({}) app.config.compilerOptions.isCustomElement = tag => tag === 'plastic-button'
需要注意的是,运行时配置只会影响运行时的模板编译——它不会影响预编译的模板。
定制内置元素
自定义元素规范提供了一种将自定义元素作为自定义内置元素的方法,方法是向内置元素添加 is
attribute:
<button is="plastic-button">点击我!</button>
在原生 attribute 于浏览器中普遍可用之前,Vue 对 is
这个特殊的 attribute 的使用就已经在模拟其行为。但是,在 2.x 中,它被解释为渲染一个名为 plastic-button 的 Vue 组件,这将阻碍上面所提到的自定义内置元素的原生用法。
在 3.0 中,我们将 Vue 对 is
attribute 的特殊处理限制在了 <component>
标签中。
-
在保留的
<component>
标签上使用时,它的行为将与 2.x 中完全相同。 -
在普通组件上使用时,它的行为将类似于普通的 attribute:
<foo is="bar" />
- 2.x 行为:渲染
bar
组件。 - 3.x 行为:渲染
foo
组件,并将is
attribute 传递给它
- 2.x 行为:渲染
-
在普通元素上使用时,它将作为
is
attribute 传递给createElement
调用,并作为原生 attribute 渲染。这支持了自定义内置元素的用法。<button is="plastic-button">点击我!</button>
-
2.x 行为:渲染
plastic-buttton
组件 -
3.x 行为:通过调用以下函数渲染原生button
document.createElement('button', { is: 'plastic-button' })
使用
vue:
前缀来解决 DOM 内模板解析问题提示:本节仅影响直接在页面的 HTML 中写入 Vue 模板的情况。在 DOM 模板中使用时,模板受原生 HTML 解析规则的约束。一些 HTML 元素,例如
<ul>
、<ol>
、<table>
和<select>
对它们内部可以出现的元素有限制,以及像<li>
、<tr>
、和<option>
只能出现在特定的其他元素中。 -
2.x 语法
在 Vue 2.x 中,我们建议在原生标签上使用 is
attribute 来绕过这些限制:
<table>
<tr is="blog-post-row"></tr>
</table>
3.x 语法
随着 is
的行为发生变化,现在将元素解析为 Vue 组件需要添加一个 vue:
前缀:
<table>
<tr is="vue:blog-post-row"></tr>
</table>
移除的 APIs
按键修饰符
概览
一下是变更的简要总结:
- 非兼容:不再支持使用数字(即键码)作为 v-on 修饰符
- 非兼容:不再支持 config.keyCodes
2.x 语法
在 Vue 2中,keyCodes
可以作为修改 v-on
方法的一种方式。
<!-- 键码版本 -->
<input v-on:keyup.13="submit" />
<!-- 别名版本 -->
<input v-on:keyup.enter="submit" />
此外,也可以通过全局的 config.keyCodes 选项定义自己的别名。
Vue.config.keyCodes = {
f1: 112
}
<!-- 键码版本 -->
<input v-on:keyup.112="showHelpText" />
<!-- 自定义别名版本 -->
<input v-on:keyup.f1="showHelpText" />
3.x 语法
从 keyboardEvent.keyCode
已经废弃 开始,Vue 3 继续支持这一点就不再有意义了。因此,现在建议对任何要用作修饰符的键使用 kebab-cased(短横线)名称。
<!-- Vue 3 在 v-on 上使用按键修饰符 -->
<input v-on:keyup.page-down="nextPage">
<!-- 同时匹配 q 和 Q -->
<input v-on:keypress.q="quit">
因此,这意味着 config.keyCodes
现在也弃用,不再受支持。
事件 API
概览
$on
, $off
和 $once
实例方法已被移除,组件实例不再实现事件触发接口。
2.x 语法
在 2.x 中,Vue 实例可用于触发由事件触发器 API 通过指令方式添加的处理函数($on
, $off
和 $once
)。这可以用于创建一个事件总线,以创建在整个应用中可用的全局事件监听器:
// eventBus.js
const eventBus = new Vue()
export default eventBus
// ChildComponent.vue
import eventBus from './eventBus'
export default {
mounted() {
// 添加 eventBus 监听器
eventBus.$on('custom-event', () => {
console.log('Custom event triggered!')
})
},
beforeDestroy() {
// 移除 eventBus 监听器
eventBus.$off('custom-event')
}
}
// ParentComponent.vue
import eventBus from './eventBus'
export default {
methods: {
callGlobalCustomEvent() {
eventBus.$emit('custom-event') // 当 ChildComponent 已被挂载时,控制台中将显示一条消息
}
}
}
3.x 更新
我们从实例中完全移除了
o
n
‘
,
‘
on`, `
on‘,‘off和
o
n
c
e
方法。
‘
once方法。`
once方法。‘emit` 仍然包含于现有的 API ,因为它用于触发由父组件声明式添加的事件处理函数。
迁移策略
在 Vue 3 中,借助这些 API 从一个组件内部监听其自身触发的事件已经不再可能了。该用例没有办法迁移。
根组件事件
静态的事件监听器可以通过 prop 的形式传递给 createApp 以添加到根组件中。
createApp(App, {
// 监听 'expand' 事件
onExpand() {
console.log('expand')
}
})
事件总线
事件总线模式可以被替换为使用外部的、实现了事件触发器接口的库,例如 mitt 或 tiny-emitter。
// eventBus.js
import emitter from 'tiny-emitter/instance'
export default {
$on: (...args) => emitter.on(...args),
$once: (...args) => emitter.once(...args),
$off: (...args) => emitter.off(...args),
$emit: (...args) => emitter.emit(...args),
}
它提供了与 Vue 2 相同的事件触发器 API。
在绝大多数情况下,不鼓励使用全局的事件总线在组件之间进行通信。虽然在短期内往往是最简单的解决方案,但从长期来看,它维护起来总是令人头疼。根据具体情况来看,有多种事件总线的替代方案:
- Props 和事件应该是父子组件之间的沟通首选。兄弟节点可以通过它们的父节点通信。
provide
/inject
允许一个组件与它的插槽内容进行通信。这对于总是一起使用的紧密耦合的组件非常有用。provide
/inject
也能够用于组件之间的远距离通信。它可以帮助“prop 逐级透传”,即prop 需要通过许多层级的组件传递下去,但这些组件本身可能并不需要那些 prop- Peop 逐级透传也可以通过重构以使用插槽来避免。如果一个中间组件不需要某些 prop,那么表明它可能存在关注点分离的问题。在该类组件中使用 slot 可以允许父节点直接为它创建内容,因此 prop 可以被直接传递而不需要中间组件的参与。
- 全局状态管理,如 Pinia
过滤器
概览
从 Vue 3.0 开始,过滤器已移除,且不再支持。
2.x语法
在2.x 中,开发者可以使用过滤器来处理通用文本格式。
例如:
<template>
<h1>Bank Account Balance</h1>
<p>{{ accountBalance | currencyUSD }}</p>
</template>
<script>
export default {
props: {
accountBalance: {
type: Number,
required: true
}
},
filters: {
currencyUSD(value) {
return '$' + value
}
}
}
</script>
虽然这看起来很方便,但是它需要一个自定义语法,打破了大括号内的表达式“只是 JavaScript”的假设,这不仅有学习成本,而且有实现成本。
3.x 语法
在 3.x 中,过滤器已移除,且不再支持。取而代之的是,我们建议用方法调用或者计算属性来替换它们。
以上面的案例为例,以下是一种实现方法。
<template>
<h1>Bank Account Balance</h1>
<p>{{ accountInUSD }}</p>
</template>
<script>
export default {
props: {
accountBalance: {
type: Number,
required: true
}
},
computed: {
accountInUSD() {
return '$' + this.accountBalance
}
}
}
</script>
内联模板 Attribute
概览
对内联模板特性的支持已被移除。
2.x 语法
在 2.x 中,Vue 为子组件提供了 inline-template
attribute,以便将其内部内容作为模板使用,而不是作为分发内容。
<my-component inline-template>
<div>
<p>它们将被编译为组件自己的模板,</p>
<p>而不是父级所包含的内容。</p>
</div>
</my-component>
3.x 语法
将不再支持此功能
$children
概览
$children
实例 property 已从 Vue 3.0 中移除,不再支持。
2.x 语法
在 2.x 中,开发者可以使用 this.$children 访问当前实例的直接子组件:
<template>
<div>
<img alt="Vue logo" src="./assets/logo.png">
<my-button>Change logo</my-button>
</div>
</template>
<script>
import MyButton from './MyButton'
export default {
components: {
MyButton
},
mounted() {
console.log(this.$children) // [VueComponent]
}
}
</script>
3.x 更新
在 3.x 中,$children property 已被移除,且不再支持。如果你需要访问子组件实例,我们建议使用模板引用。
propsData
概述
propsData 选项之前用于在创建 Vue 实例的过程中传入 prop,现在它被移除了。如果想为 Vue 3 应用的根组件传入 prop,请使用 createApp 的第二个参数。
2.x 语法
在 2.x 中,我们可以在创建 Vue 实例的时候传入 prop:
const Comp = Vue.extend({
props: ['username'],
template: '<div>{{ username }}</div>'
})
new Comp({
propsData: {
username: 'Evan'
}
})
3.x 更新
propsData 选项已经被移除。如果你需要在实例创建时向根组件传入prop,你应该使用 createApp 的第二个参数:
const app = createApp(
{
props: ['username'],
template: '<div>{{ username }}</div>'
},
{ username: 'Evan' }
)
其他一些小的变化
attribute强制行为
概览
以下是对变化的总体概览:
- 移除枚举 attribute 的内部概念,并将这些attribute 视为普通的非布尔 attribute
- 非兼容:如果值为布尔值 false,则不再移除 attribute。取而代之的是,它将被设置为 attr=“false”。若要移除 attribute,应该使用 null 或者 undefined。
2.x 语法
在 2.x,我们有一下策略来强制 v-bind 的值:
- 对于某些 attribute/元素对,Vue 始终使用相应的 IDL attribute(property):比如
<input>
,<select>
,<progress>
中的value
等等 - 对于 “布尔 attribute” 和 xlinks,如果它们是 falsy(undefined, null或false)的,Vue 会移除它们,否则会加上。
- 对于“枚举 attribute”(目前来说包括 contenteditable、draggable和spllcheck),Vue会尝试将它们强制装换为字符串。
- 对于其他 attribute,我们将移除 falsy的值,其他值按原样设置。
下表描述了 Vue 如果强制装换“枚举 attribute”,以及与普通非布尔 attribute 的不同之处:
绑定表达式 | foo 正常 | draggable 枚举 |
---|---|---|
:attr=“null” | - | draggable=“false” |
:attr=“undefined” | - | - |
:attr=“true” | foo=“true” | draggable=“true” |
:attr=“false” | - | draggable=“false” |
:attr=“0” | foo=“0” | draggable=“true” |
:attr=“” | foo=“” | draggable=“true” |
:attr=“foo” | foo=“foo” | draggable=“true” |
attr | foo=“” | draggable=“true” |
从上表可以看出,当前实现将 true 强制装换为了 ‘true’,但如果 attribute 为了 false,则将其移除。这也导致了不一致性,并要求用户在非常常见的用例中手动将布尔值强制转换为字符串,例如 aria-*
attribute,例如 aria-selected
、aria-hidden
等等。
3.x 语法
我们打算放弃“枚举型 attribute ”的内部概念,并将它们视为普通的非布尔型 HTML attribute。
- 这就解决了普通非布尔型 attribute 和“枚举型 attribute” 之间的不一致性问题
- 这个改动还使得对于诸如 contenteditable 这样的 attribute,我们可以使用 ‘true’ 和 ‘false’ 以外的值,甚至是未来新增的关键字。
对于非布尔型 attribute,如果其值为 false, Vue 将不再移除它们,而是将其强制装换为 ‘false’.
- 这就解决了 true 和 false 之间的不一致性,并更易于输出 aria-* attribute
下标描述了新的行为:
绑定表达式 | foo 正常 | draggable 枚举 |
---|---|---|
:attr=“null” | - | -* |
:attr=“undefined” | - | - |
:attr=“true” | foo=“true” | draggable=“true” |
:attr=“false” | oo=“false”* | draggable=“false” |
:attr=“0” | foo=“0” | draggable=“0” |
:attr=“” | foo=“” | draggable=“” |
:attr=“foo” | foo=“foo” | draggable=“foo”* |
attr | foo=“” | draggable=“” |
*:已改变
布尔型 attribute 的强制装换保持不变
迁移策略
枚举 attribute
枚举 attribute 如果不存在,可能会和 attr=“false” 会产生不同的 IDL attribute 值(将反映实际状态),描述如下:
不存在的枚举attr | IDL attr & 值 |
---|---|
contenteditable | contenteditable -> ‘inherit’ |
draggable | draggable -> false |
spellcheck | spellcheck -> true |
对于“枚举 attribute”,由于我们不再把 null 装换为 false,在 3.x 中,对于 contenteditable 和 spellcheck,开发者需要将原来解析为 null 的 v-bind 表达式变更为 false 或 ‘false’ 才能保持和 2.x 中的行为一致。
在 2.x 中,枚举 attribute 的无效值将被强制装换为 ‘true’。这通常是不符合预期的,所以该行为不太可能被大规模依赖。在 3.x 中,应显式指定 true 或 ‘true’。
将 false 强制装换为 ‘false’ 而不是移除 attribute
在3.x 中,应该使用 null 或 undefined 以显式移除 attribute
2.x 和 3.x 行为的比较
2.x “枚举 attribute”即 contenteditable、draggable和spllcheck。
v-bind 的值(2.x) | v-bind 的值(3.x) | HTML 输出 |
---|---|---|
undefined | undefined, null | 被移除 |
true,‘true’,’ ',1,‘foo’ | ture,‘true’ | “true” |
null,false,‘false’ | false,‘false’ | “false” |
其他非布尔 attribute,如 aria-checked、tabindex、alt等等
v-bind 的值(2.x) | v-bind 的值(3.x) | HTML 输出 |
---|---|---|
undefined,null,false | undefined, null | 被移除 |
‘false’ | false,‘false’ | “false” |
自定义指令
概览
指令的钩子函数已经被重命名,以更好地与组件的生命周期保持一致。
额外地,expression 字符串不再作为 binding 对象的一部分被传入。
2.x 语法
在 Vue 2 中,自定义指令通过使用下列钩子来创建,以对齐元素的生命周期,它们都是可选的:
- bind - 指令绑定到元素后调用。只调用一次
- insertd - 元素插入父 DOM 后调用。
- update - 当元素更新,但子元素尚未更新时,就会调用此钩子
- componentUpdated - 一旦组件和子级被更新,就会调用这个钩子
- unbind - 一旦指令被移除,就会调用这个钩子。也只调用一次。
<p v-highlight="'yellow'">以亮黄色高亮显示此文本</p>
Vue.directive('highlight', {
bind(el, binding, vnode) {
el.style.background = binding.value
}
})
此外,在这个元素的初始设置中,通过给指令传递一个值来绑定样式,该值可以在应用中任意更改。
3.x 语法
然而,在 vue 3 中,我们为自定义指令创建了一个更具有凝聚力的 API。正如你所看到的,它们与我们组件生命周期方法有很大的不同,即使钩子的目标事件十分相似。我们现在把它们统一起来了:
- created - 新增!在元素的 attribute 或事件监听器被应用之前调用。
- bind - beforeMounted
- inserted - mounted
- beforeUpdate: 新增!在元素本身被更新之前调用,与组件的生命周期钩子十分相似。
- update -> 移除!该钩子与 update 有太多相似之处,因此它是多余的。请改用 updated。
- componentUpdated -> updated
- beforeUnmount:新增!与组件的生命周期钩子类似,它将在元素被卸载之前调用。
- unbind -> unmounted
最终 API 如下:
const MyDirective = {
created(el, binding, vnode, prevVnode) {}, // 新增
beforeMount() {},
mounted() {},
beforeUpdate() {}, // 新增
updated() {},
beforeUnmount() {}, // 新增
unmounted() {}
}
因此,API 可以这样使用,与前面的示例相同:
<p v-highlight="'yellow'">以亮黄色高亮显示此文本</p>
const app = Vue.createApp({})
app.directive('highlight', {
beforeMount(el, binding, vnode) {
el.style.background = binding.value
}
})
边界情况:访问组件实例
通常来说,建议在组件实例中保持所使用的指令的独立性。从指令中访问组件实例,通常意味着该指令本身应该是一个组件。然而,在某些情况下这种用法是有意义的。
在 Vue 2 中,必须通过 vnode 参数访问组件实例:
bind(el, binding, vnode) {
const vm = vnode.context
}
在 Vue 3 中,实例现在是 binding 参数的一部分:
mounted(el, binding, vnode) {
const vm = binding.instance
}
Data 选项
概览
- 非兼容: 组件选项 data 的声明不再接收纯 JavaScript object,而是接收一个 function。
- 非兼容:当合并来自 mixin 或 extend 的多个 data 返回值时,合并操作现在是浅层次的而非深层次的(只合并根级属性)。
2.x 语法
在 2.x 中,开发者可以通过 object 或者是 function 定义 data 选项。
<!-- Object 声明 -->
<script>
const app = new Vue({
data: {
apiKey: 'a1b2c3'
}
})
</script>
<!-- Function 声明 -->
<script>
const app = new Vue({
data() {
return {
apiKey: 'a1b2c3'
}
}
})
</script>
虽然这种做法对于具有共享状态的根实例提供了一些便利,但是由于其只可能在根实例上,因此变得混乱。
3.x 更新
在 3.x 中,data 选项已标准化为只接受返回 object 的 function,如下:
<script>
import { createApp } from 'vue'
createApp({
data() {
return {
apiKey: 'a1b2c3'
}
}
}).mount('#app')
</script>
Mixin 合并行为变更
此外,当来自组件的 data() 及其 mixin 或 extends 基类被合并时,合并操作现在将浅层地执行:
const Mixin = {
data() {
return {
user: {
name: 'Jack',
id: 1
}
}
}
}
const CompA = {
mixins: [Mixin],
data() {
return {
user: {
id: 2
}
}
}
}
在 vue 2.x 中,生成的 $data 是:
{
"user": {
"id": 2,
"name": "Jack"
}
}
在 3.0 中,其结果将会是:
{
"user": {
"id": 2
}
}
迁移策略
对于依赖对象式声明的用户,我们建议:
- 将共享数据提取到外部对象并将其用作 data 中的一个 property
- 重写对共享数据的引用以指向新的共享对象
对于依赖 mixin 的深度合并行为的用户,我们建议重构代码以完全避免这种依赖,因为 mixin 的深度合并非常隐式,这让代码逻辑更难理解和调试。
被挂载的应用不会替换元素
概览
在 Vue 2.x 中,当挂载一个具有 template 的应用时,被渲染的内容会替换我们要挂载的目标元素。在 Vue 3.x 中,被渲染的应用会作为子元素插入,从而替换目标元素 innerHTML。
2.x 语法
在 Vue 2.x 中,我们为 new Vue() 或 $mount 传入一个 HTML 元素选择器:
new Vue({
el: '#app',
data() {
return {
message: 'Hello Vue!'
}
},
template: `
<div id="rendered">{{ message }}</div>
`
})
// 或
const app = new Vue({
data() {
return {
message: 'Hello Vue!'
}
},
template: `
<div id="rendered">{{ message }}</div>
`
})
app.$mount('#app')
当我们吧应用挂载到拥有匹配被传入选择器(在这个例子中是 id=“app”)的 div 页面时:
<body>
<div id="app">
Some app content
</div>
</body>
结果渲染结果中,上面提及的 div 将会被应用所渲染的内容替换:
<body>
<div id="rendered">Hello Vue!</div>
</body>
3.x 语法
在 Vue 3.x 中,当我们挂载一个应用时,其渲染内容会替换我们传递给 mount 的元素的 innerHTML:
const app = Vue.createApp({
data() {
return {
message: 'Hello Vue!'
}
},
template: `
<div id="rendered">{{ message }}</div>
`
})
app.mount('#app')
当这个应用挂载到拥有匹配 id=“app” 的 div 的页面时,结果会是:
<body>
<div id="app" data-v-app="">
<div id="rendered">Hello Vue!</div>
</div>
</body>
在 Prop 的默认函数中访问 this
生成 prop 默认值的工厂函数不再能访问 this。
取而代之的是:
-
组件接收到的原始 prop 将作为参数传递给默认函数;
-
inject API 可以在默认函数中使用
import { inject } from 'vue' export default { props: { theme: { default (props) { // `props` 是传递给组件的、 // 在任何类型/默认强制转换之前的原始值, // 也可以使用 `inject` 来访问注入的 property return inject('theme', 'default-theme') } } } }
过渡的 class 名更改
概览
过渡类名V-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from
2.x 语法
在 v2.18 版本之前,每个过渡方向都有两个过渡类:初始状态与激活状态
在 v2.18 版本之后,引入了 v-enter-to 来定义 enter 或 leave 变换之间的过渡动画插帧。然而,为了向下兼容,并没有变动 v-enter 类名:
.v-enter, .v-leave-to { opacity: 0; } .v-leave, .v-enter-to { opacity: 1; }
这样做会带了很多困惑,类似 enter 和 leave 含义过于宽泛,并且没有遵循类名钩子的命名约定。
3.x 语法
为了更加明确易读,我们现在将这些初始状态重命名为:
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.v-leave-from,
.v-enter-to {
opacity: 1;
}
<transition>
组件的相关 prop 名称也发生了变化:
- leave-class 已经被重命名为 leave-from-class (在渲染函数或 JSX 中可以写为 leaveFromClass)
- enter-class 已经被重命名为 enter-from-class (在渲染函数或 JSX 中可以写为 enterFromClass)
迁移策略
- 将 v-enter 字符串实例替换为 v-enter-from
- 将v-leave 字符串实例替换为 v-leave-from
- 过渡组件相关的 prop 名称也需要进行字符串实例替换
Transition 作为根节点
概览
当使用 transition
作为根节点的组件从外部被切换时将不再触发过渡效果。
2.x 行为
在 Vue 2 中,通过使用 <transition>
作为一个组件的根节点,过渡效果存在从组件外部触发的可能性:
<!-- 模态组件 -->
<template>
<transition>
<div class="modal"><slot/></div>
</transition>
</template>
<!-- 用法 -->
<modal v-if="showModal">hello</modal>
切换 showModel
的值将会在模态组件内部触发一个过渡效果。
这是无意为之的,并不是设计效果。一个 transition
原本是希望被其子元素触发的,而不是 <transition>
自身
这个怪异的现象现在被移除了。
迁移策略
换做向组件传递一个 prop 就可以达到类似效果:
<template>
<transition>
<div v-if="show" class="modal"><slot/></div>
</transition>
</template>
<script>
export default {
props: ['show']
}
</script>
</script>
html
<!-- 用法 -->
<modal :show="showModal">hello</modal>
TransitionGroup 根元素
概览
<transition-group>
不再默认渲染根元素,但仍然可以用 tag attribute 创建根元素。
2.x 语法
在 Vue 2 中,<transition-group>
像其他自定义组件一样,需要一个根元素。默认的根元素是一个 <span>
,但可以通过 tag attribute 定制。
<transition-group tag="ul">
<li v-for="item in items" :key="item">
{{ item }}
</li>
</transition-group>
3.x 语法
在 Vue 3.x 中,我们有了片段的支持,因此组件不再需要根节点。所以,<transition-group>
不再默认渲染根节点。
-
如果像上面的示例一样,已经在 Vue 2 代码中定义了 tag attribute,那么一切都会和之前一样
-
如果没有定义 tag attribute,而且样式或者其他行为依赖于
<span>
根元素的存在才能正常工作,那么只需将tag="span"
添加到<transition-group>
:<transition-group tag="span"> <!-- --> </transition-group>
Vnode 声明周期事件
概述
在 Vue 2 中,我们可以通过事件来监听组件生命周期中的关键阶段。这些事件名都是以 hook: 前缀开头,并且跟随相应的声明周期钩子的名字。
在 Vue 3 中,这个前缀已被更改为 vue: 。额外地,这些事件现在也可用于 HTML 元素,和在组件上的用法一样。
2.x 语法
在 Vue 2 中,这些事件名和响应的生命周期钩子一致,并带有 hook: 前缀:
<template>
<child-component @hook:updated="onUpdated">
</template>
3.x 语法
在 Vue 3 中,事件名附带的是 vue: 前缀:
<template>
<child-component @vue:updated="onUpdated">
</template>
Watch 监听数组
概览
- 非兼容:当监听一个数组时,只有当数组被替换时才会触发回调。如果你需要在数组被改变时触发回调,必须指定 deep 选项。
3.x 语法
当使用 watch 选项监听数组时,只有在数组被替换时才会触发回调。换句话说,在数组被改变时监听回调将不再被触发。想要在数组被改变时触发监听回调,必须指定 deep 选项。
watch: {
bookList: {
handler(val, oldVal) {
console.log('book list changed')
},
deep: true
},
}
内容来源于Vue 3 迁移指南