Vue 3 setup函数深度解析

1. setup函数概述

setup函数是Vue 3组合式API的核心入口,它是组件内部使用组合式API的起点。setup函数在组件创建之前执行,用于初始化组件的状态、计算属性、方法等,并将它们暴露给模板使用。

1.1 setup函数的基本语法

setup函数的基本语法如下:

export default {
  setup(props, context) {
    // 组件逻辑
    
    return {
      // 暴露给模板的属性
    }
  }
}

1.2 setup函数的执行时机

setup函数的执行时机非常重要,它在组件生命周期中的位置如下:

  1. 组件实例创建之前执行
  2. beforeCreate钩子之前执行
  3. created钩子之前执行
  4. 只执行一次,不会在组件更新时重新执行

1.3 setup函数的this指向

在setup函数内部,this的指向是undefined,这是有意设计的,目的是:

  • 避免与选项式API中的this混淆
  • 鼓励使用组合式API的方式访问组件属性
  • 更好地支持TypeScript类型推断

2. setup函数的参数

setup函数接收两个参数:propscontext

2.1 props参数

props参数是一个响应式对象,包含了组件接收的所有props。它具有以下特点:

  • 响应式:当props发生变化时,setup函数内部使用props的地方会自动更新
  • 只读:不能直接修改props的值,否则会触发警告
  • 类型安全:在TypeScript环境下,props会自动进行类型检查
export default {
  props: {
    message: {
      type: String,
      default: 'Hello'
    },
    count: {
      type: Number,
      default: 0
    }
  },
  setup(props) {
    console.log(props.message) // 访问props
    console.log(props.count) // 访问props
    
    // 错误:不能直接修改props
    // props.count = 10
    
    return {
      // ...
    }
  }
}

2.2 context参数

context参数是一个普通对象,包含了组件的上下文信息,它具有以下属性:

  • attrs:包含了组件接收的所有非props属性
  • slots:包含了组件的所有插槽内容
  • emit:用于触发组件的自定义事件
  • expose:用于暴露组件的公共方法,供父组件通过ref访问

context对象不是响应式的,可以使用解构赋值:

export default {
  setup(props, { attrs, slots, emit, expose }) {
    // 使用attrs
    console.log(attrs.class)
    
    // 使用emit触发事件
    const handleClick = () => {
      emit('custom-event', 'event data')
    }
    
    // 使用expose暴露公共方法
    expose({
      publicMethod() {
        console.log('This is a public method')
      }
    })
    
    return {
      handleClick
    }
  }
}

2.2.1 attrs属性

attrs是一个普通对象,包含了组件接收的所有非props属性。它具有以下特点:

  • 非响应式:attrs不是响应式的,当父组件传递的非props属性变化时,attrs不会自动更新
  • 可以修改:可以直接修改attrs的值,不会触发警告
  • 包含所有非props属性:包括HTML属性和自定义属性

2.2.2 slots属性

slots是一个普通对象,包含了组件的所有插槽内容。它具有以下特点:

  • 非响应式:slots不是响应式的,当插槽内容变化时,slots不会自动更新
  • 包含所有插槽:包括默认插槽、具名插槽和作用域插槽
  • 需要在渲染时访问:通常在render函数或setup函数返回的渲染函数中使用

2.2.3 emit属性

emit是一个函数,用于触发组件的自定义事件。它的用法与选项式API中的this.$emit类似:

// 触发自定义事件
emit('event-name', eventData)

// 触发带修饰符的v-model事件
emit('update:modelValue', newValue)

2.2.4 expose属性

expose是一个函数,用于暴露组件的公共方法,供父组件通过ref访问。它的用法如下:

// 在子组件中
export default {
  setup(props, { expose }) {
    const privateMethod = () => {
      console.log('This is a private method')
    }
    
    const publicMethod = () => {
      console.log('This is a public method')
      privateMethod()
    }
    
    // 只暴露publicMethod
    expose({
      publicMethod
    })
    
    return {
      // ...
    }
  }
}

// 在父组件中
<template>
  <ChildComponent ref="childRef" />
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
  setup() {
    const childRef = ref(null)
    
    onMounted(() => {
      // 可以访问子组件暴露的publicMethod
      childRef.value.publicMethod()
      
      // 不能访问子组件的privateMethod
      // childRef.value.privateMethod() // 报错
    })
    
    return {
      childRef
    }
  }
}
</script>

