一文讲清楚vue3中setup钩子的使用

基本结构

首先,我们需要理解 setup 函数的整体结构。这个函数接收两个参数:propscontext

setup(props, context) {  
  // ...  
}
  • props: 是一个包含了传入组件所有 prop 的对象。
  • context: 是一个包含了组件的许多有用的属性和方法的对象,如 attrsemit 等。

返回值

setup 函数中,我们可以使用 Vue 3 提供的新的响应式 API,如 refreactive来创建响应式的数据,然后返回一个对象。

import { ref, computed } from 'vue';  
  
export default {  
  setup() {  
    const count = ref(0); // 使用 ref 创建响应式数据  
    const doubled = computed(() => count.value * 2); // 使用 computed 创建计算属性  
    return {  
      count,  
      doubled  
    };  
  }  
}

在模板中访问从 setup 返回的ref时,它会自动浅层解包,因此你无须再在模板中为它写 .value

setup 函数中返回的对象中的属性和方法会自动成为组件实例上的属性和方法。

import { ref, computed } from 'vue';  
  
export default {  
  setup() {  
    const count = ref(0);  
    const doubled = computed(() => count.value * 2);  
    return {  
      increment: () => count.value++ // 这个方法会自动成为组件实例上的 increment 方法  
    };  
  }  
}

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

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)
  }
}

此时父组件可以通过模板引用来访问这个 increment 方法。

第一个参数props

先定义一个子组件如下

let template = `
    <div>{{mes}}</div>
`
export default {
    props: {
        mes: String
    },
    setup: function (props) {
        let { mes } = props

        return { mes }
    },
    template
}

再定义一个父组件如下

import { ref } from 'https://cdn.bootcdn.net/ajax/libs/vue/3.3.4/vue.esm-browser.js'
import PropsC from "./components/PropsC.js"

let template = `
    <PropsC v-on:click='changeMes' :mes='mes'/>
`

export default {
    components: {
        PropsC,
    },
    setup: function () {
        let mes = ref('收到请回复')
        let changeMes = () => mes.value += '!'

        return { mes, changeMes }
    },
    template
}

父组件引入子组件并注册使用,再定义一个响应式数据mes,并把它传递给子组件,另外还提供了一个方法用于改变mes,用于模拟传递给子组件的数据的变化。

当我们点击组件上的文字的时候,未能看到视图的刷新变化。setup 函数的 props 是响应式的,并且会在传入新的 props 时同步更新,但我们在子组件对props进行了解构let { mes } = props,导致的结果是解构出的变量将会丢失响应性,因而我们就看不到页面有变化。

所以我们推荐通过 props.xxx 的形式来使用其中的 props。如果你确实需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs() 和 toRef() 这两个工具函数:

import { toRefs} from 'https://cdn.bootcdn.net/ajax/libs/vue/3.3.4/vue.esm-browser.js'

let template = `
    <div>{{mes}}</div>
`

export default {
    props: {
        mes: String
    },
    setup: function (props) {
        let { mes } = toRefs(props)

        return { mes }
    },
    template
}

如果使用toRef():

import { toRef} from 'https://cdn.bootcdn.net/ajax/libs/vue/3.3.4/vue.esm-browser.js'

let mes = toRef(props, 'mes')

通过上面提到的三种方式改写,我们就能看到页面的变化了。

第二个参数context

Vue 3 中的 setup 钩子的第二个参数是 context,它是一个上下文对象,包含了一些有用的属性和方法。

context 对象中的属性有:

  1. attrs: 是一个包含了当前组件上所有属性的对象,这些属性未在 props 中声明。
setup(props, { attrs }) {    
  // ...  
}
  1. slots: 是一个包含了当前组件的所有插槽的对象。
setup(props, { slots }) {  
  // ...  
}

vue官网中描述到:”attrsslots 都是有状态的对象,它们总是会随着组件自身的更新而更新。这意味着你应当避免解构它们,并始终通过 attrs.xslots.x 的形式使用其中的属性。此外还需注意,和 props 不同,attrsslots 的属性都不是响应式的。如果你想要基于 attrsslots 的改变来执行副作用,那么你应该在 onBeforeUpdate 生命周期钩子中编写相关逻辑。“对这句话的解读是:

  1. “attrs 和 slots总是会随着组件自身的更新而更新”:这意味着,当组件的状态或属性发生变化并导致组件重新渲染时,attrsslots 也会相应地更新。这是因为它们与组件的状态紧密相关。
  2. “attrs 和 slots 的属性都不是响应式的”:这意味着,与 props 不同,attrsslots 的属性值不会自动响应其值的变化。如果你更改了 attrsslots 的某个属性,Vue.js 不会自动检测到这些变化并更新 DOM。

