目录
- 普通 setup 和
<script setup>(重点普通 setup + 官网链接) import {} from ''讲解- 组合式:业务代码抽离复用,demo + 解释
- ref /reactive 详细用法(对象数组怎么改,是否需要 import)
- ref & reactive 底层原理、什么时候选哪个
- hooks 是什么,怎么写怎么用(demo)
- 选项式 + setup 混合写法:
setup(){}详解 + 官网链接 <script setup>props、emit demodefineExpose讲解 + demo- 什么是宏(macro) ,defineProps/defineEmits 为什么叫宏
- 学习路线:先学普通 setup,再学 script setup,该怎么学
1. 普通 setup 和 <script setup>
一句话: 普通 setup :Vue3 组件配置项里的一个函数,属于组合式 API 原始形态 ;
<script setup>:是普通 setup 的编译期语法糖,简化写法,uniapp、vue 项目最常用。
① 普通 setup(Options 配置里的 setup 函数)
官网文档:cn.vuejs.org/api/composi...
setup是 Vue3 组件的一个选项 ,和data()、methods、mounted同级。 执行时机:在组件创建前,props 解析之后,beforeCreate 之前执行。 👉 setup 里面没有 this! 这是重点,setup 函数内部this是undefined。
规则:
-
所有响应式变量、函数,最后必须 return 暴露出去,模板才能访问;
-
生命周期、ref/reactive 都要 import;
-
接收两个参数:
setup(props, context)- props:父传过来的属性(只读,不能直接改)
- context:上下文,包含
emit, attrs, slots, expose
普通 setup 完整示例
xml
<template>
<view>count:{{count}} <button @click="add">+1</button></view>
</template>
<script>
import {ref, onMounted} from 'vue'
export default {
// 普通setup,是组件的一个选项
setup(props, context) {
const count = ref(0)
const add = () => {
count.value++
}
onMounted(()=>{
console.log('挂载了')
})
// ⚠️ 必须return!模板才能拿到count、add
return {
count,
add
}
}
}
</script>
② <script setup> 语法糖
官网:cn.vuejs.org/api/sfc-scr... 本质:编译阶段自动帮你生成上面的普通 setup 代码,自动 return。 不需要手动 return;顶层变量、函数,模板直接可用。
xml
<template>
<view>count:{{count}} <button @click="add">+1</button></view>
</template>
<script setup>
import {ref,onMounted} from 'vue'
const count = ref(0)
const add = ()=> count.value++
onMounted(()=>console.log('挂载'))
// ✅ 不用return
</script>
两者对比
表格
| 普通 setup | |
|---|---|
在export default里面,作为选项 |
单独标签,sfc 语法糖 |
| 必须手动 return 变量 / 方法 | 自动暴露,不用 return |
| 可以写在选项式组件里,支持和 data/methods 共存 | 不能和普通<script>export default 直接混用在同一个 script 块 |
需要手动 context.emit() |
用defineEmits宏 |
为什么很少见到普通 setup?因为写起来麻烦,每个变量都要 return,业务项目都直接上 script setup。但是理解普通 setup,你才能看懂组合式底层。
2. import {} from ''
csharp
import {ref, reactive, onMounted} from 'vue'
ES6 模块语法,解构导入。
vue:包名,从 vue 这个库里面导入;{ref, reactive}:代表只导入 vue 里面ref、reactive 这几个函数,不是全部导入。
等价理解:vue 内部导出了一堆函数
javascript
// vue源码简化理解
export const ref = ()=>{}
export const reactive = ()=>{}
export const onMounted = ()=>{}
你用大括号,只拿你需要的。
两种导入对比:
csharp
// 1. 解构导入(最常用,你一直看到的)
import {ref} from 'vue'
// 2. 全部导入(不推荐,体积大,写起来麻烦)
import * as Vue from 'vue'
const count = Vue.ref(0)
简单记:组合式 API 的所有工具函数,都需要手动 import 导入;选项式不用,因为选项式是 Vue 自动挂到 this。
3. 组合式:业务抽离复用(demo + 解释)
核心思想:把一组关联的变量、函数、监听打包成一个函数,导出,在多个组件导入使用。
普通 setup 和 script setup 都可以使用抽离的逻辑。
新建文件 useCounter.js(hooks 文件)
javascript
// useCounter.js 抽离复用逻辑
import {ref, computed, watch} from 'vue'
export function useCounter(initVal = 0) {
// 这一组变量、方法,全部是【同一个业务】
const count = ref(initVal)
const doubleCount = computed(()=> count.value *2)
const add = ()=> count.value++
const reduce = ()=> count.value--
watch(count, (newVal)=>{
console.log('count变化', newVal)
})
// 把需要对外使用的变量函数return出去
return {
count,
doubleCount,
add,
reduce
}
}
组件 A 使用:
xml
<!-- A.vue 普通setup版本 -->
<script>
import { useCounter } from './useCounter.js'
export default {
setup(){
// 直接拿到一整套逻辑
const {count,doubleCount,add,reduce} = useCounter(10)
return {count,doubleCount,add,reduce}
}
}
</script>
组件 B 使用:
xml
<!-- B.vue script setup -->
<script setup>
import { useCounter } from './useCounter.js'
const {count,doubleCount,add,reduce} = useCounter(0)
</script>
✅ 复用特点:
- 每个组件调用
useCounter(),都会生成独立的响应式数据,组件之间互不干扰; - 选项式的 mixins 最大坑:命名冲突,不知道变量来自哪个 mixin;hooks 没有这个问题,变量显式导入。
4. ref /reactive 详细用法
✅ 使用前,都必须 import
csharp
import {ref, reactive} from 'vue'
ref
ref 接收任意值,返回一个包装对象,.value存真实数据。
- 基础类型:数字、字符串、布尔
dart
const num = ref(10)
num.value = 20 // js修改要.value
console.log(num.value)
//模板直接 {{num}},不用.value
ref也可以放对象、数组(只是一般对象推荐 reactive)
ini
const user = ref({name:'张三'})
user.value.name = '李四' // ref存对象,修改必须 .value.xxx
reactive
专门用来包装对象、数组 ,返回响应式代理,不需要.value
ini
// 对象
const user = reactive({
name:'张三',
age:20
})
user.age = 21
user.name = '李四'
// 数组
const list = reactive([1,2,3])
list.push(4)
list[0] = 100
⚠️ reactive 限制:
- 不能直接整体替换!会丢失响应式
ini
const user = reactive({name:'张三'})
// ❌ 错误,直接覆盖,丢失响应式
user = reactive({name:'李四'})
// ✅ 正确,修改属性
user.name = '李四'
如果需要整体替换对象,推荐用 ref 存对象。
选型口诀
- 简单基础类型(数字字符串布尔)👉 ref
- 对象 / 数组,不需要整体替换 👉 reactive
- 对象,未来可能直接替换整个对象 👉 ref
5. ref & reactive 核心原理(入门理解)
Vue3 响应式底层基于 Proxy。
reactive:直接对传入对象做 Proxy 代理。返回代理对象。所以直接操作属性。ref:因为 Proxy不能代理基础类型 (数字、字符串不是对象),所以把基础值包装成一个{ value:xxx }对象,再给这个对象做 proxy。 所以读取修改必须.value。
模板里面 Vue 自动做 ref 自动解包,模板写 {{count}},不用 count.value。
小知识点:ref 里面如果传入对象,内部底层会自动调用 reactive 包装这个对象。
6. hooks 是什么,怎么写怎么用
hooks(vue 组合式 hooks):以 use 开头的函数,封装一组响应式状态 + 相关逻辑 就是上面useCounter例子。 命名约定:useXxx,约定俗成,方便识别。
特点:
- 普通 js 函数;
- 内部使用 ref/reactive/ 生命周期等组合式 api;
- 返回响应式变量和方法;
- 组件导入调用,实现逻辑复用。
注意:hooks只能在 setup 函数内部调用(普通 setup 或者 script setup 都可以,不能放在普通函数、定时器顶层全局)
例子:useList 列表加载 hooks
javascript
// useList.js
import {ref} from 'vue'
export function useList(){
const list = ref([])
const loading = ref(false)
async function fetchData(){
loading.value=true
//模拟请求
const res = await new Promise(res=> setTimeout(()=>res([1,2,3]),500))
list.value = res
loading.value=false
}
return {list,loading,fetchData}
}
组件使用:
xml
<script setup>
import {useList} from './useList'
const {list,loading,fetchData} = useList()
fetchData()
</script>
7. setup(){} 混合写法详解(选项式里面写 setup)
官网链接:cn.vuejs.org/api/composi...
xml
<script>
import {ref} from 'vue'
export default {
data(){
return {
b:2
}
},
setup(){
const a = ref(1)
return {a}
}
}
</script>
解读:
- 这是选项式组件,export default 是选项配置对象;
setup只是其中一个选项,和 data 同级;- setup 内部写组合式 API;return 出来的变量,会挂载到组件实例,和 data 里面的变量合并;
- 模板里
{{a}}、{{b}}都可以访问; - setup 执行时机早于 data;
- setup 里没有 this ,setup 内部不能访问
this.b; - 业务不推荐这么写:两套写法混在一起,阅读成本翻倍,容易混乱,学习看懂即可。
用途:老 Vue3 选项式项目,想局部使用组合式逻辑,才会这么写。新项目不要混合。
8. <script setup> props、emit demo
官网:cn.vuejs.org/api/sfc-scr...
子组件(接收 props,触发 emit)Child.vue
xml
<template>
<view>
<p>父传的值:{{title}}</p>
<button @click="handleClick">通知父组件</button>
</view>
</template>
<script setup>
// 宏,接收props
const props = defineProps({
title: {
type:String,
required:true
}
})
// 宏,声明可以触发的事件
const emit = defineEmits(['change'])
const handleClick = ()=>{
emit('change','子组件传给父的数据')
}
</script>
父组件 Parent.vue
xml
<template>
<Child title="hello" @change="onChildChange"></Child>
</template>
<script setup>
import Child from './Child.vue'
const onChildChange = (val)=>{
console.log('收到子组件消息',val)
}
</script>
9. defineExpose 讲解 + demo
官网:cn.vuejs.org/api/sfc-scr...
<script setup>默认:组件内部所有变量、方法对外完全私有 。父组件通过 ref 拿到子组件实例,默认访问不到子组件里面的函数 。defineExpose:把你想暴露给父组件的方法 / 变量显式导出。
子组件 Child.vue
xml
<template>
<view>子组件</view>
</template>
<script setup>
const testFn = ()=>{
console.log('子组件方法执行')
}
const innerVal = ref(100)
// 暴露给父组件
defineExpose({
testFn,
innerVal
})
</script>
父组件(通过 ref 调用子组件方法)
xml
<template>
<Child ref="childRef"></Child>
<button @click="callChild">调用子组件方法</button>
</template>
<script setup>
import {ref} from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
const callChild = ()=>{
childRef.value?.testFn()
}
</script>
👉 就是你 uniapp right-window 那段代码用到的!
10. 什么是宏(macro)?defineProps /defineEmits/defineExpose 为什么叫宏?
宏:编译期宏,不是运行时 js 函数 。 在
<script setup>里面:defineProps、defineEmits、defineExpose特点:
- 不需要 import 导入,直接使用;
- 不是 js 运行时函数,是 Vue 编译器在编译模板阶段做处理;
- 编译完成之后,这些
defineXxx代码直接消失,不会打包进最终 js;
普通 js 函数是浏览器运行的时候执行;宏是打包编译阶段处理。
只有在
<script setup>里面可以直接使用这些宏;普通 setup 函数里不能用宏,普通 setup 要用 props 参数、context.emit。
一句话区分:
- 普通 API:
import {ref} from 'vue'运行时函数; - 宏:
defineProps,编译期语法,无需 import。
11. 学习路线:先学普通 setup,再学 script setup,怎么学
目标:看懂组合式本质,而不是只会写语法糖。按下面顺序练习,不要跳。
阶段 1:只写普通 setup(不碰 script setup,1~3 天练习)
练习任务,全部使用普通 setup(export default + setup 函数)
- 计数器 ref,return 变量
- 用 reactive 写对象,修改对象属性
- computed、watch
- onMounted 生命周期
- 学习 setup 参数:props, context.emit
- 写一个简单 hooks,在 setup 里面导入使用
重点体会:
- setup 没有 this
- 所有变量必须 return模板才能看见
- 所有 api 都要 import
阶段 2:理解普通 setup 之后,再切换到<script setup>
对比同一个页面,分别写普通 setup 版本 和 script setup 版本,观察差异:
- 不用手动 return
- 宏 defineProps/defineEmits/defineExpose
- 自动暴露顶层变量
阶段 3:练习 ref 获取子组件实例 + defineExpose(就是你 uniapp 场景)
阶段 4:uniapp 项目实战,全部 script setup
避坑
不要一上来直接写 script setup,很多人只会复制代码,不懂底层 ref、实例暴露、setup 执行顺序,遇到问题就懵,就像你最开始看 right-window 代码一样。