3. setup函数的返回值

setup函数可以返回两种类型的值:

  1. 对象:返回的对象会被合并到组件的渲染上下文,供模板使用
  2. 渲染函数:直接返回一个渲染函数,用于渲染组件

3.1 返回对象

返回对象是setup函数最常用的方式,返回的对象中的属性会被暴露给模板使用:

import { ref, computed } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const double = computed(() => count.value * 2)
    
    const increment = () => {
      count.value++
    }
    
    return {
      count,
      double,
      increment
    }
  }
}

在模板中,可以直接使用这些属性:

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ double }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

3.2 返回渲染函数

setup函数也可以返回一个渲染函数,直接控制组件的渲染:

import { h, ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    const increment = () => {
      count.value++
    }
    
    return () => h('div', [
      h('p', `Count: ${count.value}`),
      h('button', { onClick: increment }, 'Increment')
    ])
  }
}

这种方式通常用于需要动态生成组件结构的场景,或者与JSX一起使用:

import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    const increment = () => {
      count.value++
    }
    
    return () => (
      <div>
        <p>Count: {count.value}</p>
        <button onClick={increment}>Increment</button>
      </div>
    )
  }
}

3.3 注意事项

  • 如果setup函数返回对象,那么对象中的属性必须是响应式的,否则模板不会自动更新
  • 如果setup函数返回渲染函数,那么它将忽略组件的template选项
  • 不要在setup函数中返回undefined,否则模板将无法访问任何属性

4. setup函数与响应式系统

setup函数是组合式API与响应式系统结合的核心,它通过以下API来创建和管理响应式数据:

4.1 使用ref创建响应式数据

ref用于创建基本类型的响应式数据:

import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0) // 创建响应式数据
    const message = ref('Hello') // 创建响应式字符串
    
    const increment = () => {
      count.value++ // 访问和修改ref值需要使用.value
    }
    
    return {
      count,
      message,
      increment
    }
  }
}

4.2 使用reactive创建响应式对象

reactive用于创建对象类型的响应式数据:

import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({
      count: 0,
      message: 'Hello',
      user: {
        name: 'Alice',
        age: 20
      }
    })
    
    const increment = () => {
      state.count++ // 直接修改reactive对象的属性
    }
    
    return {
      state,
      increment
    }
  }
}

4.3 使用computed创建计算属性

computed用于创建计算属性:

import { ref, computed } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    // 创建计算属性
    const double = computed(() => count.value * 2)
    
    // 创建可写计算属性
    const fullName = computed({
      get: () => `${firstName.value} ${lastName.value}`,
      set: (value) => {
        const [first, last] = value.split(' ')
        firstName.value = first
        lastName.value = last
      }
    })
    
    return {
      count,
      double,
      fullName
    }
  }
}

4.4 使用watch监听数据变化

watch用于监听数据变化:

import { ref, watch } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    // 监听单个ref
    watch(count, (newVal, oldVal) => {
      console.log(`Count changed from ${oldVal} to ${newVal}`)
    })
    
    // 监听多个ref
    watch([count1, count2], ([newCount1, newCount2], [oldCount1, oldCount2]) => {
      console.log('Count1 or Count2 changed')
    })
    
    return {
      count
    }
  }
}

5. setup函数与生命周期钩子

在setup函数中,可以使用组合式API的生命周期钩子,它们的名称与选项式API的生命周期钩子类似,但前面加上了on前缀。

5.1 常用生命周期钩子

选项式API钩子 组合式API钩子 执行时机
beforeCreate 无(setup函数本身替代) 组件实例创建之前
created 无(setup函数本身替代) 组件实例创建之后
beforeMount onBeforeMount 组件挂载之前
mounted onMounted 组件挂载之后
beforeUpdate onBeforeUpdate 组件更新之前
updated onUpdated 组件更新之后
beforeUnmount onBeforeUnmount 组件卸载之前
unmounted onUnmounted 组件卸载之后
errorCaptured onErrorCaptured 捕获子组件错误时
renderTracked onRenderTracked 组件渲染时跟踪依赖
renderTriggered onRenderTriggered 组件重新渲染时触发

