最近进入第二个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>