Code前端首页关于Code前端联系我们

一、Vue3里的setup到底是干啥的?

terry 2周前 (09-30) 阅读数 44 #Vue
文章标签 setup

想学Vue3开发,却被setup语法搞得晕头转向?setup作为Vue3组合式API的核心入口,新手入门时得先把“它是干啥的”“怎么写基础逻辑”“和响应式、生命周期咋配合”这些关键点理清楚,这篇文章用问答形式,把Vue3 setup从入门到实战的核心问题拆开来讲,帮你把知识点串成能用的技能!

简单说,setup是Vue3给组件提供的逻辑组织入口,在Vue2里,我们写组件用datamethodscomputed这些选项分开管理逻辑;但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>

重点关注这几个点:

  1. 参数:传统写法的setup接收props(父组件传的属性,是响应式的,不能直接解构!后面讲坑的时候会说)和context(包含emitslotsattrs这些工具),而<script setup>里,props要用defineProps声明,emitdefineEmits声明,更简洁。

  2. 执行时机setup在组件创建前就运行,比beforeCreate还早,所以里面拿不到thisthisundefined),也不能访问datamethods这些选项里的内容(因为它们还没初始化)。

  3. 返回值:传统setupreturn的对象,模板才能用;但<script setup>会自动把顶层声明的变量、函数暴露给模板,不用手动return,写起来更爽。

新手刚开始可以先从<script setup>入手,这是Vue3官方推荐的写法,能省很多事~

用setup时,响应式数据咋处理?

Vue2里靠data返回对象实现响应式,但Vue3的setup里,得用refreactive这两个“响应式工具”:

  • 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),这样nameage都是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属性、插槽:useAttrsuseSlots

useAttrsuseSlots获取父组件传的非props属性、插槽内容,比如父组件给子组件传了classstyle,或者自定义属性:

子组件:

<script setup>
import { useAttrs, useSlots } from 'vue'
const attrs = useAttrs() // 拿到class、style、自定义属性等
const slots = useSlots() // 拿到父组件传的插槽,lt;slot name="header" />
</script>

这些API把组件通信的场景全覆盖了,按需求选对应的方式就行~

setup和生命周期钩子咋配合?

Vue2里的createdmounted这些钩子,在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分散在createddata里更清晰~

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(如onMountedonUpdated)。

组合式函数里响应式处理不当

比如在useMouse里,把xy写成普通变量let x = 0

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

上一篇:chart 下一篇:ref到底是干啥的?

发表评论:

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

热门