setup 方法

为了开始使用组合式 API,我们首先需要一个可以实际使用它的地方。在 Vue 3 的组件中,我们将此位置称为 setup 方法,如示例代码 4-2-1 所示。

<div id="app">
    <component-b user="John" />
</div>
const componentB = {
    props: {
        user: {
            type: String,
            required: true
        }
    },
    template:'<div></div>',
    setup(props, context) {
        console.log(props.user) // 打印'John'
        return {} // 这里返回的任何内容都可以用于组件的其余部分
    }
}
Vue.createApp({
    components: {
        'component-b': componentB
    }
}).mount("#app")

setup 方法的参数

setup 方法接收两个参数,一个参数是 props,它和之前讲解的组件通信中的 props 一样,可以接收到父组件传递的数据,同样,如果 props 是一个动态值,那么它就是 响应式的,会随着父组件的改变而更新

请注意如果你解构了 props 对象,解构出的变量将会丢失响应性。因此我们推荐通过 props.xxx 的形式来使用其中的 props

但是,因为 props 是响应式的,用户不能使用 ES 6 解构,它会消除 props 的响应性。如果需要解构 props,可以在 setup 方法中使用 toRefs 函数来完成此操作,代码如下:

setup(props, context) {
    // 将 `props` 转为一个其中全是 ref 的对象,然后解构
    const { user } = Vue.toRefs(props)
    console.log(user.value) // 打印'John'
}

注意,如果采用 npm 来管理项目,可以采用如下 import 方式引入 toRefs,包括后续的组合式 API 相关的方法:

import { toRefs } from 'vue'

如果 user 是可选的 props,则传入的 props 中可能没有 user。在这种情况下,需要使用 toRef 替代它,代码如下:

setup(props,context) {
    // 将 `props` 的单个属性转为一个 ref
    const { user } = Vue.toRef(props,'user')
    console.log(user.value) // 打印'John'
}

setup 方法的另一个参数是 context 对象,context 是一个普通的 JavaScript 对象,它暴露组件的三个属性,分别是 attrsslotsemitexpose,并且由于是普通的 JavaScript 对象,因此使用 ES 6 解构,如示例代码 4-2-2 所示。

示例代码 4-2-2 setup 方法
<div id="app">
    <component-b attrone="one" @emitcallback="emitcallback">
        <template v-slot:slotone>
            <span>slot</span>
        </template>
    </component-b>
</div>

<script type="text/javascript">
    const componentB = {
        template: '<div></div>',
        setup(props, { attrs, slots, emit, expose }) {
            // Attribute(非响应式对象)
            console.log(attrs) // 打印 {attrone: 'one'} 相当于 this.$attrs

            // 插槽 (非响应式对象)
            console.log(slots.slotone) // 打印 { slotone: function() {} },相当于 this.$slots

            // 触发事件 (方法)
            console.log(emit) // 可调用 emit('emitcallback') 相当于 this.$emit

            // 暴露公共属性(函数)
            console.log(expose)
        },
    }
    const vm = Vue.createApp({
        components: {
            'component-b': componentB
        },
        methods: {
            emitcallback() {
                console.log('emitcallback')
            }
        }
    }).mount("#app")
</script>

其中,attrs 对象是父组件传递给子组件且不在 props 中定义的静态数据,它是 非响应式的,相当于在没有使用 setup 方法时调用的 this.$attrs 效果。

slots 对象主要是父组件传递的插槽内容,注意 v-slot:slotone 需要配置插槽名字,这样 slots 才能接收到,它是非响应式的,相当于在没有使用 setup 方法时调用的 this.$slots 效果。

emit 对象主要用来和父组件通信,相当于在没有使用 setup 方法时调用的 this.$emit 效果。

expose 函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问 expose 函数暴露出的内容:

export default {
  setup(props, { expose }) {
    // 让组件实例处于 “关闭状态”
    // 即不向父组件暴露任何东西
    expose()

    const publicCount = ref(0)
    const privateCount = ref(0)
    // 有选择地暴露局部状态
    expose({ count: publicCount })
  }
}

该上下文对象是非响应式的,可以安全地解构。

setup 方法结合模版使用

如果 setup 方法返回一个对象,那么该对象的属性以及传递给 setupprops 参数中的属性都可以在模板中访问 ,如示例代码 4-2-3 所示。

<div id="app">
    <component-b user="John" />
</div>

<script type="text/javascript">
    const componentB = {
        props: {
            user: {
                type: String,
                required: true
            }
        },
        template: '<div>{{user}} {{person.name}}</div>',
        setup(props) {
            const person = Vue.reactive({name: 'Son'})
            // 暴露给 template
            return {
                person
            }
        }
    }

    const vm = Vue.createApp({
        components: {
            'component-b': componentB
        }
    }).mount("#app")
</script>

注意,props 中的数据不必在 setup 中返回,Vue 会自动暴露给模板使用。

setup 方法的执行时机和 getCurrentInstance 方法

setup 方法在组件的 beforeCreate 之前执行,此时由于组件还没有实例化,是无法像配置式 API 一样直接使用 this.xx 访问当前实例的上下文对象的,例如 datacomputedmethods 都没法访问,因此 setup 在和其他配置式 API 一起使用时可能会导致混淆,需要格外注意。

但是,Vue 还是在组合式 API 中提供了 getCurrentInstance 方法来访问组件实例的上下文对象,如示例代码 4-2-4 所示。

Vue.createApp({
    setup() {
        Vue.onMounted(()=>{
            const internalInstance = Vue.getCurrentInstance()
            internalInstance.ctx.add()// 打印'methods add'
        })
    },
    methods:{
        add(){
            console.log('methods add')
        }
    }
}).mount("#app")

需要注意的是,不要把 getCurrentInstance 当作在配置式 API 中的 this 的替代方案来随意使用,另外 getCurrentInstance 方法只能在 setup 或生命周期钩子中调用,并且不建议在业务逻辑中使用该方法,可以在开发一些第三方库时使用。

与渲染函数一起使用

setup 也可以返回一个渲染函数,此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态:

import { h, ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return () => h('div', count.value)
  }
}

返回一个渲染函数将会阻止我们返回其他东西。对于组件内部来说,这样没有问题,但如果我们想通过模板引用将这个组件的方法暴露给父组件,那就有问题了。

我们可以通过调用 expose() 解决这个问题:

import { h, ref } from 'vue'

export default {
  setup(props, { expose }) {
    const count = ref(0)
    const increment = () => ++count.value

    // 将这个组件的方法暴露给父组件
    expose({
      increment
    })

    return () => h('div', count.value)
  }
}