Skip to content

Vue3 setup 函数详解

一、概述

setup 是 Vue3 引入的新选项/函数,它是组合式 API 的入口点。setup 在组件创建之前执行,在 props 解析之后、组件实例化之前调用。


二、选项式 API 中的 setup

2.1 基本用法

在选项式 API 中,setup 作为组件的一个选项存在:

javascript
export default {
  props: {
    title: String
  },

  setup(props, context) {
    // 组合式 API 逻辑

    return {
      // 返回给模板使用的数据和方法
    }
  },

  data() {
    return {
      message: 'hello'
    }
  },

  methods: {
    handleClick() {
      console.log('clicked')
    }
  },

  mounted() {
    console.log('mounted')
  }
}

2.2 选项式 API 中 setup 的 this 指向

this 指向:undefined

javascript
export default {
  setup(props, context) {
    console.log(this) // undefined
    return {}
  }
}

为什么是 undefined?

  1. 执行时机早于组件实例化:setup 函数在组件实例(this)创建之前执行,此时组件实例尚未生成。

  2. 设计决策:Vue 团队故意将 this 设为 undefined,原因如下:

    • 避免开发者混淆选项式 API 和组合式 API 的使用方式
    • 防止在 setup 中错误地访问其他选项(如 data、methods)
    • 强制开发者使用组合式 API 的方式(通过返回值暴露数据)
  3. 源码层面:在 Vue 源码中,setup 被调用时没有绑定任何 this:

    javascript
    // 简化的源码逻辑
    const setupResult = setup(props, setupContext)
    // setup 被普通调用,而非 call/apply 绑定 this

2.3 选项式 API 中 setup 的执行时机

父组件 beforeCreate

父组件 setup (如果有)

父组件 created

父组件 beforeMount

子组件 beforeCreate

子组件 setup (如果有)

子组件 created

子组件 beforeMount

子组件 mounted

父组件 mounted

setup 执行顺序:在 beforeCreate 之前,created 之前执行。


三、组合式 API 中的 setup

3.1 基本用法(<script setup> 语法糖)

vue
<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'

const props = defineProps({
  title: String
})

const emit = defineEmits(['update', 'close'])

const count = ref(0)
const state = reactive({
  name: 'Vue3',
  version: '3.x'
})

const doubledCount = computed(() => count.value * 2)

function increment() {
  count.value++
  emit('update', count.value)
}

watch(count, newVal => {
  console.log('count changed:', newVal)
})

