本文是个人对Vue3 Composition API的学习记录总结。
setup
setup是组件的一个配置项,值是一个函数。组件中所使用到的数据,方法,计算属性等都要配置在setup里。
setup的执行时机是在beforeCreate之前执行一次。this值是undefined。
// App.vue
<template>
<h1>App</h1>
</template>
<script>
export default {
name: 'App',
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log(this) // undefined
console.log('created')
},
setup() {
console.log('setup')
}
}
</script>
// setup
// beforeCreate
// created
setup 函数接收 props 和 context 二个参数。
props 参数
props 是组件内部声明的属性。props是响应式的,不能使用ES6解构。如下例子:
// 父组件: App.vue
<template>
<child name="张三" :age="20" />
</template>
<script>
import Child from './components/Child'
export default {
name: 'App',
components: { Child }
}
</script>
// 子组件: /components/Child.vue
<template>
<p>Child子组件</p>
</template>
<script>
export default {
props: ['name', 'age', 'sex'],
name: 'Child',
setup(props, context) {
console.log(props) // Proxy {name: "张三", age: 20, sex: undefined}
}
}
</script>
context 参数
context是一个普通的JavaScript对象,它暴露组件的三个属性:
attrs: 组件外部传递过来的,但是没有在props配置中声明的属性。slots: 收到的插槽内容。默认名是default。emit: 分发自定义事件的函数。
// 父组件 App.vue
<template>
<child name="张三" :age="20" @customClick="customClick">
<template>
<p>名为default插槽</p>
</template>
<template #footer>
<p>名为footer插槽</p>
</template>
</child>
</template>
<script>
import Child from './components/Child'
export default {
name: 'App',
components: { Child },
setup() {
function customClick(num) {
console.log('触发customClick-->', num) // 触发customClick--> 666
}
return {
customClick
}
}
}
</script>
// 子组件: /components/Child.vue
<template>
<p>Child子组件</p>
</template>
<script>
export default {
props: ['name'],
name: 'Child',
setup(props, context) {
console.log(context.attrs) // Proxy {age: 20, onCustomClick: ƒ}
console.log(context.slots) // Proxy {footer: ƒ, default: ƒ}
context.emit('customClick', 666) // 触发自定义事件
}
}
</script>
ref
ref定义一个响应式数据,返回一个包含响应式数据的对象。一般用来定义一个基本数据类型,但是也可以定义引用类型。
ref如果传入基本数据类型依然是基于Object.defineProperty()的get和set完成的。如果传入的是引用类型,内部是调用了reactive函数,是基于Proxy实现对象的代理。
通过ref包装的数据,返回值是一个对象,需要通过.value获取。
// App.vue
<template>
<!-- 模版里不需要.value -->
<p>{{ msg }}</p>
<p>{{ person.name }}</p>
<button @click="handleClick">修改</button>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
// 基本数据类型
const msg = ref('一条消息')
// 引用类型
const person = ref({ name: '张三', age: 20 })
// 修改数据的时候使用.value来操作
function handleClick() {
msg.value = '新的消息'
person.value.name = '李四'
}
return {
msg,
person,
handleClick
}
}
}
</script>
shallowRef
shallowRef只处理基本类型的响应式,不进行引用类型的响应式处理。
如果有一个对象后续功能不会修改该对象中的属性,而是生成新的对象来替换:
<template>
<p>{{ msg }}</p>
<p>{{ person.age }}</p>
<!--msg基本数据类型可以监听到-->
<input v-model="msg" />
<!--对象监听不到-->
<input v-model.number="person.age" />
<button @click="handleClick">修改为新对象</button>
</template>
<script>
import { shallowRef } from 'vue'
export default {
name: 'App',
setup() {
const msg = shallowRef('一条消息')
let person = shallowRef({
age: 20
})
function handleClick() {
// 生成新的对象
person.value = {
age: 30
}
}
return {
msg,
person,
handleClick
}
}
}
</script>
reactive
reactive 函数内部机遇ES6的Proxy实现。
定义一个对象类型的响应式函数(基本数据类型不要使用reactive函数)。返回一个Proxy的代理对象。
reactive的响应式数据是深层次的。
// App.vue
<template>
<p>{{ person.name }}</p>
<p>{{ person.job.jobName }}</p>
<button @click="handleClick">修改</button>
</template>
<script>
import { reactive } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
name: '张三',
age: 20,
job: {
jobName: '工程师'
}
})
function handleClick() {
person.name = '李四'
person.job.jobName = '设计师'
}
return {
person,
handleClick
}
}
}
</script>
shallowReactive
shallowReactive只考虑对象类型的第一层响应式,属于浅响应式。
如果有一个对象数据结构比较深,但是变化时只是第一层属性变化:
<template>
<p>{{ person.name }}</p>
<p>{{ person.job.jobName }}</p>
<input v-model="person.name" />
<!-- 内层属性监听不到 -->
<input v-model="person.job.jobName" />
</template>
<script>
import { shallowReactive } from 'vue'
export default {
name: 'App',
setup() {
const person = shallowReactive({
name: '张三',
age: 20,
job: {
jobName: '工程师'
}
})
return {
person
}
}
}
</script>
computed
computed 有两种编写方式:
-
接受一个
getter函数的简写方式。 -
接收
get和set函数的对象的方式。
getter简写方式
<template>
<p>{{ person.fullName }}</p>
</template>
<script>
import { reactive, computed } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
firstName: '张',
lastName: '三'
})
// 可以直接放到对象里
person.fullName = computed(() => {
return `${person.firstName}-${person.lastName}`
})
return {
person
}
}
}
</script>
get和set方式
<template>
<input type="text" v-model="person.fullName" />
</template>
<script>
import { reactive, computed } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
firstName: '张',
lastName: '三'
})
person.fullName = computed({
get() {
return `${person.firstName}-${person.lastName}`
},
set(newVal) {
console.log(newVal)
}
})
return {
person
}
}
}
</script>
watch
Vue3watch功能与Vue2的watch是相同的。但是使用起来还是有一些小细节需要注意。⚠️
watch传入三个参数:
params:一个响应式属性或getter函数。handler:回调函数。object:可选配置项。
watch(params,handler(newValue, oldValue), { immediate: true, deep: true })
监听ref定义的响应式数据
<template>
App
</template>
<script>
import { ref, watch } from 'vue'
export default {
name: 'App',
setup() {
const msg = ref('一条消息')
const person = ref({
name: '张三',
age: 20
})
// 监听ref定义的基本数据类型
watch(msg, (newValue, oldValue) => {
console.log(`msg的newValue:${newValue},msg的oldValue:${oldValue}`)
})
// 监听使用ref定义的引用类型,需要使用{ deep: true }。否则无法监听
watch(
person,
(newValue, oldValue) => {
// 这里的name是相等的,下面会说。
console.log(`person的newValue.name:${newValue.name},person的oldValue.name:${oldValue.name}`)
},
{ deep: true }
)
msg.value = '新消息' // msg的newValue:新消息,msg的oldValue:一条消息
person.value.name = '李四' // person的newValue.name:李四,person的oldValue.name:李四
return { msg, person }
}
}
</script>
监听使用ref定义的引用类型的时候,需要配置{ deep: true },否则无法进行监听。
同时监听ref定义的多个响应式数据
可以使用数组的方式同时监听ref所定义的多个响应式数据。
<template>
<input type="text" v-model="name" />
<input type="text" v-model.number="age" />
</template>
<script>
import { ref, watch } from 'vue'
export default {
name: 'App',
setup() {
const name = ref('张三')
const age = ref(20)
// newValue和oldValue分别是一个数组,和监听变量的顺序一一对应
watch([name, age], (newValue, oldValue) => {
console.log(newValue) // [newName, newAge]
console.log(oldValue) // [oldName, oldAge]
})
return {
name,
age
}
}
}
</script>
监听reactive定义的响应式数据
监视reactive所定义的一个响应式对象的时候,无法正确的获得oldValue。是因为监听一个响应式对象或数组始终返回该对象的当前值(newValue)和上一个状态值的引用(oldValue)。
<template>
App
</template>
<script>
import { reactive, watch } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
name: '张三',
age: 20,
job: {
jobName: '工程师'
}
})
// 侦听一个响应式对象或数组将始终返回该对象的当前值和上一个状态值的引用
watch(person, (newValue, oldValue) => {
console.log(newValue === oldValue) // true
console.log(newValue.name === oldValue.name) // true
})
person.name = '李四'
return { person }
}
}
</script>
为了完全监听深度嵌套的对象和数组,需要对值进行深拷贝。可以通过比如lodash.cloneDeep来实现,并且以getter函数形式返回:
watch(
() => _.cloneDeep(person),
(newValue, oldValue) => {
console.log(newValue === oldValue) // false
console.log(newValue.name === oldValue.name) // false
}
)
监听reactive所定义的一个响应式对象,分两种情况:
- 如果传入的第一个参数是响应式属性。则默认是深度监听,而且设
{deep:false}是无效的。 - 如果传入的第一个参数是函数,则是正常配置。
<template>
App
</template>
<script>
import { reactive, watch } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
name: '张三',
age: 20,
job: {
jobName: '工程师'
}
})
// 以对象的方式传入
watch(
person,
(newValue, oldValue) => {
console.log(newValue.job.jobName) // 设计师
},
{ deep: false } // deep 始终为true,设置false无效
)
person.job.jobName = '设计师'
const book = reactive({
bookName: 'JavaScript高级程序设计',
category: 'IT',
area: {
areaName: '北京'
}
})
// 以函数返回值的方式传入
watch(
() => book,
(newValue, oldValue) => {
console.log(newValue.area.areaName) // 不会输出
},
{ deep: false } // 配置有效
)
book.area.areaName = '上海'
return { person, book }
}
}
</script>
如果监视reactive所定义的一个响应式对象中的某个属性是基本数据类型,监听的属性要通过一个回调函数返回。
<template>
App
</template>
<script>
import { reactive, watch } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
name: '张三',
age: 20,
job: {
jobName: '工程师',
area: {
areaName: '上海'
}
}
})
// 基本数据类型以函数形式返回,否则无效
watch(
() => person.name,
(newValue, oldValue) => {
console.log(newValue) // 李四
}
)
person.name = '李四'
// 引用类型
watch(
() => person.job,
(newValue, oldValue) => {
console.log(newValue.area.areaName) // 北京
},
{ deep: true }
)
person.job.area.areaName = '北京'
return { person }
}
}
</script>
同时监听reactive定义的多个响应式数据
监视reactive所定义的一个响应式对象中的某些属性,监听的属性要通过一个数组传入。
<template>
App
</template>
<script>
import { reactive, watch } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
name: '张三',
age: 20,
job: {
jobName: '工程师',
area: {
areaName: '上海'
}
}
})
watch(
[() => person.name, () => person.age, () => person.job],
(newValue, oldValue) => {
console.log(newValue)
},
{ deep: true }
)
person.name = '李四'
person.age = 30
person.job.jobName = '设计师'
return { person }
}
}
</script>
watchEffect
watchEffect函数不用指明监听哪个属性,监听的回调中用到哪个属性,就监听哪个属性。默认初始化时会执行一次。
<template>
<input type="text" v-model="person.name" />
<input type="text" v-model="person.job.jobName" />
</template>
<script>
import { reactive, watchEffect } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
name: '张三',
age: 20,
job: {
jobName: '工程师',
area: {
areaName: '上海'
}
}
})
watchEffect(() => {
console.log(person.name)
console.log(person.job.jobName)
})
return { person }
}
}
</script>
生命周期
Vue3对生命周期进行了修改,下图摘自官方网站:
<template>
App
</template>
<script>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated,
onErrorCaptured
} from 'vue'
export default {
beforeCreate() {
console.log('Before Create!')
},
created() {
console.log('Create!')
},
setup() {
console.log('setup()')
onBeforeMount(() => {
console.log('Before Mount!')
})
onMounted(() => {
console.log('Mounted!')
})
onBeforeUpdate(() => {
console.log('Before Update!')
})
onUpdated(() => {
console.log('Updated!')
})
onBeforeUnmount(() => {
console.log('Before Unmount!')
})
onUnmounted(() => {
console.log('Unmounted!')
})
onActivated(() => {
console.log('Activated!')
})
onDeactivated(() => {
console.log('Deactivated!')
})
onErrorCaptured(() => {
console.log('Error Captured!')
})
}
}
</script>
toRef 和 toRefs
toRef可以为响应式对象上的某个属性创建一个新的ref。
toRefs和toRef功能一致,可以创建多个toRef对象。
如果对象嵌套层级很深并且在模版中使用的时候,避免不了obj.property这种形式,可以利用toRef和 toRefs简化开发:
<template>
<p>{{ name }}</p>
<p>{{ areaName }}</p>
<p>{{ bookName }}</p>
<input v-model="areaName" />
</template>
<script>
import { reactive, toRef, toRefs } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
name: '张三',
age: 20,
job: {
jobName: '工程师',
area: {
areaName: '上海'
}
}
})
const book = reactive({
bookName: 'JavaScript高级程序设计',
category: 'IT'
})
/* 对象属性变为响应式,拆散对象简化使用 */
return {
name: toRef(person, 'name'),
areaName: toRef(person.job.area, 'areaName'),
...toRefs(book)
}
}
}
</script>
readOnly和shallowReadOnly
-
readonly:让一个响应式数据变为只读的(深度只读)。 -
shallowReadOnly:让一个响应式数据变为只读的(浅只读)。
如果不希望数据被修改的时候可以使用它们:
<template>
<div>
<p>{{ rPerson.job.jobName }}</p>
<!-- 不是响应式的,不可修改 -->
<input type="text" v-model="rPerson.job.jobName" />
</div>
<div>
<p>{{ srPerson.job.jobName }}</p>
<!-- 是响应式的,可以修改 -->
<input type="text" v-model="srPerson.job.jobName" />
</div>
</template>
<script>
import { reactive, readonly, shallowReadonly } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
name: '张三',
age: 20,
job: {
jobName: '工程师'
}
})
// 不可修改
const rPerson = readonly(person)
// 只考虑第一层数据不可修改
const srPerson = shallowReadonly(person)
return {
rPerson,
srPerson
}
}
}
</script>
toRaw 和 markRaw
toRaw:将一个由reactive生成的响应式对象转为普通对象。
markRaw:标记一个对象,使其永远不会再成为响应式对象。
<template>
App
</template>
<script>
import { reactive, toRaw, markRaw } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
name: '张三',
age: 20,
job: {
jobName: '工程师'
}
})
const rawPerson = toRaw(person) // 返回了一个原始对象
person.car = markRaw({ carName: '奥迪', price: '30万' }) // 永远不会转换为proxy
return {
person,
rawPerson
}
}
}
</script>
customRef
创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显式控制。
customRef需要返回一个函数,该函数接收 track 和 trigger 函数作为参数,并且应该返回一个带有 get 和 set 的对象。
<template>
<input type="text" v-model="keyword" />
<p>{{ keyword }}</p>
</template>
<script>
import { customRef } from 'vue'
export default {
name: 'App',
setup() {
// 自定义ref,延迟一秒更新
function myRef(value, delay = 1000) {
return customRef((track, trigger) => {
return {
get() {
track() // 通知vue追踪value变化
return value
},
set(newValue) {
setTimeout(() => {
value = newValue
trigger() // 通知vue重新渲染模版
}, delay)
}
}
})
}
let keyword = myRef('hello', 1000)
return {
keyword
}
}
}
</script>
provide 和 inject
provide和inject实现祖孙组件间通信。父组件有一个provide选项来提供数据,子组件有一个inject选项来开始使用这些数据。下图来自官网:
// App.vue
<template>
App
<child />
</template>
<script>
import { reactive, provide } from 'vue'
import Child from './components/Child'
export default {
name: 'App',
components: { Child },
setup() {
const person = reactive({
name: '张三',
age: 20
})
provide('person', person) // 给后代组件传递数据
return {
person
}
}
}
</script>
// components/child.vue
<template>
<p>孩子组件</p>
<son />
</template>
<script>
import son from './son'
export default {
components: { son }
}
</script>
// components/son.vue
<template>
<p>孙子组件</p>
<p>{{ person.name }}</p>
</template>
<script>
import { inject } from 'vue'
export default {
setup() {
const person = inject('person') // 接收上层组件数据
console.log(person) // Proxy {name: "张三", age: 20}
return {
person
}
}
}
</script>
响应式数据判断
isRef:检查一个值是否为一个ref对象。
isReactive:检查一个对是否是由reactive创建的响应式代理。
isReadonly: 检直一个对象是否是由readonly创建的只读代理。
isProxy:检查一个对象否是由reactive或者readonly方法创建的代理。
<template>
App
</template>
<script>
import {
ref,
reactive,
readonly,
isRef,
isReactive,
isReadonly,
isProxy
} from 'vue'
export default {
name: 'App',
setup() {
const msg = ref('一条消息')
const person = reactive({ name: '张三', age: 20 })
const readonlyPerson = readonly(person) // readonly返回值依然是proxy对象
console.log(isRef(msg)) // true
console.log(isReactive(person)) // true
console.log(isReadonly(readonlyPerson)) // true
console.log(isProxy(person)) // true
console.log(isProxy(readonlyPerson)) // true
return {
msg,
person
}
}
}
</script>
自定义指令
在 Vue3 中,自定义指令名称进行了修改以及增加了一些方法,如下:
created:元素创建后,但是属性和事件还没有生效时调用。beforeMount:当指令第一次绑定到元素并且在挂载父元素之前调用。mounted:元素被挂载到父元素时调用。beforeUpdate:新的!这是在组件本身更新之前调用。updated:组件或者子组件更新之后调用。beforeUnmount:元素卸载前调用。unmounted:当指令卸载后调用,仅调用一次。
看下面例子:
// App.vue
<template>
<div v-if="isShow" class="parent-title">
<h1 v-title class="title">{{ titleMsg }}</h1>
<p>{{ p }}</p>
</div>
<!--点击后输出beforeUpdate 和 updated-->
<button @click="titleMsg = '新标题'">更新标题</button>
<!--点击后输出beforeUpdate 和 updated-->
<button @click="p = '新段落'">更新段落</button>
<!--点击后输出beforeUnmount,unmounted-->
<button @click="isShow = false">卸载组件</button>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
const titleMsg = ref('标题')
const p = ref('段落')
const isShow = ref(true)
return {
titleMsg,
p,
isShow
}
}
}
</script>
// 自定义指令 main.js
const app = createApp(App)
app.directive('title', {
created(el, binding, vnode) {
console.log(el.className) // 值为空字符串,因为属性和事件都没有生效。
},
beforeMount(el, binding, vnode) {
console.log(el.className) //值为title,元素已经创建完整。
console.dir(el.parentNode) // 值为null 因为还没有插入父节点
},
mounted(el, binding, vnode) {
console.log(el.parentNode) //值为父元素 div.parent-title,因为元素被插入到父元素
},
beforeUpdate(el, binding, vnode) {
console.log('beforeUpdate')
},
updated(el, binding, vnode) {
console.log('update')
},
beforeUnmount(el, binding, vnode) {
console.log('beforeUnmount')
},
unmounted(el, binding, vnode) {
console.log('unmounted')
}
})
函数简写是在 mounted 和 updated 时触发相同行为的时候调用:
const app = createApp(App)
app.directive('font-size', (el, binding, vnode) => {
el.style.fontSize = binding.value + 'px'
})
Teleport组件
Teleport组件是一种能够将组件html结构移动到指定位置的技术。
比如一个组件想要插入到body元素下面:
<template>
App
<teleport to="body">
<div>插入到body元素下,成为body元素的子元素</div>
</teleport>
</template>
<script>
export default {
name: 'App',
setup() {}
}
</script>
试验性 Suspense 组件
Suspense组件可以在等待异步组件时渲染一些后备内容。比如说增加loading动画。
<suspense> 组件有两个插槽。default 插槽里的节点会尽可能展示出来。如果不能,则示 fallback 插槽里的节点。
// App.vue
<template>
<h1>App</h1>
<Suspense>
<template #default>
<child />
</template>
<template #fallback>
<p>异步组件,加载中....</p>
</template>
</Suspense>
</template>
<script>
// import child from './components/child.vue' // 静态引入
import { defineAsyncComponent } from 'vue'
const Child = defineAsyncComponent(() => import('./components/Child')) // 异步引入
export default {
components: { Child },
name: 'App',
setup() {}
}
</script>
// components/Child.vue
<template>
<p>Child子组件</p>
</template>
<script>
export default {
name: 'Child',
setup() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 2000)
})
}
}
</script>
异步引入组件的时候
setup()可以返回Promise对象。
其他调整
全局API
Vue3将全局的 API Vue.xxx 调整到了实例(app)上,如下:
| 2.x全局API | 3.0全局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 |
data选项
data选项应该始终被声明一个函数,不能直接声明一个对象。
过渡动画类名更改
Vue3的动画类名v-enter改成了v-enter-form,v-leave改成了v-leave-from。
Vue2写法:
/* 进入的起点 */
.v-enter {}
/* 进入的过程 */
.v-enter-active {}
/* 进入的终点 */
.v-enter-to {}
/* 离开的起点 */
.v-leave {}
/* 离开的过程 */
.v-leave-active {}
/* 离开的终点 */
.v-leave-to {}
Vue3写法:
/* 进入的起点 */
.v-enter-from {}
/* 进入的过程 */
.v-enter-active {}
/* 进入的终点 */
.v-enter-to {}
/* 离开的起点 */
.v-leave-from {}
/* 离开的过程 */
.v-leave-active {}
/* 离开的终点 */
.v-leave-to {}
Vue3 移除项
-
移除
keycode作为v-on的修饰符,同时也不再支持config.keyCodes。 -
过滤器
filter已删除,不再支持。官方文档建议用方法调用或计算属性替换它们。 -
移除
v-on.native修饰符。
// 父组件定义事件
<template>
<h1>App</h1>
<child @close="handleClose" />
</template>
// 子组件声明自定义事件
<script>
export default {
name: 'Child',
emits: ['close']
}
</script>
这里介绍的比较重要的几点,详细还需要看官方文档:v3.cn.vuejs.org/guide/migra…
组合式函数
通过setup函数可以将每个功能的代码提取到一个独立的JS文件中,更加方便的复用。类似Vue2的mixin。
// hooks/usePoint.js
import { reactive, onMounted, onBeforeUnmount } from 'vue'
// 点击页面获取鼠标位置功能
export default function() {
const point = reactive({
x: 0,
y: 0
})
function clickHandle({ pageX, pageY }) {
point.x = pageX
point.y = pageY
}
onMounted(() => {
window.addEventListener('click', clickHandle)
})
onBeforeUnmount(() => {
window.removeEventListener('click', clickHandle)
})
return point
}
// App.vue
<template>
App
<h2>我的鼠标位置是X:{{ point.x }},Y:{{ point.y }}</h2>
</template>
<script>
import usePoint from './hooks/usePoint'
export default {
name: 'App',
setup() {
// 引入usePoint
const point = usePoint()
return { point }
}
}
</script>