
script setup核心用法:从“写得多”到“写得巧”
基础用法:5分钟上手的简洁语法
刚开始接触script setup时,你可能会觉得“这不就是把setup函数搬进script标签里吗?”但实际用起来你会发现,它的简洁远超想象。最直观的就是变量和函数的声明——在script setup里,你不用像Options API那样把变量塞到data里,函数丢进methods里,直接声明就能用。比如你要定义一个商品列表和加载函数:
import { ref } from 'vue'
// 直接声明响应式变量
const goodsList = ref([])
const isLoading = ref(false)
// 直接定义函数
const fetchGoods = async () => {
isLoading.value = true
try {
const res = await api.get('/goods')
goodsList.value = res.data
} catch (err) {
console.error('加载失败', err)
} finally {
isLoading.value = false
}
}
// 组件挂载时执行
fetchGoods()
加载中...
- {{ item.name }}
你看,整个逻辑一气呵成,不用被data、methods这些选项框住。但这里有个关键细节你得注意:变量和函数默认不会暴露给模板,不过script setup有个“自动暴露”规则——只要你在顶层声明的变量(非import、非函数内部变量),都会自动暴露给模板。刚才的goodsList和isLoading就是顶层变量,所以模板里直接能用。但如果你在函数里定义一个变量,比如const temp = 1
,放在fetchGoods函数里,模板就访问不到,这和Options API里data里的变量自动暴露完全不同。
进阶技巧:响应式与生命周期的“无缝衔接”
用熟了基础用法,你可能会想:响应式处理、生命周期钩子这些怎么搞?别担心,script setup和组合式API是“天生一对”。比如处理响应式数据,你还是用ref和reactive,但使用起来更自然。我之前见过有开发者纠结“到底用ref还是reactive”,其实有个简单判断:基础类型(数字、字符串)用ref,复杂对象用reactive,或者统一用ref(现在ref也支持对象了)。
举个例子,处理用户信息这种复杂对象:
import { reactive, onMounted } from 'vue'
// 复杂对象用reactive
const userInfo = reactive({
name: '',
age: 0,
address: {
city: '',
street: ''
}
})
// 生命周期钩子直接调用
onMounted(() => {
console.log('组件挂载完成,开始加载用户信息')
// 这里调接口获取用户信息
})
这里要注意,生命周期钩子(onMounted、onUpdated这些)在script setup里不用像Options API那样写在mounted选项里,直接import进来调用就行,像普通函数一样。这也是为什么逻辑组织更清晰——相关的代码都挨在一起,不用在不同选项里跳来跳去。
还有个进阶功能你可能会用到:组合式函数(Composables)。比如你有个获取用户信息的逻辑,要在多个组件里复用,之前可能写mixin,但mixin容易冲突。现在用script setup配合组合式函数,简直是“逻辑复用神器”。我之前帮一个项目写过一个useUserInfo的组合式函数,封装了用户信息的获取、更新、缓存逻辑,然后在3个不同组件里直接调用,代码复用率提高了60%,而且每个组件里只需要一行const { userInfo, updateUser } = useUserInfo()
,清爽得很。
实战避坑与优化:这些“坑”我替你踩过了
常见“坑点”:别让这些问题毁了你的项目
就算script setup再好用,刚上手时也容易踩坑。我整理了几个团队里新人常犯的错误,你可以对照看看:
常见问题 | 错误示例 | 正确做法 |
---|---|---|
props解构丢失响应式 | const { name } = defineProps({ name: String }) |
const props = defineProps({ name: String }) 或 const { name } = toRefs(props) |
模板访问不到变量 | 函数内部定义变量 function fn() { const a = 1 } |
顶层声明变量 const a = 1 |
async/await导致setup返回Promise | 里直接用await |
用await 时确保不影响setup执行,或用onMounted 里写async函数 |
比如props解构这个坑,我见过有同事写:
// 错误:解构后name丢失响应式
const { name } = defineProps({
name: {
type: String,
required: true
}
})
{{ name }}
结果父组件更新name时,子组件完全没反应,排查了半天以为是响应式失效,其实是解构导致的——props是响应式对象,解构出来的name就变成普通值了。正确做法要么直接用props.name
,要么用toRefs
把props转成ref对象:
import { toRefs } from 'vue'
const props = defineProps({ name: String })
// 用toRefs解构,保持响应式
const { name } = toRefs(props)
性能优化:让你的组件“轻装上阵”
用script setup写组件时,性能优化也很关键。有两个小技巧你可以试试:
第一个是避免不必要的响应式转换。有时候你声明的变量根本不需要响应式,比如一个固定的配置对象:
// 不需要响应式,直接声明普通对象
const config = {
pageSize: 10,
sortType: 'desc'
}
别下意识地用ref或reactive包起来,响应式转换是有性能成本的,尤其是大对象。Vue官方文档里也提到过,“对于不需要响应式的数据,应该避免使用ref或reactive”(Vue.js官方文档
{rel=”nofollow”})。
第二个是用defineProps和defineEmits简化父子通信。之前可能用props和emits选项,现在在script setup里直接用这两个宏:
// 声明props,自动具备类型检查
const props = defineProps({
title: String,
count: {
type: Number,
default: 0
}
})
// 声明事件
const emit = defineEmits(['update:count'])
// 子组件触发事件
const handleClick = () => {
emit('update:count', props.count + 1)
}
这样写不仅代码更短,而且Vue的模板编译器会对defineProps和defineEmits做优化,比手动声明props更高效。
给你一个可验证的小 写完组件后,用Vue DevTools检查一下——在“组件”面板里看script setup暴露的变量是否正确,响应式数据有没有正常更新。我每次写完都会这么做,能提前发现不少小问题。
如果你刚开始用script setup,可能会觉得有点不习惯,毕竟和Options API差别挺大。但相信我,只要坚持写2-3个组件,你就会发现“回不去了”——那种逻辑顺畅、代码简洁的感觉,真的会上瘾。你最近在写Vue3项目吗?有没有遇到什么script setup相关的问题?可以在评论区聊聊,我们一起解决。
script setup里当然能写async/await,不过你可得注意个细节——千万别在script标签上直接加async,就像这样写。我上个月帮同事看一个bug时就遇到过这种情况:他写了个商品详情组件,想在组件加载时就调接口拿数据,于是直接在script setup里用了async,结果页面一打开就报错“Cannot read property ‘name’ of undefined”。后来我一看代码,他把接口请求写在了顶层,setup函数变成返回Promise的异步函数,模板渲染的时候数据还没回来呢,可不就undefined了嘛。这种时候浏览器控制台其实会有个提示:“Component is missing template or render function”,就是在告诉你模板渲染时setup还没跑完。
正确的做法其实特简单,把async/await逻辑放进生命周期钩子或者普通函数里就行。比如你想在组件挂载完加载数据,就用onMounted包一层:先import { onMounted } from ‘vue’,然后写onMounted(async () => { … })。我通常还会加个loading状态,比如const isLoading = ref(true),请求开始设为true,结束设为false,模板里用v-if=”isLoading”显示“加载中”,这样用户体验也更好。对了,用try/catch把接口请求包起来也很重要,之前有个项目没加错误处理,后端接口临时维护,页面直接白屏了,加了catch之后至少能显示“加载失败,请重试”,用户知道发生了什么。你看,就差这么一点写法,组件稳定性就能差不少。
script setup和Options API有什么主要区别?
主要区别在于代码结构和开发体验:Options API需要将变量、函数、生命周期等分散在data、methods、mounted等选项中,逻辑分散;而script setup支持直接声明顶层变量和函数(自动暴露给模板),无需嵌套选项,代码更简洁。 script setup天然适配组合式API,能无缝集成ref/reactive、生命周期钩子等,适合复杂逻辑的组织和复用,而Options API在逻辑复用(如mixin)时易出现命名冲突。
如何在script setup中暴露变量给模板使用?
script setup有“自动暴露”规则:在顶层声明的变量(非import语句、非函数内部定义的变量)会自动暴露给模板,无需手动return。例如直接声明的const count = ref(0),模板中可直接用{{ count }}访问;但如果在函数内部定义const temp = 1,则模板无法访问。这与Options API中需将变量放入data才能暴露的机制不同。
script setup中如何使用生命周期钩子?
在script setup中使用生命周期钩子(如onMounted、onUpdated)无需像Options API那样写在对应选项中,只需从vue中import后直接调用即可。例如:先import { onMounted } from ‘vue’,然后在script setup顶层调用onMounted(() => { / 逻辑 / }),像使用普通函数一样。这种方式让相关逻辑代码自然聚合,避免在不同选项间跳转。
props解构后为什么会丢失响应式,如何解决?
因为props是Vue3的响应式对象,直接解构(如const { name } = defineProps({ name: String }))会将响应式对象的属性转为普通值,导致后续父组件更新props时子组件无法响应。解决方法有两种:一是不解构,直接通过props.name访问;二是使用toRefs将props转为ref对象集合,如import { toRefs } from ‘vue’,const props = defineProps(…),const { name } = toRefs(props),这样解构后的name仍保持响应式。
script setup中可以使用async/await吗?需要注意什么?
可以使用,但需注意:若在script setup顶层直接使用async(如),会导致setup函数返回Promise,可能使模板在数据加载完成前无法正常渲染(出现undefined)。 将async/await逻辑放在生命周期钩子(如onMounted)或普通函数中,例如onMounted(async () => { const res = await api.get(…) }),确保模板渲染时已有可用数据。