onMounted(() => {
  console.log('component mounted')
})
</script>

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>Count: {{ count }}</p>
    <p>Doubled: {{ doubledCount }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

3.2 组合式 API 中 setup 的 this 指向

<script setup> 中没有 this

vue
<script setup>
console.log(this) // undefined 或报错(严格模式)
</script>

为什么没有 this?

  1. 编译时转换<script setup> 会在编译时被转换为标准的 setup 函数,所有顶层变量自动成为暴露给模板的属性。

  2. 设计理念:组合式 API 的核心思想是:

    • 使用函数而非 this 来访问响应式系统
    • 通过 import 导入 API(ref、reactive、computed 等)
    • 不依赖组件实例上下文
  3. 编译后的代码

    javascript
    // 编译前
    ;<script setup>const count = ref(0)</script>
    
    // 编译后(简化)
    export default {
      setup() {
        const count = ref(0)
        return { count }
      }
    }

3.3 标准 setup 函数语法

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

export default {
  props: {
    title: String
  },

  setup(props, context) {
    const count = ref(0)

    // context 包含:attrs, slots, emit, expose
    const { attrs, slots, emit, expose } = context

    function handleClick() {
      emit('click', count.value)
    }

    return {
      count,
      handleClick
    }
  }
}
</script>

四、setup 函数做了什么事情

4.1 核心职责

职责说明
初始化响应式状态创建 ref、reactive 等响应式数据
定义计算属性使用 computed 创建派生状态
设置侦听器使用 watch/watchEffect 监听数据变化
注册生命周期钩子使用 onMounted、onUnmounted 等
组织代码逻辑将相关逻辑组合在一起,提高可维护性
返回模板需要的内容返回数据和方法供模板使用

4.2 执行流程详解

javascript
setup(props, context) {
  // 1. 接收 props(响应式的)
  console.log(props.title)

  // 2. 解构 context
  const { attrs, slots, emit, expose } = context

  // 3. 创建响应式数据
  const state = reactive({ count: 0 })
  const message = ref('hello')

  // 4. 创建计算属性
  const doubled = computed(() => state.count * 2)

  // 5. 设置侦听器
  watch(() => state.count, (newVal) => {
    console.log('count changed')
  })

  // 6. 注册生命周期
  onMounted(() => {
    console.log('mounted')
  })

  onUnmounted(() => {
    console.log('unmounted')
  })

  // 7. 定义方法
  function increment() {
    state.count++
  }

  // 8. 使用 expose 暴露给父组件
  expose({
    increment,
    state
  })

  // 9. 返回给模板
  return {
    state,
    message,
    doubled,
    increment
  }
}

4.3 setup 参数详解

props

javascript
setup(props) {
  // props 是响应式的,不能解构(会失去响应性)
  console.log(props.title)

  // 如需解构,使用 toRefs
  import { toRefs } from 'vue'
  const { title } = toRefs(props)

  // 或 toRef 单个属性
  import { toRef } from 'vue'
  const title = toRef(props, 'title')
}

context

javascript
setup(props, context) {
  // attrs: 非响应式对象,包含父组件传递的非 prop 属性
  console.log(context.attrs.class)
  console.log(context.attrs.id)

  // slots: 非响应式对象,包含插槽
  console.log(context.slots.default)

  // emit: 触发事件的函数
  context.emit('update', newValue)

  // expose: 暴露组件公共属性
  context.expose({
    publicMethod,
    publicProperty
  })
}

五、选项式 API 与组合式 API 中 setup 的区别

为什么 setup 里 this 是 undefined? 因为: setup 执行时机非常早(在组件创建之前) 此时组件实例还没生成,所以 this 没有指向 Vue 故意把 this 设为 undefined,避免你误用;
非要拿实例使用 getCurrentInstance()

特性选项式 API 中的 setup组合式 API (<script setup>)
语法作为选项之一语法糖,编译后生成 setup 函数
thisundefinedundefined (无 this 概念 )
返回值必须显式 return自动暴露顶层变量
props 定义在 props 选项中使用 defineProps()
emit 定义在 emits 选项中使用 defineEmits()
与选项共存可与 data、methods 等共存独立使用
代码组织分散在各选项中逻辑可集中组织

六、使用注意事项

6.1 this 相关注意事项

javascript
export default {
  setup(props, context) {
    // ❌ 错误:无法访问 this
    console.log(this.data) // undefined
    console.log(this.message) // undefined

    // ❌ 错误:无法调用 methods
    this.handleClick() // TypeError

    // ❌ 错误:无法访问组件实例属性
    this.$router // undefined
    this.$route // undefined

    // ✅ 正确:使用组合式 API 方式
    import { useRouter, useRoute } from 'vue-router'
    const router = useRouter()
    const route = useRoute()
  },

  data() {
    return {
      message: 'hello'
    }
  },

  methods: {
    handleClick() {
      // ✅ 这里可以访问 setup 返回的数据
      console.log(this.count) // 如果 setup 返回了 count
    }
  }
}

6.2 props 使用注意事项

javascript
setup(props) {
  // ❌ 错误:直接解构会失去响应性
  const { title } = props

  // ✅ 正确:使用 toRefs
  import { toRefs, toRef } from 'vue'
  const { title } = toRefs(props)

  // ✅ 正确:使用 toRef
  const title = toRef(props, 'title')

  // ❌ 错误:修改 props
  props.title = 'new title' // 警告,不应修改 props

  // ✅ 正确:创建本地副本
  const localTitle = ref(props.title)
}

6.3 生命周期钩子注意事项

javascript
setup() {
  // ❌ 错误:在 setup 之外调用钩子
  onMounted(() => {}) // 必须在 setup 内同步调用

  // ❌ 错误:在异步回调中调用
  setTimeout(() => {
    onMounted(() => {}) // 错误!
  }, 100)

  // ✅ 正确:在 setup 内同步注册
  onMounted(() => {
    console.log('mounted')
  })

  // ✅ 正确:异步操作放在钩子内
  onMounted(async () => {
    const data = await fetchData()
  })
}

6.4 返回值注意事项

javascript
setup() {
  const count = ref(0)
  const state = reactive({ name: 'Vue' })

  // ❌ 错误:返回 reactive 对象会失去响应性
  return { ...state }

  // ✅ 正确:返回 ref
  return { count }

  // ✅ 正确:返回 reactive 对象本身
  return { state }

  // ✅ 正确:使用 toRefs
  return { ...toRefs(state) }

  // ❌ 错误:返回函数会变成方法而非渲染函数
  return () => 'render' // 这会作为方法,不是渲染函数

  // ✅ 正确:返回渲染函数
  import { h } from 'vue'
  return () => h('div', count.value)
}

6.5 与选项式 API 混用注意事项

javascript
export default {
  props: ['title'],

  setup(props) {
    const count = ref(0)
    return { count }
  },

  data() {
    return {
      message: 'hello'
    }
  },

  computed: {
    // ✅ 可以访问 setup 返回的数据
    doubledCount() {
      return this.count * 2
    }
  },

  methods: {
    increment() {
      // ✅ 可以访问 setup 返回的数据
      this.count++
    }
  },

  mounted() {
    // ✅ 可以访问 setup 返回的数据
    console.log(this.count)
  }
}

注意:setup 返回的数据在选项式 API 中可以访问,但选项式 API 的数据在 setup 中无法访问。

6.6 响应式丢失问题

javascript
import { ref, reactive, toRefs, toValue } from 'vue'

setup() {
  const count = ref(0)
  const state = reactive({ name: 'Vue', version: 3 })

  // ❌ 响应式丢失场景
  const { name } = state // 失去响应性
  const countValue = count.value // 失去响应性

  // ✅ 保持响应性
  const { name } = toRefs(state)
  const countRef = count // 保持 ref

  // 传递给子组件时
  // ❌ 可能丢失响应性
  provide('state', state.name)

  // ✅ 正确传递
  provide('state', toRef(state, 'name'))
  provide('state', state) // 传递整个对象
}

6.7 异步 setup(Suspense)

javascript
// 异步 setup 需要配合 Suspense 使用
async setup() {
  const data = await fetchUserData()
  return { data }
}

// 父组件
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>
</template>

6.8 expose 使用注意事项

javascript
setup(props, { expose }) {
  const count = ref(0)
  const privateData = ref('secret')

  function increment() {
    count.value++
  }

  function privateMethod() {
    console.log('private')
  }

  // 只暴露指定的内容给父组件
  expose({
    count,
    increment
    // privateData 和 privateMethod 不会被暴露
  })

  return {
    count,
    increment,
    privateData,
    privateMethod
  }
}

// 父组件通过 ref 访问
const childRef = ref()
childRef.value.count // ✅ 可以访问
childRef.value.increment() // ✅ 可以调用
childRef.value.privateData // ❌ undefined

七、最佳实践

7.1 代码组织

javascript
// ✅ 推荐:按功能组织代码
setup() {
  // 用户相关逻辑
  const { user, login, logout } = useUser()

  // 购物车相关逻辑
  const { cart, addToCart, removeFromCart } = useCart()

  // UI 相关逻辑
  const { isOpen, open, close } = useModal()

  return {
    user, login, logout,
    cart, addToCart, removeFromCart,
    isOpen, open, close
  }
}

// ✅ 推荐:提取为组合式函数
function useUser() {
  const user = ref(null)

  async function login(credentials) {
    user.value = await api.login(credentials)
  }

  function logout() {
    user.value = null
  }

  return { user, login, logout }
}

7.2 命名规范

javascript
setup() {
  // ✅ ref 使用语义化命名
  const isLoading = ref(false)
  const hasError = ref(false)
  const userList = ref([])

  // ✅ reactive 用于对象状态
  const form = reactive({
    username: '',
    password: ''
  })

  // ✅ computed 使用描述性命名
  const filteredUsers = computed(() =>
    userList.value.filter(u => u.active)
  )

  // ✅ 方法使用动词开头
  function handleSubmit() {}
  function handleCancel() {}

  return {
    isLoading,
    hasError,
    userList,
    form,
    filteredUsers,
    handleSubmit,
    handleCancel
  }
}

7.3 TypeScript 支持

typescript
import { ref, reactive, computed, type Ref, type ComputedRef } from 'vue'

interface User {
  id: number
  name: string
  email: string
}

setup() {
  // 类型推断
  const count = ref(0) // Ref<number>
  const user = ref<User | null>(null) // Ref<User | null>

  // reactive 类型
  const state = reactive<{
    users: User[]
    loading: boolean
  }>({
    users: [],
    loading: false
  })

  // computed 类型
  const userCount: ComputedRef<number> = computed(() => state.users.length)

  return {
    count,
    user,
    state,
    userCount
  }
}

八、总结

setup 函数核心要点

要点说明
执行时机在 beforeCreate 之前,组件实例化之前
this 指向undefined(设计如此,避免混淆)
参数props(响应式)和 context(包含 attrs、slots、emit、expose)
返回值对象(暴露给模板)或渲染函数
核心作用组合式 API 的入口,组织组件逻辑

选项式 vs 组合式 setup 选择

  • 选项式 API + setup:渐进式迁移,与现有代码兼容
  • 组合式 API(<script setup>:新项目推荐,代码更简洁,TypeScript 支持更好

记住这些规则

  1. setup 中没有 this,不要尝试访问
  2. props 不要直接解构,使用 toRefs
  3. 生命周期钩子必须在 setup 中同步调用
  4. 返回渲染函数时,不要返回其他属性
  5. 使用组合式函数组织复杂逻辑

基于 VitePress 的本地知识库