当前位置: 首页 > news >正文

Vue.js从零到精通系列(三):组件化基础——Props、Emits、插槽与生命周期

摘要:组件化是Vue的灵魂,也是现代前端开发的基石。前两篇我们掌握了响应式数据和模板语法,本篇将正式踏入组件化的世界。我们将从“为什么需要组件”讲起,逐步拆解父子组件通信的两大支柱——Props(父传子)和Emits(子传父),并学习如何使用TypeScript为它们添加编译时的安全保障。随后深入Vue强大的插槽系统(默认插槽、具名插槽、作用域插槽),理解内容分发与作用域穿透的精妙设计。最后,我们将全面掌握组件生命周期的各个阶段,知道“什么时候该做什么事”。通过升级版的待办事项应用,你将亲手把这些知识整合成一个结构清晰、职责分明的组件树。


一、组件:前端开发的“乐高积木”

1.1 什么是组件?

如果把网页比作一栋房子,组件就是砖、门窗和家具。组件是一个独立、可复用、可组合的UI单元,它封装了自己的模板、逻辑和样式。当你需要一扇窗户时,不是重新画一遍,而是直接拿一个“窗户组件”装上去,并告诉它尺寸、颜色(Props),它也能告诉你何时被打开(Emits)。

1.2 为什么需要组件化?

你已经写过一个Todo应用,所有代码挤在一个App.vue里。当应用扩大到几十个页面、数百个功能时,单文件将变得不可维护。组件化带来三大好处:

  • 复用性:一个按钮组件可以在多处使用,一处修改全局更新。

  • 隔离性:每个组件的样式和逻辑互不污染,出了问题容易定位。

  • 可测试性:独立的小组件单元测试更容易编写。

1.3 Vue单文件组件的三件套

上一篇我们已经见过.vue文件,它由三个块组成:

<template> ← 结构(HTML) <script> ← 逻辑(JS/TS) <style> ← 样式(CSS)

这种高内聚的设计让你在修改一个组件时,所有相关内容都在一个文件里,不再在HTML/JS/CSS三个文件间来回跳转。


二、组件注册:全局 vs 局部

在Vue 3中,组件有两种注册方式。我们先从项目结构入手,创建一个components文件夹。

2.1 局部注册:按需引入

局部注册是推荐做法,配合<script setup>可以极其简洁地使用:

<script setup lang="ts"> import MyButton from './components/MyButton.vue' import MyInput from './components/MyInput.vue' </script> ​ <template> <MyButton /> <MyInput /> </template>

导入后,直接在模板中使用,无需任何额外注册步骤。<script setup>的编译魔法会在背后处理一切。如果未使用setup语法糖,则需要在components选项中显式注册。

组件命名规范

  • 在模板中,我们使用PascalCase(大驼峰)<MyButton>或kebab-case(短横线)<my-button>都可以。

  • 在JavaScript/TypeScript中导入时,通常用PascalCase与构造函数区分,也符合ES模块的命名习惯。

2.2 全局注册:全局可用

某些高频使用的组件(如通用图标、模态框)可以全局注册。在main.ts中:

import { createApp } from 'vue' import App from './App.vue' import GlobalIcon from './components/GlobalIcon.vue' ​ const app = createApp(App) app.component('GlobalIcon', GlobalIcon) app.mount('#app')

现在所有组件中都可以直接使用<GlobalIcon />,无需导入。但全局注册有副作用:即使不用该组件,它也会被打包进来,影响Tree Shaking。所以非必要不全局注册


三、父传子 Props:让组件“可配置”

Props是组件的输入。它定义了从父组件传递数据给子组件的接口。使用Props,同一个组件可以根据不同输入展现出不同形态。

3.1 Props的基础定义

在子组件中,使用defineProps定义props:

<!-- UserCard.vue --> <script setup lang="ts"> const props = defineProps({ name: String, age: Number, isActive: Boolean }) </script> ​ <template> <div class="user-card" :class="{ active: isActive }"> <h3>{{ name }}</h3> <p>年龄:{{ age }}</p> </div> </template>

父组件传入:

