指南
Vue3和Vue2的相比
- Vue3是2020年9月18日,Vue.js发布3.0版本,代号:One Piece(海贼王)
- Vue3支持大多数的Vue2的特性
- Vue2对
TypeScript
支持不友好(所有属性都放在了this
对象上,难以推倒组件的数据类型) - Vue3中设计了一套强大的组合式API代替了Vue2中的选项式API,对TypeScript更加友好
- Vue2大量的
API
挂载在Vue
对象的原型上,难以实现TreeShaking。 - 最主要:Vue3中使用了Proxy配合Reflect 代替了Vue2中object.defineProperty()方法实现数据的响应式(数据代理)
- Vue3重写了虚拟DOM,速度更快了
- 性能的提升:打包大小减少41%;初次渲染快55%, 更新渲染快133%;内存减少54%等
- Vue3新的组件:Fragment(片段)/ Teleport(传送门)/Suspense等
- Vue3设计了一个新的脚手架工具:vite
值得注意的新特性
Vue 3 中需要关注的一些新特性包括:
- 组合式 API*
- 单文件组件中的组合式 API 语法糖 (
<script setup>
)* - Teleport 组件
- Fragments 片段
- Emits 组件选项**
- 来自
@vue/runtime-core
的createRenderer
API 用来创建自定义渲染函数 - 单文件组件中的状态驱动的 CSS 变量 (
<style>
中的v-bind
)* - SFC
<style scoped>
新增全局规则和针对插槽内容的规则 - Suspense 实验性
***** 现在也支持在 Vue 2.7 中使用
** Vue 2.7 中支持,但仅用于类型推断
新的框架级别推荐
Vue 3 的支持库进行了重大更新。以下是新的默认建议的摘要:
- 新版本的 Router, Devtools & test utils 来支持 Vue 3
- 构建工具链: Vue CLI -> Vite
- 状态管理: Vuex -> Pinia
- IDE 支持: Vetur -> Volar
- 新的 TypeScript 命令行工具: vue-tsc
- 静态网站生成: VuePress -> VitePress
- JSX:
@vue/babel-preset-jsx
->@vue/babel-plugin-jsx
全局 API
createApp
调用 createApp
返回一个应用实例,一个 Vue 3 中的新概念。
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
import { createApp } from 'vue'
const app = createApp({})
如果你使用的是 Vue 的 CDN 构建版本,那么 createApp
将通过全局的 Vue
对象暴露。
const { createApp } = Vue
const app = createApp({})
应用实例暴露了 Vue 2 全局 API 的一个子集,经验法则是,任何全局改变 Vue 行为的 API 现在都会移动到应用实例上,以下是 Vue2 全局 API 及其相应的实例 API 列表:
2.x 全局 API | 3.x 实例 API (app ) |
---|---|
Vue.config | app.config |
Vue.config.productionTip | 移除 |
Vue.config.ignoredElements | 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 现在都是具名导出,文档见全局 API Treeshaking。
config.productionTip
移除
在 Vue 3.x 中,“使用生产版本”提示仅在使用“dev + full build”(包含运行时编译器并有警告的构建版本) 时才会显示。
对于 ES 模块构建版本,由于它们是与打包器一起使用的,而且在大多数情况下,CLI 或脚手架已经正确地配置了生产环境,所以本提示将不再出现。
Vue.prototype
替换为 config.globalProperties
在 Vue 2 中, Vue.prototype
通常用于添加所有组件都能访问的 property。
在 Vue 3 中与之对应的是 config.globalProperties
。这些 property 将被复制到应用中,作为实例化组件的一部分。
// 之前 - Vue 2
Vue.prototype.$http = () => {}
// 之后 - Vue 3
const app = createApp({})
app.config.globalProperties.$http = () => {}
Vue.extend
移除
在 Vue 2.x 中,Vue.extend
曾经被用于创建一个基于 Vue 构造函数的“子类”,其参数应为一个包含组件选项的对象。在 Vue 3.x 中,我们已经没有组件构造器的概念了。应该始终使用 createApp
这个全局 API 来挂载组件:
// 之前 - Vue 2
// 创建构造器
const Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data() {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建一个 Profile 的实例,并将它挂载到一个元素上
new Profile().$mount('#mount-point')
// 之后 - Vue 3
const Profile = {
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data() {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
}
Vue.createApp(Profile).mount('#mount-point')
插件开发者须知
在 UMD 构建中,插件开发者使用 Vue.use
来自动安装插件是一个通用的做法。例如,官方的 vue-router
插件是这样在浏览器环境中自行安装的:
var inBrowser = typeof window !== 'undefined'
/* … */
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
由于 use
全局 API 在 Vue 3 中已无法使用,因此此方法将无法正常工作,并且调用 Vue.use()
现在将触发一个警告。取而代之的是,开发者必须在应用实例上显式指定使用此插件:
const app = createApp(MyApp)
app.use(VueRouter)
挂载 App 实例
使用 createApp(/* options */)
初始化后,应用实例 app
可通过 app.mount(domTarget)
挂载根组件实例:
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
经过所有的这些更改,我们在指南开头编写的组件和指令现在将被改写为如下内容:
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() 挂载的应用实例及其组件树,
// 将具有相同的 “button-counter” 组件和 “focus” 指令,
// 而不会污染全局环境
app.mount('#app')
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
。
创建根组件实例
每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的:
const app = Vue.createApp({
/* 选项 */
})
传递给 createApp
的选项用于配置根组件。当我们挂载应用时,该组件被用作渲染的起点。
一个应用需要被挂载到一个 DOM 元素中。例如,如果你想把一个 Vue 应用挂载到 <div id="app"></div>
,应该传入 #app
:
const app = Vue.createApp({
/* 选项 */
})
// 组件配置对象,删除el属性,使用 .mount('#app') 挂载组件实例
const vm = app.mount('#app')
与大多数应用方法不同的是,mount
不返回应用本身。相反,它返回的是根组件实例。
组件的 data
选项必须是一个函数。Vue 会在创建新组件实例的过程中调用此函数。它应该返回一个对象,然后 Vue 会通过响应性系统将其包裹起来,并以 $data
的形式存储在组件实例中。为方便起见,该对象的任何顶级 property 也会直接通过组件实例暴露出来:
var app = Vue.createApp({
// 组件的data必须是一个函数
data(){
return {
count: 4,
}
},
});
const vm = app.mount('#app')
console.log(vm.$data.count) // => 4
console.log(vm.count) // => 4
模板指令
v-model指令
概览
- 非兼容:用于自定义组件时,v-model prop 和事件默认名称已更改:
- prop:
value
->modelValue
; - 事件:
input
->update:modelValue
;
- prop:
- 非兼容:
v-bind
的.sync
修饰符和组件的model
选项已移除,可在v-model
上加一个参数代替; - 新增:现在可以在同一个组件上使用多个
v-model
绑定; - 新增:现在可以自定义
v-model
修饰符。
组件上的v-model
用于自定义组件时,v-model
prop 和事件默认名称已更改:
- prop:
value
->modelValue
; - 事件:
input
->update:modelValue
;
在 3.x 中,自定义组件上的 v-model
相当于传递了 modelValue
prop 并接收抛出的 update:modelValue
事件:
<custom-input
:model-value="searchText"
@update:model-value="searchText = $event"
></custom-input>
- 将其 value 属性绑定到一个名叫 modelValue 的 prop 上
- 在其 input 事件被触发时,将新的值通过自定义的 update:modelValue 事件抛出
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>`;
})
现在 v-model 就应该可以在这个组件上完美地工作起来了:
<custom-input v-model="searchText"></custom-input>
getter 和 setter实现 v-model
在自定义组件中创建 v-model 功能的另一种方法是使用 computed 的功能来定义 getter 和 setter
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `<input v-model="value">`,
computed: {
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
}
}
})
定义自定义事件
Vue 3 现在提供一个 emits
选项,和现有的 props
选项类似。这个选项可以用来定义一个组件可以向其父组件触发的事件。
app.component('custom-form', {
emits: ['inFocus', 'submit']
})
-
当在
emits
选项中定义了原生事件 (如click
) 时,将使用组件中的事件替代原生事件侦听器。 -
建议定义所有发出的事件,以便更好地记录组件应该如何工作。
验证抛出的事件
与 prop 类型验证类似,如果使用对象语法而不是数组语法定义发出的事件,则可以对它进行验证。
要添加验证,请为事件分配一个函数,该函数接收传递给 $emit
调用的参数,并返回一个布尔值以指示事件是否有效。
app.component('custom-form', {
emits: {
// 没有验证
click: null,
// 验证 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
},
methods: {
submitForm(email, password) {
this.$emit('submit', { email, password })
}
}
})
v-model
参数
默认情况下,组件上的 v-model
使用 modelValue
作为 prop 和 update:modelValue
作为事件。若需要更改 model
的名称,现在我们可以为 v-model
传递参数来修改这些名称,以作为组件内 model
选项的替代:
<my-component v-model:title="pageTitle"></my-component>
<!-- 是以下的简写: -->
<my-component :title="pageTitle" @update:title="pageTitle = $event"></my-component>
在本例中,子组件将需要一个 title
prop 并发出 update:title
事件来进行同步:
app.component('my-component', {
props: ['title'],
emits: ['update:title'],
template: `
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)">
`
})
这也可以作为 .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
绑定
正如我们之前在 v-model
参数中所学的那样,通过利用以特定 prop 和事件为目标的能力,我们现在可以在单个组件实例上创建多个 v-model 绑定。
- 默认情况下
v-model:modelValue
简写为v-model
,省略了modelValue
- 使用其他参数,名字不能省略
v-model:firstName
每个 v-model 将同步到不同的 prop,而不需要在组件中添加额外的选项:
<user-name
v-model="age"
v-model:firstName="firstName"
v-model:lastName="lastName"
></user-name>
app.component('user-name', {
props: {
modelValue: Number,
firstName: String,
lastName: String
},
emits: ['update:modelValue', 'update:firstName', 'update:lastName'],
template: `
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)">
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)">
<input
type="number"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value*1)">
`
})
自定义v-model
修饰符
除了像 .trim
这样的 2.x 硬编码的 v-model
修饰符外,现在 3.x 还支持自定义修饰符:
<my-component v-model.capitalize.x="text"></my-component>
app.component('my-component', {
props: {
modelValue: String,
// 添加到 v-model 的修饰符会自动添加到 modelModifiers 对象上 提供给组件
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
template: `<input type="text" :value="modelValue" @input="emitValue">`,
methods: {
emitValue(e) {
// console.log(this.modelModifiers); // {capitalize: true, x: true}
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
},
})
带参数的v-model修饰符绑定
对于带参数的 v-model
绑定,生成的 prop 名称将为 arg + "Modifiers"
:
<my-div v-model:text.capitalize.x="text"></my-div>{{text}}
app.component('my-component', {
props: {
modelValue: String,
// 添加到 v-model 的修饰符将自定添加到 textModifiers 对象上提供给组件
textModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
template: `<input type="text" :value="modelValue" @input="emitValue">`,
methods: {
emitValue(e) {
// console.log(this.textModifiers); // {capitalize: true, x: true}
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
},
})
key
概览
- 新增:对于
v-if
/v-else
/v-else-if
的各分支项key
将不再是必须的,因为现在 Vue 会自动生成唯一的key
。- 非兼容:如果你手动提供
key
,那么每个分支必须使用唯一的key
。你将不再能通过故意使用相同的key
来强制重用分支。
- 非兼容:如果你手动提供
- 非兼容:
<template v-for>
的key
应该设置在<template>
标签上 (而不是设置在它的子节点上)。
背景
特殊的 key
attribute 被作为 Vue 的虚拟 DOM 算法的提示,以保持对节点身份的持续跟踪。这样 Vue 就可以知道何时能够重用和修补现有节点,以及何时需要对它们重新排序或重新创建。关于其它更多信息,可以查看以下章节:
在条件分支中
在 Vue 2.x 中,建议在 v-if
/v-else
/v-else-if
的分支中使用 key
。
<!-- Vue 2.x -->
<div v-if="condition" key="yes">Yes</div>
<div v-else key="no">No</div>
这个示例在 Vue 3.x 中仍能正常工作。但是我们不再建议在 v-if
/v-else
/v-else-if
的分支中继续使用 key
attribute,因为没有为条件分支提供 key
时,也会自动生成唯一的 key
。
<!-- Vue 3.x -->
<div v-if="condition">Yes</div>
<div v-else>No</div>
非兼容变更体现在如果你手动提供了 key
,那么每个分支都必须使用一个唯一的 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>
结合 <template v-for>
在 Vue 2.x 中,<template>
标签不能拥有 key
。不过,你可以为其每个子节点分别设置 key
。
<!-- Vue 2.x -->
<template v-for="item in list">
<div :key="'heading-' + item.id">...</div>
<span :key="'content-' + item.id">...</span>
</template>
在 Vue 3.x 中,key
则应该被设置在 <template>
标签上。
<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
<div>...</div>
<span>...</span>
</template>
类似地,当使用 <template v-for>
时如果存在使用 v-if
的子节点,则 key
应改为设置在 <template>
标签上。
<!-- Vue 2.x -->
<template v-for="item in list">
<div v-if="item.isVisible" :key="item.id">...</div>
<span v-else :key="item.id">...</span>
</template>
<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
<div v-if="item.isVisible">...</div>
<span v-else>...</span>
</template>
v-if 与 v-for 的优先级对比
概览
- 非兼容:两者作用于同一个元素上时,
v-if
会拥有比v-for
更高的优先级。
介绍
Vue.js 中使用最多的两个指令就是 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,那么绑定的声明顺序将决定它们如何被合并。换句话说,相对于假设开发者总是希望独立 attribute 覆盖 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>
移除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>
组件
组件注册
- 注册全局组件使用 app.component()
- 注册全局组件还是使用 components:{}
const app = Vue.createApp({
// 注册局部组件
components: {
'my-div3':{},
'my-div4':{},
}
});
// 注册全局组件
app.component('my-div1', {
//组件的模板中可以添加多个根标签
});
app.component('my-div2', {
//组件的模板中可以添加多个根标签
});
app.mount('#app);
emits
选项 新增
概述
Vue 3 现在提供一个 emits
选项,和现有的 props
选项类似。这个选项可以用来定义一个组件可以向其父组件触发的事件。
2.x 的行为
在 Vue 2 中,你可以定义一个组件可接收的 prop,但是你无法声明它可以触发哪些事件:
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text']
}
</script>
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
记录每个组件所触发的所有事件。
这尤为重要,因为我们移除了 .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
事件。当你真的在<my-button>
的事件处理器上加入了一些逻辑时,这会很有用。 - 移除透传的事件,因为现在父组件可以很容易地监听原生事件,而不需要添加
.native
。适用于你只想透传这个事件。
异步组件
概览
以下是对变化的总体概述:
- 新的
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 语法
现在,在 Vue 3 中,由于函数式组件被定义为纯函数,因此异步组件需要通过将其包裹在新的 defineAsyncComponent
助手方法中来显式地定义:
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 Router 支持一个类似的机制来异步加载路由组件,也就是俗称的懒加载。尽管类似,但是这个功能和 Vue 所支持的异步组件是不同的。当用 Vue Router 配置路由组件时,你不应该使用 defineAsyncComponent
。你可以在 Vue Router 文档的懒加载路由章节阅读更多相关内容。
对 2.x 所做的另一个更改是,component
选项现在被重命名为 loader
,以明确组件定义不能直接被提供。
import { defineAsyncComponent } from 'vue'
const asyncModalWithOptions = defineAsyncComponent({
loader: () => import('./Modal.vue'),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})
此外,与 2.x 不同,loader 函数不再接收 resolve
和 reject
参数,且必须始终返回 Promise。
// 2.x 版本
const oldAsyncComponent = (resolve, reject) => {
/* ... */
}
// 3.x 版本
const asyncComponent = defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
/* ... */
})
)
新的组件
Fragment
Vue 3 现在正式支持了多根节点的组件,也就是片段!
- 在Vue2中: 组件必须有一个根标签
- 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
- 好处: 减少标签层级, 减小内存占用
如果你的组件有多个根元素,你需要定义哪些部分将接收这个类。可以使用 $attrs 组件属性执行此操作
<my-div :class="classObj" class="static"></my-div>
template:`
<div :class="$attrs.class">1</div>
<div>2</div>
`,
Teleport传送门
Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。
Teleport
是一种能够将我们的组件html结构移动到指定位置的技术。
<style>
.modal {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, .5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.modal div {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
background-color: white;
width: 300px;
height: 200px;
padding: 5px;
}
</style>
<div id="app">
<!-- 当在初始的 HTML 结构中使用这个组件时,我们可以看到一个问题——模态框是在深度嵌套的 div.box 中渲染的,而模态框的 position:absolute 以父级相对定位的 div.outer 作为引用。 -->
<div style="position: relative;" class="outer">
<h3>传送门</h3>
<div class="box">
<modal-button></modal-button>
</div>
</div>
</div>
<!--
Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。
使用 <teleport>,并告诉 Vue “将这个 HTML 传送到‘body’标签下”。
-->
<script id="modal-button" type="text/html">
<button @click="modalOpen = true">打开全屏的弹窗</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
全屏的模态框,父元素是body标签
<button @click="modalOpen = false">关闭</button>
</div>
</div>
</teleport>
</script>
<script>
const app = Vue.createApp({});
app.component('modal-button', {
template: `#modal-button`,
data () {
return {
modalOpen: false
}
}
})
app.mount('#app');
</script>
Suspense
<Suspense>
是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。
- 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
引入异步组件
import {defineAsyncComponent} from 'vue'
const Child = defineAsyncComponent(()=>import('./components/Child.vue'))
加载状态
使用Suspense
包裹组件,并配置好default
与 fallback
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<h3>加载中.....</h3>
</template>
</Suspense>
</div>
</template>
在初始渲染时,<Suspense>
将在内存中渲染其默认的插槽内容。如果在这个过程中遇到任何异步依赖,则会进入挂起状态。在挂起状态期间,展示的是后备内容。当所有遇到的异步依赖都完成后,<Suspense>
会进入完成状态,并将展示出默认插槽的内容。
如果在初次渲染时没有遇到异步依赖,<Suspense>
会直接进入完成状态。
进入完成状态后,只有当默认插槽的根节点被替换时,<Suspense>
才会回到挂起状态。组件树中新的更深层次的异步依赖不会造成 <Suspense>
回退到挂起状态。
发生回退时,后备内容不会立即展示出来。相反,<Suspense>
在等待新内容和异步依赖完成时,会展示之前 #default
插槽的内容。这个行为可以通过一个 timeout
prop 进行配置:在等待渲染新内容耗时超过 timeout
之后,<Suspense>
将会切换为展示后备内容。若 timeout
值为 0
将导致在替换默认内容时立即显示后备内容。
事件
<Suspense>
组件会触发三个事件:pending
、resolve
和 fallback
。pending
事件是在进入挂起状态时触发。resolve
事件是在 default
插槽完成获取新内容时触发。fallback
事件则是在 fallback
插槽的内容显示时触发。
例如,可以使用这些事件在加载新组件时在之前的 DOM 最上层显示一个加载指示器。
渲染函数
渲染函数 API
概览
此更改不会影响 <template>
用户。
以下是更改的简要总结:
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 中,注册一个组件后,把组件名作为字符串传递给渲染函数的第一个参数,它可以正常地工作:
// 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.$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
& style
概览
$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 行为
Vue3中$attrs
现在包含了所有传递给组件的 attribute,包括 class
和 style
,这使得把它们全部应用到另一个元素上变得更加容易了。现在上面的示例将生成以下 HTML:
<label>
<input type="text" id="my-id" class="my-class" />
</label>
与单个根节点组件不同,具有多个根节点的组件不具有自动 属性 贯穿行为。如果未显式绑定 $attrs,将发出运行时警告。
<script id="my-div" type="text/html">
<div class="my-div" v-bind="$attrs"></div>
<div></div>
</script>
移除的 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 更新
我们从实例中完全移除了 $on
、$off
和 $once
方法。$emit
仍然包含于现有的 API 中,因为它用于触发由父组件声明式添加的事件处理函数。
过滤器
概览
从 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>
$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 已被移除,且不再支持。如果你需要访问子组件实例,我们建议使用模板引用。
其他变化
自定义指令
概览
指令的钩子函数已经被重命名,以更好地与组件的生命周期保持一致。
额外地,expression
字符串不再作为 binding
对象的一部分被传入。
2.x 语法
在 Vue 2 中,自定义指令通过使用下列钩子来创建,以对齐元素的生命周期,它们都是可选的:
- bind - 指令绑定到元素后调用。只调用一次。
- inserted - 元素插入父 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 → beforeMount
- inserted → mounted
- beforeUpdate:新增!在元素本身被更新之前调用,与组件的生命周期钩子十分相似。
- update → 移除!该钩子与
updated
有太多相似之处,因此它是多余的。请改用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
}
WARNING
有了片段的支持,组件可能会有多个根节点。当被应用于多根组件时,自定义指令将被忽略,并将抛出警告。
Data 选项
概览
- 非兼容:组件选项
data
的声明不再接收纯 JavaScriptobject
,而是接收一个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
}
}
被挂载的应用不会替换元素
概述
在 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')
}
}
}
}
侦听数组
概览
- 非兼容: 当侦听一个数组时,只有当数组被替换时才会触发回调。如果你需要在数组被改变时触发回调,必须指定
deep
选项。
3.x 语法
当使用 watch
选项侦听数组时,只有在数组被替换时才会触发回调。换句话说,在数组被改变时侦听回调将不再被触发。要想在数组被改变时触发侦听回调,必须指定 deep
选项。
watch: {
bookList: {
handler(val, oldVal) {
console.log('book list changed')
},
deep: true
},
}
迁移策略
如果你依赖于侦听数组的改变,添加 deep
选项以确保回调能被正确地触发。
提供注入 Provide / Inject
通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。
对于这种情况,我们可以使用一对 provide
和 inject
。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide
选项来提供数据,子组件有一个 inject
选项来开始使用这些数据。
实际上,你可以将依赖注入看作是“长距离的 prop”,除了:
- 父组件不需要知道哪些子组件使用了它 provide 的 property
- 子组件不需要知道 inject 的 property 来自哪里
<div id="app">
<input type="number" v-model.number="number">
<my-div></my-div>
</div>
const app = Vue.createApp({
data () {
return {
message: 'hello app!',
number: 10,
}
},
// 要访问组件实例的属性,我们需要将 provide 定义为函数,并在函数中返回对象
provide () {
return {
user: '张三',
msg: this.message,
// 想对祖先组件中的更改做出反应,我们需要为我们提供的 num 分配一个组合式 API computed 属性
num: Vue.computed(() => this.number),
}
}
});
app.component('my-div', {
name: 'my-div',
inject: ['user'],
template: `<div>组件my-div: {{user}}<my-list></my-list></div>`,
components: {
'my-list': {
name: 'my-list',
inject: ['user', 'msg', 'num'],
template: `<div>组件my-list: {{user}} msg:{{msg}} num:{{num.value}}</div>`,
created () {
this.msg = 'xxxx';
},
}
},
});
app.mount('#app');
生命周期函数的变化
- beforeDestroy -> beforeUnmount
- destroy -> unmounted
v-for 中的 Ref 数组
在 Vue 2 中,在 v-for
中使用的 ref
属性会用 ref 数组填充相应的 $refs
。当存在嵌套的 v-for
时,这种行为会变得不明确且效率低下。
在 Vue 3 中,此类用法将不再自动创建 $ref
数组。要从单个绑定获取多个 ref,请将 ref
绑定到一个更灵活的函数上 (这是一个新特性):
<div v-for="item in list" :ref="setItems"></div>
export default {
data() {
return {
list:[2,4,6,8],
divs: []
}
},
methods: {
setItems(el) {
if (el) {
this.divs.push(el)
}
},
},
// 确保在每次更新之前重置divs
beforeUpdate() {
this.divs = []
},
}
动画的变化
过渡的 class 名更改
概览
过渡样式类名的变化:
v-enter
修改为v-enter-from
v-leave
修改为v-leave-from
3个进入过渡类:
-
v-enter-from:定义进入过渡的开始状态。
-
v-enter-to:定义进入过渡的结束状态。
-
v-enter-active:定义进入过渡生效时的状态。
3个离开过渡类:
-
v-leave-from:定义离开过渡的开始状态。
-
v-leave-to:定义离开过渡生效时的状态。
-
v-leave-active:离开过渡的结束状态。
<transition>
组件的相关 prop 名称也发生了变化:
leave-class
已经被重命名为leave-from-class
enter-class
已经被重命名为enter-from-class
2.x 语法
在 v2.1.8 版本之前,每个过渡方向都有两个过渡类:初始状态与激活状态。
在 v2.1.8 版本中,引入了 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;
}
现在,这些状态之间的区别就清晰多了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xwZnWzqm-1680606240145)(null)]
<transition>
组件的相关 prop 名称也发生了变化:
leave-class
已经被重命名为leave-from-class
(在渲染函数或 JSX 中可以写为:leaveFromClass
)enter-class
已经被重命名为enter-from-class
(在渲染函数或 JSX 中可以写为:enterFromClass
)
Transition 作为根节点
概览
当使用 <transition>
作为根结点的组件从外部被切换时将不再触发过渡效果。
2.x 行为
在 Vue 2 中,通过使用 <transition>
作为一个组件的根节点,过渡效果存在从组件外部触发的可能性:
<!-- 模态组件 -->
<template>
<transition>
<div class="modal"><slot/></div>
</transition>
</template>
<!-- 用法 -->
<modal v-if="showModal">hello</modal>
切换 showModal
的值将会在模态组件内部触发一个过渡效果。
这是无意为之的,并不是设计效果。一个 <transition>
原本是希望被其子元素触发的,而不是 <transition>
自身。
这个怪异的现象现在被移除了。
迁移策略
换做向组件传递一个 prop 就可以达到类似的效果:
<template>
<transition>
<div v-if="show" class="modal"><slot/></div>
</transition>
</template>
<script>
export default {
props: ['show']
}
</script>
<!-- 用法 -->
<modal :show="showModal">hello</modal>
Transition Group 根元素
概览
<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 中,我们有了片段的支持,因此组件不再需要根节点。所以,<transition-group>
不再默认渲染根节点。
- 如果像上面的示例一样,已经在 Vue 2 代码中定义了
tag
attribute,那么一切都会和之前一样 - 如果没有定义
tag
attribute,而且样式或其它行为依赖于<span>
根元素的存在才能正常工作,那么只需将tag="span"
添加到<transition-group>
:
<transition-group tag="span">
<!-- -->
</transition-group>
Vue 3 如何使用eventBus
如果你刚开始使用Vue3,很可能会发现,原本用得得心应手的eventBus(事件总线)突然不灵了。
因为Vue3不再提供$on
函数,Vue实例不再实现事件接口。官方推荐引入外部工具实现,或者自己手撸一个事件类
api变更文档
-
$on
,$off
和$once
实例方法已被移除,应用实例不再实现事件触发接口。 -
$emit
仍然是现有 API 的一部分,因为它用于触发由父组件以声明方式附加的事件处理程序
如何使用
想在Vue3上把EventBus再次用起来也非常简单,大体就是三个步骤
- 引入/编写事件库
- 在入口文件中挂载
- 在组件中引入并使用
通过Vue3-Eventbus(推荐)
不需要在入口文件中编写额外逻辑,不需要每次引入inject函数,不需要每次为bus赋值,import进来一把梭直接用。
安装
$ npm install --save vue3-eventbus
挂载
import bus from 'vue3-eventbus'
app.use(bus)
使用
// Button.vue
import bus from 'vue3-eventbus'
export default {
setup() {
bus.emit('foo', { a: 'b' })
}
}
更多用法和配置可以参照github上的文档
不借助插件的原生使用方式
引入/编写事件库
- 直接引入官方推荐的mitt
- 手撸一个简单的发布/订阅类
这两种方式都没啥差别,因为代码逻辑也很简单,贴一个代码实现,可以直接copy去用
// eventBus.js
export default class EventBus{
constructor(){
this.events = {};
}
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(function(fn) {
fn(data);
});
}
}
on(eventName, fn) {
this.events[eventName] = this.events[eventName] || [];
this.events[eventName].push(fn);
}
off(eventName, fn) {
if (this.events[eventName]) {
for (var i = 0; i < this.events[eventName].length; i++) {
if (this.events[eventName][i] === fn) {
this.events[eventName].splice(i, 1);
break;
}
};
}
}
}
在入口文件中执行挂载
入口文件默认是main.js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
// ① 引入事件类
// 自己编写的或者mitt皆可
import EventBus from 'lib/bus.js'
// 或者:import EventBus from 'mitt'
const $bus = new EventBus()
// ② 挂载
// 1.使用provide提供
app.provide('$bus', $bus)
// 2.挂载到this上
app.config.globalProperties.$bus = $bus
在组件中引入并使用
在created中使用
// Button.vue
export default {
created() {
this.$bus.emit('ButtonCreated')
}
}
复制代码
在setup中使用
注意: 因为在setup中无法访问到应用实例(this
),如果你需要在setup中使用eventBus,则需要通过provide/inject方式引入
// Button.vue
import { inject } from 'vue'
export default {
setup() {
const $bus = inject('$bus')
$bus.emit('ButtonSetup')
}
}
使用小结
通过上面三个步骤,EventBus就可以正常使用啦,还是很简单的。不过也能看到在挂载的时候需要多写两行代码,使用的时候,每个组件在setup内都要引入inject函数,并且初始化一次。有朋友就要问了?有没有更优雅的办法咧?
没错!用了 Vue3-Eventbus,只需要在入口文件里use一下,每个组件里引入就能直接用起来啦!
Vue3响应式原理
Vue3与Vue2的响应式原理
Vue的响应式是怎么实现的?
- Vue2的响应式是基于
Object.defineProperty
实现的。 - Vue3的响应式是基于ES6的
Proxy
来实现的。
Vue3的响应式 优于 Vue2响应式,两个版本响应式原理的不同也是体现在Object.defineProperty
和Proxy
的差异上,那么Vue3的响应式到底比Vue2的响应式好在哪?
Vue2.0 有哪些缺点或者说不足:
- 递归,大量的递归,如果数据嵌套层级过多,会特别的消耗内存资源,性能不好
- 更改数组,改变数组的长度是无效的
- 对象不存在的属性是不能被拦截的
如果你只说 Vue2.0 是基于Object.definePropery;Vue3.0是基于ES6的proxy来架构的,仅此而已的话,那显然是不够的。
模拟Vue的响应式原理
Vue2的响应式
大家都知道Vue2的响应式是基于Object.defineProperty
的,那我就拿Object.defineProperty
来举个例子
<script>
// 模拟 Vue 中的 data
let data = {
name: '张三',
age: 12
}
// 更新视图
function updateView (key, value) {
document.getElementById(key).innerHTML = key + '=' + value
}
// 模拟Vue的响应式函数reactive
function reactive (target, key, value) {
Object.defineProperty(target, key, {
get () {
console.log(`访问了${key}属性`)
return value
},
set (newValue) {
console.log(`将${key}由->${value}->设置成->${newValue}`)
if (value !== newValue) {
value = newValue;
updateView(key, newValue);
}
}
})
}
// 模拟 Vue 实例,代理data中的每一个属性,后增加的属性,不具有响应式
Object.keys(data).forEach(key => reactive(data, key, data[key]));
updateView('name', data.name);
updateView('age', data.age);
// 模拟值的改变
function addClick (key, value) {
if (data[key]) {
data[key] = data[key] + value;
} else {
data[key] = value;
}
}
</script>
上面使用Object.defineProperty
定义的响应式函数到底有什么弊端呢?使得尤大大在Vue3中抛弃了它,如下例子:
<div id="app">
<button onclick="addClick('age', 1)">点击改变age</button>
<button onclick="addClick('weight', 2)">点击增加weight属性</button>
<p id="name"></p>
<p id="age"></p>
<p id="weight"></p>
</div>
data新增了weight
属性,进行访问和设值,但是都不会触发get和set
,所以弊端就是:Object.defineProperty
只对初始对象里的属性有监听作用,而对新增的属性无效。这也是为什么Vue2中对象新增属性的修改需要使用Vue.$set
来设值的原因。
Vue3的响应式
从上面,知道了Object.defineProperty
的弊端,Vue3中响应式原理的核心Proxy
是怎么弥补这一缺陷的,如下例子:
<div id="app">
<button onclick="addClick('age', 1)">点击改变age</button>
<button onclick="addClick('weight', 2)">点击增加weight属性</button>
<p id="name">name=张三</p>
<p id="age">age=12</p>
<p id="weight"></p>
</div>
<script>
// 模拟 Vue 中的 data
let data = {
name: '张三',
age: 12
}
function updateView (key, value) {
document.getElementById(key).innerHTML = key + '=' + value
}
// 模拟vue的响应式函数
function reactive (target) {
const handler = {
get (target, key, receiver) {
console.log(`访问了${key}属性`)
return Reflect.get(target, key, receiver);
},
set (target, key, value, receiver) {
console.log(`将${key}由->${target[key]}->设置成->${value}`);
Reflect.set(target, key, value, receiver);
updateView(key, target[key]);
}
}
return new Proxy(target, handler)
}
// 模拟 Vue 实例,代理data中的每一个属性
// Proxy直接会代理监听data的内容,不管data有多少属性,使用proxy代理整个对象,非常的简单方便,而 defineProperty 需要手动循环代理每个对象里的每个属性
const vm = reactive(data);
updateView('name', data.name);
updateView('age', data.age);
// 模拟值的改变
function addClick (key, value) {
if (vm[key]) {
vm[key] = vm[key] + value;
} else {
vm[key] = value;
}
}
</script>
可以看到,其实效果与上面的Object.defineProperty
没什么差别,那为什么尤大大要抛弃它,选择Proxy
呢?注意了,最最最关键的来了,那就是对象新增属性,依然会触发get和set
,vm对象代理了data中的每一个属性。
Vue2.0和Vue3.0响应式的差异
Vue2.0
- 基于
Object.defineProperty
,不具备监听数组的能力,需要重新定义数组的原型来达到响应式。 Object.defineProperty
无法检测到对象属性的添加和删除 。- 由于Vue会在初始化实例时对属性执行getter/setter转化,所有属性必须在data对象上存在才能让Vue将它转换为响应式。
- 深度监听需要一次性递归,对性能影响比较大。
Vue3.0
- 基于Proxy和Reflect,可以原生监听数组,可以监听对象属性的添加和删除。
- 不需要一次性遍历data的属性,可以显著提高性能。
- 因为Proxy是ES6新增的属性,有些浏览器还不支持,只能兼容到IE11 。