5.2 使用示例

import { ref, onMounted, onUpdated, onUnmounted } from 'vue'

export default {
  setup() {
    const count = ref(0)
    let timer = null
    
    onMounted(() => {
      console.log('Component mounted')
      // 组件挂载后启动定时器
      timer = setInterval(() => {
        count.value++
      }, 1000)
    })
    
    onUpdated(() => {
      console.log('Component updated')
    })
    
    onUnmounted(() => {
      console.log('Component unmounted')
      // 组件卸载前清理定时器
      clearInterval(timer)
    })
    
    return {
      count
    }
  }
}

6. setup函数与选项式API的交互

虽然setup函数是组合式API的核心,但它可以与选项式API共存,用于渐进式迁移或在某些场景下结合使用。

6.1 共存规则

当setup函数与选项式API共存时,需要遵循以下规则:

  1. setup函数返回的属性会被合并到组件实例中
  2. 选项式API中的属性可以访问setup函数返回的属性
  3. setup函数中不能访问选项式API中的属性
  4. 生命周期钩子会同时执行,组合式API的钩子先执行

6.2 示例

import { ref } from 'vue'

export default {
  data() {
    return {
      // 选项式API的data
      message: 'Hello'
    }
  },
  methods: {
    // 选项式API的方法可以访问setup返回的属性
    incrementFromOptions() {
      this.count++
    }
  },
  setup() {
    // 组合式API的状态
    const count = ref(0)
    
    // 组合式API的方法
    const incrementFromSetup = () => {
      count.value++
    }
    
    return {
      count,
      incrementFromSetup
    }
  }
}

6.3 渐进式迁移

对于现有的Vue 2组件,可以通过以下步骤渐进式迁移到组合式API:

  1. 保留原有的选项式API代码
  2. 添加setup函数,逐步将逻辑迁移到setup函数中
  3. 最后,将所有逻辑迁移到setup函数中,移除选项式API

7. setup函数的最佳实践

7.1 逻辑分组

将相关的逻辑组织在一起,提高代码的可读性和可维护性:

import { ref, computed, watch, onMounted } from 'vue'

export default {
  setup() {
    // 计数器逻辑
    const count = ref(0)
    const double = computed(() => count.value * 2)
    const increment = () => count.value++
    
    watch(count, (newVal) => {
      console.log(`Count changed to: ${newVal}`)
    })
    
    // 用户信息逻辑
    const user = ref(null)
    const loading = ref(false)
    
    const fetchUser = async () => {
      loading.value = true
      // 模拟API请求
      user.value = await fetch('/api/user')
      loading.value = false
    }
    
    onMounted(() => {
      fetchUser()
    })
    
    return {
      count,
      double,
      increment,
      user,
      loading
    }
  }
}

7.2 合理使用组合式函数

将可复用的逻辑封装成组合式函数,提高代码复用性:

// useCounter.js
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const increment = () => count.value++
  const decrement = () => count.value--
  
  return { count, increment, decrement }
}

// 组件中使用
import { useCounter } from './useCounter'

export default {
  setup() {
    const { count, increment, decrement } = useCounter(10)
    
    return {
      count,
      increment,
      decrement
    }
  }
}

7.3 避免在setup函数中使用this

setup函数中的thisundefined,不要尝试使用它来访问组件属性:

// 错误做法
export default {
  setup() {
    // 这里的this是undefined
    console.log(this.message) // 报错
    
    return {
      // ...
    }
  }
}

// 正确做法
export default {
  props: {
    message: String
  },
  setup(props) {
    console.log(props.message) // 正确
    
    return {
      // ...
    }
  }
}

7.4 合理使用ref和reactive

根据数据类型选择合适的响应式API:

  • 对于基本类型(string、number、boolean等),使用ref
  • 对于对象类型,使用reactive
  • 对于需要解构的响应式对象,使用toRefs
import { ref, reactive, toRefs } from 'vue'

export default {
  setup() {
    // 基本类型使用ref
    const count = ref(0)
    
    // 对象类型使用reactive
    const state = reactive({
      name: 'Alice',
      age: 20
    })
    
    // 解构响应式对象使用toRefs
    const { name, age } = toRefs(state)
    
    return {
      count,
      name,
      age
    }
  }
}