<UserCard name="张三" :age="25" :is-active="true" />

注意:非字符串类型的prop需要用v-bind(即冒号)传递,否则会当作字符串。

3.2 使用TypeScript纯类型声明

有了TypeScript,我们可以使用更简洁的纯类型语法:

<script setup lang="ts"> interface Props { name: string age?: number // 可选 isActive?: boolean tags?: string[] // 数组类型 config?: { // 对象类型 showAvatar: boolean size: 'small' | 'medium' | 'large' } } ​ const props = withDefaults(defineProps<Props>(), { age: 0, isActive: false, tags: () => [] // 数组/对象默认值必须用工厂函数 }) </script>

defineProps加上泛型<Props>,TypeScript会在编译时检查父组件传入的prop类型是否匹配。withDefaults为可选prop设置默认值,对于数组和对象,必须使用箭头函数返回,避免共享引用。

这就是静态类型的威力:当你传入错误的类型,开发环境会立刻报错,减少运行时bug。

3.3 Props的单向数据流

Vue强调单向数据流:数据从父组件流向子组件,子组件不应直接修改props。这确保了数据流的可预测性。

<script setup lang="ts"> const props = defineProps<{ count: number }>() ​ // ❌ 不要这样做! function badIncrement() { props.count++ // 报错!props是只读的 } </script>

如果子组件需要基于prop进行计算或转换,有两种正确做法:

使用计算属性做派生

import { computed } from 'vue' const doubleCount = computed(() => props.count * 2)

将prop作为本地数据的初始值

import { ref } from 'vue' const localCount = ref(props.count) // 此后操作localCount,不影响父组件

3.4 运行时类型校验

除了TypeScript的编译时检查,Vue还支持运行时校验,提供更详细的警告:

defineProps({ status: { type: String as PropType<'active' | 'inactive' | 'pending'>, required: true, validator: (value: string) => ['active', 'inactive', 'pending'].includes(value) } })

如果父组件传入非法值,控制台会输出明确的警告信息,帮助调试。


四、子传父 Emits:让组件“会说话”

Props解决父→子通信,Emits解决子→父通信。子组件通过触发事件向父组件发送消息,就像孩子喊“爸爸,按钮被点了!”

4.1 基本用法

在子组件中,使用defineEmits声明事件,然后调用emit函数:

<!-- CounterButton.vue --> <script setup lang="ts"> const emit = defineEmits(['increase', 'decrease']) ​ function handleIncrease() { emit('increase') } function handleDecrease() { emit('decrease') } </script> ​ <template> <div> <button @click="handleDecrease">-</button> <button @click="handleIncrease">+</button> </div> </template>

父组件监听事件,就像处理原生DOM事件:

<CounterButton @increase="total++" @decrease="total--" /> <p>总计:{{ total }}</p>

4.2 带参数的事件

事件可以携带数据:

<!-- 子组件 --> <script setup lang="ts"> const emit = defineEmits<{ (e: 'submit', value: string, id: number): void }>() ​ function onSubmit() { emit('submit', 'hello', 123) } </script> <!-- 父组件 --> <ChildComponent @submit="handleSubmit" /> ​ <script setup lang="ts"> function handleSubmit(value: string, id: number) { console.log(value, id) } </script>

使用TypeScript泛型声明事件,父组件的回调参数类型会自动推断,获得完整的智能提示。

4.3 v-model在组件上的双向绑定

v-model本质是props + emits的语法糖。默认情况下,组件上的v-model使用modelValue作为prop,update:modelValue作为事件。

子组件:

<script setup lang="ts"> const props = defineProps<{ modelValue: string }>() const emit = defineEmits<{ (e: 'update:modelValue', value: string): void }>() ​ function updateValue(event: Event) { const target = event.target as HTMLInputElement emit('update:modelValue', target.value) } </script> ​ <template> <input :value="modelValue" @input="updateValue" /> </template>

父组件:

<MyInput v-model="username" /> <p>用户名:{{ username }}</p>

Vue 3支持多个v-model绑定,只需指定名字:

<!-- 子组件 --> <script setup lang="ts"> defineProps<{ firstName: string; lastName: string }>() defineEmits<{ (e: 'update:firstName', val: string): void (e: 'update:lastName', val: string): void }>() </script> ​ <!-- 父组件 --> <UserForm v-model:first-name="first" v-model:last-name="last" />

这种设计让自定义组件可以像原生表单元素一样使用v-model,极大简化了表单开发。


五、插槽 Slots:内容分发魔法

有时,一个组件不仅需要数据,还需要“内容”——父组件想往子组件里塞一段HTML。插槽就是为此而生。

5.1 默认插槽

最简单的插槽,在子组件中使用<slot>标签作为占位符:

<!-- Panel.vue --> <template> <div class="panel"> <div class="panel-header">标题栏</div> <div class="panel-body"> <slot /> </div> </div> </template>

使用:

<Panel> <p>这是放入面板的内容</p> <button>确认</button> </Panel>

<Panel>标签之间的所有内容会被投射到<slot />所在位置。

5.2 默认内容

当父组件没有提供内容时,插槽可以显示默认内容:

<slot> <span class="placeholder">暂无内容</span> </slot>

5.3 具名插槽

一个组件可以有多个插槽,用name属性区分:

<!-- Card.vue --> <template> <div class="card"> <header> <slot name="header" /> </header> <main> <slot /> </main> <footer> <slot name="footer" /> </footer> </div> </template>

使用v-slot指令(简写#)指定内容插入哪个插槽:

<Card> <template #header> <h2>卡片标题</h2> </template> ​ <p>这是主体内容,默认插槽</p> ​ <template #footer> <button>操作按钮</button> </template> </Card>

注意v-slot只能用在<template>标签或组件标签上。默认插槽的隐式名称是default

5.4 作用域插槽:插槽中的数据回传

当子组件有一些数据需要暴露给父组件的插槽内容时,可以使用作用域插槽。这解决了“内容由父组件定义,但数据在子组件”的矛盾。

子组件:

<!-- List.vue --> <script setup lang="ts"> import { ref } from 'vue' ​ interface Item { id: number name: string score: number } ​ const items = ref<Item[]>([ { id: 1, name: '张三', score: 95 }, { id: 2, name: '李四', score: 82 } ]) </script> ​ <template> <ul> <li v-for="item in items" :key="item.id"> <slot name="item" :item="item" :index="item.id" /> </li> </ul> </template>

父组件使用:

<List> <template #item="{ item, index }"> <span class="rank">{{ index }}</span> <span>{{ item.name }}</span> <span :class="{ excellent: item.score >= 90 }"> {{ item.score }}分 </span> </template> </List>

子组件通过slot的属性绑定向父组件传递数据,父组件用解构语法接收。这种模式是无渲染组件的基础——只提供逻辑,不提供UI,渲染完全由父组件决定。像Vue Router的<router-link>和Element Plus的表格自定义列都大量使用了作用域插槽。


六、生命周期:组件的生老病死

每个Vue组件实例从创建到销毁,会经历一系列预定义的步骤,称为生命周期。在这些步骤的间隙,Vue暴露了生命周期钩子函数,让你可以在特定时刻注入自己的代码。

6.1 生命周期的几个阶段

用人的一生类比:

创建期(setup):组件实例初始化,响应式数据建立。

挂载期(onBeforeMount → onMounted):组件被插入到DOM树。

更新期(onBeforeUpdate → onUpdated):响应式数据变化导致重新渲染。

销毁期(onBeforeUnmount → onUnmounted):组件从DOM移除并被销毁。

6.2 常用钩子详解

setup()

组合式API的入口,在组件创建之前执行。此时组件实例尚未完全创建,无法访问this(也没有this)。这是初始化响应式数据、计算属性、侦听器的地方。由于<script setup>就是整个脚本块运行在此阶段,你通常不需要显式写setup()

onMounted

组件挂载到DOM后调用。此时可以:

  • 访问DOM元素(通过ref模板引用)

  • 发起API请求

  • 初始化第三方库(如echarts图表需要DOM容器)

<script setup lang="ts"> import { ref, onMounted } from 'vue' ​ const canvasRef = ref<HTMLCanvasElement | null>(null) ​ onMounted(() => { // canvasRef.value 现在可以安全使用 const ctx = canvasRef.value?.getContext('2d') // ... }) </script> ​ <template> <canvas ref="canvasRef" /> </template>
onBeforeUnmount / onUnmounted

组件被移除前/后调用。适合做清理工作:

import { onBeforeUnmount } from 'vue' ​ let timer: number onMounted(() => { timer = window.setInterval(() => { console.log('tick') }, 1000) }) ​ onBeforeUnmount(() => { clearInterval(timer) // 防止内存泄漏 })

清理操作至关重要,尤其是定时器、事件监听、WebSocket连接等,否则会造成内存泄漏。

onUpdated

组件重新渲染后调用。一般用于需要访问更新后DOM的场景。初次渲染不会触发。

6.3 生命周期的父子和兄弟关系

父子组件的挂载顺序:

父 setup → 父 onBeforeMount → 子 setup → 子 onBeforeMount → 子 onMounted → 父 onMounted

子组件的mounted在父组件之前完成,确保父组件挂载时子组件已经就位。

更新时:

父 onBeforeUpdate → 子 onBeforeUpdate → 子 onUpdated → 父 onUpdated

6.4 与Vue 2的差异

如果你看过Vue 2的文档,会发现钩子名称变了:

  • beforeCreate/created→ 被setup()替代

  • beforeMountonBeforeMount

  • mountedonMounted

  • beforeUpdateonBeforeUpdate

  • updatedonUpdated

  • beforeDestroyonBeforeUnmount

  • destroyedonUnmounted

统一加了on前缀,更清晰,都是从vue中导入的函数。


七、综合案例:升级待办事项组件树

让我们将之前的Todo应用重构为组件树,实践Props、Emits、插槽和生命周期。

7.1 组件结构设计

src/ ├── types.ts # 统一 TS 类型 ├── App.vue # 根组件 └── components/ ├── TodoHeader.vue # 标题 + 统计 ├── TodoInput.vue # 输入添加 ├── TodoList.vue # 列表容器(插槽) ├── TodoItem.vue # 单项 └── TodoFooter.vue # 全选 + 清理已完成

7.2 统一类型文件:types.ts

export interface Todo { id: number text: string done: boolean }

为什么要用 TS 接口?

答:给待办事项做类型约束,避免传错参数、提高代码可维护性,让编辑器有智能提示。

7.3 TodoHeader.vue

// 接收父组件数据,展示统计信息 <script setup lang="ts"> defineProps<{ activeCount: number total: number }>() </script> <template> <div class="todo-header"> <h1>📋 Vue3 Todo 组件树案例</h1> <p class="stats"> 总计 {{ total }} 项 · 未完成 {{ activeCount }} 项 </p> </div> </template> <style scoped> .todo-header { text-align: center; margin-bottom: 24px; } .todo-header h1 { font-size: 26px; font-weight: 600; color: #2d3748; margin: 0 0 8px 0; } .stats { font-size: 14px; color: #718096; margin: 0; } </style>

7.4 TodoInput.vue

<script setup lang="ts"> import { ref } from 'vue' // 输入框,定义自定义事件,输入完成后,向父组件发送数据 const text = ref('') const emit = defineEmits<{ (e: 'add', text: string): void }>() function submit() { const trimmed = text.value.trim() if (trimmed) { emit('add', trimmed) text.value = '' } } </script> <template> <div class="todo-input"> <input v-model="text" @keyup.enter="submit" placeholder="输入新任务,回车添加" /> <button @click="submit">添加</button> </div> </template> <style scoped> .todo-input { display: flex; gap: 10px; margin-bottom: 20px; } .todo-input input { flex: 1; padding: 12px 16px; border: 1px solid #e2e8f0; border-radius: 10px; outline: none; font-size: 15px; } .todo-input input:focus { border-color: #4299e1; box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.15); } .todo-input button { padding: 12px 20px; background: #4299e1; color: white; border: none; border-radius: 10px; cursor: pointer; font-weight: 500; } .todo-input button:hover { background: #3182ce; } </style>

7.5 TodoItem.vue

<script setup lang="ts"> import type { Todo } from '../types' // 单个任务,接收单个任务数据,发出切换 / 删除事件 const props = defineProps<{ todo: Todo }>() const emit = defineEmits<{ (e: 'toggle', id: number): void (e: 'remove', id: number): void }>() </script> <template> <li class="todo-item" :class="{ done: todo.done }"> <input type="checkbox" :checked="todo.done" @change="emit('toggle', todo.id)" /> <span class="text">{{ todo.text }}</span> <button class="remove-btn" @click="emit('remove', todo.id)">删除</button> </li> </template> <style scoped> .todo-item { display: flex; align-items: center; gap: 12px; padding: 14px 16px; background: #f7fafc; border-radius: 10px; } .todo-item input { width: 18px; height: 18px; accent-color: #48bb78; } .text { flex: 1; font-size: 15px; color: #2d3748; word-break: break-all; } .done .text { text-decoration: line-through; color: #a0aec0; } .remove-btn { padding: 6px 10px; background: #fef2f2; color: #dc2626; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; } .remove-btn:hover { background: #fecaca; } </style>

7.6 TodoList.vue(使用作用域插槽)

<script setup lang="ts"> import type { Todo } from '../types' defineProps<{ todos: Todo[] }>() </script> // 作用域插槽,只负责循环,不负责渲染,父组件决定如何渲染每一项,高复用、解耦 <template> <ul class="todo-list" v-if="todos.length > 0"> <slot name="item" v-for="todo in todos" :key="todo.id" :todo="todo" /> </ul> <div class="empty" v-else>暂无任务,添加一个吧 🎯</div> </template> <style scoped> .todo-list { list-style: none; padding: 0; margin: 0 0 20px 0; display: flex; flex-direction: column; gap: 10px; } .empty { text-align: center; padding: 30px 0; color: #aaa; font-size: 14px; } </style>

注意:这里使用作用域插槽将每个todo数据抛出。这种方式让TodoList成为“无渲染组件”的趋势——它只负责循环逻辑,每个项的渲染由父组件决定。当然,直接使用v-for渲染TodoItem也可以,此处为了演示插槽的高级用法。

7.7 TodoFooter.vue

<script setup lang="ts"> // 全选加清理,实现 自定义 v-model,固定语法:modelValue + update:modelValue defineProps<{ modelValue: boolean }>() const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void (e: 'clear-completed'): void }>() </script> <template> <div class="todo-footer"> <label class="toggle-all"> <input type="checkbox" :checked="modelValue" @change="emit('update:modelValue', $event.target.checked)" /> 全部{{ modelValue ? '未完成' : '完成' }} </label> <button class="clear-btn" @click="emit('clear-completed')"> 清理已完成 </button> </div> </template> <style scoped> .todo-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 16px; border-top: 1px solid #e2e8f0; font-size: 14px; color: #718096; } .toggle-all { display: flex; align-items: center; gap: 8px; cursor: pointer; } .toggle-all input { cursor: pointer; accent-color: #4299e1; } .clear-btn { background: none; border: none; color: #718096; cursor: pointer; font-size: 14px; } .clear-btn:hover { color: #dc2626; } </style>

7.8 App.vue(组装)

<script setup lang="ts"> import { ref, computed, onMounted, watch } from 'vue' import type { Todo } from './types' import TodoHeader from './components/TodoHeader.vue' import TodoInput from './components/TodoInput.vue' import TodoList from './components/TodoList.vue' import TodoItem from './components/TodoItem.vue' import TodoFooter from './components/TodoFooter.vue' // 数据 // todos:存储所有任务,ref 包裹数组 const todos = ref<Todo[]>([]) // nextId:自增 ID,无需响应式 let nextId = 1 // 计算 // 计算未完成任务数量,具有缓存,依赖变化才重新计算 const activeCount = computed(() => todos.value.filter(t => !t.done).length ) // 可写计算属性,get:获取全选状态,set:批量设置任务状态,用于实现 v-model 全选 const allDone = computed({ get: () => todos.value.length > 0 && activeCount.value === 0, set: (val: boolean) => { todos.value.forEach(t => t.done = val) } }) // 方法 // 添加任务,接收子组件 emit 的数据,push 新增任务,ID 自增 function addTodo(text: string) { todos.value.push({ id: nextId++, text, done: false }) } // 切换完成状态,根据 ID 找到任务,取反 done 状态 function toggleTodo(id: number) { const todo = todos.value.find(t => t.id === id) if (todo) todo.done = !todo.done } // 删除任务,filter 返回新数组,直接替换整个数组 → 必须用 ref function removeTodo(id: number) { todos.value = todos.value.filter(t => t.id !== id) } // 清理已完成 function clearCompleted() { todos.value = todos.value.filter(t => !t.done) } // 生命周期 + 本地存储 // 页面加载后从 localStorage 恢复数据 onMounted(() => { const saved = localStorage.getItem('vue3-todos') if (saved) { try { const list = JSON.parse(saved) as Todo[] todos.value = list nextId = list.length > 0 ? list.reduce((max, item) => Math.max(max, item.id), 0) + 1 : 1 } catch {} } }) // 自动保存 // 深度监听 todos,数据变化自动保存到本地,刷新不丢失 watch(todos, (val) => { localStorage.setItem('vue3-todos', JSON.stringify(val)) }, { deep: true }) </script> // Props 父传子,Emits 子传父,作用域插槽:子组件把数据抛给父组件,自定义 v-model <template> <div class="app"> <div class="todo-card"> <TodoHeader :active-count="activeCount" :total="todos.length" /> <TodoInput @add="addTodo" /> <TodoList :todos="todos"> <template #item="{ todo }"> <TodoItem :todo="todo" @toggle="toggleTodo" @remove="removeTodo" /> </template> </TodoList> <TodoFooter v-if="todos.length > 0" v-model="allDone" @clear-completed="clearCompleted" /> </div> </div> </template> <style scoped> .app { min-height: 100vh; display: flex; justify-content: center; align-items: center; background: linear-gradient(135deg, #f5f7fa 0%, #e4eaf5 100%); padding: 20px; } .todo-card { width: 100%; max-width: 520px; background: white; border-radius: 16px; padding: 32px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); } </style>

7.9 组件树价值总结

重构后,每个组件职责单一:

  • TodoInput只管输入与添加。

  • TodoItem只管单条待办的展示和事件触发。

  • TodoList负责列表循环,通过插槽提供灵活性。

  • TodoFooter管理全选和清理。

  • App作为总调度,持有数据,协调通信。

修改任何一个组件的样式或逻辑,不会影响其他部分。这就是组件化的核心价值。

7.10 功能测试

7.10.1 基础展示测试

页面加载 → 显示标题、统计、输入框,无任务时 → 显示「暂无任务」

7.10.2 添加任务

在输入框输入文字,点击【添加】或按回车,任务出现在列表

7.10.3 切换完成 / 未完成

点击任务复选框,文字变灰 + 加删除线,顶部未完成数量自动变化

7.10.4 删除单条任务

点击某条任务的【删除】,该条消失,数量同步更新

7.10.5 全选 / 取消全选

点击底部「全部完成 / 未完成」,所有任务一键切换状态,未完成数量变为 0

7.10.6 本地存储持久化

添加 / 删除 / 切换一些任务,刷新页面,数据依然存在,不会丢失

7.10.7 清理已完成

点击「清理已完成」,所有已完成任务被删除

7.11 问题解答

为什么要用 ref 而不是 reactive?

因为删除任务时会直接替换整个数组,reactive 包裹数组不能直接赋值,会丢失响应式,而 ref 更适合数组和基本类型。

什么是作用域插槽?

子组件把数据通过插槽抛给父组件,父组件决定如何渲染,实现了解耦和复用。

自定义组件如何实现 v-model?

子组件接收modelValue,并触发update:modelValue事件。

为什么要用 computed?

有缓存,依赖不变不会重复计算,性能更好,代码更简洁。

组件通信方式有哪些?
  • Props 父传子

  • Emits 子传父

  • 插槽

  • provide/inject

  • pinia/vuex


八、总结

本文我们正式进入了Vue组件化的世界,从组件注册、Props定义与TypeScript类型、Emits事件通信、v-model双向绑定、三种插槽机制,到生命周期的各个关键钩子,构成了组件化开发的完整知识拼图。

  • 组件是独立可复用的UI单元,.vue文件将模板、逻辑、样式内聚在一起。

  • defineProps接收父组件数据(单向数据流),withDefaults设置默认值;TypeScript泛型提供编译时类型安全。

  • defineEmits向父组件发送事件;v-model是props+emits的语法糖,支持多绑定。

  • 插槽分为默认、具名和作用域三种,作用域插槽实现“数据在子组件,渲染在父组件”的灵活模式。

  • 生命周期钩子让你在组件的不同阶段执行代码,onMounted适合DOM操作和API请求,onBeforeUnmount用于清理。

  • 组件树设计应遵循“高内聚、低耦合”,数据单向流动,通过事件反馈。


如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。

http://www.jsqmd.com/news/989705/

相关文章:

  • BoilR终极指南:多平台游戏库整合与Steam同步实战手册
  • 嵌入式通信实战:用C语言把浮点数拆成HEX-ASCII码(附完整代码)
  • 树莓派可用的MLX90614红外测温Python驱动包(Py2/Py3双支持)
  • Java实现阶乘的三种写法:for循环、while循环和递归函数源码
  • 高架桥304不锈钢防护护栏厂家选择分析:基于区域服务能力与工程适配性的多维度考察 - 优质品牌商家
  • 如何用VDesk实现Windows虚拟桌面效率翻倍:终极指南
  • 保姆级教程:在CW32L083开发板上手把手移植FreeRTOS V9.0.0(附完整源码)
  • 论文双审难题破解:兼顾重复率与AIGC检测,百考通AI实操指南
  • 3步掌握B站视频AI智能总结:用BiliTools高效提取视频精华
  • 别再硬解方程了!用Python+NumPy实现RBF曲面重建,处理百万点云也不怕
  • 终极指南:如何快速优化腾讯游戏性能的ACE-Guard资源限制器
  • 已认证微信服务号可用的三级分销H5商城PHP源码,带加粉裂变+后台一键部署指南
  • 别再只收藏了!用这197个SOTA模型源码,手把手教你复现经典论文(附保姆级环境配置)
  • 5大理由:为什么SyZOJ是算法竞赛爱好者的最佳选择
  • 深入解析MC9S12G Flash命令集:从寄存器操作到可靠嵌入式存储实践
  • 大模型辅助的数据库 Schema 设计:从业务需求到表结构的智能生成
  • Nomacs图像查看器:免费开源的终极图像管理解决方案
  • 告别官网卡顿!手把手教你用Python脚本批量下载NASA SRTM 30米DEM数据
  • 终极抖音去水印批量下载指南:3步搞定高清无水印视频
  • 从“大概还剩一半”到“精确到1%”:手把手教你配置BQ28Z610电量计与STM32通信(含电芯均衡与安全功能)
  • 深入解析MCU端口集成模块:引脚复用、路由配置与嵌入式开发实战
  • Python工程师如何选择适合自己水平的AI工程化工具链?
  • 别再死记硬背了!图解贪心算法:从排会议室到装轮船,一看就懂的思路解析
  • 车载Android设备CAN通信避坑指南:从RK3568硬件配置到应用层数据解析
  • 如何永久保存微信聊天记录?WeChatMsg完整指南帮你轻松搞定
  • FanControl:重新定义Windows散热控制的交响乐指挥家
  • 数据的加密与解密(03:15)
  • 别再只做GO/KEGG了!用GSVA给你的TCGA数据换个“打分”视角(附R代码实战)
  • 设计师和前端必看:Figma、Photoshop里那些让你困惑的RGB颜色模式到底怎么选?
  • MC9S12XE PIM模块深度解析:GPIO配置、引脚复用与工程实践指南