最近进入第二个Vue3项目的开发,因与第一个Vue3项目开发的间隔时间比较长,所以此次开发,还是明显感觉到对Vue3一些常用的新特性不是很清晰,使用的过程中需要查文档、翻代码。故此处,将项目中用到的Vue3新特性进行汇总总结一下,以期明确用法,加深记忆。
响应式API
在vue2
中,我们只要定义在data()
方法中的数据就是响应式数据。但在vue3
中主要是使用ref
和reactive
来定义响应式数据。由于vue3
使用的是proxy
进行响应式监听,所以新增、删除属性也都是响应式的。
一、ref、reactive
ref
一般用来定义基本类型的响应式数据。接受一个内部值并返回一个响应式且可变的 ref
对象。ref
对象仅有一个 .value
属性,指向该内部值。
使用ref
定义的响应式数据在setup
函数中使用需要加上.value
,但在模板中可以直接使用。
<template>
<div>{{ num }}</div>
</template>
<script setup>
import { ref } from "vue";
const num = ref(1);
const addNum = () => {
num.value++;
}
</script>
reactive
用来定义引用类型的响应式数据,不能用来定义基本类型的响应式数据。
定义的对象是不能直接使用es6
语法解构的,不然就会失去它的响应式。
<template>
<div>{{ state.num }}</div>
</template>
<script setup>
import { reactive } from "vue";
const state = reactive({
num: 0,
});
const addNum = () => {
state.num++;
}
</script>
二、isRef、unref、toRef、toRefs
isRef
检查值是否为一个 ref
对象。
unref
如果参数是一个ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val
的语法糖函数。
toRef
可以用来为源响应式对象上的某个 property
新创建一个 ref
。然后,ref
可以被传递,它会保持对其源 property
的响应式连接。(复制 reactive 里的单个属性并转成 ref。)
toRefs
复制 reactive 里的所有属性并转成 ref。
reactive
定义的对象不能直接使用es6
语法解构的,不然就会失去它的响应式,如果要保持响应性,解构时需要使用toRefs()
方法。
组合式API
为了让相关代码更紧凑vue3
提出了组合式api
,组合式api
能将同一个逻辑关注点相关代码收集在一起。 组合式api
的入口就是setup
方法。
setup
setup
选项是一个接收 props
和 context
的函数,它在beforeCreate
之前执行。
// 写法一
export default {
setup(){}
}
// 写法二
export default defineComponent({
setup(){}
})
一、参数
props
props
就是我们父组件给子组件传递的参数。
在setup
函数中的 props
是响应式的,不能使用 ES6 解构,它会消除 prop 的响应性。如果需要解构需使用toRefs
方法。
如果props
中的某个属性是可选的,则传入的props
中可能没有该属性。在这种情况下,toRefs
将不会为 该属性创建一个 ref
,需要使用 toRef
替代它。
context
context
是一个普通的 JavaScript
对象,也就是说,它不是响应式的,可以对 context
使用 ES6
解构。
setup(props, context) {
// Attribute
console.log(context.attrs)
// 插槽
console.log(context.slots)
// 触发事件 (方法)
console.log(context.emit)
// 暴露公共 property (函数)
console.log(context.expose)
}
})
二、返回值
我们在模板,或者vue2
选项式写法的计算属性、方法、生命周期钩子等等中使用的数据都需要在setup
方法中通过return
返回出来。
如果 setup
返回一个对象,那么该对象的 property
以及传递给 setup
的 props
参数中的 property
就都可以在模板中访问到。
setup
还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态。
三、属性/方法暴露
假如我们想在父组件中直接调用子组件的方法,我们可以在子组件setup
中使用return或expose
把属性或方法暴露出去,然后在父组件我们就可以通过子组件的ref
直接调用了。
当组件的setup
中没定义expose
暴露内容的时候,通过ref
获取到的就是组件自身的内容,也就是setup
函数return
的内容和props
属性的内容。
当定义了expose
暴露内容的时候,通过ref获取到的就只是组件expose
暴露内容,并且setup
函数return
的内容会失效,也就是会被覆盖。
四、单文件setup
要使用这个语法,需要将 setup
添加到 <script>
代码块上。
<script setup>
</script>
当使用 <script setup>
的时候,任何在 <script setup>
声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用。
defineProps
和defineEmits
在 <script setup>
中必须使用 defineProps
和 defineEmits
API 来声明 props
和 emits
。
<template>
<div>{{ props.title }}</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
// 定义属性
const props = defineProps({
title: String
})
// 定义事件
const emits = defineEmits(['change'])
//触发事件 类似 context.emit()
emits('change')
</script>
defineExpose
使用 <script setup>
的组件是默认关闭的,也即通过模板 ref 或者 $parent
链获取到的组件的公开实例,不会暴露任何在 <script setup>
中声明的绑定。
如果 <script setup>
组件中明确要暴露出去的属性,必须使用 defineExpose
。
也就是说,setup函数
组件默认会暴露props和return里面的内容,而<script setup>
语法糖不会暴露任何内容出去。
computed 和watch
一、computed
在vue2
中computed 很简单,是一个对象,只需要简单定义就可以使用。
在vue3
中,computed 是函数式的,需要先引入再使用。
computed 可以接受一个函数,并根据 函数的返回值返回一个不可变的响应式 ref 对象。
也接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。
import { reactive, computed } from "vue"
const user = reactive({ name: "张三", age: 24 });
// 接受一个函数,并根据返回值返回一个不可变的响应式ref对象
// 这里的fullName1是不能修改的
const fullName1 = computed(() => {
return `${user1.name}今年${user1.age}岁啦`;
});
// 接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。
// 这里的fullName2是可以修改的
let fullName2 = computed({
get() {
return `${user2.name}今年${user2.age}岁啦`;
},
set(val) {
user2.name = val;
},
});
const updateUser2Name = () => {
// 需要使用value访问
fullName2.value = "新的name";
};
二、watch
const user = reactive({ name: "张三", age: 24 });
// source: 可以支持 string,Object,Function,Array; 用于指定要侦听的响应式变量
// callback: 执行的回调函数
// options:支持 deep、immediate 和 flush 选项。
// 监听单一源:基本类型
watch(()=> user.name, (newVal, oldVal)=> {})
// 监听单一源:引用类型
watch( user1, (newVal, oldVal)=> {})
watch(()=> user1, (newVal, oldVal)=> {}, {deep: true})
//监听多个源
watch(
[() => user.name, () => user.age],
([newVal1, newVal2], [oldVal1, oldVal2]) => {
console.log(newVal1, newVal2);
console.log(oldVal1, oldVal2);
}
);
监听基本数据类型需要使用箭头函数方式,否则监听不到。
监听引用数据类型可以直接监听,但是新老值是一样的,如果需要对比新老值需要使用箭头函数并搭配深拷贝或浅拷贝的方式。
使用箭头函数也能监听引用数据类型,我们如果不需要对比新老值,可以直接使用第三个参数deep:true
开启深度监听即可。如果需要对比新老值就没必要使用这种方式了,需要看情况使用深拷贝和浅拷贝。
对于监听引用数据类型里面的引用数据类型需要格外注意,需要判断引用数据类型是属性值改变还是地址改变,属性值改变可以直接监听,地址改变需要使用箭头函数的方式或者看情况使用深拷贝和浅拷贝。
三、watchEffect
import { watchEffect,reactive } from "vue";
const user = reactive({ name: "张三", age: 27 });
const updateUser2Age = () => {
user.age++;
};
watchEffect(() => {
console.log("watchEffect", user.age);
});
watchEffect自动收集依赖,不需要手动传入依赖。当里面用到的数据发生变化时就会自动触发watchEffect
。并且watchEffect
会先执行一次用来自动收集依赖。而且watchEffect
无法获取到变化前的值,只能获取变化后的值。
生命周期
vue3
虽然提倡把生命周期函数都放到setup
中,但是vue2
那种选项式写法还是支持的。
vue2
相较于vue3
少了renderTracked
、renderTriggered
两个生命周期方法。
销毁生命周期方法名也发生了变化,由beforeDestroy
、destroyed
变为beforeUnmount
、unmounted
,这样是为了更好的与beforeMount
、mounted
相对应。
vue3
写在setup
函数中生命周期方法名就是前面多加了on
。
模板指令
一、v-model
在vue2
中,如果在自定义组件上使用v-model
需要在组件内通过model
参数指明v-model
的属性和事件。
// 父组件
<Child v-model="value" />
// 子组件
<template>
<div>
<input type="text" :value="value" @input="handleInput" />
</button>
</div>
</template>
<script>
export default {
// 定义v-model传过来的值名字是value1 修改值的事件是change事件
model: {
prop: "value",
event: "change",
},
props: {
value: String,
},
methods: {
handleInput(e) {
this.$emit("change", e.target.value);
},
},
};
</script>
在vue3
中自定义组件也可以使用v-model
,但不用去指定model
或者使用.sync
参数了。
默认情况下,组件上的 v-model
使用 modelValue
作为 prop 和 update:modelValue
作为事件。
// 父组件
<Child v-model="name" />
// 子组件
<template>
<div>
<button @click="changeName">改变值</button>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
props: {
modelValue: String,
},
setup() {
const changeName = () => {
context.emit("update:modelValue", "李四");
};
return {
changeName,
};
},
});
</script>
也可以自定义参数名。
// 父组件
<Child v-model:name="name1" />
// 子组件
<template>
<div>
<button @click="changeName">改变值</button>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
props: {
name: String,
},
setup() {
const changeName = () => {
context.emit("update:name", "李四");
};
return {
changeName,
};
},
});
</script>
还可以通过向 v-model
传递多个参数,这在vue2
中是不可以的。
<Child v-model:name1="name1" v-model:name2="name2" />
二、key支持在template使用
在vue2
中,key
是不能定义在template
节点上的。但是在vue3
中支持了。
三、v-if和v-for优先级
在vue2
中v-for
的优先级是比v-if
高的,在一个元素上同时使用 v-if
和 v-for
时,v-for
会优先作用。
但在vue3
中,v-if
的优先级比v-for
更高。
组件
一、异步组件
在vue2
中异步组件是通过将组件定义为返回 Promise
的函数来创建的。
const asyncModal = () => import('./Modal.vue')
在vue3
中异步组件通过defineAsyncComponent
定义。
import { defineAsyncComponent } from 'vue'
// 不带选项的异步组件
const asyncModal = defineAsyncComponent(() => import('./Modal.vue'))
二、emits
组件里面新增了emits
选项,可以通过 emits
选项在组件上定义发出的事件。
export default defineComponent({
// 与props类似,支持数组和对象
// emits: ["submit"],
emits: {
submit: null
},
setup(props, { emit }) {
const handleClick = () => {
emit("submit", { name: "randy" });
};
return {
handleClick,
};
},
});
其他
一、插槽
在vue2中,插槽的写法如下
// 子组件
<slot name="content" :user="user"></slot>
// 父组件
<template slot="content" slot-scope="scoped">
<div>name: {{scoped.user.name}}</div>
<div>age: {{scoped.user.age}}</div>
<template>
在 vue3 中将slot
和slot-scope
进行了合并使用,使用v-slot
代替。
<!-- 父组件中使用 -->
<template v-slot:content="scoped">
<div>name: {{scoped.user.name}}</div>
<div>age: {{scoped.user.age}}</div>
</template>
<!-- 也可以简写成: -->
<template #content="{user}">
<div>name: {{user.name}}</div>
<div>age: {{user.age}}</div>
</template>