8. setup函数的性能优化

8.1 避免在setup函数中创建不必要的响应式数据

只对需要在模板中使用或需要监听变化的数据创建响应式:

// 好的做法
const count = ref(0) // 需要在模板中使用,创建响应式
const config = { /* ... */ } // 不需要响应式,直接创建普通对象

// 不好的做法
const config = reactive({ /* ... */ }) // 不需要响应式,却创建了响应式对象

8.2 使用shallowRef和shallowReactive优化性能

对于深层嵌套的对象,如果只需要监听顶层属性的变化,可以使用shallowRefshallowReactive

import { shallowRef, shallowReactive } from 'vue'

export default {
  setup() {
    // 只监听list本身的变化,不监听list内部元素的变化
    const list = shallowRef([{ id: 1, name: 'Item 1' }])
    
    // 只监听user顶层属性的变化,不监听嵌套属性的变化
    const user = shallowReactive({
      name: 'Alice',
      address: { city: 'Beijing', country: 'China' }
    })
    
    return {
      list,
      user
    }
  }
}

8.3 使用markRaw避免不必要的响应式转换

对于不需要响应式的对象,可以使用markRaw标记,避免Vue对其进行响应式转换:

import { markRaw } from 'vue'

export default {
  setup() {
    // 标记为非响应式,提高性能
    const nonReactiveObj = markRaw({
      /* ... */
    })
    
    return {
      nonReactiveObj
    }
  }
}

9. setup函数与TypeScript

setup函数在TypeScript环境下有很好的支持,可以提供完善的类型推断和类型检查。

9.1 基本类型支持

import { ref, computed, defineComponent } from 'vue'

export default defineComponent({
  setup() {
    // 自动推断为Ref<number>
    const count = ref(0)
    
    // 手动指定类型
    const message = ref<string>('Hello')
    
    // 自动推断为ComputedRef<number>
    const double = computed(() => count.value * 2)
    
    return {
      count,
      message,
      double
    }
  }
})

9.2 Props类型支持

import { defineComponent } from 'vue'

interface Props {
  message?: string
  count: number
}

export default defineComponent({
  props: {
    message: {
      type: String,
      default: 'Hello'
    },
    count: {
      type: Number,
      required: true
    }
  } as const,
  
  setup(props: Props) {
    // 类型安全的props访问
    console.log(props.message)
    console.log(props.count)
    
    return {
      // ...
    }
  }
})

9.3 Context类型支持

import { defineComponent } from 'vue'

export default defineComponent({
  emits: ['custom-event'],
  
  setup(props, context) {
    // context具有正确的类型
    context.emit('custom-event', 'data')
    
    return {
      // ...
    }
  }
})

10. 总结

setup函数是Vue 3组合式API的核心入口,它具有以下特点:

  • 在组件实例创建之前执行
  • 接收props和context两个参数
  • 返回对象或渲染函数
  • this指向undefined
  • 与响应式系统紧密结合
  • 支持组合式API的生命周期钩子
  • 可以与选项式API共存
  • 提供完善的TypeScript支持

掌握setup函数的使用方法和最佳实践,是学习Vue 3组合式API的关键。通过合理使用setup函数,可以编写出更具可读性、可维护性和可复用性的Vue组件。

11. 实战练习

练习1:使用setup函数创建计数器组件

创建一个使用setup函数的计数器组件,包含以下功能:

  • 显示当前计数
  • 提供增加、减少、重置按钮
  • 显示计数的双倍值
  • 当计数变化时,在控制台打印日志

练习2:使用setup函数和组合式函数

创建一个组合式函数useMousePosition,用于跟踪鼠标位置,然后在组件中使用它:

  • 跟踪鼠标的x和y坐标
  • 在组件挂载时开始跟踪
  • 在组件卸载时停止跟踪
  • 在模板中显示鼠标位置

12. 扩展阅读

通过本集的学习,我们深入理解了Vue 3组合式API的核心入口setup函数。在下一集中,我们将学习生命周期钩子在setup中的使用,进一步掌握组合式API的生命周期管理。

« 上一篇 组合式API设计哲学 下一篇 » 生命周期钩子在setup中的使用