外观
组合式API相关
组合式API组件的相关变量是在beforeCreate事件前执行的。
<script>
export default {
setup() {
console.log('setup.')
},
beforeCreate() {
console.log('beforeCreate.')
}
}
</script>执行结果是先输出setup后输出beforeCreate。
定义变量
在默认导出时若在setup函数中定义变量,则需要返回这些变量以便于外部使用。
<script>
export default {
setup() {
const message = 'Hello World';
function print() {
console.log(message)
}
return {
message,
print
}
}
}
</script>
<template>
<span> Message is: {{ message }} </span>
<button @click="print"> Print Message </button>
</template>可以给script标签添加setup标签,添加了setup标签的script标签为我们省去了上述定义和返回的步骤,我们可以直接在组件中使用这些变量。
<script setup>
const message = 'Hello World'
function print() {
console.log(message)
}
</script>
<template>
<span> Message is: {{ message }} </span>
<button @click="print"> Print Message </button>
</template>这里带setup的script标签就是语法糖。
若要定义可变值的数据,不能简单地将它赋值给一个变量,而是要使用vue为我们提供的响应式函数。定义对象使用reactive函数,定义普通变量则使用ref函数。使用这两个函数定义响应式变量之后,只要这两个变量的内容被改变,包括对象的键值和普通变量的值,Vue都能检测到并更改每一个引用这个变量的组件中所有值。
<script setup>
import {reactive, ref} from "vue"
const person = reactive({
name: "张三",
age: 18
})
const count = ref(0)
</script>
<template>
<span> {{ person.name }} 的年龄为 {{ person.age }} </span>
<button @click="person.age ++">增加person的年龄</button>
<span>{{ count }}</span>
<button @click="count++"> count+1 </button>
</template>在脚本区域更改count的值时则必须通过.value属性来更改。
import {reactive, ref} from "vue"
const count = ref(0)
count++ // 错误
count.value++ // 正确
reactive只能监听对象类型,不能监听简单数据类型。并且因为 Vue 的响应式跟踪是通过监听属性实现的,因此如果修改整个对象,响应式跟踪就会失效。它也对解包不友好,不能使用const { val } = reactive({ val: 0})
计算属性
由于响应式数据在更新后需要在其他组件内也实时更新,还是需要像上方那样使用vue提供的响应式函数来处理数据。计算变量的属性时需要使用vue提供的conputed函数,它接收一个函数作为参数,返回运算后的结果。
<script setup>
import {computed, ref} from "vue"
const arr = ref([1, 2, 3, 4])
const computedArr = computed(function () {
return arr.value.filter(e => e > 2)
})
setTimeout(function () {
arr.value.push(5, 6)
}, 3000)
</script>
<template>
<span>原始响应式数组: {{ arr }}</span>
<span>更新后的数组:{{ computedArr }}</span>
</template>刚开始时输出的两个数组分别是1234和34,三秒之后数组元素被更新,两者就分别是123456和3456。
使用函数和conputed函数的区别
假如有以下一段代码:
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})需求是判断它是否有书籍,通过检测 books 的长度获取,传统方式可以是:
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>这种方式将获取方式以插值的方式写在了模板内,难以阅读,还可以是:
<script setup>
// 组件中
function calculateBooksMessage() {
return author.books.length > 0 ? 'Yes' : 'No'
}
</script>
<template>
<p>{{ calculateBooksMessage() }}</p>
</template>这种方式中,每次渲染文档,该函数就会被重新调用一次,而使用computed:
<script setup>
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>这种方式的好处在于,如果length的值没有改变,该值就会被缓存,不需要重复计算length的值。
计算属性是只读的。如果要提供一个可以修改的计算属性,可为它设置getter和setter方法。
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})
</script>现在当你再运行 fullName.value = 'John Doe' 时,setter 会被调用而 firstName 和 lastName 会随之更新。
监听数据变化
组合式API的响应式数据更新时,使用watch函数侦听响应式数据源,并在数据更新后执行回调函数。watch函数需要导入,它接收两个参数。第一个参数为响应式对象,即ref对象,第二个参数为一个回调函数,可以是匿名函数。这个匿名函数可以包含两个参数,第一个参数为更新后的值,第二个参数为更新前的值。
<script setup>
import {ref, watch} from "vue"
const count = ref(0)
watch(count, function (newVal, oldVal) {
console.log(`count被更新,更新前的值:${oldVal},更新后的值:${newVal}`)
})
</script>
<template>
<button @click="count++"> {{ count }} + 1 </button>
</template>这里的watch只侦听一个变量的变化,可以同时侦听多个变化,但这些变量中有一个变化后都会触发这个侦听器,这样会判断不出来哪个发生了变化。
<script setup>
import {ref, watch} from "vue"
const count1 = ref(0)
const count2 = ref(0)
watch([count1, count2], function (
[newCount1, newCount2],
[oldCount1, oldCount2]
) {
console.log(`count1和count2有一个值发生改变,这两个数的新值:${newCount1}, ${newCount2},这两个数的旧值:${oldCount1}, ${oldCount2}`)
})
</script>
<template>
<button @click="count1++"> {{ count1 }} + 1</button>
<button @click="count2++"> {{ count2 }} + 1</button>
</template>watch函数此时接受的第一个参数为一个列表,这个列表中要侦听的变量具有顺序,第二个参数还是回调函数,这个回调函数内的第一个参数仍然是新值,第二个参数仍然是旧值,这两个值也是列表,且和watch函数的第一个参数列表的变量声明顺序一致。
除watch函数还接受第三个参数,它是一个对象,它可以带有一个immediate属性,如果它为真,则在侦听器床架初期时会执行一次回调函数,在响应式数据改变之后还会执行回调函数。
watch(count, function(newVal, oldVal){ console.log(newVal, oldVal) }, {immediate: true})watch函数侦听ref对象默认是浅层监听,直接修改对象的属性不会触发回调。watch的第三个参数可以加一个deep属性,若它为true,则代表开启深度监听,若响应式对象的属性值发生改变则会触发回调函数。
<script setup>
import {ref, watch} from 'vue'
const obj = ref({count: 0})
watch(obj, function () {
console.log('obj变化了')
},
{
deep: true // 若注释掉这行,在obj更改后不会触发回调函数
}
)
</script>
<template>
<button @click="obj.count++"> {{ obj.count }} + 1 </button>
</template>deep深度监听会产生性能损耗,可以只监听对象的某一个属性。这时候watch的第一个参数就是一个返回此属性的回调函数。
<script setup>
import {ref, watch} from 'vue'
const obj = ref({count: 0, age: 0})
watch(function () {
return obj.value.count
}, function () {
console.log('obj变化了')
},
{
deep: true // 若注释掉这行,在obj更改后不会触发回调函数
}
)
</script>
<template>
<button @click="obj.count++"> {{ obj.count }} + 1</button>
</template>组件生命周期
以组件渲染后为例,选项式API中使用mounted函数来在组件渲染完毕后执行回调函数。
<script>
export default {
mounted: ()=> {
console.log("组件被渲染完成")
}
}
</script>
<template>
</template>在组合式API中,它对应的代码如下。
<script setup>
import {onMounted} from 'vue'
onMounted(function () {
console.log("组件被渲染完成")
})
</script>
<template>
</template>除beforeCreate和created,其他生命周期都是对应的名称前面加on,如beforeMounted在组合式API中的生命周期名称为onBeforeMounted。
beforeCreate和created对应的组合式API的生命周期则被封装进了setup中。
组件数据传递 - 父传子
组合式API中,组件间的信息传递和选项式API一样,都是通过属性名="属性值"传递的,不同的地方还是在脚本代码上。得益于setup提供的语法糖,在父组件中导入子组件后,可以直接使用。
<script setup>
import ChildComponent from "./ChildComponent.vue";
</script>
<template>
<ChildComponent message="Hello World"/>
</template>在子组件中,使用defineProps宏定义组件内变量名称。
<script setup>
const props = defineProps({
message: String
})
</script>
<template>
{{ message }}
</template>若要传递变量,即响应式数据,需要在引用组件时使用v-bind,其他都和原来的一致。
<!--父组件-->
<script setup>
import ChildComponent from "./ChildComponent.vue";
import {ref} from "vue";
const number = ref(0)
setTimeout(() => {
number.value ++
}, 3000)
</script>
<template>
<ChildComponent :number="number"/>
</template><!--子组件-->
<script setup>
const props = defineProps({
number: String
})
</script>
<template>
{{ number }}
</template>class与v-bind的增强使用最主要体现在:class="activeClass"这样的用法中,style和v-bind的用法可以用在<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>这样的语法中。
组件数据传递 - 子传父
即子组件调用父组件中的函数。
在选项式API中,子传父是通过以下方法实现的。
父组件:
- 导入并引入子组件,在子组件用v-on绑定一个自定义事件,事件名假如为customEvent,值为本组件内使用methods定义的countPlus。
- 在data()中定义一个count,初始值为0,在本组件内输出count的值。
- countPlus定义一个参数,调用时将这个组件内的count加上参数的值。
子组件
- 在emits中声明上方声明的自定义事件名,在本例中为customEvent。
- 在子组件中定义一个按钮,按钮点击后触发f函数,f函数使用
this.$emit("customEvent", number),customEvent为自定义事件,number为在本组件中使用data()定义的一个变量,它与一个input使用v-model绑定。
在组合式API中,子传父是通过以下方式实现的:
父组件(使用setup语法糖)
- 直接导入子组件,可直接使用;
- 定义一个函数,这个函数要被子组件调用。
- 在引用组件时使用v-on传递一个自定义事件,事件名称这里假如为customEvent,值为上方定义的函数。
子组件(使用setup语法糖)
- 使用defineProps宏定义上方传入的自定义事件名,该方法返回一个此组件内可用的emit对象,和组合式API的
this.$emit很像; - 使用emit(customEvent, args)调用即可。
<!--父组件-->
<script setup>
import ChildComponent from "@/ChildComponent.vue";
import {ref} from "vue";
const count = ref(0)
function func(number) {
count.value += number
}
</script>
<template>
count = {{ count }}
<ChildComponent @custom-event="func" />
</template><!--子组件-->
<script setup>
import {ref} from "vue";
const number = ref(0)
const emit = defineEmits([
"customEvent"
])
function clickFunc() {
emit("customEvent", number.value)
}
</script>
<template>
<h2>子组件</h2>
<input v-model="number" type="number">
<button @click="clickFunc">点击</button>
</template>模板引用
组合式API中,在初始声明响应式变量时,将初始值设为null,然后在被引用的组件上使用ref引用这个变量即可。
<script setup>
import {ref, onMounted} from "vue";
const span = ref(null);
onMounted(function(){
console.log(span) //组件渲染完成才能获取
})
</script>
<template>
<span ref="span">span元素</span>
<button @click="console.log(span)">输出span元素</button>
</template>若在组件上使用ref属性,变量引用的将是组件本身。
默认情况下,setup语法糖内部的属性和方法是不开放给父组件使用的,可以通过defineExpose宏指定哪些属性和方法能被访问。
import {ref} from "vue";
const count = ref(0);
const f = function() {
console.log('f函数');
}
defineExpose([
count, f
])跨层传递数据
在多个具有嵌套关系的组件中,更顶层的组件可以用provide函数定义数据,它接受两个参数,第一个参数为键类型,第二个参数为一个普通数据或一个响应式对象。底层的组件使用inject接受组件,它接受一个参数,为键名,返回一个对象,可用const承接。
<!--父组件-->
<script setup>
import {provide, ref} from "vue";
import ChildComponent from "./SonComponent.vue";
const constant = '这是一个常量';
const count = ref(0);
const countPlus = () => {
count.value++
}
provide('constant-key', constant);
provide('count-key', count);
provide('countPlus-key', countPlus)
</script>
<template>
<h2>父组件</h2>
<ChildComponent />
</template><!--子组件-->
<script setup>
import GrandComponent from './GrandComponent.vue';
</script>
<template>
<h2>子组件</h2>
<GrandComponent/>
</template><!--孙子组件-->
<script setup>
import {inject, ref} from "vue";
const constant = inject('constant-key');
const count = inject('count-key');
const countPlus = inject('countPlus-key');
</script>
<template>
<span>{{ constant }}</span>
<span>{{ count }}</span>
<button @click="countPlus"> + 1 </button>
</template>组件间的v-model指令
可以替代传统的子组件与父组件之间的通信,方法:
<UserName
v-model:first-name="first"
v-model:last-name="last"
/><script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>
<template>
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
</template>透传Attributes
如果父组件引用子组件时,使用了类似id或class的属性,但子组件没有使用defineProps声明这些属性,这些属性会被添加到子组件的根元素,作为根元素的属性之一(若已有则合并)。如果子组件有多个根节点,会出现运行时警告。
可以通过$attrs将其显示绑定至一个根节点:
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>在<script setup>中使用useAttrs()可获取这些属性。
具名插槽
子组件BaseLayout中,有一个如下的结构。
可以为<slot>提供一个唯一的name属性,为每一个插槽分配唯一id,不带该属性的为default:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>要为具名插槽传入内容,我们需要使用一个含 v-slot(缩写为#) 指令的 <template> 元素,并将目标插槽的名字传给该指令:
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
<template v-slot:footer>
<!-- footer 插槽的内容放这里 -->
</template>
</BaseLayout>