本文主要讲解Vue3常用的新特性,详细可以查看Vue3.0官网教程
Vue3 比 Vue2 到底强在哪里?
- Vue3 具有更明显的性能的提升(具体体现在:打包大小更小,初次渲染更快,更新更快,内存使用减少等优点)
- Vue3 的 composition API 解决了组件碎片化的问题,使组件更具逻辑化,方便后期维护。
- Vue3 增加了一些好用的新特性,如片段(Fragment)、Tree-shaking 和 Teleport 等。
Fragment(片段)
在 Vue 2.x 中,由于不支持多根节点组件,当开发者意外创建一个时会发出警告
(The template root requires exactly one element.
)。
在 Vue 3.x 中,组件可以包含多个根节点!
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
复制代码
一个新的全局 API:createApp
在 Vue 2.x 中使用 new 的方式来创建一个Vue实例,而 Vue 3.x 则用 Vue.createApp 来初创建一个Vue实例。
const app = Vue.createApp({
/* 选项 */
})
复制代码
以下是 Vue2 全局 API 对应 Vue3 全局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 |
Vue.prototype | app.config.globalProperties |
Tree-shaking
Tree-shaking的本质是消除无用的js代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination)。
Tree-shaking 是 DCE 的一种新的实现,Javascript同传统的编程语言不同的是,javascript绝大多数情况需要通过网络进行加载,然后执行,加载的文件大小越小,整体执行时间更短,所以去除无用代码以减少文件体积,对javascript来说更有意义。
在 Vue 3.x 中,全局和内部 API 都经过了重构,并考虑到了 tree-shaking 的支持。因此,全局 API 现在只能作为 ES 模块构建的命名导出进行访问。例如:
// vue 2
import Vue from 'vue'
Vue.nextTick(() => {
// 一些和DOM有关的东西
})
// vue 3
import { nextTick } from 'vue'
nextTick(() => {
// 一些和DOM有关的东西
})
复制代码
//vue 2
import { shallowMount } from '@vue/test-utils'
import { MyComponent } from './MyComponent.vue'
test('an async feature', async () => {
const wrapper = shallowMount(MyComponent)
// 执行一些DOM相关的任务
await wrapper.vm.$nextTick()
// 运行你的断言
})
//vue 3
import { shallowMount } from '@vue/test-utils'
import { MyComponent } from './MyComponent.vue'
import { nextTick } from 'vue'
test('an async feature', async () => {
const wrapper = shallowMount(MyComponent)
// 执行一些DOM相关的任务
await nextTick()
// 运行你的断言
})
复制代码
通过这一更改,如果模块打包工具支持 tree-shake,则 Vue 应用程序中未使用的全局 API 将从最终的打包产物中排除,从而获得最佳的文件大小。
Vue 2.x 中的这些全局 API 受此更改的影响:
- Vue.nextTick
- Vue.observable (用 Vue.reactive 替换)
- Vue.version
- Vue.compile (仅完整构建版本)
- Vue.set (仅兼容构建版本)
- Vue.delete (仅兼容构建版本)
生命周期
Vue2 和 Vue3 生命周期的对比
emits选项(新增)
和 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 定义里的验证器类似。
Teleport
Teleport 是一种能够将我们的模板移动到 DOM 中 Vue app 之外的其他位置的技术,就有点像哆啦A梦的“任意门”。下面举个?:
//index.html
<div id="app"></div>
+ <div id="teleport-target"></div>
<script type="module" src="/src/main.js"></script>
复制代码
//src/components/HelloWorld.vue 中,添加如下,留意 to 属性跟上面的 id 选择器一致
<button @click="showToast" class="btn">打开 toast</button>
<!-- to 属性就是目标位置 -->
<teleport to="#teleport-target">
<div v-if="visible" class="toast-wrap">
<div class="toast-msg">我是一个 Toast 文案</div>
</div>
</teleport>
<script>
import { ref } from 'vue';
export default {
setup() {
// toast 的封装
const visible = ref(false);
let timer;
const showToast = () => {
visible.value = true;
clearTimeout(timer);
timer = setTimeout(() => {
visible.value = false;
}, 2000);
}
return {
visible,
showToast
}
}
}
</script>
复制代码
效果图以及更佳详细地理解可以看这篇文章(文章清晰易懂)Vue 3 任意传送门——Teleport
移除了 $children
在 Vue2.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>
复制代码
在 Vue3.x 中,$childrens 用法已移除,不再支持。
移除了过滤器 filter
在 Vue2.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>
复制代码
在 Vue3.x 中,过滤器已删除,不再支持。相反地,Vue官方建议用方法调用或计算属性来替换它们。
移除了 .sync 修饰符
在 Vue2.x 中,我们有时候需要对某一个 prop 进行 “双向绑定” ,可以进行以下操作。
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
// 简写
<ChildComponent :title.sync="pageTitle" />
复制代码
相当于 Vue3.x 中以v-model修饰符代替.sync修饰符。
<ChildComponent v-model:title="pageTitle" />
复制代码
移除 v-on.native 修饰符
在 Vue2.x 中,要将原生 DOM 监听器添加到子组件的根元素中,可以使用 .native 修饰符
<my-component
v-on:close="handleComponentEvent"
v-on:click.native="handleNativeClickEvent"
/>
复制代码
在 Vue3.x 中,v-on 的 .native 修饰符已被移除。同时,新增的 emits 选项允许子组件定义真正会被触发的事件。
<my-component
v-on:close="handleComponentEvent"
v-on:click="handleNativeClickEvent"
/>
复制代码
// MyComponent.vue
<script>
export default {
emits: ['close']
}
</script>
复制代码
移除 $listeners
$listeners 对象在 Vue 3 中已被移除。具体可以看官方文档
移除 $(on、off、once)
在 Vue3 中$(on,off,once) 实例方法已被移除,应用实例不再实现事件触发接口。
<script>
created() {
console.log(this.$on, this.$once, this.$off) // undefined undefined undefined
}
</script>
复制代码
!!!重头戏来了,Composition API是 Vue3里最重要的特性之一!!!
Composition API
什么是Composition API?
为了更好地理解Composition API,我找到了 大帅老猿
关于Composition API的4张动画图,图出自 做了一夜动画,就为让大家更好的理解Vue3的Composition Api
我们先来看看 Vue 2 中的Options API是什么样子的:
当我们的项目有了新的需求时,往往是需要在Options API中添加如下功能代码:
当我们的组件开始变得更大时,逻辑关注点的列表也会增长。尤其对于那些一开始没有编写这些组件的人来说,这会导致组件难以阅读和理解。因此Vue3 Composition API就是为了解决这种问题。
当我们使用了Vue3的 Composition API后可以使同一个逻辑关注点相关代码收集在一起,这样就增加了可阅读性:
既然我们知道了为什么,我们就可以知道怎么做。为了开始使用组合式 API,我们首先需要一个可以实际使用它的地方。在 Vue 组件中,我们将此位置称为 setup
新的 setup 选项在组件创建之前执行,一旦 props 被解析,就将作为组合式 API 的入口。setup 选项是一个接收 props 和 context 的函数。
(⚠️注意:在 setup 中你应该避免使用 this,因为它不会找到组件实例。setup 的调用发生在 data property、computed property 或 methods 被解析之前,所以它们无法在 setup 中被获取。)
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup(props) {
console.log(props) // { user: '' }
return {} // 这里返回的任何内容都可以用于组件的其余部分
}
// 组件的“其余部分”
}
复制代码
带 ref 的响应式变量
在 Vue 3.0 中,我们可以通过一个新的 ref 函数使任何响应式变量在任何地方起作用,如下所示:
import { ref } from 'vue'
const counter = ref(0)
复制代码
ref 接收参数并将其包裹在一个带有 value property 的对象中返回,然后可以使用该 property 访问或更改响应式变量的值:
import { ref } from 'vue'
const counter = ref(0)
console.log(counter) // { value: 0 }
console.log(counter.value) // 0
counter.value++
console.log(counter.value) // 1
复制代码
将值封装在一个对象中,看似没有必要,但为了保持 JavaScript 中不同数据类型的行为统一,这是必须的。这是因为在 JavaScript 中,Number 或 String 等基本类型是通过值而非引用传递的,在任何值周围都有一个封装对象,这样我们就可以在整个应用中安全地传递它,而不必担心在某个地方失去它的响应性。
在 setup 内注册生命周期钩子
为了使组合式 API 的功能和选项式 API 一样完整,我们还需要一种在 setup 中注册生命周期钩子的方法。
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted } from 'vue'
// 在我们的组件中
setup (props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
onMounted(getUserRepositories) // 在 `mounted` 时调用 `getUserRepositories`
return {
repositories,
getUserRepositories
}
}
复制代码
watch 响应式更改
就像我们在组件中使用 watch 选项并在 user property 上设置侦听器一样,我们也可以使用从 Vue 导入的 watch 函数执行相同的操作。它接受 3 个参数:
- 一个想要侦听的响应式引用或 getter 函数
- 一个回调
- 可选的配置选项
import { ref, watch } from 'vue'
const counter = ref(0)
watch(counter, (newValue, oldValue) => {
console.log('The new counter value is: ' + counter.value)
})
复制代码
每当 counter 被修改时,例如 counter.value=5,侦听将触发并执行回调 (第二个参数),在本例中,它将把 ‘The new counter value is:5’ 记录到控制台中。
独立的 computed 属性
与 ref 和 watch 类似,也可以使用从 Vue 导入的 computed 函数在 Vue 组件外部创建计算属性。让我们回到 counter 的例子:
import { ref, computed } from 'vue'
const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)
counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2
复制代码
这里我们给 computed 函数传递了第一个参数,它是一个类似 getter 的回调函数,输出的是一个只读的响应式引用。为了访问新创建的计算变量的 value,我们需要像 ref 一样使用 .value property。
接下来我们来看由Options API 转换成 Composition API的两段代码:
// Options API
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
data () {
return {
repositories: [], // 1
filters: { ... }, // 3
searchQuery: '' // 2
}
},
computed: {
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch: {
user: 'getUserRepositories' // 1
},
methods: {
getUserRepositories () {
// 使用 `this.user` 获取用户仓库
}, // 1
updateFilters () { ... }, // 3
},
mounted () {
this.getUserRepositories() // 1
}
}
复制代码
// src/composables/useRepositoryNameSearch.js
import { ref, computed } from 'vue'
export default function useRepositoryNameSearch(repositories) {
const searchQuery = ref('')
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(repository => {
return repository.name.includes(searchQuery.value)
})
})
return {
searchQuery,
repositoriesMatchingSearchQuery
}
}
// Composition API
import { toRefs } from 'vue'
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup(props) {
const { user } = toRefs(props)
const { repositories, getUserRepositories } = useUserRepositories(user)
const {
searchQuery,
repositoriesMatchingSearchQuery
} = useRepositoryNameSearch(repositories)
const {
filters,
updateFilters,
filteredRepositories
} = useRepositoryFilters(repositoriesMatchingSearchQuery)
return {
// 因为我们并不关心未经过滤的仓库
// 我们可以在 `repositories` 名称下暴露过滤后的结果
repositories: filteredRepositories,
getUserRepositories,
searchQuery,
filters,
updateFilters
}
}
}
复制代码
setup
使用 setup 函数时,它将接收两个参数:
1、props
2、context
Props
setup 函数中的第一个参数是 props。正如在一个标准组件中所期望的那样,setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。
// MyBook.vue
export default {
props: {
title: String
},
setup(props) {
console.log(props.title)
}
}
复制代码
但是,因为 props 是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。
如果需要解构 prop,可以在 setup 函数中使用 toRefs 函数来完成此操作:
// MyBook.vue
import { toRefs } from 'vue'
setup(props) {
const { title } = toRefs(props)
console.log(title.value)
}
复制代码
如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:
// MyBook.vue
import { toRef } from 'vue'
setup(props) {
const title = toRef(props, 'title')
console.log(title.value)
}
复制代码
Context
传递给 setup 函数的第二个参数是 context。context 是一个普通的 JavaScript 对象,它暴露组件的三个 property:
// MyBook.vue
export default {
setup(props, context) {
// Attribute (非响应式对象)
console.log(context.attrs)
// 插槽 (非响应式对象)
console.log(context.slots)
// 触发事件 (方法)
console.log(context.emit)
}
}
复制代码
context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。
// MyBook.vue
export default {
setup(props, { attrs, slots, emit }) {
...
}
}
复制代码
WatchEffect
该方法接收一个函数并且立即执行,并当该函数里的变量变更时,重新执行该函数。该方法无法获取到原值,只能是改变之后的值。
watchEffect(() => {
console.log(nameObj.name)
})
复制代码
取消监听
const stop = watchEffect(() => {
console.log(nameObj.name)
setTimeout(() => {
stop()
}, 5000)
})
复制代码
总结一下 watch 和 watchEffect 的区别:
watch:
1.具有一定的惰性lazy 第一次页面展示的时候不会执行,只有数据变化的时候才会执行
2.参数可以拿到当前值和原始值
3.可以侦听多个数据的变化,用一个侦听起承载
watchEffect:
1.立即执行,没有惰性,页面的首次加载就会执行。
2.不能获取之前数据的值 只能获取当前值
有关响应性API的可以查看
官方文档响应性API