1.ref
ref响应式 API,用于创建响应式数据。 函数接收一个初始值作为参数,并返回一个响应式的引用对象(Ref 对象)。这个对象有一个 .value 属性,用于访问和修改其内部的值。
import { ref } from 'vue'
// 创建一个响应式的数字
const count = ref(0)
// 访问值
console.log(count.value) // 输出: 0
// 修改值
count.value = 1
console.log(count.value) // 输出: 1
不同数据类型的处理
-
基本类型(number、string、boolean 等):
ref会将其包装为 Ref 对象,必须通过.value访问和修改 -
对象类型(object、array 等):
ref内部会自动调用reactive进行处理,所以修改对象属性时不需要.value// 基本类型 const message = ref('Hello') message.value = 'Hello Vue3' // 需要 .value // 对象类型 const user = ref({ name: '张三', age: 20 }) user.value.name = '李四' // 修改整个对象需要 .value user.age = 21 // 修改对象属性不需要 .value
模板中使用
ref 创建的响应式数据时,Vue 会自动解包(不需要写 .value)
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="count++">增加</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
在响应式对象
import { ref, reactive } from 'vue'
const count = ref(0)
const data = reactive({
count // 这里会自动解包
})
console.log(data.count) // 0,不需要 .value
data.count = 1
console.log(count.value) // 1,原 ref 也会同步更新
与 reactive 的区别
ref可以处理基本类型,reactive只能处理对象类型ref需要通过.value访问值(模板中除外),reactive直接访问属性ref返回的是 Ref 对象,reactive返回的是 Proxy 对象
父组件访问子组件:通过 ref 获取实例
父组件可通过 ref 绑定子组件,获取子组件的实例(包括方法、属性),主动调用子组件的逻辑。
<!-- 父组件 Parent.vue -->
<template>
<Child ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
// 定义 ref 绑定子组件
const childRef = ref(null)
// 调用子组件的方法
const callChildMethod = () => {
// 子组件挂载后,childRef.value 才是子组件实例
if (childRef.value) {
childRef.value.clearData() // 调用子组件的 clearData 方法
console.log("子组件的属性:", childRef.value.msg) // 访问子组件的属性
}
}
</script>
子组件暴露方法 / 属性(<script setup> 中需用 defineExpose 显式暴露):
<!-- 子组件 Child.vue -->
<script setup>
import { ref } from 'vue'
// 子组件的属性
const msg = ref("子组件消息")
// 子组件的方法
const clearData = () => {
msg.value = ""
}
// 显式暴露给父组件(<script setup> 中默认私有,必须暴露才能被访问)
defineExpose({
msg,
clearData
})
</script>
2.toRaw
Vue3 的响应式系统通过 Proxy(代理)实现:当用 reactive() 或 ref() 创建响应式对象时,Vue 会生成一个代理对象(而非直接修改原始数据),这个代理会追踪数据访问和修改,从而触发视图更新或依赖执行。
toRaw 的作用就是穿透这个代理,返回被代理的原始对象 。
import { reactive, toRaw } from 'vue'
const original = { name: 'test' }
const reactiveObj = reactive(original) // 创建响应式代理
console.log(reactiveObj === original) // false(reactiveObj 是代理)
console.log(toRaw(reactiveObj) === original) // true(toRaw 返回原始对象)
toRaw 主要用于需要操作原始数据,且不希望触发响应式更新或避免代理带来的性能开销的场景
运用场景
1.避免不必要的视图更新(中间状态处理)
当执行复杂业务逻辑(如数据计算、批量处理)时,可能需要临时修改数据,但这些中间修改不需要立即触发视图更新(避免界面频繁闪烁或无效渲染)
import { reactive, toRaw } from 'vue'
const formData = reactive({ name: '', age: 0 })
// 复杂校验逻辑:临时修改数据但不触发更新
function validate() {
const rawForm = toRaw(formData) // 获取原始数据
// 临时修改原始数据(不会触发视图更新)
rawForm.age = rawForm.age || 18 // 补默认值
// ... 其他校验逻辑
// 校验完成后,若需要更新响应式数据,可再同步回去
formData.age = rawForm.age
}
2.提高复杂操作的性能(减少代理开销)
响应式代理的追踪逻辑会带来少量性能损耗,对于频繁读写的复杂数据操作(如大数据量循环、深拷贝、序列化),直接操作原始数据可显著提升性能。
import { reactive, toRaw } from 'vue'
import { cloneDeep } from 'lodash'
const bigData = reactive({ list: [...Array(10000).keys()] }) // 大数据量响应式对象
// 直接深拷贝响应式对象(会处理代理,性能低)
const slowCopy = cloneDeep(bigData)
// 先转原始对象再拷贝(性能更高)
const rawData = toRaw(bigData)
const fastCopy = cloneDeep(rawData)
3.临时存储原始状态(用于对比或回滚)
import { reactive, toRaw } from 'vue'
const data = reactive({ value: 0 })
// 记录事务开始前的原始状态
const snapshot = toRaw(data)
// 执行事务(修改响应式数据)
data.value = 100
// 回滚:用原始快照覆盖当前数据
Object.assign(data, snapshot)
仅对 reactive 创建的对象有效:toRaw 用于 reactive 生成的响应式对象;对于 ref 对象,需先访问 .value 再使用 toRaw(因为 ref 的响应式代理在 .value 上):
import { ref, toRaw } from 'vue'
const countRef = ref(0)
const rawCount = toRaw(countRef.value) // 正确:获取 ref.value 的原始值
修改原始对象不会触发响应式更新:toRaw 返回的原始对象与响应式代理指向同一内存地址,但修改原始对象不会触发视图更新或依赖执行(需手动同步到响应式对象才会更新)
3.$el
$el 是组件实例的一个内置属性,指向当前组件渲染后在 DOM 中对应的根元素(即组件最终渲染出来的真实 DOM 节点),每个 Vue 组件在挂载(渲染到页面)后,都会生成对应的 DOM 结构,而 $el 就是这个 DOM 结构的 “根节点” 的引用。让开发者能直接访问组件对应的 DOM 元素,从而进行一些需要操作 DOM 的场景。
//在父组件的脚本中,通过 `ref(null)` 创建一个响应式引用(`ref` 对象),初始值为 `null`。这个引用专门用于存储子组件 `runHeader` 的实例。
//作为 “容器”,等待 Vue 框架自动填充子组件实例(当子组件挂载后)。
//由于是响应式的,当子组件实例被赋值或更新时,依赖它的逻辑(如 DOM 操作、状态判断)会自动响应。
const headerRef = ref(null)
const headerDom = headerRef.value?.$el
//在父组件的模板中,给子组件 `runHeader` 添加 `ref="headerRef"` 属性,将子组件与脚本中定义的 `headerRef` 关联起来。
//告诉 Vue 框架:“当 `runHeader` 组件挂载(渲染到 DOM)后,把它的实例赋值给 `headerRef.value`”。
//当子组件卸载时,`headerRef.value` 会自动变回 `null`,避免内存泄漏。
<runHeader ref="headerRef"/>
通过这种绑定,父组件可以在脚本中通过 `headerRef.value` 直接访问子组件 `runHeader` 的:
- **组件实例属性**:比如子组件的 `state`、`props` 等状态。
- **组件实例方法**:比如子组件定义的 `handleMenuItemClick` 等方法(如果需要调用)。
- **DOM 元素**:通过子组件实例的 `$el` 属性(如之前提到的 `headerRef.value?.$el`),获取子组件渲染后的根 DOM 元素(用于位置计算、样式操作等)。
场景:
获取元素位置 / 尺寸
通过 $el.getBoundingClientRect() 可以获取组件在视口中的位置(坐标、宽高),这在拖拽、定位等交互中非常常用。
const headerRect = headerDom.getBoundingClientRect()
通过 $el 获取了头部组件的位置边界,用于判断鼠标是否点击在头部区域(从而限制只有头部可拖拽)。
直接操作 DOM 样式 / 属性
Vue 推荐通过数据绑定(如 :style、:class)控制样式,但某些场景下需要直接操作 DOM 样式,此时可通过 $el 实现:
headerRef.value?.$el.style.backgroundColor = 'red'
绑定原生 DOM 事件
需要监听组件根元素的原生事件(如 scroll、resize),可通过 $el 直接绑定:
headerRef.value?.$el.addEventListener('scroll', handleScroll)
判断组件是否已挂载
组件未挂载时,$el 为 undefined 或 null;挂载后 $el 为有效的 DOM 元素。因此可通过 $el 是否存在判断组件状态:
if (headerRef.value?.$el) {
console.log('组件已挂载')
}
$el 是 Vue 组件连接 “虚拟 DOM” 和 “真实 DOM” 的桥梁,它让开发者能直接访问组件渲染后的根 DOM 元素,主要用于需要手动操作 DOM 的场景
Vue 设计理念是 “数据驱动视图”,优先通过响应式数据(ref/reactive)和指令(v-bind/v-on)操作视图,直接通过 $el 操作 DOM 可能破坏数据与视图的同步,仅在必要时使用。
功能:拖动
鼠标按下拖动元素在页面移动功能。
<runHeader ref="headerRef"
@mousedown="startDrag"
/>
// 默认为false(未拖拽),点击部时设为true
const isDragging = ref(false)
//(窗口位置)
const position = ref({
x: 40, // 初始水平位置:距离页面左侧40px,后续随拖拽动态更新
y: 10 // 初始垂直位置:距离页面顶部10px,后续随拖拽动态更新
})
// 存储鼠标开始拖拽时的初始位置
const initialMousePos = ref({
x: 0,
y: 0
})
// 存储元素开始拖拽时的初始位置
const initialPos = ref({
x: 0,
y: 0
})
const runningRef = ref(null) //绑定拖拽容器
// 节流的计时器
let animationFrameId = null
//开始拖拽
const startDrag = (e) => {
// 过滤非左键点击:仅鼠标左键(button=0)触发,排除右键/中键
if (e.button !== 0) return
// 获取真实DOM:从ref中提取header和容器的DOM(避免组件未渲染时操作)
const headerDom = headerRef.value?.$el; // 组件实例 -> 根DOM元素
const runningDom = runningRef.value;
//确保DOM存在,避免未渲染时调用
if (!headerDom || !runningDom) {
console.warn('拖拽元素渲染中,无法启动拖拽');
return
}
//仅点击头部区域才允许拖拽
const headerRect = headerDom.getBoundingClientRect()// 获取头部在视口中的位置
if (
e.clientX < headerRect.left || // 鼠标X < 头部左边界 → 不在头部
e.clientX > headerRect.right || // 鼠标X > 头部右边界 → 不在头部
e.clientY < headerRect.top || // 鼠标Y < 头部上边界 → 不在头部
e.clientY > headerRect.bottom // 鼠标Y > 头部下边界 → 不在头部
) {
return; // 鼠标不在头部区域,不触发拖拽
}
e.preventDefault(); // 阻止默认行为(如文本选中)
e.stopPropagation(); // 阻止事件冒泡(避免影响父组件)
// 启动拖拽
isDragging.value = true;
// 记录初始位置
initialMousePos.value = {
x: e.clientX,
y: e.clientY
};
initialPos.value = { ...position.value }
}
//拖拽过程中更新位置
const onMouseMove = (e) => {
// 非拖拽状态不执行
if (!isDragging.value) return
// 取消上一帧动画:避免短时间内多次触发mousemove导致的性能浪费
if (animationFrameId) cancelAnimationFrame(animationFrameId)
//requestAnimationFrame实现平滑拖拽
animationFrameId = requestAnimationFrame(() => {
//防止组件卸载后仍操作DOM
const runningDom = runningRef.value
if (!runningDom) return
// 计算位置偏移
const deltaX = e.clientX - initialMousePos.value.x
const deltaY = e.clientY - initialMousePos.value.y
let newX = initialPos.value.x + deltaX
let newY = initialPos.value.y + deltaY
// 边界限制:避免元素拖出可视区域
const windowWidth = window.innerWidth//浏览器可视区域宽度
const windowHeight = window.innerHeight// 浏览器可视区域高度
const elementWidth = runningDom.offsetWidth // 拖拽元素自身宽度
const elementHeight = runningDom.offsetHeight// 拖拽元素自身高度
// 左边界:最小为0(不超出左侧);右边界:最大为"视口宽 - 元素宽"(不超出右侧)
newX = Math.max(0, Math.min(newX, windowWidth - elementWidth))
// 上边界:最小为0(不超出顶部);下边界:最大为"视口高 - 元素高"(不超出底部)
newY = Math.max(0, Math.min(newY, windowHeight - elementHeight))
// 仅在位置变化时更新,减少响应式更新次数
if (newX !== position.value.x || newY !== position.value.y) {
position.value = { x: newX, y: newY }
}
});
}
//结束拖拽
const stopDrag = () => {
//非拖拽状态不执行:避免重复清理
if (!isDragging.value) return
// 重置拖拽状态:设为false,终止mousemove的位置更新
isDragging.value = false
// 清除动画帧,避免内存泄漏
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
animationFrameId = null
}
}
// 组件挂载时注册事件
onMounted(() => {
// 全局事件监听(组件挂载时绑定,卸载时移除,避免内存泄漏)
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', stopDrag)
document.addEventListener('mouseleave', stopDrag) // 窗口外释放也停止拖拽
})
onUnmounted(() => {
// 移除全局事件,防止内存泄漏
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', stopDrag)
document.removeEventListener('mouseleave', stopDrag)
// 兜底清除动画帧
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
})
4.getBoundingClientRect()
getBoundingClientRect() 是 DOM 元素的一个原生方法,用于获取元素在浏览器视口中的位置和尺寸信息,是实现元素定位、拖拽、碰撞检测等交互功能的核心工具。
| 属性 | 含义 |
|---|---|
top | 元素上边缘到浏览器视口顶部的距离 |
bottom | 元素下边缘到浏览器视口顶部的距离(top + height) |
left | 元素左边缘到浏览器视口左侧的距离 |
right | 元素右边缘到浏览器视口左侧的距离(left + width) |
width | 元素的宽度(right - left) |
height | 元素的高度(bottom - top) |
x | 等同于 left(元素左边缘到视口左侧的距离) |
y | 等同于 top(元素上边缘到视口顶部的距离) |
- 获取元素的视口坐标以浏览器可视区域的左上角为原点(
(0,0)),精准定位元素的位置。这区别于相对于文档(document)的坐标(会包含滚动距离),更适合判断元素在当前可见区域的位置。 - 判断元素是否在可视区域内通过比较
top、bottom、left、right与视口宽高,可判断元素是否完全 / 部分可见(如滚动加载、曝光统计等场景)。 - 限制交互范围
const headerRect = headerDom.getBoundingClientRect() // 获取头部元素的视口位置
// 鼠标坐标(e.clientX/e.clientY)与头部边界比较
if (
e.clientX < headerRect.left || // 鼠标在头部左侧 → 不在头部
e.clientX > headerRect.right || // 鼠标在头部右侧 → 不在头部
e.clientY < headerRect.top || // 鼠标在头部上方 → 不在头部
e.clientY > headerRect.bottom // 鼠标在头部下方 → 不在头部
) {
return; // 不在头部区域,不触发拖拽
}
“边界框”,只有当鼠标点击在这个框内时,才允许启动拖拽,避免点击窗口其他区域(如文本域)时误触发拖拽。
- 坐标受滚动影响由于是相对于 “视口” 的坐标,当页面滚动时,
top、left等属性会实时变化(元素位置相对于视口的位置改变)。 - 隐藏元素返回全 0对于
display: none的元素,调用该方法会返回所有属性为0的DOMRect对象(因为元素未渲染到视口)。 - 边框和滚动条计入尺寸
width和height包含元素的内容区、内边距(padding)和边框(border),但不包含外边距(margin);如果元素有滚动条,滚动条宽度也会计入width。
5.props 和 emit 实现父子组件通信
props 负责父组件向子组件传递数据,emit 负责子组件向父组件传递事件(可附带数据)
props:父组件向子组件传递数据
defineProps 用于定义子组件允许接收的来自父组件的数据,相当于子组件的 “输入参数”。
const props = defineProps({
isRunning: {
type: Boolean,
default: false,
description:''
}
})
明确规定了子组件能接收的参数名称isRunning、类型(Boolean、String、Array)、默认值和描述,确保父组件传递的数据符合预期(避免类型错误)。
子组件只能通过 props 接收父组件的数据,避免了直接修改父组件数据的风险(Vue 中 props 是单向数据流,子组件不应直接修改 props 的值)。
emit:子组件向父组件传递事件
defineEmits 用于定义子组件可以向父组件触发的事件,相当于子组件的 “输出信号”,父组件通过监听这些事件来响应子组件的操作。
const emit = defineEmits({
'update:isRunning': (value) => typeof value === 'boolean',
})
明确规定了子组件能触发的事件名称(update:isRunning、nodes)和事件参数的校验规则(如 update:isRunning 的参数必须是布尔值),确保事件传递的数据合法。
-
update:isRunning:子组件通过该事件通知父组件 “需要更新窗口的显示状态”。例如,当用户点击窗口关闭按钮时,子组件会触发emit('update:isRunning', false),父组件接收后会将isRunning设为false,从而隐藏窗口(这是 Vue 中v-model双向绑定的底层原理,update:xxx是约定的事件名)。 -
update:isRunning:子组件通过该事件通知父组件 “需要更新窗口的显示状态”。例如,当用户点击窗口关闭按钮时,子组件会触发emit('update:isRunning', false),父组件接收后会将isRunning设为false,从而隐藏窗口(这是 Vue 中v-model双向绑定的底层原理,update:xxx是约定的事件名)。
update:isRunning
`update:xxx`本质上就是一个自定义事件名,
但它是 Vue 社区约定的一种 “语义化命名规范”,核心目的就是**让事件的含义更直观、更便于开发者理解和协作
从命名上就能直接看出来:这个事件是用来 “更新(`update`)`isRunning`这个 props 的值”。
父组件开发者看到这个事件名,不需要查文档就能猜到:“监听这个事件,就能接收到`isRunning`的新值,从而更新我的数据
props是 “父传子” 的桥梁,让子组件能获取父组件的数据(如窗口是否显示)。emit是 “子传父” 的桥梁,让子组件能通知父组件自身的状态变化(如窗口需要关闭)。
二者配合实现了父子组件的双向通信,确保数据和操作在组件间有序传递,是 Vue 组件化开发的核心机制。
6.父组件监听方法
Vue 中 props 遵循 “单向数据流” 原则:父组件传递给子组件的 props,子组件不能直接修改(否则会导致数据流向混乱,难以调试)。
但实际开发中,子组件往往需要 “修改” 父组件传递的 props(比如子组件的 “关闭窗口” 操作需要让父组件把 isRunning 从 true 改为 false)。这时就需要通过 update:xxx 事件来实现:
- 子组件通过
emit('update:xxx', 新值)触发事件,告诉父组件 “需要更新xxx这个 props 的值”; - 父组件监听
update:xxx事件,接收新值并更新自己的数据(从而间接更新子组件的 props)。
子组件定义了 'update:isRunning' 事件:
const emit = defineEmits({
'update:isRunning': (value) => typeof value === 'boolean' // 校验参数为布尔值
})
当用户点击窗口关闭按钮时,子组件会触发这个事件:
// 关闭窗口时,通知父组件将 isRunning 改为 false
emit('update:isRunning', false)
父组件可以通过两种方式监听这个事件:
显式监听:
<!-- 父组件模板 -->
<run-input
:isRunning="showWindow"
@update:isRunning="showWindow = $event"
/>
简化为 v-model 语法:
Vue 对 update:xxx 事件有特殊支持,父组件可以用 v-model:xxx 简化监听逻辑:
<!-- 父组件模板:等价于上面的显式监听 -->
<run-input v-model:isRunning="showWindow" />
这里的 v-model:isRunning 会自动监听 update:isRunning 事件,并将父组件的 showWindow 更新为事件传递的值,代码更简洁。
- 子组件通过
emit('update:isRunning', 新值)主动触发事件; - 父组件通过
@update:isRunning="处理函数"或v-model:isRunning接收事件,获取新值并更新自身数据。
总结:
父组件 → 子组件:通过 props 传递数据
1. 子组件定义接收的 props
父组件通过属性(props)向子组件传递数据,子组件通过 defineProps 声明接收的数据格式。
<!-- 子组件 Child.vue -->
<script setup>
// 定义接收的 props(对象形式,支持类型、默认值、描述)
const props = defineProps({
// 基础类型:布尔值(控制显示)
isShow: {
type: Boolean,
default: false,
description: "控制组件显示/隐藏"
},
// 复杂类型:对象(业务数据)
userInfo: {
type: Object,
default: () => ({ name: "", age: 0 }), // 引用类型默认值用函数返回
required: true // 必须传递
}
})
// 使用 props 数据(直接访问,无需 .value)
console.log("父组件传递的显示状态:", props.isShow)
console.log("父组件传递的用户信息:", props.userInfo.name)
</script>
2. 父组件传递 props 数据
父组件在使用子组件时,通过属性绑定(: 或 v-bind)传递数据:
<!-- 父组件 Parent.vue -->
<template>
<!-- 传递 props 给子组件 -->
<Child
:isShow="showChild" <!-- 传递布尔值 -->
:user-info="currentUser" <!-- 传递对象(kebab-case 对应子组件的 camelCase) -->
/>
</template>
<script setup>
import { ref, reactive } from 'vue'
import Child from './Child.vue'
// 父组件的数据
const showChild = ref(true) // 控制子组件显示
const currentUser = reactive({ name: "张三", age: 20 }) // 业务数据
</script>
- 单向数据流:子组件不能直接修改 props(如
props.isShow = false是错误的),若需修改,需通过事件通知父组件更新(见下文 “子传父”)。 - 类型校验:
defineProps的类型校验(type)能避免父组件传递错误类型的数据(如预期Boolean却传String)。 - 命名规范:父组件传递时推荐用
kebab-case(如user-info),子组件接收时用camelCase(如userInfo),Vue 会自动转换。
组件 → 父组件:通过 emit 触发事件
子组件通过 emit 触发自定义事件,父组件监听事件并接收子组件传递的数据,实现 “子组件向父组件反馈”。
子组件定义并触发事件
使用 <script setup> 时,通过 defineEmits 声明可触发的事件(名称、参数校验),再通过 emit 方法触发:
<!-- 子组件 Child.vue -->
<script setup>
// 定义可触发的事件(对象形式,支持参数校验)
const emit = defineEmits({
// 事件1:通知父组件更新显示状态(参数必须是布尔值)
"update:isShow": (value) => typeof value === "boolean",
// 事件2:传递用户输入的数据(参数必须是字符串)
"input-change": (value) => typeof value === "string"
})
// 触发事件的示例(比如按钮点击时)
const handleClose = () => {
// 触发 update:isShow 事件,传递新值 false(通知父组件隐藏)
emit("update:isShow", false)
}
const handleInput = (value) => {
// 触发 input-change 事件,传递输入值
emit("input-change", value)
}
</script>
<template>
<button @click="handleClose">关闭</button>
<input @input="handleInput($event.target.value)" />
</template>
父组件监听事件并响应
父组件在使用子组件时,通过 @事件名 监听子组件触发的事件,并在处理函数中接收数据:
<!-- 父组件 Parent.vue -->
<template>
<Child
:isShow="showChild"
@update:isShow="showChild = $event" <!-- 监听 update 事件,更新显示状态 -->
@input-change="handleInputChange" <!-- 监听输入事件,处理数据 -->
/>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const showChild = ref(true)
// 处理子组件传递的输入数据
const handleInputChange = (value) => {
console.log("子组件输入的数据:", value)
// 父组件可基于此数据做后续处理(如保存到数据库)
}
</script>
- 事件命名:推荐用
kebab-case(如input-change),与 HTML 事件命名风格一致。 - 参数传递:
emit可传递多个参数(如emit('event', arg1, arg2)),父组件处理函数用对应参数接收((arg1, arg2) => {})。 - 校验机制:
defineEmits的校验函数返回false时,会在控制台警告(如参数类型错误)。
双向绑定:v-model 语法糖(父子互传的简化)
v-model 是 props + emit('update:xxx') 的语法糖,用于实现 “父组件数据与子组件状态的双向同步”,本质还是基于前两种方式。
基础用法(单个 v-model)
子组件接收 modelValue props,触发 update:modelValue 事件:
<!-- 子组件 Child.vue -->
<script setup>
const props = defineProps({
modelValue: { type: String, default: "" } // 固定名称 modelValue
})
const emit = defineEmits(["update:modelValue"]) // 固定事件名
// 输入变化时,触发事件更新父组件
const handleInput = (value) => {
emit("update:modelValue", value)
}
</script>
<template>
<input :value="props.modelValue" @input="handleInput($event.target.value)" />
</template>
父组件用 v-model 绑定数据,自动同步:
<!-- 父组件 Parent.vue -->
<template>
<!-- 等价于 :modelValue="username" @update:modelValue="username = $event" -->
<Child v-model="username" />
<p>父组件数据:{{ username }}</p>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const username = ref("") // 自动与子组件同步
</script>
多个 v-model(带参数)
Vue3 支持多个 v-model,通过参数区分不同的双向绑定字段
<!-- 子组件 Child.vue -->
<script setup>
const props = defineProps({
isShow: { type: Boolean, default: false },
count: { type: Number, default: 0 }
})
const emit = defineEmits(["update:isShow", "update:count"])
</script>
<template>
<button @click="emit('update:isShow', !props.isShow)">切换显示</button>
<button @click="emit('update:count', props.count + 1)">+1</button>
</template>
父组件用 v-model:参数名 绑定:
<template>
<Child
v-model:isShow="showChild" <!-- 绑定 isShow -->
v-model:count="num" <!-- 绑定 count -->
/>
</template>
<script setup>
import { ref } from 'vue'
const showChild = ref(true)
const num = ref(0)
</script>
| 方式 | 方向 | 适用场景 | 核心 API |
|---|---|---|---|
| props | 父 → 子 | 父组件向子组件传递初始数据 / 配置 | defineProps |
| emit 事件 | 子 → 父 | 子组件向父组件反馈操作结果 / 数据 | defineEmits |
| v-model | 双向同步 | 父子组件数据双向绑定(如表单、开关) | v-model:xxx + update:xxx |
| ref 获取实例 | 父 → 子(主动) | 父组件主动调用子组件的方法 / 访问属性 | ref + defineExpose |
| provide + inject | 跨层级(含父子) | 深层级组件共享数据 / 方法(避免 props 透传) | provide + inject |
优先使用 props + emit(最基础、低耦合),双向绑定用 v-model,跨层级用 provide/inject,尽量避免 $parent。
7. 跨层级通信:provide + inject
父子组件层级较深(如父 → 子 → 孙),可通过 provide(父组件提供数据)和 inject(子 / 孙组件注入数据)通信,避免 props 层层传递,少代码冗余)。
provide:由父组件(或祖先组件)调用,用于 “提供” 数据或方法(可以理解为 “广播” 数据)。inject:由子组件(或后代组件)调用,用于 “注入” 并获取祖先组件提供的数据或方法(可以理解为 “接收” 广播)。- 无论组件层级多深(比如父 → 子 → 孙 → 曾孙),只要在同一组件树中,后代组件都能通过
inject获取祖先组件provide的数据。
<!-- 父组件 Parent.vue -->
<script setup>
import { provide } from 'vue'
// 提供数据(key 为注入标识,value 为数据)
provide("theme", "dark")
// 提供方法
provide("changeTheme", (newTheme) => {
console.log("切换主题:", newTheme)
})
</script>
<!-- 子组件 Child.vue(无论层级多深) -->
<script setup>
import { inject } from 'vue'
// 注入父组件提供的数据(第二个参数为默认值)
const theme = inject("theme", "light")
// 注入父组件提供的方法
const changeTheme = inject("changeTheme")
// 使用注入的数据和方法
console.log("当前主题:", theme)
changeTheme("light") // 调用父组件提供的方法
</script>
基本使用步骤
1. 父组件(祖先)通过 provide 提供数据
在父组件中,从 vue 导入 provide 函数,调用时传入两个参数:
- 第一个参数:
key(注入标识,字符串或 Symbol,推荐用 Symbol 避免命名冲突)。 - 第二个参数:
value(要提供的数据 / 方法,可以是基本类型、对象、函数等)
<!-- 父组件 Parent.vue -->
<script setup>
import { provide, ref, reactive } from 'vue'
// 1. 提供基本类型数据(字符串)
provide('siteName', 'Vue教程')
// 2. 提供响应式数据(ref)
const userCount = ref(100)
provide('userCount', userCount) // 传递ref对象,保持响应式
// 3. 提供响应式对象(reactive)
const config = reactive({ theme: 'dark', size: 'medium' })
provide('appConfig', config)
// 4. 提供方法(用于子组件修改父组件数据,保持单向数据流)
const updateUserCount = (newCount) => {
userCount.value = newCount
}
provide('updateUserCount', updateUserCount)
</script>
2. 子组件(后代)通过 inject 注入数据
在子组件中,从 vue 导入 inject 函数,调用时传入两个参数:
- 第一个参数:
key(必须与父组件provide的key一致)。 - 第二个参数(可选):
defaultValue(当找不到对应key时的默认值)。
<!-- 子组件 Child.vue(可以是父组件的子组件、孙组件等) -->
<script setup>
import { inject } from 'vue'
// 1. 注入基本类型数据
const siteName = inject('siteName')
console.log('父组件提供的站点名:', siteName) // 输出:Vue教程
// 2. 注入响应式数据(ref)
const userCount = inject('userCount')
console.log('当前用户数:', userCount.value) // 输出:100(注意ref需要.value)
// 3. 注入响应式对象(reactive)
const appConfig = inject('appConfig')
console.log('当前主题:', appConfig.theme) // 输出:dark
// 4. 注入方法(用于修改父组件数据)
const updateUserCount = inject('updateUserCount')
// 调用方法修改父组件的响应式数据
const handleAddCount = () => {
updateUserCount(userCount.value + 10) // 父组件的userCount会自动更新
}
</script>
<template>
<div>
<p>站点名:{{ siteName }}</p>
<p>用户数:{{ userCount }}</p> <!-- ref在模板中自动解包,无需.value -->
<p>主题:{{ appConfig.theme }}</p>
<button @click="handleAddCount">增加用户数</button>
</div>
</template>
1. 响应式处理
- 如果
provide传递的是响应式数据(ref或reactive),inject接收后仍保持响应式(数据变化时,子组件会自动更新)。 - 推荐:子组件不直接修改注入的响应式数据(破坏单向数据流),而是通过
provide提供的方法修改(如示例中的updateUserCount)。
2. 避免命名冲突:用 Symbol 作为 key
如果多个组件都用相同的字符串 key 提供数据,会导致冲突。推荐用 Symbol 作为 key(唯一标识):
// 新建一个 keys.js 文件,定义唯一的 Symbol
export const siteNameKey = Symbol('siteName')
export const userCountKey = Symbol('userCount')
// 父组件中使用
import { siteNameKey, userCountKey } from './keys.js'
provide(siteNameKey, 'Vue教程')
provide(userCountKey, userCount)
// 子组件中使用
import { siteNameKey, userCountKey } from './keys.js'
const siteName = inject(siteNameKey)
const userCount = inject(userCountKey)
7.eventBus(事件总线)
eventBus(事件总线)是一种跨组件通信机制,用于解决非父子组件(如兄弟组件、跨多层级组件)之间的消息传递问题。它的核心思想是:创建一个全局的 “事件中心”,组件可以通过这个中心 “发布事件” 和 “订阅事件”,从而实现解耦的通信。
eventBus 本质是一个全局对象,内部维护了一个 “事件 - 回调函数” 的映射表(键值对):
- 当组件 A 想给组件 B 发消息时,组件 A 通过
eventBus.emit('事件名', 数据)发布事件(“说话”); - 组件 B 提前通过
eventBus.on('事件名', 回调函数)订阅事件(“监听”),当事件被触发时,回调函数会执行并接收数据。
on(订阅)、emit(发布)、off(取消订阅)
假设有三个组件:
Parent.vue:父组件,用于展示两个子组件;Sender.vue:发送组件(子组件),负责 “发布事件”(发送消息);Receiver.vue:接收组件(子组件),负责 “订阅事件”(接收消息)。
需求:Sender 组件点击按钮时,向 Receiver 组件发送一条消息,Receiver 组件接收后显示消息内容。
1:创建 eventBus 实例
首先在 @/utils/eventBus.js 中实现事件总线(单例模式,确保全局唯一):
// @/utils/eventBus.js
class EventBus {
constructor() {
// 用对象存储事件映射:key是事件名,value是回调函数数组
this.events = {};
}
// 订阅事件:注册事件及对应的回调
on(eventName, callback) {
// 若事件名不存在,初始化一个空数组
if (!this.events[eventName]) {
this.events[eventName] = [];
}
// 将回调加入数组(支持多个组件订阅同一事件)
this.events[eventName].push(callback);
}
// 发布事件:触发事件并传递数据
emit(eventName, ...args) {
// 若事件名存在,执行所有订阅的回调
if (this.events[eventName]) {
// 复制回调数组(避免执行时数组被修改)
const callbacks = [...this.events[eventName]];
callbacks.forEach(callback => {
callback(...args); // 传递参数给回调
});
}
}
// 取消订阅:移除事件对应的回调(防止内存泄漏)
off(eventName, callback) {
if (this.events[eventName]) {
if (callback) {
// 只移除指定的回调
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
} else {
// 若未传回调,清空该事件的所有订阅
this.events[eventName] = [];
}
}
}
}
// 导出单例实例(全局唯一)
export default new EventBus();
2:发送组件(Sender.vue)
该组件通过按钮点击触发 eventBus.emit,发布事件并传递数据:
<!-- Sender.vue -->
<template>
<div class="sender">
<h3>发送组件</h3>
<input v-model="message" placeholder="输入要发送的消息" />
<button @click="sendMessage">发送消息</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
import eventBus from '@/utils/eventBus';
// 输入框绑定的消息
const message = ref('');
// 发送消息:调用 eventBus.emit 发布事件
const sendMessage = () => {
if (message.value.trim()) {
// 发布事件:事件名为 "message:send",传递消息内容和发送时间
eventBus.emit('message:send', {
content: message.value,
time: new Date().toLocaleTimeString()
});
// 清空输入框
message.value = '';
}
};
</script>
<style scoped>
.sender {
border: 1px solid #ccc;
padding: 10px;
margin: 10px;
}
button {
margin-left: 10px;
padding: 4px 8px;
}
</style>
3:实现接收组件(Receiver.vue)
该组件在挂载时通过 eventBus.on 订阅事件,接收并展示消息;卸载时通过 eventBus.off 取消订阅:
<!-- Receiver.vue -->
<template>
<div class="receiver">
<h3>接收组件</h3>
<p v-if="messages.length === 0">暂无消息</p>
<ul v-else>
<li v-for="(msg, index) in messages" :key="index">
{{ msg.time }}:{{ msg.content }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import eventBus from '@/utils/eventBus';
// 存储接收的消息
const messages = ref([]);
// 事件回调:接收消息并添加到数组
const handleMessage = (msg) => {
messages.value.push(msg);
};
// 组件挂载时订阅事件
onMounted(() => {
// 订阅 "message:send" 事件,绑定回调 handleMessage
eventBus.on('message:send', handleMessage);
});
// 组件卸载时取消订阅(关键:防止内存泄漏)
onUnmounted(() => {
// 移除 "message:send" 事件的 handleMessage 回调
eventBus.off('message:send', handleMessage);
});
</script>
<style scoped>
.receiver {
border: 1px solid #ccc;
padding: 10px;
margin: 10px;
min-height: 100px;
}
ul {
list-style: none;
padding: 0;
}
li {
margin: 5px 0;
padding: 5px;
background: #f5f5f5;
}
</style>
父组件整合(Parent.vue)
父组件只需引入并使用两个子组件,无需关心它们之间的通信逻辑(解耦):
<!-- Parent.vue -->
<template>
<div class="parent">
<h2>父组件(事件总线示例)</h2>
<Sender />
<Receiver />
</div>
</template>
<script setup>
import Sender from './Sender.vue';
import Receiver from './Receiver.vue';
</script>
<style scoped>
.parent {
padding: 20px;
}
</style>
- 事件订阅(
on) :Receiver组件挂载时,通过eventBus.on('message:send', handleMessage)注册了对message:send事件的监听。当该事件被触发时,handleMessage会被调用并接收数据。 - 事件发布(
emit) :Sender组件点击按钮时,通过eventBus.emit('message:send', 数据)触发message:send事件,并将消息数据传递给所有订阅该事件的回调函数(这里即Receiver的handleMessage)。 - 取消订阅(
off) :Receiver组件卸载时,通过eventBus.off('message:send', handleMessage)移除回调。如果不取消,当Receiver已销毁但Sender仍发送事件时,handleMessage仍会执行(导致内存泄漏或错误)。
- 事件名一致性:发布和订阅的事件名必须完全一致(如示例中的
message:send),否则无法通信。 - 避免事件名冲突:推荐用 “命名空间” 风格(如
user:login、order:update),防止不同功能的事件名重复。 - 及时取消订阅:组件卸载前必须用
off移除回调,这是避免内存泄漏的核心。
适用场景
eventBus 适合简单的跨组件通信(如兄弟组件、隔代组件),例如:
- 通知全局状态变化(如主题切换、登录状态更新);
- 传递临时消息(如通知、警告);
- 非核心业务的组件交互(核心业务建议用 Pinia/Vuex)。
8.import.meta.globEager
使用 Vite 的 import.meta.globEager 同步导入匹配的模块。它会在构建/运行时,将符合 glob 模式的所有文件都导入并返回一个对象:
键是相对路径(由 Vite 决定的格式,如 './some/path/index.jsx' 或带 alias 的路径),值为模块对象(包含 module.default 等)。
这里匹配路径是组件文件夹下每个子目录的 index.jsx 或 index.vue,用于收集所有预览组件。
globEager 会立即导入所有模块(与异步懒加载相反),适合需要在运行时同步遍历和查找组件的场景,但会增加初始包大小。
import.meta 是一个给 JavaScript 模块暴露特定上下文元数据的对象
globEager 是 Vite 提供的一个特殊函数,用于静态地、贪婪地匹配文件
- “静态”意味着 Vite 会在打包时就执行这个匹配过程,而不是在浏览器运行时。这让它可以进行树摇(Tree-shaking) 等优化。
- “贪婪(Eager)” 意味着它会一次性将所有匹配到的模块全部导入,而不是像
import.meta.glob(不带Eager) 那样返回一个需要动态调用的函数。
'(@/view/card/components/*/index.(jsx|vue)' :
glob 模式字符串,用于匹配文件路径。
@: 在 Vite 项目中,通常配置为 src/ 目录的别名,方便书写。
view/card/components/: 这是要搜索的根目录。
*: 这是一个通配符,表示匹配该目录下的任意子目录。例如,它会匹配 button、card、modal 等子目录。
/index.(jsx|vue): 表示在每个匹配到的子目录中,寻找名为 index 且后缀为 jsx 或 vue 的文件。
目录结构如下:
src/
└── view/
└── Preview/
└── components/
├── button/
│ └── index.vue
├── card/
│ └── index.jsx
└── modal/
└── index.vue
const modules = import.meta.globEager('@/view/card/components/*/index.(jsx|vue)')
modules 对象的内容会类似于:
const modules = {
'/src/view/Preview/components/button/index.vue': { default: [Vue Component Object], ... },
'/src/view/Preview/components/card/index.jsx': { default: [React Component Function], ... },
场景
组件库或 UI 组件自动注册
内部组件库或者一个页面中有很多独立的小部件:批量导入
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 批量导入并注册组件
const modules = import.meta.globEager('@/view/card/components/*/index.(jsx|vue)')
for (const path in modules) {
// modules[path].default 是组件对象
const component = modules[path].default
// 假设组件的name选项是其 PascalCase 名称,如 Button, Card
// 或者可以从路径中提取组件名
const componentName = component.name || path.split('/').at(-2) // 从路径 '.../button/index.vue' 中获取 'button'
// 全局注册
app.component(componentName, component)
}
app.mount('#app')
Vue 模板中就可以直接使用 <Button>, <Card> 等组件,无需在每个使用的地方单独导入
路由自动生成
一个页面目录,每个页面都是一个独立的组件,你可以用这种方式自动生成路由配置
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = []
// 批量导入 views 目录下的所有页面组件
const modules = import.meta.globEager('@/views/*/index.vue')
for (const path in modules) {
// 例如,路径是 '/src/views/Home/index.vue'
// 提取路由路径: '/home'
const routePath = '/' + path.split('/').at(-2).toLowerCase()
// 提取组件名作为 name
const routeName = path.split('/').at(-2)
routes.push({
path: routePath,
name: routeName,
component: modules[path].default
})
}
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
插件或工具函数的自动加载
项目中有许多插件或者工具函数,它们遵循统一的命名和目录规范,用这种方式自动加载它们。
// src/plugins/index.js
import { app } from './main.js' // 假设 app 实例在这里
const pluginModules = import.meta.globEager('@/plugins/*/index.js')
for (const path in pluginModules) {
const plugin = pluginModules[path].default
// 假设每个插件都是一个接收 app 实例的函数
if (typeof plugin === 'function') {
plugin(app)
}
}
-
提高开发效率:无需手动编写大量重复的
import语句。当你新增一个组件或页面时,只要遵循目录规范,它就会被自动导入,无需修改其他代码。 -
保持代码整洁:减少了在入口文件或配置文件中大量的导入声明,使代码更干净、更易于维护。
-
约定优于配置:强制项目结构遵循一定的规范,有助于团队协作和项目的一致性。
-
不适用于动态内容:如果你的组件或模块是通过后端 API 动态生成的,或者需要根据非常灵活的规则来加载,这种静态批量导入方式就不适用了。
-
目录结构强依赖:代码的正确性严重依赖于文件目录结构和命名的规范性。任何不符合
*和index.(jsx|vue)模式的文件都不会被正确导入。团队成员必须严格遵守约定。 -
可能导入多余模块:
globEager会导入所有匹配的模块,即使某些模块在应用中实际上从未被使用到。不过,现代的打包工具(如 Vite 使用的 Rollup)通常能智能地进行树摇,移除未被引用的代码。但对于组件注册这种场景,因为在for循环中都被注册了,所以无法树摇,会全部打包进最终产物。如果你的组件库很大但每个页面只用少数几个,这可能会导致首屏加载体积变大。在这种情况下,考虑使用动态导入(import.meta.glob不带Eager)和异步组件。
适用于需要批量导入并注册符合特定目录规范的模块的场景,如 UI 组件库、路由自动生成等。它通过 “约定优于配置” 的方式,极大地提升了开发效率并保持了代码的整洁性。但同时它是静态的、依赖于构建时分析的特性.
9.jxs
JSX 本质是 JavaScript 的语法糖,允许你在 JavaScript 文件中混合使用 HTML 标签和 JavaScript 逻辑。 用 JSX 直观地描述组件的 UI 结构,就像写 HTML 一样。
function App() {
return (
<div>
<h1>Welcome</h1>
<p>This is a JSX example.</p>
</div>
);
}
可以用 {} 嵌入任何 JavaScript 表达式
const name = "Alice";
const element = <h1>Hello, {name}!</h1>;
- JSX 元素最终会被 Babel 等工具编译成 JavaScript 对象。
- 在 Vue 中,JSX 会被编译成虚拟 DOM 渲染函数。
- JSX 天然适合组件化开发。
- 每个组件可以清晰地包含结构(JSX)和逻辑(JavaScript)
创建 .jsx 或 .tsx 后缀的文件来编写组件
jsx文件
export default {
name: 'HelloWorld',
props: {
msg: String
},
// 使用 render 函数返回 JSX
render() {
return (
<div class="hello">
<h1>{this.msg}</h1>
<button onClick={() => console.log('Clicked')}>Click me</button>
</div>
);
}
}
.vue文件
<script setup lang="jsx">
import { ref } from 'vue';
const count = ref(0);
// 定义一个返回JSX的函数
const renderCount = () => {
return <span>{count.value}</span>;
};
</script>
<template>
<div>
<!-- 在模板中调用JSX函数 -->
<p>Count: {renderCount()}</p>
<button @click="count++">Increment</button>
</div>
</template>
非常适合处理复杂的、动态的渲染逻辑
10.defineComponent
defineComponent 是 Vue 3 中定义组件的核心辅助函数
import { defineComponent } from 'vue';
export default defineComponent({
name: 'MyComponent', // 组件名称(可选,用于调试和递归引用)
props: {
// 定义 props
msg: {
type: String,
required: true
}
},
data() {
// 响应式数据
return {
count: 0
};
},
methods: {
// 方法
increment() {
this.count++;
}
},
mounted() {
// 生命周期钩子
console.log('组件挂载完成');
},
template: `
<div>
<p>{{ msg }}</p>
<p>计数: {{ count }}</p>
<button @click="increment">+1</button>
</div>
`
});
结合 Composition API(setup 函数)
import { defineComponent, ref, onMounted } from 'vue';
export default defineComponent({
name: 'MyComponent',
props: {
msg: {
type: String,
required: true
}
},
// setup 函数:组合式 API 的核心
setup(props) {
// 响应式数据
const count = ref(0);
// 方法
const increment = () => {
count.value++;
};
// 生命周期钩子
onMounted(() => {
console.log('组件挂载完成');
});
// 暴露给模板的数据和方法
return {
count,
increment
};
},
template: `
<div>
<p>{{ msg }}</p>
<p>计数: {{ count }}</p>
<button @click="increment">+1</button>
</div>
`
});
11.toRefs 的作用
在 Vue 3 中,ref 和 reactive 是创建响应式数据的两种主要方式,当直接对 reactive 创建的响应式对象进行解构赋值或展开时,会丢失其响应性。
import { reactive } from 'vue';
const state = reactive({
name: '张三',
age: 20
});
// 直接解构,会丢失响应性
const { name, age } = state;
// 修改解构后的数据,不会触发视图更新
name = '李四';
age = 21;
toRefs将一个响应式对象(reactive 创建的)转换为一个普通对象,其中每个属性都是一个 ref 类型的响应式数据。
这样,当你解构这个普通对象时,得到的每个属性依然是响应式的。
toRefs 的运行场景
在 setup 函数中解构 props
这是 toRefs 最常见的用法。setup 函数接收的 props 参数是一个响应式对象,但如果你直接解构它,会丢失响应性。使用 toRefs 可以确保解构后的属性依然是响应式的。
import { defineComponent, toRefs } from 'vue';
export default defineComponent({
props: {
name: String,
age: Number
},
setup(props) {
// 使用 toRefs 解构 props,确保响应性
const { name, age } = toRefs(props);
// 可以通过 .value 访问属性值
console.log(name.value, age.value);
// 当父组件修改 props 时,这里的 name 和 age 会自动更新
return {
name,
age
};
}
});
解构 reactive 创建的响应式对象
如前所述,当你需要解构 reactive 创建的对象,又希望保持响应性时,可以使用 toRefs。
import { reactive, toRefs } from 'vue';
const state = reactive({
name: '张三',
age: 20
});
// 使用 toRefs 解构
const { name, age } = toRefs(state);
// 修改 .value,会触发视图更新
name.value = '李四';
age.value = 21;
在 setup 函数中返回多个响应式数据
当 setup 函数需要返回多个响应式数据时,使用 toRefs 可以让你更方便地在模板中使用这些数据,而无需每次都写 .value。
javascript
运行
import { reactive, toRefs } from 'vue';
export default defineComponent({
setup() {
const state = reactive({
name: '张三',
age: 20
});
// 返回 toRefs(state),模板中可以直接使用 name 和 age
return {
...toRefs(state)
};
}
});
在模板中:
vue
<template>
<div>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
</div>
</template>
:配合 use 函数返回响应式数据
在 Vue 3 中,我们经常会封装一些 use 函数来复用逻辑,这些函数通常会返回一个 reactive 对象。使用 toRefs 可以让调用者更方便地解构使用返回的数据。
// useUser.js
import { reactive, toRefs } from 'vue';
export function useUser() {
const state = reactive({
name: '张三',
age: 20
});
// 其他逻辑...
return toRefs(state);
}
import { defineComponent } from 'vue';
import { useUser } from './useUser';
export default defineComponent({
setup() {
// 直接解构,保持响应性
const { name, age } = useUser();
return {
name,
age
};
}
});
12.setup(props, ctx)
setup 函数是 Vue 3 中组合式 API(Composition API)的入口,它的参数是由 Vue 框架在组件实例创建时自动传入的,无需手动调用。
-
props:- 来源:父组件传递给当前组件的属性(在组件选项
props中定义的)。 - 性质:
props是一个响应式对象,当父组件修改传递的属性时,setup中的props会自动更新。
- 来源:父组件传递给当前组件的属性(在组件选项
-
ctx(上下文对象):- 来源:Vue 框架提供的组件上下文,包含了组件实例的一些属性和方法。
- 性质:
ctx不是响应式的,它是一个普通对象,主要提供了一些在setup中需要访问的组件实例特性。
二、props 和 ctx 的意义与作用
1. props 的意义与作用
-
意义:
props是组件接收外部数据的主要方式,实现了组件之间的数据传递。 -
作用:
- 在
setup中访问父组件传递的属性值。 - 基于
props的值执行逻辑(例如计算、条件判断等)。 - 由于
props是响应式的,可以配合watch或computed监听其变化。
- 在
示例:
// 组件定义
export default {
props: {
title: String,
count: Number
},
setup(props) {
console.log(props.title, props.count); // 访问父组件传递的属性
// 基于 props 计算
const doubleCount = props.count * 2;
return {
doubleCount
};
}
}
2. ctx 的意义与作用
-
意义:
ctx提供了在setup中访问组件实例其他特性的途径,弥补了组合式 API 中this不指向组件实例的问题。 -
作用:
ctx.emit:触发组件事件,用于向父组件传递消息(替代 Vue 2 中的this.$emit)。ctx.slots:访问组件的插槽内容(替代 Vue 2 中的this.$slots)。ctx.attrs:访问组件的非props属性(即父组件传递的未在props中定义的属性,替代 Vue 2 中的this.$attrs)。
示例:
setup(props, ctx) {
// 触发事件
const handleClick = () => {
ctx.emit('change', 'new value');
};
// 访问插槽
console.log(ctx.slots.default);
// 访问非 props 属性
console.log(ctx.attrs.class);
return {
handleClick
};
}
setup 的其他参数
在 Vue 3.3+ 版本中,setup 函数支持第三个参数 expose,用于显式暴露组件的公共方法或属性,供父组件通过模板引用(ref)访问。
-
expose:- 作用:将组件内部的方法或属性暴露给父组件。
- 使用方式:调用
expose({ ... })传入需要暴露的内容。
setup(props, ctx, expose) {
const internalMethod = () => {
console.log('内部方法');
};
// 暴露给父组件
expose({
internalMethod
});
}
<template>
<Child ref="childRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue';
import Child from './Child.vue';
const childRef = ref(null);
onMounted(() => {
childRef.value.internalMethod(); // 调用子组件暴露的方法
});
</script>
setup 函数的核心作用是:
- 组织组件逻辑:将组件的响应式数据、计算属性、方法、生命周期钩子等逻辑集中管理,替代了 Vue 2 中
data、computed、methods等分散的选项。 - 访问组件特性:通过
props和ctx访问组件的属性、事件、插槽等。 - 返回公共接口:返回组件模板中需要使用的数据和方法,或通过
expose暴露给父组件。 - 集成生命周期:在
setup中可以使用onMounted、onUpdated等生命周期钩子,替代 Vue 2 中的选项式生命周期。
setup 的运用场景
1. 复杂组件逻辑拆分
当组件逻辑复杂时(例如包含大量数据处理、事件监听、异步操作等),可以将逻辑拆分成多个函数,在 setup 中组合调用,使代码结构更清晰。
import { ref, onMounted } from 'vue';
// 拆分逻辑函数
const useData = () => {
const data = ref([]);
const fetchData = async () => {
const res = await api.fetch();
data.value = res.data;
};
return { data, fetchData };
};
export default {
setup() {
const { data, fetchData } = useData();
onMounted(() => {
fetchData();
});
return { data };
}
}
2.组合式函数复用
// useCounter.js
import { ref } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const increment = () => count.value++;
const decrement = () => count.value--;
return { count, increment, decrement };
}
// 组件中使用
import { useCounter } from './useCounter';
export default {
setup() {
const { count, increment } = useCounter(10);
return { count, increment };
}
}
3.与模板引用交互
通过 ref 创建模板引用,访问 DOM 元素或子组件实例。
<template>
<input ref="inputRef" />
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const inputRef = ref(null);
onMounted(() => {
inputRef.value.focus(); // 自动聚焦输入框
});
return { inputRef };
}
}
</script>
setup(props, ctx)的参数由 Vue 自动传入,props是响应式的组件属性,ctx是包含组件上下文的普通对象。setup的核心作用是组织组件逻辑、访问组件特性、返回公共接口。- 运用场景包括复杂逻辑拆分、响应式数据定义、生命周期管理、逻辑复用、模板引用交互等。
setup是 Vue 3 组合式 API 的核心,提供了更灵活、更强大的组件逻辑组织方式。