一、Vue3里的setup到底是干啥的?
想学Vue3开发,却被setup语法搞得晕头转向?setup作为Vue3组合式API的核心入口,新手入门时得先把“它是干啥的”“怎么写基础逻辑”“和响应式、生命周期咋配合”这些关键点理清楚,这篇文章用问答形式,把Vue3 setup从入门到实战的核心问题拆开来讲,帮你把知识点串成能用的技能!
简单说,setup是Vue3给组件提供的逻辑组织入口,在Vue2里,我们写组件用data、methods、computed这些选项分开管理逻辑;但Vue3的组合式API(Composition API)把这些逻辑收拢到setup里,让代码能按功能拆分、复用,不再被选项“分割”。
举个例子:做一个带搜索和筛选的表格组件,Vue2里搜索逻辑可能在methods,筛选的响应式数据在data,分页的计算属性在computed,代码分散在不同选项里;但Vue3的setup里,你可以把“搜索相关逻辑”“筛选相关逻辑”“分页逻辑”各自打包成函数,塞到setup里,结构更集中,后期改需求也容易定位代码。
setup还是组件生命周期的“前站”——它在组件实例创建前(beforeCreate钩子之前)就执行,所以里面不能用this(因为组件实例还没生成),它的返回值会暴露给模板(template)和其他选项(比如mounted钩子),所以你在setup里定义的变量、函数,想在模板里用,得return出去(不过用<script setup>语法糖时不用手动return,后面会讲)。
setup的基本语法和执行时机要注意啥?
先看语法结构,setup是组件选项里的一个函数,现在更推荐用<script setup>语法糖写法(更简洁),也可以了解传统写法:
<!-- 语法糖写法(推荐) -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() { count.value++ }
// 不需要return,语法糖自动把顶层变量/函数暴露给模板
</script>
<!-- 传统写法(了解即可) -->
<script>
export default {
setup(props, context) {
// 逻辑写这里
return { /* 暴露给模板的内容 */ }
}
}
</script>
重点关注这几个点:
-
参数:传统写法的
setup接收props(父组件传的属性,是响应式的,不能直接解构!后面讲坑的时候会说)和context(包含emit、slots、attrs这些工具),而<script setup>里,props要用defineProps声明,emit用defineEmits声明,更简洁。 -
执行时机:
setup在组件创建前就运行,比beforeCreate还早,所以里面拿不到this(this是undefined),也不能访问data、methods这些选项里的内容(因为它们还没初始化)。 -
返回值:传统
setup里return的对象,模板才能用;但<script setup>会自动把顶层声明的变量、函数暴露给模板,不用手动return,写起来更爽。
新手刚开始可以先从<script setup>入手,这是Vue3官方推荐的写法,能省很多事~
用setup时,响应式数据咋处理?
Vue2里靠data返回对象实现响应式,但Vue3的setup里,得用ref和reactive这两个“响应式工具”:
ref:让基本类型(字符串、数字、布尔等)变成响应式,比如const count = ref(0),修改时要通过count.value,模板里用的时候不用写.value(Vue会自动解包)。reactive:让对象/数组变成响应式,比如const user = reactive({ name: '小明', age: 18 }),修改时直接user.age++就行。
举个实际场景:做一个表单,输入框绑定的用户名是字符串(用ref),表单数据整体是对象(用reactive):
<script setup>
import { ref, reactive } from 'vue'
// 基本类型响应式
const username = ref('')
// 对象响应式
const formData = reactive({
password: '',
agree: false
})
function handleSubmit() {
console.log(username.value, formData)
}
</script>
<template>
<input v-model="username" placeholder="用户名" />
<input v-model="formData.password" placeholder="密码" />
<button @click="handleSubmit">提交</button>
</template>
还有几个常用工具帮你更灵活处理响应式:
toRefs:把reactive对象转成带ref的对象,解构后还能保持响应式,比如const { name, age } = toRefs(user),这样name和age都是ref,修改时name.value = '小红'能触发更新。computed:计算属性,比如const fullName = computed(() => firstname.value + lastname.value),依赖变化时自动更新。watch:监听响应式数据变化,比如watch(count, (newVal, oldVal) => { /* 逻辑 */ }),能替代Vue2的watch选项。
普通变量(没包ref/reactive)在setup里修改不会触发界面更新,所以一定要用这两个工具包一层!
setup里怎么处理组件通信?
组件通信是Vue开发的核心场景,setup里得用新的API来搞:
父传子:props接收
在<script setup>里,用defineProps声明接收的属性,它是响应式的,比如父组件传title:
子组件:
<script setup>
// 简单声明
const props = defineProps(['title'])
// 或指定类型、默认值
const props = defineProps({ {
type: String,
required: true,
default: '默认标题'
}
})
</script>
<template>{{ props.title }}</template>
子传父:emit触发事件
用defineEmits声明可触发的事件,然后通过emit函数触发,比如子组件触发update事件:
子组件:
<script setup>
const emit = defineEmits(['update'])
function handleClick() {
emit('update', /* 传参 */)
}
</script>
<template><button @click="handleClick">点我通知父组件</button></template>
父组件用@update接收:
<Child @update="handleUpdate" />
祖孙/跨层级通信:provide + inject
祖先组件用provide提供数据,后代组件用inject接收,比如祖先组件提供主题色:
祖先组件:
<script setup>
import { provide } from 'vue'
provide('themeColor', 'blue')
</script>
后代组件:
<script setup>
import { inject } from 'vue'
const themeColor = inject('themeColor', 'defaultColor') // 第二个参数是默认值
</script>
处理非props属性、插槽:useAttrs和useSlots
用useAttrs和useSlots获取父组件传的非props属性、插槽内容,比如父组件给子组件传了class、style,或者自定义属性:
子组件:
<script setup>
import { useAttrs, useSlots } from 'vue'
const attrs = useAttrs() // 拿到class、style、自定义属性等
const slots = useSlots() // 拿到父组件传的插槽,lt;slot name="header" />
</script>
这些API把组件通信的场景全覆盖了,按需求选对应的方式就行~
setup和生命周期钩子咋配合?
Vue2里的created、mounted这些钩子,在Vue3的setup里要导入对应的函数来用。
<script setup>
import { onMounted, onUpdated, onUnmounted } from 'vue'
onMounted(() => {
console.log('组件挂载完成,能操作DOM了')
})
onUpdated(() => {
console.log('组件更新后,DOM变化了')
})
onUnmounted(() => {
console.log('组件卸载,清理定时器、事件监听等')
})
</script>
注意几个关键点:
- 钩子函数要从
vue里导入,名字是onXxx(首字母大写,比如onMounted对应Vue2的mounted)。 - 执行顺序:
setup的执行时机更早,但onMounted的回调是在组件挂载完成后触发,和Vue2的mounted逻辑时机一致。 - 可以在
setup里写多个相同钩子(比如多个onMounted),它们会按顺序执行,这在拆分逻辑到组合式函数时很有用(每个组合式函数里都可以写自己的onMounted)。
举个实际例子:做一个组件,挂载后请求接口拿数据,用onMounted再合适不过:
<script setup>
import { onMounted, ref } from 'vue'
const list = ref([])
onMounted(async () => {
const res = await fetch('/api/list')
list.value = res.data
})
</script>
<template><ul><li v-for="item in list">{{ item }}</li></ul></template>
这样数据请求和界面渲染的逻辑就很集中,比Vue2分散在created和data里更清晰~
setup里写逻辑,代码组织有啥技巧?
Vue2的选项式API容易让逻辑“碎片化”,而setup的组合式API能让逻辑“聚合”,还能通过组合式函数(Composables)复用代码。
按功能拆分逻辑到函数
比如做一个带搜索和分页的表格,把“搜索逻辑”和“分页逻辑”拆成两个函数:
<script setup>
import { ref, computed } from 'vue'
// 搜索逻辑
function useSearch() {
const searchKey = ref('')
const filteredList = computed(() => {
// 假设原始列表是父组件传的props.list
return props.list.filter(item => item.includes(searchKey.value))
})
return { searchKey, filteredList }
}
// 分页逻辑
function usePagination() {
const page = ref(1)
const pageSize = ref(10)
const currentPageData = computed(() => {
// 结合filteredList做分页
})
function changePage(newPage) { page.value = newPage }
return { page, pageSize, currentPageData, changePage }
}
// 引入逻辑
const { searchKey, filteredList } = useSearch()
const { page, pageSize, currentPageData, changePage } = usePagination()
</script>
这样组件里的逻辑按功能拆分,每个函数负责一块,后期维护时找代码更方便。
抽离成可复用的组合式函数(Composables)
把通用逻辑(鼠标位置监听”“本地存储数据”)写成单独的js文件,到处复用,比如写一个useMouse.js:
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function handleMove(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', handleMove))
onUnmounted(() => window.removeEventListener('mousemove', handleMove))
return { x, y }
}
然后在任何组件里用:
<script setup>
import { useMouse } from './useMouse.js'
const { x, y } = useMouse()
</script>
<template>鼠标位置:{{ x }}, {{ y }}</template>
这种复用方式比Vue2的mixin更清晰(mixin容易出现命名冲突、逻辑来源不明确),组合式函数的返回值和依赖关系一目了然,再比如做一个“本地存储同步”的组合式函数:
// useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, initialValue) {
// 从localStorage读取数据,没有则用初始值
const data = ref(JSON.parse(localStorage.getItem(key)) || initialValue)
// 数据变化时,同步到localStorage
watch(data, (newVal) => {
localStorage.setItem(key, JSON.stringify(newVal))
}, { deep: true }) // 对象/数组变化时深度监听
return data
}
组件里用它保存用户配置:
<script setup>
import { useLocalStorage } from './useLocalStorage.js'
const userConfig = useLocalStorage('user-config', { theme: 'light', lang: 'zh' })
// 修改userConfig会自动同步到localStorage
function switchTheme() {
userConfig.value.theme = userConfig.value.theme === 'light' ? 'dark' : 'light'
}
</script>
保持组件setup的简洁
组件里的setup只做“组装逻辑”:引入组合式函数、处理props/emit、给模板提供数据,复杂逻辑全丢到组合式函数里,让组件文件更轻量。
setup模式下,CSS作用域和动态样式咋搞?
Vue3的setup对CSS的支持更灵活了,尤其是<style scoped>和CSS变量绑定(v-bind)这两个点:
作用域CSS(<style scoped>)
和Vue2一样,加scoped属性后,样式只作用于当前组件的DOM,但如果要修改子组件的样式,可以用::v-deep(或>>>、:deep()):
<style scoped>
/* 修改子组件的.class */
.parent ::v-deep .child-class {
color: red;
}
</style>
动态CSS(v-bind在<style>里用)
Vue3.2+支持在<style>里用v-bind绑定setup里的响应式数据,比如根据主题色动态改按钮颜色:
<script setup>
const theme = ref('dark')
const buttonColor = computed(() => theme.value === 'dark' ? '#333' : '#fff')
</script>
<template><button>按钮</button></template>
<style scoped>
button {
background-color: v-bind(buttonColor);
color: v-bind('theme === "dark" ? "#fff" : "#333"'); // 也能写表达式
}
</style>
这里v-bind括号里可以是setup里的变量名,也能写简单的JS表达式,样式会随着响应式数据变化而更新,特别适合做主题切换、动态样式的场景~
新手容易踩的setup坑有哪些?
刚用setup时,这些“雷区”很容易踩,提前避坑能省很多debug时间:
props解构后响应式丢失
比如父组件传list,子组件用const { list } = defineProps(['list']),然后修改list?不行!因为props是响应式对象,直接解构会变成普通变量,失去响应式。
解决方法:要么直接用props.list,要么用toRefs包一下:
const props = defineProps(['list'])
const { list } = toRefs(props) // 这样list是ref,修改list.value才会触发更新
忘记用ref/reactive包普通数据
比如写const count = 0,然后count++,界面不会更新,必须用const count = ref(0),修改时count.value++。
emit事件名写错(大小写、拼写)
defineEmits(['updateData']),触发时得写emit('updateData'),名字必须完全一致,少个字母或大小写错了都没效果。
生命周期钩子导入错误
比如想写onMounted,结果导入成onMount(不存在的API),控制台会报错,要记准钩子名字是onXxx(如onMounted、onUpdated)。
组合式函数里响应式处理不当
比如在useMouse里,把x和y写成普通变量let x = 0
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网




发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。