vue 例子

javascript 复制代码
<transition name="fade" mode="out-in">
    <component :is="tabList[currentTab].component"></component>
</transition>

//  这样  会立即执行
import TabA from './tab-a.vue'
import TabB from './tab-b.vue'

// defineAsyncComponent异步组件 这样不会立即加载
// 不会立即加载,只有组件被渲染时才去请求对应的 chunk
const TabA = defineAsyncComponent({
  loader: () => import('./tab-a.vue'),
  loadingComponent: () => null, // 我们自己处理 loading
  onError: () => {},
})

const TabB = defineAsyncComponent({
  loader: () => import('./tab-b.vue'),
  loadingComponent: () => null,
  onError: () => {},
})

const currentTab = ref(0)

const tabList = [
  { label: '用户管理', icon: 'fas fa-users', component: TabA },
  { label: '数据统计', icon: 'fas fa-chart-bar', component: TabB },
]

defineAsyncComponent + 动态 import() 就行,组件代码在真正被渲染时 才会加载和初始化。
index.vue

vue 复制代码
  <transition name="fade" mode="out-in">
       <!-- KeepAlive 缓存已加载的组件,避免来回切换重复请求 -->
         <component :is="tabList[currentTab].component"></component>
  </transition>

<script setup>
import { ref, shallowRef } from 'vue'
import { defineAsyncComponent } from 'vue'
// 不会立即加载,只有组件被渲染时才去请求对应的 chunk
const TabA = defineAsyncComponent({
  loader: () => import('./tab-a.vue'),
  loadingComponent: () => null, // 我们自己处理 loading
  onError: () => {},
})
const TabB = defineAsyncComponent({
  loader: () => import('./tab-b.vue'),
  loadingComponent: () => null,
  onError: () => {},
})
const currentTab = ref(0)
const loading = ref(false)
// 用 shallowRef 避免深层响应式代理异步组件对象
const tabList = shallowRef([
  { label: '用户管理', icon: 'fas fa-users', component: TabA },
  { label: '数据统计', icon: 'fas fa-chart-bar', component: TabB },
])
</script>

区别在哪:

写法 行为
import TabA from './tab-a.vue' 页面一打开,tab-a.vue 立刻被下载、解析、执行 ,里面的 setup 全部跑完,不管你切不切到它
defineAsyncComponent(() => import('./tab-a.vue')) 页面打开时什么都不发生 ,只有 <component :is> 实际渲染到它时,才发起网络请求加载这个 chunk,加载完才执行 setup
执行顺序:
复制代码
页面打开 → 只加载 index.vue 自身的代码
          → TabA / TabB 的代码根本没下载
点击"用户管理" → 请求 tab-a.vue 的 chunk → 下载完 → 执行 setup → 渲染
点击"数据统计" → 请求 tab-b.vue 的 chunk → 下载完 → 执行 setup → 渲染
再切回"用户管理" → KeepAlive 命中缓存,直接显示,不重新加载

shallowRef 包裹 tabList 是因为异步组件对象不需要深层响应式代理,用 shallowRef 避免不必要的开销。

这个emit是什么意思 submit email payload password 是哪来的 为啥要怎么写

javascript 复制代码
<script setup lang="ts">
const emit = defineEmits({
  submit(payload: { email: string, password: string }) {
    // 通过返回值为 `true` 还是为 `false` 来判断
    // 验证是否通过
  }
})
</script> 

先把你的代码和官方文档对应起来,再逐个词解释。

1. 这段代码到底在干嘛?

你的代码(来自官方文档):

vue 复制代码
<script setup lang="ts">
const emit = defineEmits({
  submit(payload: { email: string, password: string }) {
    // 通过返回值为 `true` 还是为 `false` 来判断
    // 验证是否通过
  }
})
</script>

对应官方文档的"事件校验"部分:对象形式的 defineEmits,其中事件对应的值是一个校验函数 ,接收 emit 时传进来的参数,返回 true/false 表示是否合法。

所以这段代码的含义:

  • "我这个组件可以触发一个叫 submit 的自定义事件。"
  • "并且,当有人 emit('submit', payload) 时,我会用这个函数检查参数 :payload 必须长这样 { email: string, password: string },你可以在函数里写更多业务校验;返回 true 表示合法,false 表示不合法。"

2. 这些名字都是哪来的?

emit

  • defineEmits 的返回值。官方说明:在 <script setup> 里,不能用 $emit,要用 defineEmits() 返回的这个函数。
  • 习惯上把它命名为 emit,只是命名约定,不是硬性要求:
    • const emit = defineEmits(...)
    • 后面在逻辑里调用:emit('submit', { email: 'a@b.com', password: '123' })

