组合式API
我们先来看一个简单的vue2小例子
<template lang="pug">
div
div {{ number }}
button(@click="handleDoubleNumber") 倍增数字
div {{ string }}
button(@click="handleDoubleString") 倍增字符串
</template>
<script>
export default {
data() {
return {
number: 1,
string: 'hi',
}
},
methods: {
handleDoubleNumber() {
this.number = this.number * 2
},
handleDoubleString() {
this.string += this.string
},
},
}
当我们继续新增功能时,代码块就会越来越多,导致书写以及阅读困难。相信小伙伴们有在公司碰到过要去修改前人很臭很长的代码,明明只要改动一点逻辑,但是却要捋清楚整个代码,让人着实焦躁。另外如果有部分功能有多个页面需要使用时,一般我们需要抽离出来使用mixin混入,但是mixin存在命名冲突,变量的来源未知,代码难以理解,难以维护等弊端。这种碎片化使得理解和维护复杂组件变得困难,选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
我们能够将与同一个逻辑关注点相关的代码配置在一起会更好,而这正是组合式API使我们能够做到的。我们使用组合式API来重写下上面的代码。
新建一个useDoubleNumber.js
import { ref } from 'vue'
export default function useDoubleNumber() {
const number = ref(1)
const handleDoubleNumber = () => {
number.value = number.value * 2
}
return {
number,
handleDoubleNumber,
}
}
新建一个useDoubleString.js
import { ref } from 'vue'
export default function useDoubleString() {
const string = ref('hi')
const handleDoubleString = () => {
string.value += string.value
}
return {
string,
handleDoubleString,
}
}
页面中使用
<template lang="pug">
div
div {{ number }}
button(@click="handleDoubleNumber") 倍增数字
div {{ string }}
button(@click="handleDoubleString") 倍增字符串
</template>
<script>
import { defineComponent } from 'vue'
import useDoubleNumber from './useDoubleNumber'
import useDoubleString from './useDoubleString'
export default defineComponent({
setup() {
const { number, handleDoubleNumber } = useDoubleNumber()
const { string, handleDoubleString } = useDoubleString()
return {
number,
handleDoubleNumber,
string,
handleDoubleString,
}
},
})
</script>
通过上面的改造,我们可以发现页面的逻辑非常的清晰,功能复用也非常的方便。这样代码的可读性和可维护性也大大的提高了。
setup组件选项
为了开始使用组合式API,我们首先需要一个可以实际使用它的地方。在Vue组件中,我们将此位置称为setup。
新的setup组件选项在创建组件之前执行,一旦props被解析,并充当合成API的入口点。由于在执行setup的时候尚未创建组件实例,因此在setup选项中没有this。
setup选项应该是一个接受props和context的函数。此外,我们从setup返回的所有内容都将暴露给组件的其余部分(计算属性、方法、生命周期钩子等等)以及组件的模板。
Props
setup中的第一个参数就是props。setup函数中的props是props是响应式的,当传入新的prop时,它将被更新。因为props是响应式的,所以我们不能对其进行解构,这样会消除它的响应式。
export default {
props: {
title: String
},
setup(props) {
console.log(props.title)
// 对props进行解构是错误的
const { title } = props
}
}
那如果我们既想对props进行解构又想保持它的响应性要怎么做呢,下文中我们将会介绍。
上下文
传递给setup函数的第二个参数是context。context是一个普通的JavaScript对象,它暴露三个组件的property:
export default {
setup(props, context) {
// Attribute (非响应式对象),相当于this.$attrs
console.log(context.attrs)
// 插槽 (非响应式对象),相当于this.$slots
console.log(context.slots)
// 触发事件 (方法),相应于this.$emits
console.log(context.emit)
}
}
上面我们有说到setup中是没有this的,那么在setup中我们如何获取组件的实例呢?
我们可以使用getCurrentInstance方法,该方法只能在setup或者生命周期钩子中使用。
<template>
<p @click="handleClick">{{ num }}</p>
</template>
<script>
import { ref, getCurrentInstance } from 'vue'
export default {
setup() {
const num = ref(3)
const instance = getCurrentInstance() // works
console.log(instance)
const handleClick = () => {
getCurrentInstance() // doesn't work
}
return { num, handleClick }
},
}
</script>
打印出来后,会发现有两个属性ctx和proxy,如下(仅截取部分属性):
这便是我们想要获取的实例的内容,两者唯一的区别是proxy属性是响应式的。
ref、reactive、toRefs
在vue2中数据都是定义在data中,那vue3怎么来定义响应式变量呢?
我们在上面的例子中看到了从vue中引入了一个ref函数声明了一个变量,该变量发生变化的时候我们的视图会随着更新。在vue3中,我们可以通过一个新的ref函数使任何响应式变量在任何地方起作用。ref接受参数并返回它包装在具有value的property的对象中,然后可以使用该property访问或者更改响应式变量的值,在template里可以直接访问.
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
一般ref是用来定义一个基本类型的响应式数据(也可用来定义引用类型的),定义引用类型的响应式数据采用reactive(不可用来定义基本类型)。
reactive返回对象的响应式副本。响应式转换时"深层"的-它影响所有嵌套property。
import { reactive } from 'vue'
const state = reactive({ counter: 0 })
console.log(state.counter) // 0
state.counter++
console.log(state.counter) // 1
原本我们在上文中有提到不能对props进行解构,否则会失去其响应性。这个时候我们可以使用toRefs来将响应式对象转换为普通对象,其中结果对象的每个property都是指向原始对象相应property的ref。
import { reactive } from 'vue'
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
state.foo++
console.log(stateAsRefs.foo.value) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
ref其实还有一个作用,那就是可以获取到标签元素和组件,在vue2中,我们获取元素都是通过给元素一个ref属性,然后通过this.$refs.xx来访问,但是在vue3中已经不适用了,我们来看看vue3中怎么获取元素的:
<template>
<div>
<div ref="el">div元素</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
// 创建一个DOM引用,名称必须与元素的ref属性名相同
const el = ref(null)
// 在挂载后才能通过 el 获取到目标元素
onMounted(() => {
el.value.innerHTML = '内容被修改'
})
// 把创建的引用 return 出去
return {el}
}
}
</script>
生命周期钩子
为了使组合式API的特性与选项式API相比更加完整,我们还需要一种在setup中注册生命周期钩子的方法。组合式API上的生命周期钩子与选项式API的名称相同,但前缀为on,如下:
| 选项式API | Hook inside setup |
|---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
这些函数接受一个回调函数,当钩子被组件调用时将会被执行:
import { onMounted } from 'vue'
export default {
setup() {
// mounted
onMounted(() => {
console.log('Component is mounted!')
})
}
}
watch 响应式更改
在vue2中,如果我们想要监听某个数据的改变从而执行一些操作的时候,我们一般会使用watch。
export default {
data() {
return {
counter: 0
}
},
watch: {
counter(newValue, oldValue) {
console.log('The new counter value is: ' + this.counter)
}
}
}
在vue3中,我们可以使用从Vue导入的watch函数执行相同的操作。它接受3个参数:
- 一个响应式引用或我们想要侦听的getter函数
- 一个回调
- 可选的配置选项
import { ref, reactive, watch } from 'vue'
// 侦听一个getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听一个ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
它还可以使用数组同时侦听多个源:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
在vue3中,还有个侦听器watchEffect,它与watch的不同在于它是立即执行的,同时响应式追踪其依赖,并在依赖变更时重新运行该函数。但是watchEffect访问不了依赖变更之前的值。
import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
watch和watchEffect均在组件的setup()函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。也可以根据需要,显示调用返回值以停止侦听。
const stop = watchEffect(() => {
/* ... */
})
// later
stop()
独立的computed属性
依赖于其它状态的状态,在vue中,我们一般是用计算属性来处理的,在vue2中,计算属性使用方法如下:
export default {
data() {
return {
counter: 0
}
},
computed: {
doubleCounter() {
return this.counter * 2
}
}
}
在vue3中,计算属性使用方法如下:
import { ref, watchEffect } from 'vue'
const count = ref(1)
const plusOne = computed(() => count.value++)
console.log(plusOne.value) // 2
plusOne.value++ // error
或者,它可以使用一个带有get和set函数的对象来创建一个可写的ref对象
import { ref, watchEffect } from 'vue'
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
Teleport
Teleport这个单词是"传输"的意思,利用它,我们可以把子组件或者dom节点插入到任何我们想要插入的地方。
<template>
<div>
第一层包裹元素
<div>
第二层包裹元素
<teleport to="#app"> 瞅瞅我在哪 </teleport>
</div>
</div>
</template>
我们打开控制台看下
可以看到我们的元素跑到了id为app的元素下,这个暂时还不理解到底什么情况下可以使用。。。
片段
在vue2中,不支持多根组件,当用户意外创建多根组件时会发出警告,因此,为了修复此错误,许多组件被包装在一个<div>中。但是在vue3中,组件可以有多个根节点。
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>
非兼容的变更
全局API
vue2有许多的全局API和配置,这些API和配置可以全局改变Vue的行为。我们定义的应用只是通过new Vue()创建的根Vue实例。从同。一个Vue构造函数创建的每个根实例共享相同的全局配置。这会使得同一个页面上的多个"app"之间共享同一个Vue副本变的困难。
// 这会影响两个根实例
Vue.mixin({
/* ... */
})
const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })
为了避免这些问题,vue3中有个新的全局API:createApp,调用createApp返回一个应用实例。
import { createApp } from 'vue'
const app = createApp({})
应用实例暴露当前API的子集,任何全局改变Vue行为的API都会移动到应用实例下:
| 2.x全局API | 3.x实例API(app) |
|---|---|
| Vue.config | app.config |
| Vue.config.productionTip | removed |
| Vue.config.ignoredElements | app.config.isCustomElement |
| Vue.component | app.component |
| Vue.directive | app.directive |
| Vue.mixin | app.mixin |
| Vue.use | app.use |
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()
})
// 现在所有应用实例都挂载了,与其组件树一起,将具有相同的 “button-counter” 组件 和 “focus” 指令不污染全局环境
app.mount('#app')
所有其它不全局改变行为的全局API只能作为ES模块构建的命名导出进行访问,例如:
import { nextTick } from 'vue'
nextTick(() => {
// 一些和DOM有关的东西
})
若多个应用之间需要共享配置,一种方式是创建工厂功能,如下所示:
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指令。
v-model
在vue2中,在组件上使用v-model相当于绑定valueprop和input时间:
<ChildComponent v-model="pageTitle" />
<!-- 简写: -->
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
如果要将属性或事件名称更改为其它名称,则需要在ChildComponent组件中添加model选项(也可使用v-bind.sync):
<!-- 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" />
在vue3中,自定义组件上的v-model相当于传递了modelValueprop并接收抛出的update:modelValue事件:
<ChildComponent v-model="pageTitle" />
<!-- 简写: -->
<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>
若需要更改model名称,而不是修改组件内的model选项,那么现在我们可以将一个argument传递给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"
/>
<template v-for> 和非 - v-for 节点上 key 用法已更改
特殊的 key attribute 被用于提示 Vue 的虚拟 DOM 算法来保持对节点身份的持续跟踪。这样 Vue 可以知道何时能够重用和修补现有节点,以及何时需要对它们重新排序或重新创建。
Vue2建议在v-if/v-else/v-else-if 的分支中使用 key。若设置了key在 Vue3中仍能正常工作。但是我们不再建议在 v-if/v-else/v-else-if 的分支中继续使用 key attribute,因为没有为条件分支提供 key 时,也会自动生成唯一的 key。
在 Vue 2中 <template> 标签不能拥有 key。不过你可以为其每个子节点分别设置 key。在 Vue 3 中 key 则应该被设置在 <template> 标签上。
v-if 与 v-for 的优先级对比
vue2版本中在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用。vue3版本中 v-if 总是优先于 v-for 生效。
slot具名插槽的语法
在vue2中,插槽的用法如下
// 子组件
<slot name="content" :data="data"></slot>
export default {
data(){
return{
data:["喜羊羊","懒羊羊","美羊羊"]
}
}
}
// 父组件使用
<template slot="content" slot-scope="scoped">
<div v-for="item in scoped.data">{{item}}</div>
<template>
在vue3中父组件使用变更如下:
// 父组件中使用
<template v-slot:content="scoped">
<div v-for="item in scoped.data">{{item}}</div>
</template>
// 也可以简写成:
<template #content="{data}">
<div v-for="item in data">{{item}}</div>
</template>
注意,v-slot只能添加在<template>上,只有一种情况例外,那就是当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把v-slot直接用在组件上。
<todo-list v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</todo-list>
也可以这样写:
<todo-list v-slot="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</todo-list>
不带参数的v-slot被假定对应默认插槽。默认插槽的缩写语法不能和具名插槽混用。
被移除的语法
1、vue3中不再支持数字(即键码)作为v-on的修饰符,例如:
<!-- 键码版本 -->
<input v-on:keyup.13="submit" />
<!-- 别名版本 -->
<input v-on:keyup.enter="submit" />
2、$on,$off 和 $once 实例方法已被移除,应用实例不再实现事件触发接口。
在vue2中,我们经常使用$bus来进行组件通信,采用$on注册事件,$emit触发事件,$off移除事件,$once事件只执行一次。在Vue3中仅保留$emit,因为它用于触发父组件已声明方式附加的事件处理程序,其它的均移除。
3、过滤器已删除
在Vue2中我们经常使用过滤器,对我们要展示的数据进行格式处理等。在Vue3中,过滤器已移除,建议使用方法调用或者计算属性替换。
如果在应用中全局注册了过滤器,那就不方便在每个组件中使用计算属性或者方法来替换它了,这个时候我们可以采用全局属性。
// main.js
const app = createApp(App)
app.config.globalProperties.$filters = {
currencyUSD(value) {
return '$' + value
}
}
然后可以通过$filters对象修改所有的模板,像下面这样:
<template>
<h1>Bank Account Balance</h1>
<p>{{ $filters.currencyUSD(accountBalance) }}</p>
</template>