目录
setup() 钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:
其他情况下,都应优先使用 <script setup> 语法。
我们可以使用响应式 API 来声明响应式的状态,在 setup() 函数中返回的对象会暴露给模板和组件实例。其它的选项也可以通过组件实例来获取 setup() 暴露的属性
- <script>
- import { ref } from 'vue'
-
- export default {
- setup() {
- const count = ref(0)
-
- // 返回值会暴露给模板和其他的选项式 API 钩子
- return {
- count
- }
- },
-
- mounted() {
- console.log(this.count) // 0
- }
- }
- </script>
-
- <template>
- <button @click="count++">{{ count }}</button>
- </template>
请注意在模板中访问从 setup 返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value。当通过 this 访问时也会同样如此解包。
setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>组合式API</title>
- </head>
- <body>
- <div id="app">
- {{ count }}
- <button @click="add">加1</button>
- </div>
- </body>
- <script src="../lib/vue.global.js"></script>
- <script>
- const { ref, onMounted } = Vue
- Vue.createApp({
- setup () {
- const count = ref(0)
- const add = () => {
- count.value += 1
- }
- onMounted(() => {
- console.log(1111)
- })
- return {
- count,
- add
- }
- },
- data () {
- return {
- count: 10
- }
- },
- methods: {
- add () {
- this.count += 10
- }
- },
- mounted () {
- console.log('2222')
- }
- }).mount('#app')
- </script>
- </html>
生命周期先执行 组合式API 后执行选项式API,其余以组合式API为优先
setup 函数的第一个参数是组件的 props。和标准的组件一致,一个 setup 函数的 props 是响应式的,并且会在传入新的 props 时同步更新。
- {
- props: {
- title: String,
- count: Number
- },
- setup(props) {
- console.log(props.title)
- console.log(props.count)
- }
- }
请注意如果你解构了 props 对象,解构出的变量将会丢失响应性。因此我们推荐通过 props.xxx 的形式来使用其中的 props。
如果你确实需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs() 和 toRef() 这两个工具函数:
- {
- setup(props) {
- // 将 `props` 转为一个其中全是 ref 的对象,然后解构
- const { title } = toRefs(props)
- // `title` 是一个追踪着 `props.title` 的 ref
- console.log(title.value)
-
- // 或者,将 `props` 的单个属性转为一个 ref
- const title = toRef(props, 'title')
- }
- }
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>组合式API</title>
- </head>
- <body>
- <div id="app">
- <button @click="num++">加1</button> {{ num }}
- <my-root :num="num"></my-root>
- </div>
- </body>
- <script src="../lib/vue.global.js"></script>
- <template id="root">
- <div>{{ num }} -- {{ test }}</div>
- </template>
- <script>
- const { ref, onMounted, computed } = Vue
-
- const Root = {
- props: ['num'],
- template: '#root',
- // setup (props) { // 千万不要对 props 解构
- // console.log('111')
- // return {
- // test: computed(() => props.num) // 继续保持响应式
- // }
- // }
- setup ({ num }) {
- console.log(num)
- return {
- test: computed(() => num) // 失去了响应式 - test的值不会发生改变
- }
- }
- }
- Vue.createApp({
- setup () {
- const num = ref(10000)
- return { num }
- },
- components: {
- MyRoot: Root
- }
- }).mount('#app')
- </script>
- </html>
传入 setup 函数的第二个参数是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup 中可能会用到的值:
- {
- setup(props, context) {
- // 透传 Attributes(非响应式的对象,等价于 $attrs)
- console.log(context.attrs)
-
- // 插槽(非响应式的对象,等价于 $slots)
- console.log(context.slots)
-
- // 触发事件(函数,等价于 $emit)
- console.log(context.emit)
-
- // 暴露公共属性(函数)
- console.log(context.expose)
- }
- }
该上下文对象是非响应式的,可以安全地解构:
- {
- setup(props, { attrs, slots, emit, expose }) {
- ...
- }
- }
attrs 和 slots 都是有状态的对象,它们总是会随着组件自身的更新而更新。这意味着你应当避免解构它们,并始终通过 attrs.x 或 slots.x 的形式使用其中的属性。此外还需注意,和 props 不同,attrs 和 slots 的属性都不是响应式的。如果你想要基于 attrs 或 slots 的改变来执行副作用,那么你应该在 onBeforeUpdate 生命周期钩子中编写相关逻辑。
expose 函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问 expose 函数暴露出的内容
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>setup上下文对象</title>
- </head>
- <body>
- <div id="app">
- <my-com ref="comref" class="myBox" style="color: red" id="box" msg="hello msg" @my-event="getData">
- <template #header>
- header
- </template>
- <div>content</div>
- <template #footer>
- footer
- </template>
- </my-com>
- </div>
- </body>
- <template id="com">
- <div>
- <h1>子组件</h1>
- <button @click="sendData">发送数据</button>
- <slot name="header"></slot>
- <slot></slot>
- <slot name="footer"></slot>
- </div>
- </template>
- <script src="../lib/vue.global.js"></script>
- <script>
- const { createApp, ref, onMounted } = Vue
- const Com = {
- template: '#com',
- setup (props, context) {
- // attrs 获取透传过来的值
- // slots 如果使用了插槽
- // emit 子组件给父组件传值
- // expose 子组件暴露给父组件可以调用的属性和方法 ---- options API ref获取子组件的实例
-
- console.log(props)
- console.log(context.attrs) // ref 不在透传之列
- console.log(context.slots)
- const sendData = () => { // 子组件给父组件传值
- context.emit('my-event', 1000)
- }
-
- // 自定义的属性和方法,供给父组件使用
- const a = ref(1)
- const b = ref(2)
- const c = ref(3)
-
- const fn = () => {
- a.value = 100
- }
-
- // 暴露出去的是对象
- context.expose({
- a, b, fn
- })
-
- return {
- sendData
- }
- }
- }
-
- Vue.createApp({
- setup () {
- const getData = (val) => { // 接收子组件的值
- console.log('666', val)
- }
-
- const comref = ref() // comref 就是模版中ref="comref"
-
- onMounted(() => {
- console.log('com', comref.value) // {}
- console.log('a', comref.value.a) // 1
- console.log('b', comref.value.b) // 2
- console.log('c', comref.value.c) // undefined 因为没有暴露
- comref.value.fn()
- console.log('a', comref.value.a) // 100
- })
-
- return {
- getData,
- comref
- }
- },
- components: {
- MyCom: Com
- }
- }).mount('#app')
- </script>
- </html>
在父组件通过ref获取子组件的实例的属性和方法的需求中,需要注意:
1.如果子组件是 选项式API组件,基本不需要做任何操作
2.如果子组件是 组合式API组件,需要通过 context.expose 暴露给父组件需要使用的属性和方法
3.如果父组件使用 选项式API, 可以通过 this.$refs.refName 访问到子组件想要你看到的属性和方法
4.如果父组件使用 组合式API,需要在setup中先创建 refName,然后再访问子组件想要你看到的属性和方法(const refName = ref() refName.value.X)
setup 也可以返回一个渲染函数,此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态:
- {
- setup() {
- const count = ref(0)
- return () => h('div', count.value)
- }
- }
返回一个渲染函数将会阻止我们返回其他东西。对于组件内部来说,这样没有问题,但如果我们想通过模板引用将这个组件的方法暴露给父组件,那就有问题
我们可以通过调用 expose() 解决这个问题:
- {
- setup(props, { expose }) {
- const count = ref(0)
- const increment = () => ++count.value
-
- expose({
- increment
- })
-
- return () => h('div', count.value)
- }
- }
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>渲染函数</title>
- </head>
- <body>
- <div id="app">
- <button @click="add">加1</button>
- <my-child ref="child"></my-child>
- </div>
- </body>
- <script src="../lib/vue.global.js"></script>
- <script>
- const { h, ref } = Vue
- const Child = {
- // 写法1:
- // template: `<div>child</div>`
- // 写法2:
- // render () {
- // return [
- // h('div', 'child!')
- // ]
- // }
- // 写法3
- setup (props, { expose }) {
- const count = ref(10)
-
- const increment = () => {
- count.value += 1
- }
-
- expose({
- increment
- })
-
- // 返回一个函数 函数返回 渲染函数的结果
- return () => h('div', 'child!!' + count.value)
- }
- }
-
- Vue.createApp({
- components: {
- MyChild: Child
- },
- setup () {
- const child = ref()
-
- const add = () => {
- child.value.increment()
- }
-
- return {
- child,
- add
- }
- }
- }).mount('#app')
- </script>
- </html>