submit / email / password

  • 自定义事件名:submit
    • 完全是你自己起的,只是大家习惯表单提交叫 submit
    • 可以叫别的,比如:saveloginchange,只要和父组件 @事件名 对应即可。
    • 官方示例中也是用 submit 作为事件名。
  • 载荷里的字段名:emailpassword
    • 也完全是你自己定义的"数据结构"。
    • 校验函数的参数类型 payload: { email: string, password: string } 只是在 TS 层面约束:当这个事件被触发时,传进来的"载荷"必须长这样。
    • 真正要不要做更复杂的校验(比如非空、长度、格式),可以在那个函数里写。

payload

  • 不是 Vue 的关键字/保留字,只是一个常用变量名,表示"事件的载荷(携带的数据)"。
  • 官方文档示例里也是这么叫的:submit(payload: { email: string, password: string })
  • 你完全可以改成别的名字,比如:
ts 复制代码
const emit = defineEmits({
  submit(data: { email: string, password: string }) {
    // 这里的 data 就是事件触发时传进来的那份数据
  }
})

关键: 校验函数的参数,就是将来你在组件里调用 emit('submit', xxx) 时传的第二个参数(xxx 会被当作这个 payload 传进去)。

3. 为什么要这么写?(结合官方文档)

官方文档"事件校验"这一节明确说明了:

  • 对象形式的 defineEmits,每个事件可以配一个校验函数
  • 校验函数的参数就是抛出事件时传给 emit 的内容;
  • 返回 true 表示事件参数合法,false 表示不合法。
    你这段代码就属于这种用法,只是多了 TS 类型标注。

3.1 三个层面的作用

  1. 声明组件会触发哪些事件(文档作用)
  • 声明了 submit 事件,别人(或未来的你)一看就知道这个组件可能触发 submit
  • 官方也建议"声明所有要触发的事件",当作组件的接口文档。
  1. 带 TS 类型的事件签名
  • 类型 { email: string, password: string } 让 TS 知道:
    • 触发这个事件时,第二个参数必须是这个形状;
    • 父组件的事件处理函数参数也会被推断成这个类型。
  1. 运行时的校验函数(返回 true/false)
  • 你可以在函数里写:
    • 业务校验:比如必填、长度、格式;
    • 调试日志:不合法时 console.warn
  • 如果返回 false,Vue 会在开发模式下给你一个警告(说明这次触发的参数不合规)。

3.2 和"纯类型声明"的区别

官方还给了另一种纯类型声明写法:

ts 复制代码
// 纯类型声明(没有运行时校验,只做 TS 类型检查)
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

对比你给的对象写法:

ts 复制代码
// 带运行时校验 + 类型声明
const emit = defineEmits({
  submit(payload: { email: string, password: string }) {
    // 返回 true/false
  }
})
  • 对象形式:既有 TS 类型,又有运行时校验函数。
  • 纯类型形式:只有 TS 类型,没有运行时校验。

4. 父组件那边怎么接?

你这段代码是在子组件里声明事件。父组件那边通常长这样:

vue 复制代码
<!-- 父组件 -->
<template>
  <!-- 监听子组件的 submit 事件 -->
  <LoginForm @submit="handleSubmit" />
</template>
<script setup lang="ts">
function handleSubmit(payload: { email: string; password: string }) {
  // 这里 payload 就是子组件 emit('submit', payload) 传出来的对象
  console.log(payload.email, payload.password)
}
</script>

子组件里触发事件:

ts 复制代码
function doSubmit() {
  emit('submit', { email: 'a@b.com', password: '123' })
}

5. 小结(一问一答版)

  • emit 是什么?
    defineEmits() 返回的函数,用来触发本组件声明的自定义事件。
  • submit 是什么?
    自定义事件的名字,你自己起的,和父组件 @submit 对应即可。
  • email / password 是什么?
    你给这个事件定义的"载荷"里的字段名,表示这个事件会带一份数据,包含这两个字段。
  • payload 是什么?
    校验函数的参数名,代表"事件触发时传进来的那份数据",只是一个常用变量名,可以改名。
  • 为什么要这样写?
    为了:
    1. 声明组件会抛出哪些事件(文档化);
    2. 用 TS 标注事件参数类型;
    3. 在对象形式的写法中,还可以做运行时的参数校验(返回 true/false)。
      如果你愿意,我可以给你补一个完整的小示例:一个子组件 LoginForm.vue + 一个父组件 App.vue,用你这段 defineEmits 写法,让你直接跑起来看效果。

