一、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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。