这两句话并不矛盾。第一句描述了 attrsslots 如何随着组件的更新而更新,而第二句描述了它们的属性不是响应式的。这两点信息是互补的,而不是相互矛盾的。

我们引用上面props的代码实例,并改写进而讲解attrs。其中父组件没有丝毫改变

  • 当子组件改写成这样(注意,下面的mes我并没有声明为是一个props):
let template = `
    <div>{{attrs.mes}}</div>
`

export default {
    setup: function (props, { attrs }) {       

        return { attrs }
    },
    template
}

当点击组件组件上的文字时,我们是能看到页面有所变化的,这也照应了“始终通过 attrs.x 的形式使用其中的属性”。

  • 接下来我们看看解构attrs会是什么情况,当子组件改写成这样:
let template = `
    <div>{{mes}}</div>
`

export default {
    setup: function (props, { attrs }) {
        let { mes } = attrs

        return { mes }
    },
    template
}

当点击组件组件上的文字时,页面是没有任何变化的,这就是为什么“你应当避免解构它们”。

  • “想要基于 attrsslots 的改变来执行副作用,那么你应该在 onBeforeUpdate 生命周期钩子中编写相关逻辑”
import { onBeforeUpdate } from 'https://cdn.bootcdn.net/ajax/libs/vue/3.3.4/vue.esm-browser.js'

let template = `
    <div>{{attrs.mes}}</div>
`

export default {
    setup: function (props, { attrs }) {

        onBeforeUpdate(()=>{
            console.log(attrs.mes)
        })

        return { attrs }
    },
    template
}

当点击组件组件上的文字时,控制台会打印父组件传来的新的mes值。

其实如果要执行副作用,还可以用watch侦听attrs的变化,进而在回调函数执行副作用,但watch需要一个响应式的对象作为参数,所以你要先把attrs变为响应式的。但这种方式相对复杂。

  • 继续,“和 props 不同,attrsslots 的属性都不是响应式的”,这好办,既然不是响应式的,那我就把你变为响应式的
import { reactive } from 'https://cdn.bootcdn.net/ajax/libs/vue/3.3.4/vue.esm-browser.js'

let template = `
    <div>{{attrs_copy.mes}}</div>
`

export default {
    setup: function (props, { attrs }) {
        let attrs_copy = reactive(attrs)

        return { attrs_copy }
    },
    template
}

这样改写也能令页面有变化。

  • 既然能在onBeforeUpdate钩子里写相关逻辑,那么我们还可以使用ref
import { ref, reactive, toRef, toRefs, watch, onBeforeUpdate } from 'https://cdn.bootcdn.net/ajax/libs/vue/3.3.4/vue.esm-browser.js'

let template = `
    <div>{{mes}}</div>
`

export default {
    setup: function (props, { attrs }) {

        let mes = ref(attrs.mes)

        onBeforeUpdate(() => {
            mes.value = attrs.mes
        })

        return { mes }
    },
    template
}

这里的意思是创建一个响应式的mes,然后在每次组件更新时将新的attrs中的mes内容赋值给响应式的mes,然后模板使用的是这个响应式的mes。

  1. emit: 是一个用于派发事件的方法,可以在子组件中触发自定义事件并传递数据给父组件。
setup(props, { emit }) {  
  const handleClick = () => {  
    emit('custom-event', 'Hello from child component!'); // 触发一个名为 'custom-event' 的自定义事件,并传递一个字符串参数给父组件  
  };  
  // ...  
}
  1. expose: 是一个用于暴露公共属性的函数,可以将一些数据或方法暴露给父组件或其他组件使用。
setup(props, { expose }) {  
  expose({ myProp: 'Hello from child component!' }); // 将一个名为 'myProp' 的公共属性暴露给父组件或其他组件使用  
  // ...  
}

结束语

由于 setup 函数返回的对象会被合并到组件的实例上,因此你可以使用组合逻辑来组织和复用代码。例如,你可以在一个地方定义一个函数,然后在模板或其他逻辑中多次使用它。同时,你也可以在 setup 函数中使用如 onMountedonUpdated 等生命周期钩子来执行某些操作。这使得在 Vue 3 中组织和处理组件的生命周期逻辑变得更加简单和直观。