4.1 组件生命周期钩子
我们前面说过 setup
可以用来替代 data
、 methods
、 computed
、watch
等等这些选项,也可以替代 生命周期钩子
。
那么setup中如何使用生命周期函数呢?
- 可以使用直接导入的
onXxx
函数注册生命周期钩子。
我们在01_composition_api
项目的src
目录下新建09_生命周期钩子
文件夹,然后在该文件夹下新建:App.vue
组件。
App.vue根组件,代码如下所示:
<template> <div><button @click="increment">点击+1:{{counter}}</button></div> </template> <script> import { onMounted, onUpdated, onUnmounted, ref } from 'vue'; export default { setup() { const counter = ref(0); const increment = () => counter.value++ // 生命周期钩子函数 (同一个生命周期函数可以存在多个) onMounted(() => { console.log("App Mounted1"); }) onMounted(() => { console.log("App Mounted2"); }) onUpdated(() => { console.log("App onUpdated"); }) onUnmounted(() => { console.log("App onUnmounted"); }) return {counter,increment} } } </script>
可以看到,在App组件中注册了onBeforeMount、onMounted、onUpdated和onUnmounted
生命周期函数,其中onMounted
生命周期函数我们注册了两次。
然后我们修改main.js程序入口文件,将导入的App组件改为09_生命周期钩子/App.vue
路径下的App组件。
保存代码,运行在浏览器的效果,如图10-22所示。刷新页面,控制台会打印App onBeforeMount、App Mounted1、App Mounted2
,每当点击一次按钮会打印一次App onUpdated
。这里就不一一演示组件的销毁和其它的生命周期函数了。
图10-22 生命周期函数
那么Compostion API提供了哪些生命周期函数呢?并且Compostion API的生命周期函数和Options API的生命周期函数有什么对应关系呢?请看下表:
选项式 API | Hook inside setup |
beforeCreate |
Not needed* |
created |
Not needed* |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeUnmount |
onBeforeUnmount |
unmounted |
onUnmounted |
activated |
onActivated |
deactivated |
onDeactivated |
我们会发现Compostion API
没有提供 beforeCreate
和 created
生命周期函数,而是直接使用setup
函数来代替了(setup函数会在beforeCreate之前调用),如图10-23所示。
图10-23 setup代替了beforeCreate和created
5.1 Provide/Inject
事实上我们之前还学习过Provide
和Inject
,Composition API也可以替代之前的 Provide
和 Inject
的选项。
5.1.1 Provide函数
我们可以通过 provide
函数来给子组件或者子孙组件提供数据:
- 可以通过
provide
函数来定义每个 property。 provide
函数可以传入两个参数:
- name:提供的属性名称。
- value:提供的属性值。
下面我们来通过一个案例来学习一下Provide函数的使用。我们在01_composition_api
项目的src
目录下新建10_Provide和Inject
文件夹,然后在该文件夹下新建:App.vue
组件。
App.vue根组件,代码如下所示:
<template> <div class="app" style="border:1px solid #ddd;margin:4px"> App组件 <div>{{name}} - {{age}}</div> <div>{{counter}}</div> <button @click="increment">App组件+1</button> </div> </template> <script> import { provide, ref } from 'vue'; export default { setup() { // 1.定义普通数据 const name = "coderwhy"; const age = 18; // 2.定义响应式数据 let counter = ref(100); // 3.给子组件或者子孙组件提供数据 provide("name", name); provide("age", age); // 提供普通数据(只能读,不能修改) provide("counter", counter); // 提供响应式数据 const increment = () => counter.value++; return {name,age,increment,counter} } } </script>
可以看到,在setup函数中调用了provide函数来给子组件或者子孙组件提供了name与age
普通数据和counter
响应式数据。其中提供的普通数据是只读不能修改,提供的响应式数据默认是可读可修改,并且是响应式的。
然后我们修改main.js程序入口文件,将导入的App组件改为10_Provide和Inject/App.vue
路径下的App组件。
保存代码,运行在浏览器的效果,如图10-24所示。可以看到在自己本组件中能正常显示,点击按钮也能实现响应式刷新页面。那有些同学会问provide不是给子组件或者子孙组件提供数据吗?那么子组件和子孙组件如何获取?那我们继续来学习下一小节的inject函数。
图10-24 provide函数的基本使用
5.1.2 Inject函数
在后代组件中可以通过 inject
来注入需要的属性和对应的值:
- 可以通过
inject
函数来注入需要的内容。 inject
可以传入两个参数:
- 要 inject 的 property 的 name。
- 默认值。
上面案例的App父组件已经完成数据的提供,那么它的子组件和孙子组件怎么获取提供的数据呢?要想获取父组件通过provide提供的数据,子组件或者孙子组件需要通过inject函数来获取。
接着我们在10_Provide和Inject
文件夹下新建:Home.vue
组件。
Home.vue子组件,代码如下所示:
<template> <div style="border:1px solid #ddd;margin:8px"> Home组件 <div>{{name}} - {{age}}</div> <div>{{counter}}</div> <button @click="homeIncrement">Home组件+1</button> </div> </template> <script> import { inject } from 'vue'; export default { setup() { // 1.获取父组件provide提供的数据( 子组件和孙子组件获取的代码是一模一样的) const name = inject("name"); const age = inject("age"); const counter = inject("counter"); const homeIncrement = () => counter.value++; return {name,age, counter,homeIncrement} } } </script>
可以看到,该组件在setup函数中通过inject函数来注入父组件或者祖父组件使用provide函数提供的数据。其中name与age
是注入普通对象(只读不能修改),counter
则是响应式对象(可读可修改)。接着当点击button时,我们在子组件中修改了父组件提供的counter值。
修改App组件,代码如下所示:
<template> <div class="app" style="border:1px solid #ddd;margin:4px"> App组件...... <home/> </div> </template> <script> import Home from './Home.vue'; export default { components: { Home }, ...... } </script>
保存代码,运行在浏览器的效果,如图10-25所示。当我们点击App组件的按钮来在父组件修改counter时,App组件和Home组件的counter都同步变化,当我们点击Home组件的按钮来在子组件修改counter时,App组件和Home组件的counter也是同步变化。这就说明父组件提供的响应式数据,子组件不但能获取到,还保持了响应式。
图10-24 inject函数的基本使用
5.1.3 共享响应式属性
1.共享响应式的数据
为了增加 provide 值和 inject 值之间的响应性,其实我们可以在 provide 值时使用 ref
和 reactive
对象。其中ref
对象上面已经演示了,这里再看一下如何提供reactive响应式数据,代码如下所示:
// App父组件 let counter = ref(100) let info = reactive({ name: "why", age: 18 }) // 1.提供响应式数据 provide("counter", counter) provide("info", info) // 2.修改响应式数据 const changeInfo = () => { info.name = "coderwhy" } // 子组件(孙子组件)注入父组件(祖父组件)提供的响应式数据 const info = inject("counter"); const info = inject("info");
2.修改响应式Property
因为父组件可以通过provide提供响应式数据给子组件,该响应式数据默认是可以在父组件被修改,也可以在子组件被修改。如果子组件也可以修改父组件提供的响应式数据,那么我们就很难追踪响应数据到底是在哪被修改的,为了保证单向数据流,我们一般建议:
- 如果我们需要修改响应的数据,那么最好是在数据提供的位置来修改(如上案例应在App中修改counter)
- 其实我们还可以将修改数据的方法进行共享,在后代组件中进行调用(如上案例不应在Home中直接修改counter)。
- 有时候为了避免子组件修该父组件提供的数据,我们可以借助
readonly
函数,如下代码所示。
provide("info", readonly(info); // 子组件注入时只能读,不能修改 provide("counter", readonly(counter); // 子组件注入时只能读,不能修改
6.1 Composition API综合练习
前面我们已经学习了setup、reactive、ref、computed、watchEffect、watch、provide、inject
等等Composition API,那下面将通过一个Composition API的综合练习来巩固一下组合API的使用以及代码逻辑的封装(即Hook函数的封装)。其中该综合练习包含以下功能:
- 计数器案例的实现。
- 修改网页的标题。
- 完成一个监听界面滚动位置。
在使用Composition API之前,我们先看看用Options API是如何实现该功能。
我们在01_composition_api
项目的src
目录下新建11_compositionAPI综合练习
文件夹,然后在该文件夹下分别新建:App.vue,OptionsAPIExample.vue
组件。
OptionsAPIExample.vue子组件,代码如下所示:
<template> <div> <!--1.计数器案例 --> <div>当前计数: {{counter}}</div> <div>当前计数*2: {{doubleCounter}}</div> <button @click="increment">+1</button> <button @click="decrement">-1</button> </div> </template> <script> export default { data() { return{ // 1.2计数器案例的逻辑代码 counter:100 } }, computed: { // 1.3计数器案例的逻辑代码 doubleCounter() { return this.counter * 2 } }, methods: { // 1.4计数器案例的逻辑代码 increment() { this.counter++; }, decrement() { this.counter--; } } } </script>
可以看到,该案例我们仅实现了计数器的案例。为了保证代码的简洁易懂,其它修改网页标题和监听页面滚动的代码逻辑这里暂时先不实现(后面直接用组合API来实现)。最后我们在App.vue根组件中导入和使用OptionsAPIExample组件(不再贴代码)。
然后我们修改main.js程序入口文件,将导入的App组件改为11_compositionAPI综合练习/App.vue
路径下的App组件。
保存代码,运行在浏览器的效果,如图10-26所示。
图10-26 Options API实现的计数器
下面我们再用Composition API来实现该功能。我们在11_compositionAPI综合练习
文件夹下新建:CompositionAPIExample.vue
组件。
CompositionAPIExample.vue子组件,代码如下所示:
<template> <div> <!-- 1.计数器案例 --> <div>当前计数: {{counter}}</div> <div>当前计数*2: {{doubleCounter}}</div> <button @click="increment">+1</button> <button @click="decrement">-1</button> </div> </template> <script> import { ref, computed } from 'vue'; export default { setup() { // 1.1计数器案例的逻辑代码 const counter = ref(100); const doubleCounter = computed(() => counter.value * 2); const increment = () => counter.value++; const decrement = () => counter.value--; return { counter, doubleCounter, increment, decrement } } } </script>
可以看到,该案例我们仅实现了计数器的案例。其它修改网页标题和监听页面滚动的代码逻辑这里也暂时先不实现。最后我们在App.vue根组件中导入和使用CompositionAPIExample组件(不再贴代码)。
保存代码,运行在浏览器的效果和Options API实现的效果一模一样。通过这两个案例,我们可以发现:
- Options API的特点就是在对应的属性中编写对应的功能模块
- 但Options API有一个很大的弊端是对应的代码逻辑被拆分到各个属性中,当组件变得更大、复杂时,同一个功能的逻辑会被拆分的很分散(如上面的计数器功能逻辑被拆分到各个选项中),不利于代码的阅读和理解。
- Composition API的特点是能将同一个逻辑关注点相关的代码收集在一起,方便代码的封装和复用,也更利于代码的阅读和理解。
- Composition API用了比较多的函数,用起来稍微比Options API复杂一点,但是函数式编程对TS支持更友好。
对比完Options API和Composition API编写计数器案例的优缺点之后,下面我们来看看如何对Composition API编写的代码逻辑进行封装和复用。在Options API编写方式中,我们已知道代码逻辑的封装和复用可以使用Mixin混入,那在Composition API中我们可以将关注点相关的代码逻辑封装到一个函数中,该函数我们一般会使用useXx
来命名(社区默认准寻的规范),并且以useXx
开头的函数我们称之为自定义Hook函数。
6.1.1 useCounter
认识Hook函数之后,下面我们来把上面计数器案例的代码逻辑封装到一个useCounter
的Hook函数中。
我们在11_compositionAPI综合练习
文件夹下新建:hooks/useCounter.js
文件。
useCounter.js文件封装useCounter Hook函数,代码如下所示:
import { ref, computed } from 'vue'; export default function useCounter() { // 1.1计数器案例的逻辑代码 const counter = ref(100); const doubleCounter = computed(() => counter.value * 2); const increment = () => counter.value++; const decrement = () => counter.value--; return { counter, doubleCounter, increment, decrement } }
可以看到,我们在该文件中默认导出一个函数(也支持匿名函数),在该函数中我们把CompositionAPIExample组件实现计数器案例的代码逻辑全部抽取过来了。
接着修改CompositionAPIExample组件,代码如下所示:
..... <script> import useCounter from './hooks/useCounter' export default { setup() { // 1.计数器案例的代码逻辑抽取到useCounter hook 中了 const {counter, doubleCounter, increment, decrement} = useCounter() return {counter, doubleCounter, increment, decrement} } } </script>
可以看到,该组件之前实现计数器案例的逻辑代码已经抽取到了useCounter函数中,这时我们只要导入useCounter函数,并在setup中调用该函数便可以拿到返回的响应式数据和事件函数,然后直接返回给模板使用。保存代码,运行在浏览器的效果和没抽取前一模一样。
6.1.2 useTitle
实现完计数器案例之后,下面我们接着再CompositionAPIExample组件中来实现修改网页标题的功能。修改CompositionAPIExample组件,代码如下所示:
<script> export default { setup() { ..... // 2.修改网页的标题案例 const titleRef = ref("coder"); document.title = titleRef.value// 更新网页标题 return {counter, doubleCounter, increment, decrement} } } </script>
可以看到,只在CompositionAPIExample中的setup函数中添加两行代码即可以。保存代码,运行在浏览器的效果,如图10-27所示。已经将网页的标题修改为coder。
图10-27 修改网页的标题
像这种修改网页标题的代码逻辑可能在其它组件中还会再次使用到,那么我们就可以将该功能封装到一个Hook函数中。我们在11_compositionAPI综合练习
文件夹下新建:hooks/useTitle.js
文件。
useTitle.js文件封装useTitle Hook函数,代码如下所示:
import { ref, watch } from 'vue'; // 使用匿名函数,并该函数需接收一个参数 export default function(title = "默认的title") { const titleRef = ref(title); // 侦听titleRef变化,一旦被修改就更新 watch(titleRef, (newValue) => { document.title = newValue }, { immediate: true // 侦听的回调函数先执行一次 }) return titleRef }
修改CompositionAPIExample组件,代码如下所示:
<script> ..... import useTitle from './hooks/useTitle' export default { setup() { ..... // 2.修改网页的标题案例 const titleRef = useTitle("coder"); setTimeout(() => { // 3秒后修改titleRef的值,useTitle函数的watch侦听到会修改标题 titleRef.value = "why }, 3000); return {counter, doubleCounter, increment, decrement} } } </script>
可以看到,我们先导入useTitle函数,接着在setup中调用useTitle函数初始化标题为coder,然后过了2秒之后将标题修改为why。保存代码,运行在浏览器后。网页的标题在3秒后有coder修改为why。
6.1.3 useScrollPosition
实现完修改网页的标题之后,我们接着继续再CompositionAPIExample组件中来实现监听页面滚动位置的功能。修改CompositionAPIExample组件,代码如下所示:
<template> <div> ..... <!-- 3.显示页面滚动位置 --> <p style="width: 3000px;height: 5000px;"> width:3000px height:5000px的,模拟页面滚动 </p> <div style="position: fixed;top:20px;right:20px"> <div >scrollX: {{scrollX}}</div> <div >scrollY: {{scrollY}}</div> </div> </div> </template> <script> ..... export default { setup() { ...... // 3.监听页面滚动 const scrollX = ref(0); const scrollY = ref(0); document.addEventListener("scroll", () => { scrollX.value = window.scrollX; scrollY.value = window.scrollY; }); return {counter, doubleCounter, increment, decrement, scrollX, scrollY} } } </script>
可以看到,我们先在template中编写宽和高超出屏幕大小的p元素(模拟页面可滚动),接着在setup函数监听了页面的滚动,并在该回调函数中给scrollX和scrollY变量赋当前滚动的值。最后在return函数中返回scrollX和scrollY变量给temlpate来显示当前滚动的位置。保存代码,运行在浏览器的效果,如图10-28所示。上下滚动页面的时候,页面的右上角上能显示当前滚动位置值。
那如果该功能也会被再次使用到,我们依然可以将该功能封装到一个Hook函数中。我们在11_compositionAPI综合练习
文件夹下新建:hooks/useScrollPosition.js
文件。
useScrollPosition.js文件封装useScrollPosition Hook函数,代码如下所示:
import { ref } from 'vue'; // 自定义 useScrollPosition Hook函数 export default function useScrollPosition() { const scrollX = ref(0); const scrollY = ref(0); document.addEventListener("scroll", () => { scrollX.value = window.scrollX; scrollY.value = window.scrollY; }); return {scrollX, scrollY} // 返回ref响应式数据 }
修改CompositionAPIExample组件,代码如下所示:
....... <script> import useCounter from './hooks/useCounter' import useTitle from './hooks/useTitle' import useScrollPosition from './hooks/useScrollPosition' export default { setup() { // 1.计数器案例(可直接解构,如果返回的是reactive对象则不能直接解构使用) const {counter, doubleCounter, increment, decrement} = useCounter() // 2.修改网页标题案例 const titleRef = useTitle("coder"); setTimeout(() => { titleRef.value = "why" }, 3000); // 3.监听页面滚动位置案例 (可直接解构,因为Hook函数返回对象属性是ref对象) const { scrollX, scrollY } = useScrollPosition(); return {counter, doubleCounter, increment, decrement, scrollX, scrollY} } } </script>
可以看到,我们先导入useScrollPosition函数,接着在setup中调用useScrollPosition函数来获取到当前滚动的值。如果滚动页面了,useScrollPosition函数里会监听到并修改scrollX和scrollY响应式变量的值,同时更新页面。保存代码,运行在浏览器后。滚动网页时可以发现页面上右上角的scrollX和scrollY能显示当前滚动的位置。
7.1 script setup语法
当我们在编写单文件组件(即.vue文件)的时候,除了 <script>
语法,其实Vue3还支持<script setup>
语法,它方便我们在script顶层来编写setup相关的代码。setup script
语法的代码看起来简单了很多,开发效率大大的提高。该语法是在2020-10-28号提出,在Vue3.2版本之前它还只是一个实验性功能,但是到了Vue3.2版本<script setup>
语法已从实验状态毕业,现在被认为是稳定的了。
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 Typescript 声明 props 和抛出事件。
- 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
- 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。
7.1.1 基本使用
我们来看一下用<script setup>
语法是如何使用的:
- 要使用这个语法,需要将
setup
attribute 添加到<script>
代码块上。 - 里面的代码会被编译成组件
setup()
函数的内容。 - 这意味着与普通的
<script>
只在组件被首次引入的时候执行一次不同,<script setup>
中的代码会在每次组件实例被创建的时候执行。 - 当使用
<script setup>
的时候,任何在<script setup>
声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用。
下面我们使用<script setup>
语法来编写计数器案例,我们在01_composition_api
项目的src
目录下新建12_script_setup顶层编写方式
文件夹,然后在该文件夹下分别新建:App.vue,ScriptSetupExample.vue
组件。
ScriptSetupExample.vue子组件,代码如下所示:
<template> <div> <h4>当前计数: {{counter}}</h4> <button @click="increment">+1</button> </div> </template> // 1.script setup语法的顶层编写方式 <script setup> // 2.ref、counter、increment是在顶层绑定,所以都能在模板中直接使用 import { ref } from 'vue'; const counter = ref(0); const increment = () => counter.value++; </script>
可以看到,该组件使用了<script setup>
语法的顶层编写方式,在顶层绑定了ref、counter、increment
,所以都能在模板中直接使用他们。最后我们在App.vue根组件中导入和使用ScriptSetupExample组件(不再贴代码)。
然后我们修改main.js程序入口文件,将导入的App组件改为12_script_setup顶层编写方式/App.vue
路径下的App组件。
保存代码,运行在浏览器的效果,如图10-29所示。已实现计数器案例。
图10-8 script setup语法的基本使用
当使用 <script setup>
的时候,任何在 <script setup>
声明的顶层的绑定都能在模板中直接使用。例如:声明的普通变量,响应式变量,函数,import 引入的内容(包含函数,对象,组件,动态组件,指令等等)。当是响应式状态时需要明确使用响应式 APIs 来创建。和从 setup()
函数中返回值一样,ref 值在模板中使用的时候会自动解包,如下代码所示:
<template> <MyComponent /> <component :is="Foo" /> <h4 v-my-directive>This is a Heading</h1> <div>{{ capitalize('hello') }}</div> <button @click="count++">{{ count }}</button> <div @click="log">{{ msg }}</div> </template> // script setup语法的顶层的绑定( 下面声明的绑定都可以直接在模板中使用 ) <script setup> import MyComponent from './MyComponent.vue' // 声明绑定组件 import Foo from './Foo.vue' // 声明绑定动态组件 import { myDirective as vMyDirective } from './MyDirective.js' // 声明绑定指令 import { capitalize } from './helpers' // 声明绑定工具函数 import { ref } from 'vue' // 声明绑定ref函数 const count = ref(0) // 声明绑定响应式变量 const msg = 'Hello!' // 声明绑定普通变量 function log() { // 声明绑定函数 console.log(msg) } </script>
上面代码列举了在 <script setup>
中常用的顶层的绑定。上面代码所声明的组件,函数,指令等这里就不一一实现了。大家只要知道在<script setup>
中顶层的绑定会被暴露给模板使用就可以了。
7.1.2 defineProps和defineEmits
上面我们已经学会了<script setup>
语法的基本使用,那么在这种语法下,我们应该如何定义props和如何发出事件呢?在 <script setup>
中必须使用 defineProps
和 defineEmits
APIs 来声明 props
和 emits
,它们具备完整的类型推断并且在 <script setup>
中是直接可用的(Vue3.2版本以后不需要导入)。
我们在12_script_setup顶层编写方式
文件夹下新建:DefinePropsEmitAPI.vue
组件。
DefinePropsEmitAPI.vue子组件,代码如下所示:
<template> <div style="border:1px solid #ddd;margin:8px"> <div>DefinePropsEmitAPI组件</div> <p>{{message}}</p> <button @click="emitEvent">发射emit事件</button> </div> </template> // Vue3.2以后defineProps和defineEmits不需要导入(当前项目Vue安装的版本是:3.2.29) <script setup> // 1.定义props属性(等同于Options API的props选项) const props = defineProps({ // message: String, message: { type: String, default: "默认的message" } }) // 2.注册需要触发的emit事件 const emit = defineEmits(["increment"]); // 3.点击 发射emit事件 按钮的回调 const emitEvent = () => { console.log('子组件拿到父组件传递进来的message:' + props.message) emit('increment', 1) // 触发 increment 事件,传递参数:1 } </script>
可以看到,我们使用defineProps
函数来给组件定义了message属性,使用defineEmits
函数来给组件注册了increment事件,并返回emit函数。当点击button时,先打印父组件传递进来的message,然后使用emit函数来触发事件。
如何查看当前项目依赖Vue的具体版本:可看node_modules/vue/package.json文件中的version属性。
接着修改App组件,代码如下所示:
<template> <div class="app" style="border:1px solid #ddd;margin:4px"> App组件 <!-- <ScriptSetupExample></ScriptSetupExample> --> <DefinePropsEmitAPI message="App传递过来的message" @increment="getCounter"/> </div> </template> <script setup> import { ref } from 'vue' import ScriptSetupExample from './ScriptSetupExample.vue'; import DefinePropsEmitAPI from './DefinePropsEmitAPI.vue'; const getCounter = (number)=> console.log('App 组件拿到子组件传递过来的number:' + number) </script>
可以看到,我们先导入DefinePropsEmitAPI
组件,接着在template中使用该组件时,给它传递了message属性和监听了increment事件 。
保存代码,运行在浏览器后点击发射emit事件
按钮,便会调用emitEvent函数,控制台输出如图10-30所示。
图10-30 defineProps和defineEmits的使用
有关于defineProps
和 defineEmits
函数,我们还需要注意的是:
defineProps
和defineEmits
APIs都是只在<script setup>
中才能使用的编译器宏。他们不需要导入且会随着<script setup>
处理过程一同被编译掉。defineProps
接收与props
选项相同的值,defineEmits
也接收emits
选项相同的值。defineProps
和defineEmits
在选项传入后,会提供恰当的类型推断。- 传入到
defineProps
和defineEmits
的选项会从 setup 中提升到模块的范围。因此,传入的选项不能引用在 setup 范围中声明的局部变量。这样做会引起编译错误。但是,它可以引用导入的绑定,因为它们也在模块范围内。
7.1.3 defineExpose
使用 <script setup>
语法的组件是默认关闭的,即通过模板 ref 或者 $parent
链获取到的组件的公开实例,该实例是不会暴露任何在 <script setup>
中声明的绑定。所以为了在 <script setup>
语法组件中明确要暴露出去的属性,我们需要使用 defineExpose
编译器宏。
我们在12_script_setup顶层编写方式
文件夹下新建:DefineExposeAPI.vue
组件。
DefineExposeAPI.vue子组件,代码如下所示:
<template> <div style="border:1px solid #ddd;margin:8px"> DefineExposeAPI 组件 </div> </template> <script setup> import { ref } from 'vue' const age = 18 // 普通数据 const name = ref('coderwhy') // 响应式数据 const showMessage = ()=>{console.log('showMessage方法')} // 方法 // 该组件暴露出去的属性( age,name,showMessage ) defineExpose({age,name,showMessage}) </script>
可以看到,我们在该组件中定义了age,name和showMessage方法,然后通过defineExpose
API将这3个属性暴露出去。
接着修改App组件,代码如下所示(省略的代码已注释):
<template> <div class="app" style="border:1px solid #ddd;margin:4px">App组件 ..... <DefineExposeAPI ref="defineExposeAPI"></DefineExposeAPI> </div> </template> <script setup> import { ref, watchEffect } from 'vue' .... import DefineExposeAPI from './DefineExposeAPI.vue'; // 获取DefineExposeAPI组件的实例和该组件暴露的属性 const defineExposeAPI = ref(null) watchEffect(()=>{ console.log(defineExposeAPI.value) // 组件的实例 console.log(defineExposeAPI.value.name) // 响应式数据 console.log(defineExposeAPI.value.age) defineExposeAPI.value.showMessage() }, {flush:"post"}) .... </script>
可以看到,我们用ref定义了defineExposeAPI
变量,并绑定到DefineExposeAPI
组件的ref属性上来获取该组件的实例。然后在watchEffect函数中获取该组件实例和该组件暴露出来的:name,age和showMessage
属性。
保存代码,运行在浏览器后,控制台输出如图10-31所示。即父组件App可以访问到子组件暴露出来的name,age和showMessage
属性。
图10-30 defineExpose的使用
7.1.4 useSlots和useAttrs
在学习setup函数时,该函数主要有两个参数:props和context,其中context里面包含slots,attrs,emit
三个属性。那在 <script setup>
中应该如何拿到slots,attrs
属性?虽然在 <script setup>
使用 slots
和 attrs
的情况应该是很罕见的(因为可以在模板中通过 $slots
和 $attrs
来访问它们)。在你的确需要使用它们的罕见场景中,可以分别用 useSlots
和 useAttrs
两个辅助函数。代码如下所示:
<script setup> import { useSlots, useAttrs } from 'vue' const slots = useSlots() // 拿到该组件的插槽,等于setup函数中的context.slots const attrs = useAttrs() // 拿到该组件所有的属性,等于setup函数中的context.attrs </script>
useSlots
和 useAttrs
是真实的运行时函数(需要导入后使用),它会返回与 setupContext.slots
和 setupContext.attrs
等价的值,同样也能在普通的组合式 API 中使用。