e: 'change', id: number 这个e 是哪来的 id 是哪来的 value是哪来的?

这三个全是你自己定义的名字,没有一个来自 Vue。

先看这段代码的本质

ts 复制代码
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

去掉 Vue 的 defineEmits<...>() 外壳,里面就是一个普通的 TS 函数重载签名

ts 复制代码
// 声明一个函数的类型:它有两种调用方式
declare function emit: {
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}

仅此而已。没有任何 Vue 的魔法。

逐个解释

e

事件名参数 。你写成 eeventnameaaa 都行:

ts 复制代码
// 这三行完全等价
(e: 'change', id: number): void
(event: 'change', id: number): void
(随便起: 'change', id: number): void

它就是一个形参名,类型是字面量 'change',意思是"当第一个参数是字符串 'change' 时,第二个参数必须是 number"。

之所以习惯用 e,因为:

  • 它是 event 的首字母
  • 只是一个短小的占位符,没人会在函数体里用到它(因为函数签名没有函数体,只是类型声明)

id / value

载荷参数 。就是将来你调用 emit() 时传的第二个参数。

你叫 id、叫 value、叫 payload、叫 data,都行,它们只是形参的名字 ,真正重要的是后面的类型numberstring)。

名字是怎么"生效"的

关键在这里------这些名字会直接影响你调用 emit 时 TS 的提示

ts 复制代码
// 你的声明
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
// ---- 调用时 ----
// ✅ 第一个参数写 'change',TS 会提示第二个参数叫 id,类型是 number
emit('change', 123)
// ❌ 类型错误:第二个参数应该是 number,你传了 string
emit('change', 'hello')  // TS 报错
// ✅ 第一个参数写 'update',TS 会提示第二个参数叫 value,类型是 string
emit('update', 'hello')
// ❌ 类型错误:第二个参数应该是 string,你传了 number
emit('update', 123)  // TS 报错

你在编辑器里敲 emit('change', 的那一刻,VSCode 会弹出提示:

复制代码
id: number

这个 id 就是你签名里写的那个名字。如果你写成了 userId,提示就会变成 userId: number

换个名字写,完全没问题

ts 复制代码
const emit = defineEmits<{
  (eventName: 'change', userId: number): void
  (eventName: 'update', newText: string): void
}>()
// 调用时 TS 提示变成 userId / newText,功能一模一样
emit('change', 123)
emit('update', 'hello')

和父组件的关系

这些名字不会传到父组件去。父组件的 handler 参数名是父组件自己起的:

vue 复制代码
<!-- 子组件:声明 -->
<script setup lang="ts">
const emit = defineEmits<{
  (e: 'change', id: number): void
}>()
</script>
<!-- 父组件:接收,参数名随便起 -->
<template>
  <Child @change="(myId) => console.log(myId)" />
  <!--                        ^^^^ 和子组件的 id 没有任何关系 -->
</template>

一句话总结

eidvalue 全是你自己写的形参名 ,不是 Vue 的关键字。它们唯一的作用是:当你在编辑器里调用 emit('事件名', ...) 时,TS 根据第一个参数匹配到对应的重载签名,然后把你写的形参名作为提示显示出来,同时检查第二个参数的类型是否正确。

相关推荐
小锋java12344 分钟前
SpringBoot 4 + Spring Security 7 + Vue3 前后端分离项目设计最佳实践
java·vue.js·spring boot
一 乐4 分钟前
校园线上招聘|基于springboot + vue校园线上招聘系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·校园线上招聘系统
yuki_uix5 分钟前
虚拟 DOM 与 Diff 算法——React 性能优化的底层逻辑
前端·react.js·面试
时寒的笔记5 分钟前
js逆向7_案例惠nong网
android·开发语言·javascript
yuki_uix6 分钟前
从输入 URL 到页面显示——浏览器工作原理全解析
前端·面试
LanceJiang12 分钟前
从输入 URL 到页面:一个 Vue 项目的“奇幻漂流”
vue.js
weixin_4080996723 分钟前
【完整教程】天诺脚本如何调用 OCR 文字识别 API?自动识别屏幕文字实战(附代码)
前端·人工智能·后端·ocr·api·天诺脚本·自动识别文字脚本
吴声子夜歌24 分钟前
ES6——Generator函数详解
前端·javascript·es6
吴声子夜歌26 分钟前
ES6——Set和Map详解
前端·javascript·es6
码喽7号1 小时前
vue学习四:Axios网络请求
前端·vue.js·学习