Vue3 基础笔记

151 阅读13分钟

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 元素(用于位置计算、样式操作等)。

fb9299e3-5f96-4597-9df0-409a98c64cbf.png

场景:

获取元素位置 / 尺寸 通过 $el.getBoundingClientRect() 可以获取组件在视口中的位置(坐标、宽高),这在拖拽、定位等交互中非常常用。

const headerRect = headerDom.getBoundingClientRect()

通过 $el 获取了头部组件的位置边界,用于判断鼠标是否点击在头部区域(从而限制只有头部可拖拽)。

直接操作 DOM 样式 / 属性 Vue 推荐通过数据绑定(如 :style:class)控制样式,但某些场景下需要直接操作 DOM 样式,此时可通过 $el 实现:

headerRef.value?.$el.style.backgroundColor = 'red'

绑定原生 DOM 事件 需要监听组件根元素的原生事件(如 scrollresize),可通过 $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(元素上边缘到视口顶部的距离)
  1. 获取元素的视口坐标以浏览器可视区域的左上角为原点((0,0)),精准定位元素的位置。这区别于相对于文档(document)的坐标(会包含滚动距离),更适合判断元素在当前可见区域的位置。
  2. 判断元素是否在可视区域内通过比较 topbottomleftright 与视口宽高,可判断元素是否完全 / 部分可见(如滚动加载、曝光统计等场景)。
  3. 限制交互范围
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; // 不在头部区域,不触发拖拽
}

“边界框”,只有当鼠标点击在这个框内时,才允许启动拖拽,避免点击窗口其他区域(如文本域)时误触发拖拽。

  1. 坐标受滚动影响由于是相对于 “视口” 的坐标,当页面滚动时,topleft 等属性会实时变化(元素位置相对于视口的位置改变)。
  2. 隐藏元素返回全 0对于 display: none 的元素,调用该方法会返回所有属性为 0 的 DOMRect 对象(因为元素未渲染到视口)。
  3. 边框和滚动条计入尺寸width 和 height 包含元素的内容区、内边距(padding)和边框(border),但不包含外边距(margin);如果元素有滚动条,滚动条宽度也会计入 width

5.props 和 emit 实现父子组件通信

props 负责父组件向子组件传递数据,emit 负责子组件向父组件传递事件(可附带数据)

props:父组件向子组件传递数据

defineProps 用于定义子组件允许接收的来自父组件的数据,相当于子组件的 “输入参数”。

const props = defineProps({
  isRunning: {
    type: Boolean,
    default: false,
    description:''
  }
})

明确规定了子组件能接收的参数名称isRunning、类型(BooleanStringArray)、默认值和描述,确保父组件传递的数据符合预期(避免类型错误)。

子组件只能通过 props 接收父组件的数据,避免了直接修改父组件数据的风险(Vue 中 props 是单向数据流,子组件不应直接修改 props 的值)。

emit:子组件向父组件传递事件

defineEmits 用于定义子组件可以向父组件触发的事件,相当于子组件的 “输出信号”,父组件通过监听这些事件来响应子组件的操作。

const emit = defineEmits({
  'update:isRunning': (value) => typeof value === 'boolean', 
})

明确规定了子组件能触发的事件名称(update:isRunningnodes)和事件参数的校验规则(如 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 事件来实现:

  1. 子组件通过 emit('update:xxx', 新值) 触发事件,告诉父组件 “需要更新 xxx 这个 props 的值”;
  2. 父组件监听 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 更新为事件传递的值,代码更简洁。

  1. 子组件通过emit('update:isRunning', 新值)主动触发事件;
  2. 父组件通过@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>
  1. 事件订阅(onReceiver 组件挂载时,通过 eventBus.on('message:send', handleMessage) 注册了对 message:send 事件的监听。当该事件被触发时,handleMessage 会被调用并接收数据。
  2. 事件发布(emitSender 组件点击按钮时,通过 eventBus.emit('message:send', 数据) 触发 message:send 事件,并将消息数据传递给所有订阅该事件的回调函数(这里即 Receiver 的 handleMessage)。
  3. 取消订阅(offReceiver 组件卸载时,通过 eventBus.off('message:send', handleMessage) 移除回调。如果不取消,当 Receiver 已销毁但 Sender 仍发送事件时,handleMessage 仍会执行(导致内存泄漏或错误)。
  • 事件名一致性:发布和订阅的事件名必须完全一致(如示例中的 message:send),否则无法通信。
  • 避免事件名冲突:推荐用 “命名空间” 风格(如 user:loginorder: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/: 这是要搜索的根目录。

*: 这是一个通配符,表示匹配该目录下的任意子目录。例如,它会匹配 buttoncardmodal 等子目录。

/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)
  }
}
  1. 提高开发效率:无需手动编写大量重复的 import 语句。当你新增一个组件或页面时,只要遵循目录规范,它就会被自动导入,无需修改其他代码。

  2. 保持代码整洁:减少了在入口文件或配置文件中大量的导入声明,使代码更干净、更易于维护。

  3. 约定优于配置:强制项目结构遵循一定的规范,有助于团队协作和项目的一致性。

  4. 不适用于动态内容:如果你的组件或模块是通过后端 API 动态生成的,或者需要根据非常灵活的规则来加载,这种静态批量导入方式就不适用了。

  5. 目录结构强依赖:代码的正确性严重依赖于文件目录结构和命名的规范性。任何不符合 * 和 index.(jsx|vue) 模式的文件都不会被正确导入。团队成员必须严格遵守约定。

  6. 可能导入多余模块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 函数的核心作用是:

  1. 组织组件逻辑:将组件的响应式数据、计算属性、方法、生命周期钩子等逻辑集中管理,替代了 Vue 2 中 datacomputedmethods 等分散的选项。
  2. 访问组件特性:通过 props 和 ctx 访问组件的属性、事件、插槽等。
  3. 返回公共接口:返回组件模板中需要使用的数据和方法,或通过 expose 暴露给父组件。
  4. 集成生命周期:在 setup 中可以使用 onMountedonUpdated 等生命周期钩子,替代 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 的核心,提供了更灵活、更强大的组件逻辑组织方式。