普通视图
Vue-响应式原理深度解析(含手写源码)
Vue-常用修饰符
前言
在 Vue 开发中,修饰符(Modifiers)是指令后的一个特殊后缀(以 . 开头),它能以极简的方式帮我们处理事件冒泡、键盘监听以及复杂的双向绑定逻辑。掌握它们,能让你的模板代码既优雅又高效。
一、 事件修饰符:精准控制交互行为
事件修饰符主要用于处理 DOM 事件的细节。
-
.stop:阻止事件冒泡(调用event.stopPropagation())。 -
.prevent:阻止事件的默认行为(调用event.preventDefault())。 -
.capture:在捕获模式下触发事件监听器。 -
.self:只有当事件是从触发元素本身触发时才触发回调。 -
.once:事件只触发一次,之后自动移除监听器。 -
.passive:滚动事件的性能优化,告诉浏览器不需要等待preventDefault。
二、 键盘与鼠标修饰符:语义化监听
1. 按键修饰符
在监听键盘事件时,我们经常需要检查特定的按键,例如:<input @keyup.enter="submitForm" type="text" placeholder="按回车提交">。
-
.enter:回车键 -
.tab:Tab 键 -
.space:空格键 -
.delete:删除或退格键 -
.up/.down/.left/.right:方向键
2. 鼠标修饰符
用于限制处理程序仅响应特定的鼠标按键。
-
.left:点击鼠标左键触发。 -
.right:点击鼠标右键触发。 -
.middle:点击鼠标中键(滚轮点击)触发。
三、 v-model 修饰符:数据预处理
这些修饰符可以自动处理表单输入的数据格式。
-
.lazy: 将v-model的同步时机设置在change事件之后,一般为在输入框失去焦点时。 -
.number:自动将用户的输入值转为数值类型(内部使用parseFloat)。 -
.trim:自动过滤用户输入内容的首尾空白字符。
四、 双向绑定修饰符
这是 Vue 2 到 Vue 3 变化最大的部分。
1. Vue 2 时代的 .sync
在 Vue 2 中,.sync 是实现父子组件属性双向绑定的语法糖。
// 使用 .sync 的语法糖
<ChildComponent :title.sync="pageTitle" />
// 在子组件的方法中
this.$emit('update:title', newTitleValue);
2. Vue 3 的统一:v-model:prop
Vue 3 废弃了 .sync,将其功能合并到了 v-model 中。支持在同一个组件上绑定多个 v-model。
// 在父组件中
<ChildComponent v-model:title="pageTitle" />
// 子组件
<script setup>
defineProps(['title']);
const emit = defineEmits(['update:title']);
const updateTitle = (newVal) => {
emit('update:title', newVal);
};
</script>
3. Vue 3.4+ 的黑科技:defineModel
这是目前 Vue 3 最推荐的写法,极大简化了双向绑定的逻辑代码。
// 父组件
<ChildComponent v-model="inputValue" />
// 子组件
const inputValue = defineModel({
// inputValue为双向绑定输入框的值
type: [String],
// 默认值
default: ''
})
五、 总结
- 交互逻辑优先使用事件修饰符,减少组件内的非业务代码。
-
表单处理善用
.trim和.number,降低后端校验压力。 -
父子通信在 Vue 3 项目中全面拥抱
v-model:prop,如果是新项目(Vue 3.4+),请直接使用defineModel,它能让你的代码量减少 50% 以上。
Vue-从内置指令到自定义指令实战
前言
在 Vue 的开发世界里,“指令(Directives)”是连接模板与底层 DOM 的桥梁。除了官方提供的强大内置指令外,Vue 还允许我们根据业务需求自定义指令。本文将带你一次性梳理 Vue 指令体系,并手把手实现一个高频实用的“一键复制”指令。
一、 Vue 内置指令全家桶
在深入自定义指令之前,我们先复习一下这些每天都在用的“老朋友”。内置指令以 v- 开头,是 Vue 预设的特殊属性。
| 指令 | 作用描述 | 核心要点 | |
|---|---|---|---|
v-bind |
响应式地更新 HTML 属性 | 简写为 :,如 :src、:class
|
|
v-on |
绑定事件监听器 | 简写为 @,如 @click
|
|
v-model |
在表单及组件上创建双向绑定 | 它是 v-bind 与 v-on 的语法糖 |
|
v-if / v-else |
根据条件渲染/销毁元素 | 真正的条件渲染(销毁与重建) | |
v-show |
根据条件切换元素的显示 | 基于 CSS 的 display: none 切换 |
|
v-for |
基于源数据多次渲染元素 | 建议必须绑定唯一的 :key
|
|
v-html |
更新元素的 innerHTML
|
注意:易导致 XSS 攻击,慎用 | |
v-once |
只渲染元素和组件一次 | 随后的重新渲染将跳过该部分,用于优化性能 |
二、 自定义指令:像 v-model 一样强大
1. 核心概念
自定义指令主要用于提高代码复用性。当你发现自己在多个组件中都在操作同一个 DOM 逻辑时,就该考虑将其封装为指令了。
2. 生命周期(钩子函数)
Vue 3 重构了指令钩子,使其与组件生命周期完美对齐:
| Vue 3 钩子 | Vue 2 对应 | 执行时机 |
|---|---|---|
beforeMount |
bind |
指令第一次绑定到元素时调用 |
mounted |
inserted |
绑定元素插入父节点时调用 |
beforeUpdate |
update |
元素所在组件 VNode 更新前 |
updated |
componentUpdated |
组件及子组件全部更新后调用 |
unmounted |
unbind |
指令与元素解绑且元素已卸载 |
3. 钩子函数参数
指令对象的钩子函数中都带有如下参数:
-
el: 绑定的真实 DOM。 -
binding: 对象,包含-
name:指令名,不包括v-前缀。 -
value:指令的绑定值,例如:v-my-directive="1 + 1"中,绑定值为2。 -
oldValue:指令绑定的前一个值,仅在 ``update/beforeUpdate和componentUpdated/updated` 钩子中可用。无论值是否改变都可用。 -
expression:字符串形式的指令表达式。例如v-my-directive="1 + 1"中,表达式为"1 + 1"。 -
arg:传给指令的参数,可选。例如v-my-directive:foo中,参数为"foo"。 -
modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar中,修饰符对象为{ foo: true, bar: true }
-
-
vnode:Vue编译生成的虚拟节点 -
oldVnode:上一个虚拟节点,仅在update/beforeUpdate和componentUpdated/updated钩子中可用
三、 实战:实现“一键复制”指令 v-copy
1. 指令逻辑实现 (/libs/directives/copy.ts)
import { Directive, DirectiveBinding } from 'vue';
export const copyDirective: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
el.style.cursor = 'copy';
// 绑定点击事件
el.addEventListener('click', () => {
const textToCopy = binding.value;
if (!textToCopy) {
console.warn('v-copy: 无复制内容');
return;
}
// 现代浏览器 API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(String(textToCopy))
.then(() => alert('复制成功!'))
.catch(() => alert('复制失败'));
} else {
// 兼容降级方案
const textarea = document.createElement('textarea');
textarea.value = String(textToCopy);
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
alert('复制成功!');
} catch (err) {
console.error('复制失败', err);
}
document.body.removeChild(textarea);
}
});
}
};
2. 全局注册与使用
注册 (main.ts):
import { createApp } from 'vue';
import App from './App.vue';
import { copyDirective } from './libs/directives/copy';
const app = createApp(App);
app.directive('copy', copyDirective); // 全局注册
app.mount('#app');
使用:
<template>
<button v-copy="'这是要复制的内容'">点击复制</button>
</template>
四、 总结
-
内置指令覆盖了 90% 的开发场景,应熟练掌握其简写与区别(如
v-ifvsv-show)。 -
自定义指令是操作 DOM 的最后防线,通过
mounted和updated钩子可以实现极其灵活的逻辑。 -
注意规范:在 Vue 3 + TS 环境下,务必为指令和参数标记类型,以确保代码的健壮性。
Vue-深度解析“组件”与“插件”的区别与底层实现
前言
在 Vue 的生态系统中,“组件(Component)”和“插件(Plugin)”是构建应用的两大基石。虽然它们都承载着逻辑复用的使命,但在设计模式、注册方式和职责边界上却截然不同。本文将带你从底层原理出发,理清二者的核心差异。
一、 核心概念对比
1. 组件 (Component)
组件是 Vue 应用的最小构建单元,通常是一个 .vue 后缀的文件。
- 本质:可复用的 UI 实例。
- 职责:封装 HTML 结构、CSS 样式和 TS 交互逻辑。
2. 插件 (Plugin)
插件是用于扩展 Vue 全局功能的工具库。
-
本质:一个包含
install方法的对象或函数。 -
职责:为 Vue 添加全局方法、全局指令、全局组件或注入全局属性(如
vue-router、pinia)。
二、 关键区别总结
| 特性 | 组件 (Component) | 插件 (Plugin) |
|---|---|---|
| 功能范围 | 局部的 UI 渲染与交互 | 全局的功能扩展 |
| 代码形式 |
.vue 文件(SFC)或渲染函数 |
暴露 install 方法的 JS/TS 对象 |
| 注册方式 |
app.component() 或局部引入 |
app.use() |
| 使用场景 | 按钮、弹窗、列表等 UI 单元 | 路由管理、状态管理、全局水印指令等 |
三、 编写形式
1. 编写一个组件
组件的编写我们非常熟悉,通常使用 DefineComponent 或 <script setup>。
<template>
<button class="my-btn"><slot /></button>
</template>
<script setup lang="ts">
// 组件内部逻辑
</script>
2. 编写一个插件 (Vue 3 写法)
在 Vue 3 中,插件的 install 方法第一个参数变为 app (应用实例) ,而不再是 Vue 构造函数。
// myPlugin.ts
import type { App, Plugin } from 'vue';
export const MyPlugin: Plugin = {
install(app: App, options: any) {
// 1. 添加全局方法或属性 (通过 config.globalProperties)
app.config.globalProperties.$myGlobalMethod = () => {
console.log('执行全局方法');
};
// 2. 注册全局指令
app.directive('my-highlight', {
mounted(el: HTMLElement, binding) {
el.style.backgroundColor = binding.value || 'yellow';
}
});
// 3. 全局混入 (慎用)
app.mixin({
created() {
// console.log('插件注入的生命周期');
}
});
// 4. 注册全局组件
// app.component('GlobalComp', MyComponent);
// 5. 提供全局数据 (Provide / Inject)
app.provide('plugin-config', options);
}
};
四、 注册方式的演进
1. 组件注册
-
全局注册:
app.component('MyBtn', MyButton) -
局部注册:在父组件中直接
import导入。
2. 插件注册
在 Vue 3 中,使用应用实例的 use 方法。
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { MyPlugin } from './plugins/myPlugin';
const app = createApp(App);
// 安装插件,可以传入可选配置
app.use(MyPlugin, {
debug: true
});
app.mount('#app');
五、 总结与注意事项
-
Vue 3 的变化:Vue 3 移除了
Vue.prototype,改为使用app.config.globalProperties来挂载全局方法。 -
职责分离:如果你的代码是为了在页面上显示一段内容,请写成组件;如果你是为了给所有的组件提供某种“超能力”(如统一处理错误、多语言支持),请写成插件。
-
插件的 install 机制:
app.use内部会自动调用插件的install方法。如果插件本身就是一个函数,它也会被直接当做install函数执行。
Vue-实例从 createApp 到真实 DOM 的挂载全历程
前言
无论是 Vue 2 的 new Vue() 还是 Vue 3 的 createApp(),将组件配置转化为页面上可见的真实 DOM,中间经历了一系列复杂的转换。理解这一过程,不仅能帮我们更好地掌握生命周期,更是理解响应式原理的基础。
一、 挂载过程总览
Vue 实例的挂载过程,本质上是将组件配置转化为虚拟 DOM,最终映射为真实 DOM,并建立响应式双向绑定的过程。
二、 核心挂载步骤详解
1. 初始化阶段 (Initialization)
在 Vue 3 中,通过 createApp 开始。
-
创建实例:根据传入的根组件配置创建一个应用上下文(vue实例),接着进行数据初始化。
-
初始化数据:这是最关键的一步。Vue 会依次初始化 Props、Methods、Setup (Vue 3)、Mixins、Data、Computed。
-
校验:Vue 会校验
props和data中的变量名是否重复。 -
响应式绑定:Vue 3 使用
Proxy(Vue 2 使用Object.defineProperty)对数据进行劫持,建立依赖收集机制。
-
校验:Vue 会校验
2. 模板编译阶段 (Template Compile)
这一步将“肉眼可见”的 HTML 模板转化为机器高效执行的 JavaScript 代码。
-
解析 (Parser) :将
template字符串解析为 抽象语法树 (AST) 。 - 转换 (Transformer) :对 AST 进行静态提升、补丁标记(Patch Flags)等优化。
- 生成 (Generator) :将 AST 转换成 render 渲染函数 字符串。
3. 生成虚拟 DOM (VNode)
- Vue 调用生成的
render函数。 -
render函数根据Template执行后会返回一个 虚拟 DOM 树 (Virtual DOM) 。它是对真实 DOM 的一种轻量级 JavaScript 对象描述。
4. 挂载与 Patch (Mounting & Patching)
- 调用 Mount:执行组件的挂载方法。
- 渲染真实 DOM:渲染器(Renderer)遍历虚拟 DOM 树,递归创建真实的 HTML 元素。
-
更新页面:将生成的真实 DOM 插入到指定的容器(如
#app)中,替换掉原本的内容。
5. 完成挂载
- 一旦真实 DOM 渲染完毕,Vue 会触发
mounted(组合式 API 为onMounted)生命周期钩子,此时开发者可以安全地访问 DOM 节点。
三、 Vue 3 挂载示例
在 Vue 3 项目中,挂载通常发生在 main.ts。
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
// 1. 创建应用实例
const app = createApp(App)
// 2. 挂载到指定 DOM 容器
// 挂载过程中会执行编译、数据拦截、生成 VNode 并渲染
app.mount('#app')
四、 总结
-
AST 与 VNode 的区别:
- AST:是对 HTML 语法的描述,用于代码编译阶段。
- VNode:是对 DOM 节点的描述,用于运行时渲染和 Diff 算法。
-
双向绑定的建立时机:在
data初始化阶段,Vue 就已经通过响应式 API 拦截了数据。当render函数读取数据时,会自动触发依赖收集。 -
重新挂载:如果响应式数据发生变化,Vue 不会重新走一遍完整的挂载过程,而是通过 Diff 算法 对比新旧 VNode,仅更新发生变化的真实 DOM 部分。
Vue-路由懒加载与组件懒加载
前言
在构建大型单页应用(SPA)时,JavaScript 包体积(Bundle Size)往往会随着业务增长而膨胀,导致首屏加载缓慢、白屏时间长。懒加载(Lazy Loading) 是解决这一问题的核心方案。其本质是将代码分割成多个小的 chunk,仅在需要时才从服务器下载。
一、 路由懒加载:按需拆分页面
1. 为什么需要路由懒加载?
如果不使用懒加载,所有路由对应的组件都会被打包进同一个 app.js 中。用户访问首页时,浏览器不得不下载整个应用的逻辑,造成严重的性能浪费。
2. 实现方式:ES import()
利用动态导入语法,打包工具(如 Vite 或 Webpack)会自动进行 代码分割(Code Splitting) 。
// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
// 静态导入:会随着主包一起加载,适合首页
import Home from '@/views/Home.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// 动态导入:只有访问 /about 路径时,浏览器才会请求该组件对应的 JS 文件
component: () => import('@/views/About.vue')
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
二、 组件懒加载:细粒度控制
有些组件(如弹窗、复杂的图表、第三方重型库)并不需要在页面初次渲染时立即存在。
1. Vue 3 的 defineAsyncComponent
在 Vue 3 中,异步组件必须使用 defineAsyncComponent 进行显式声明。
示例
<template>
<div>
<h1>主页面</h1>
<button @click="showChart = true">加载并显示报表</button>
<AsyncChart v-if="showChart" />
</div>
</template>
<script setup lang="ts">
import { ref, defineAsyncComponent } from 'vue';
const showChart = ref<boolean>(false);
// 显式定义异步组件
const AsyncChart = defineAsyncComponent(() =>
import('@/components/BigChart.vue')
);
// 高级配置(可选):带加载状态
const AsyncComponentWithConfig = defineAsyncComponent({
loader: () => import('./components/MyComponent.vue'),
loadingComponent: LoadingComponent, // 加载过程中显示的组件
errorComponent: ErrorComponent, // 加载失败时显示的组件
delay: 200, // 展示加载组件前的延迟时间
timeout: 3000 // 超时时间
});
</script>
2. Vue 2 中直接使用import函数声明异步组件
export default {
components: {
// 定义一个异步组件
'MyLazyComponent': () => import('./components/MyLazyComponent.vue')
}
}
三、 底层原理与分包策略
1. 打包工具的配合
当你使用 import() 时:
-
Vite/Rollup:会自动将该组件及其依赖提取到一个独立的
.js文件中。 -
Webpack:会生成一个
chunk,你可以通过“魔法注释”自定义 chunk 的名称:const About = () => import(/* webpackChunkName: "about-group" */ './About.vue')
四、 总结
-
首屏优化:建议首页(Home)使用静态导入,而其他非核心路径、非首屏展示的弹窗/插件全部使用懒加载。
-
用户体验:使用异步组件时,建议配合
loadingComponent,避免加载过程中组件区域出现突兀的空白 。
Vue-异步更新机制与 nextTick 的底层执行逻辑
前言
在 Vue 开发中,你是否遇到过“修改了数据但立即获取 DOM 元素,拿到的却是旧值”的情况?这背后涉及 Vue 的异步更新策略。理解 nextTick,就是理解 Vue 如何与浏览器的事件循环(Event Loop)“握手”。
一、 为什么需要 nextTick?
1. 概念定义
nextTick 的核心作用是:在修改数据之后立即使用这个方法,获取更新后的 DOM。因为在vue里面当监听到我们的数据发送变化时,vue会开启一个异步更新队列,视图需要等待队列里面的所有数据变化完成后,再进行统一的更新。
2. Vue 的异步更新策略
Vue 的响应式并不是数据一变,DOM 就立刻变。
- 当数据发生变化时,Vue 会开启一个异步更新队列。
- 如果同一个 watcher 被多次触发,只会被推入队列一次(去重优化)。
- 这种机制避免了在一次同步操作中,因为多次修改数据而导致的重复渲染,极大的提高了性能。
二、 核心原理:基于事件循环(Event Loop)
nextTick 的实现逻辑紧密依赖于 JavaScript 的执行机制。
1. 任务调度逻辑
- 数据变更:修改响应式数据,Vue 将 DOM 更新任务推入一个异步队列(微任务)。
-
注册回调:调用
nextTick(callback),Vue 将该回调推入一个专用的callbacks队列。 -
执行时机:Vue 优先尝试创建一个微任务(Microtask) ,通常使用
Promise.then。如果环境不支持,则降级为宏任务(如setTimeout)。 - 顺序保证:Vue 内部通过代码执行顺序,确保 DOM 更新任务先于 nextTick 的回调任务 执行。
2. 宏任务与微任务的演进
-
优先选择:
Promise.then或MutationObserver(微任务)。 -
降级选择:如果上述不可用,则降级为宏任务
setImmediate或setTimeout(fn, 0)。
三、 使用示例:
1. 在setup中操作 DOM
在 setup 阶段,组件尚未挂载,DOM 不存在。只有在onMounted中才会创建, 所以无法直接操作,需要通过nextTick()来完成。
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue';
const message = ref<string>('初始内容');
const divRef = ref<HTMLElement | null>(null);
// 模拟 setup 阶段(相当于 Vue 2 的 created)
nextTick(() => {
// 此时 DOM 可能已挂载(取决于具体执行时机),但在 setup 同步代码中无法直接访问
console.log('setup 中的 nextTick 回调');
});
</script>
2. 数据更新后获取最新的视图信息
这是最常见的场景:例如根据动态内容计算容器高度。
<template>
<div ref="listRef" class="list">
<div v-for="item in list" :key="item">{{ item }}</div>
</div>
<button @click="addItem">新增条目</button>
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue';
const list = ref<string[]>(['Item 1', 'Item 2']);
const listRef = ref<HTMLElement | null>(null);
const addItem = async () => {
list.value.push(`Item ${list.value.length + 1}`);
// ❌ 此时获取的高度是更新前的
console.log('更新前高度:', listRef.value?.offsetHeight);
// ✅ 等待 DOM 更新
await nextTick();
// 此时可以获取到新增条目后的真实高度
console.log('更新后高度:', listRef.value?.offsetHeight);
};
</script>
四、 总结:nextTick 的“避坑”锦囊
-
同步逻辑 vs 异步逻辑:修改数据是同步的,但 DOM 变化是异步的。所有紧随数据修改后的 DOM 操作,都应该放进
nextTick。 -
Promise 语法糖:在 Vue 3 中,
nextTick返回一个 Promise。你可以使用await nextTick()代替传统的nextTick(() => { ... }),使代码更具可读性。 -
性能注意:虽然
nextTick很好用,但不要滥用。频繁的 DOM 查询依然会带来性能开销,能通过数据驱动(数据绑定)解决的问题,尽量不要手动操作 DOM。
Vue-性能优化利器:Keep-Alive
Vue-Vue2中的Mixin 混入机制
前言
在开发中,当多个组件拥有相同的逻辑(如分页、导出、权限校验)时,重复编写代码显然不够优雅。Vue 2 提供的 Mixin(混入)正是为了解决逻辑复用而生。本文将从基础用法出发,带你彻底理清 Mixin 的执行机制及其优缺点。
一、 什么是 Mixin?
Mixin 是一种灵活的分发 Vue 组件中可复用功能的方式。它本质上是一个 JS 对象,它将组件的可复用逻辑或者数据提取出来,哪个组件需要用到时,直接将提取的这部分混入到组件内部就行。类似于react和vue3中hooks。
二、 Mixin 的实战用法
1. 定义混入文件
我们通常新建一个文件(如 useUser.ts),文件中包含data、methods、created等属性(和vue文件中script部分一致),导出这个逻辑对象。
// src/mixins/index.ts
export const myMixin = {
data() {
return {
msg: "我是来自 Mixin 的数据",
};
},
created() {
console.log("执行:Mixin 中的 created 生命周期");
},
mounted() {
console.log("执行:Mixin 中的 mounted 生命周期");
},
methods: {
clickMe(): void {
console.log("执行:Mixin 中的点击事件");
},
},
};
2. 组件内引入(局部混入)
在 Vue 2 的选项式语法中通过 mixins 属性引入。
<script lang="ts">
import { defineComponent } from 'vue';
import { myMixin } from "./mixin/index";
export default defineComponent({
name: "App",
mixins: [myMixin], // 注入混入逻辑
created() {
// 此时可以正常访问 mixin 中的 msg
console.log("组件访问 Mixin 数据:", this.msg);
},
mounted() {
console.log("执行:组件自身的 mounted 生命周期");
}
});
</script>
三、 Mixin 的关键特性与优先级
在使用 Mixin 时,必须清楚其底层合并策略:
-
独立性:在多个组件中引入同一个 Mixin,各组件间的数据是不共享的。一个组件改动了 Mixin 里的数据,不会影响到其他组件。
-
生命周期合并:
- Mixin 的钩子会与组件自身的钩子合并。
- 执行顺序:Mixin 的钩子总是先于组件钩子执行。
-
冲突处理:
- 如果 Mixin 与组件定义了同名的
data属性或methods方法,组件自身的内容会覆盖 Mixin 的内容。
- 如果 Mixin 与组件定义了同名的
-
全局混入:
- 在
main.js中通过Vue.mixin()引入。这会影响之后创建的所有 Vue 实例(不推荐,容易污染全局环境)。
- 在
四、 进阶思考:Mixin 的局限性
虽然 Mixin 解决了复用问题,但在大型项目中存在明显的弊端,这也是为什么 Vue 3 转向了 Composition API (Hooks) :
- 命名冲突:多个 Mixin 混入时,容易发生变量名冲突,且难以追溯。
- 来源不明:在模板中使用一个变量,很难一眼看出它是来自哪个 Mixin,增加了维护成本。
- 隐式依赖:Mixin 之间无法方便地相互传参或共享状态。
五、 Vue 3 的更优选:组合式函数 (Hooks)
如果你正在使用 Vue 3,建议使用更现代的语法来复用逻辑:
// src/composables/useCount.ts
import { ref, onMounted } from 'vue'
export function useCount() {
const count = ref<number>(0)
const msg = ref<string>("我是 Vue 3 Hook 数据")
const increment = () => count.value++
onMounted(() => {
console.log("Hook 中的 mounted")
})
return { count, msg, increment }
}
Vue-插槽 (Slot) 的多种高级玩法
前言
在组件化开发中,插槽 (Slot) 是实现内容分发(Content Distribution)的核心机制。它允许我们将组件的“外壳”与“内容”解耦,让组件具备极高的扩展性。
一、 什么是插槽?
插槽是子组件提供给父组件的 “占位符” ,用 <slot></slot> 标签表示。父组件传递的任何模板代码(HTML、组件等)都会替换子组件中的 <slot> 标签。
二、 插槽的三大类型
1. 默认插槽 (Default Slot)
最基础的插槽,不需要定义 name 属性。
- 特点:一个子组件通常只建议使用一个默认插槽。
示例:
<!-- 子组件 -->
<template>
<div class="card">
<div class="card-title">通用卡片标题</div>
<div class="card-content">
<slot> 这里是默认的填充文本 </slot>
</div>
</div>
</template>
<!-- 父组件 -->
<template>
<div class="app">
<MyCard> 这是我传递给卡片的具体内容。 </MyCard>
</div>
</template>
2. 具名插槽 (Named Slots)
当子组件需要多个占位符时,通过 name 属性来区分。
-
语法糖:
v-slot:header可以简写为#header。
示例:
<!-- 子组件:LayoutComponent.vue -->
<template>
<div class="layout">
<header class="header">
<slot name="header"></slot>
</header>
<main class="content">
<slot></slot>
</main>
<footer class="footer">
<slot name="footer"></slot>
</footer>
</div>
</template>
<script setup lang="ts">
<!-- Vue 3 Composition API 模式下,逻辑部分可以保持简洁 -->
</script>
<!-- 父组件使用示例 -->
<template>
<LayoutComponent>
<template #header>
<h1>页面标题</h1>
<nav>导航菜单</nav>
</template>
<p>这是主体内容,将填充到默认插槽中...</p>
<template #footer>
<p>版权信息 © 2026</p>
</template>
</LayoutComponent>
</template>
<script setup lang="ts">
import LayoutComponent from './LayoutComponent.vue';
</script>
3. 作用域插槽 (Scoped Slots)
核心价值: “子传父” 的特殊形式。子组件将内部数据绑定在 <slot> 上,父组件在填充内容时可以接收并使用这些数据。
示例:
<!-- 子组件:`UserList.vue` -->
<template>
<ul>
<li v-for="user in users" :key="user.id">
<slot :user="user" :index="user.id">
{{ user.name }}
</slot>
</li>
</ul>
</template>
<script setup lang="ts">
interface User {
id: number;
name: string;
role: string;
}
const users: User[] = [
{ id: 1, name: '张三', role: '管理员' },
{ id: 2, name: '李四', role: '开发者' }
];
</script>
<!-- 父组件使用示例 -->
<template>
<UserList>
<template #default="{ user }">
<span :style="{ color: user.role === '管理员' ? 'red' : 'blue' }">
{{ user.name }} - 【{{ user.role }}】
</span>
</template>
</UserList>
</template>
三、 补充:插槽的默认内容
在子组件中,你可以在 <slot> 标签内部放置内容。如果父组件没有提供任何插槽内容,则会渲染这些“后备内容”;如果提供了,则会被覆盖。
<slot>这是如果没有内容时显示的默认文本</slot>
四、 总结:如何选择插槽?
| 插槽类型 | 使用场景 |
|---|---|
| 默认插槽 | 组件只有一个扩展点时使用。 |
| 具名插槽 | 组件有多个固定区域(如 Header/Main/Footer)需要自定义时使用。 |
| 作用域插槽 | 需要根据子组件的内部数据来决定父组件渲染样式的场景(如列表展示)。 |
Vue-Key唯一标识作用
前言
在开发 Vue 列表渲染时,编辑器总是提醒我们“必须绑定 key”。很多人习惯性地填入 index。但你是否思考过:key 到底在底层起到了什么作用?为什么不合理的 key 会导致组件状态错乱甚至性能崩溃?
一、 :key 的核心作用:虚拟 DOM 的“导航仪”
在 Vue 更新 DOM 时,其核心算法是 Diff 算法。key 的主要作用是更高效地更新虚拟 DOM。
1. 节点复用的关键
Vue 会通过判断两个节点是否为“相同节点”,从而决定是销毁重建还是原地复用。 判断相同节点的必要条件包括:
- 元素类型与Key 值 :Vue判断两个节点是否相同时,主要判断两者的key和元素类型是否相等,因此如果不设置key且元素类型相同的话,它的值就是undefined(而undefined恒等于undefined),则vue可能永远认为这是两个相同节点,只能去做更新操作,从而尝试“原地复用”它们。
提示:虚拟Dom与diff算法会在后续单独讲解
二、 为什么要绑定 Key?
1. 不带 key(原地复用策略)
当列表顺序被打乱时,Vue 不会移动 DOM 元素来匹配列表项的顺序,而是就地更新每个元素。
-
弊端:如果列表项包含有状态的子组件或受控输入框(如
<input>),原本属于 A 项的输入框内容会“残留”在 B 项的位置上,造成 UI 错乱。 - 性能:导致频繁的属性更新和 DOM 操作,效率低下。
2. 带有 key(精准匹配策略)
有了 key 作为唯一标识,Vue 能根据 key 精准找到旧节点树中对应的节点。
- 优势:Vue 会移动元素而非重新渲染,极大减少了不必要的 DOM 操作,显著提升性能。
三、为什么不推荐使用 Index 作为 Key?
这使用 index 在进行增删、排序操作时,如果在列表头部添加一个新子项时,原列表所有的子项index都会+1,这会让vue认为列表全改变了,需要全部重新生成,从而造成性能损耗。
示例:
<script setup lang="ts">
import { ref } from 'vue'
interface User {
id: number;
name: string;
}
const users = ref<User[]>([
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
])
const insertUser = () => {
// 在头部插入一条数据
users.value.unshift({ id: Date.now(), name: '新同学' })
}
</script>
<template>
<div>
<button @click="insertUser">头部插入数据</button>
<ul>
<li v-for="(item, index) in users" :key="index">
{{ item.name }} <input type="text" placeholder="输入评价" />
</li>
<hr />
<li v-for="item in users" :key="item.id">
{{ item.name }} <input type="text" placeholder="输入评价" />
</li>
</ul>
</div>
</template>
四、 总结
-
唯一性:
key必须在当前循环层级中是唯一的,不能重复。 -
稳定性:不要使用
Math.random()作为key,否则每次渲染都会强制销毁重建所有节点,性能极其低效。 -
undefined 陷阱:如果不设置
key,它的值就是undefined。在 Diff 对比时,Vue 会认为两个undefined节点是“相同”的,这正是导致频繁更新、影响性能的根源。
Vue-Computed 与 Watch 深度解读与选型指南
前言
在 Vue 的响应式世界里,computed(计算属性)和 watch(侦听器)是我们处理数据联动最常用的两把利器。虽然它们都能响应数据变化,但背后的设计哲学和应用场景却大相径庭。本文将结合 Vue 3 组合式 API 与 TypeScript,带你理清两者的本质区别。
一、 Computed:智能的“数据加工厂”
computed 的核心在它是一个计算属性。它会根据所依赖的数据动态计算结果,并具备强大的缓存机制。
1. 核心特性
- 具备缓存性:只有当它依赖的响应式数据发生变化时,才会重新计算。否则,无论多少次访问该属性,都会立即返回上次缓存的结果。
-
必须有返回值:它必须通过
return返回计算后的结果。 - 惰性求值:只有在被读取时才会执行计算。
2. Vue 3 + TS 示例
<script setup lang="ts">
import { ref, computed } from 'vue';
const count = ref<number>(1);
// computedValue1 为计算出的新属性
const computedValue1 = computed<number>(() => {
console.log('正在执行计算...'); // 只有 count 改变时才会打印
return count.value + 1;
});
</script>
<template>
<div>原值: {{ count }} | 计算值: {{ computedValue1 }}</div>
<button @click="count++">增加</button>
</template>
二、 Watch:敏锐的“数据监控员”
watch 的核心在于响应副作用。当监听的值发生改变时执行特定的回调函数。
1. 核心特性
-
无缓存性:它不是为了产生新值,而是为了在值变化时执行逻辑。
-
无返回值:回调函数中通常处理的是异步操作、修改 DOM 或更改其他状态。
-
配置灵活:
-
immediate:设置为true时,在初始化时立即执行一次。 -
deep:设置为true时,可以深度监听对象内部属性的变化。
-
2. Vue 3 + TS 示例
<script setup lang="ts">
import { ref, watch } from 'vue';
interface UserInfo {
name: string;
age: number;
}
const user = ref<UserInfo>({ name: '张三', age: 25 });
// 监听对象深度变化
watch(
user,
(newVal, oldVal) => {
// 注意:由于是引用类型,newVal 和 oldVal 指向的是同一个对象,只有开启deep: true才能监听到
console.log('用户信息变了', newVal.age);
},
{
deep: true, // 开启深度监听
immediate: false // 初始化时不立即执行
}
);
</script>
三、 扩展:Vue 3 中的 WatchEffect
在 Vue 3 中,除了 watch,还有一个更自动化的 watchEffect。
-
区别:
watchEffect不需要手动指定监听哪个属性,它会自动收集回调函数中用到的所有响应式变量。 -
场景:当你需要在一个函数里用到多个响应式数据,且不关心旧值时,
watchEffect代码更简洁。
<script setup lang="ts">
import { ref, watchEffect } from 'vue';
const user = ref({ name: '张三', age: 25 });
// watchEffect 会自动追踪依赖
watchEffect(() => {
console.log('watchEffect 监听 age:', user.value.age);
// 自动收集 user.value.age 作为依赖
// 当 age 变化时会自动执行
});
</script>
四、 深度对比:我该选哪一个?
| 特性 | Computed (计算属性) | Watch (侦听器) |
|---|---|---|
| 主要功能 | 生成一个新属性(派生状态) | 响应数据变化并执行代码(副作用) |
| 缓存 | 有缓存,依赖不变不计算 | 无缓存,变化即触发 |
| 异步 | 不支持异步逻辑 | 支持异步操作(如接口请求) |
| 代码结构 | 必须有 return
|
不需要 return
|
| 使用场景 | 格式化数据、多值组合、性能优化 | 异步数据请求、手动操作 DOM、监听路由变化 |
Vue-深度拆解 v-if 、 v-for 、 v-show
前言
在 Vue 模板开发中,指令的优先级和渲染机制直接决定了应用的性能。尤其是 v-if 与 v-for 的“爱恨情仇”,在 Vue 2 和 Vue 3 中经历了完全相反的变革。本文将带你从底层逻辑出发,看透这些指令的本质。
一、 v-if 与 v-for 的优先级之战
1. Vue 2 时代:v-for 称王
在 Vue 2 中,v-for 的优先级高于 v-if。
这意味着如果你在同一个元素上同时使用它们,Vue 会先执行循环,再对循环出的每一个项进行条件判断。
-
后果:即使
v-if为false,循环依然会完整执行,造成极大的性能浪费。
2. Vue 3 时代:v-if 反超
在 Vue 3 中,v-if 的优先级高于 v-for。
此时,如果两者并列,v-if 会先执行。但由于此时循环尚未开始,v-if 无法访问到 v-for 循环中的变量,会导致报错。
3. 最佳实践:永远不要同台竞技
无论哪个版本,永远不要把v-if和v-for同时用在同一个元素上。如果非要一起使用可以通过如下方式:
-
方案 A:外层包裹
template(推荐)如果判断条件与循环项无关,先判断再循环。
<template v-if="isShow"> <div v-for="item in items" :key="item.id">{{ item.name }}</div> </template> -
方案 B:使用计算属性
computed(推荐)如果需要根据条件过滤列表项,先过滤再循环。
<script setup lang="ts"> import { computed } from 'vue'; const activeItems = computed(() => items.value.filter(item => item.isActive)); </script> <template> <div v-for="item in activeItems" :key="item.id">{{ item.name }}</div> </template>
二、 v-if 与 v-show:隐藏背后的玄机
两者都能控制显隐,但“手段”截然不同。
1. 核心区别对照表
| 特性 | v-if | v-show |
|---|---|---|
| 手段 | 真正的数据驱动,动态添加/删除 DOM 元素 | CSS 驱动,切换 display: none 属性 |
| 本质 | 组件的销毁与重建 | 元素的显示与隐藏 |
| 初始渲染 | 若初始为 false,则完全不渲染 | 无论真假,都会渲染并保留 DOM |
| 切换消耗 | 较高(涉及生命周期与 DOM 增删) | 较低(仅改变 CSS) |
| 生命周期 | 切换时触发完整生命周期 | 不触发生命周期钩子 |
2. 生命周期触发逻辑(Vue 3 + TS 视角)
由于 v-if 是真实的销毁与重建,它会完整走一遍生命周期。
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
// 假设这是一个被 v-if 控制的子组件
onMounted(() => {
console.log('子组件已创建并挂载 (v-if 为 true)');
});
onUnmounted(() => {
console.log('子组件已卸载并销毁 (v-if 为 false)');
});
</script>
-
v-if 切换:
-
false -> true:触发onBeforeMount,onMounted等。 -
true -> false:触发onBeforeUnmount,onUnmounted等。
-
-
v-show 切换:
- 不会触发上述任何钩子,因为组件实例始终保存在内存中。
三、 总结:如何选型?
-
选择
v-show:如果元素在页面上频繁切换(如 Tab 标签、折叠面板),v-show的性能表现更优。 -
选择
v-if:如果运行条件下改变较少,或者该部分包含大量复杂的子组件,使用v-if可以保证初始渲染的轻量化,并在不需要时彻底释放内存。
Vue-Data 属性避坑指南
Vue-组件通信全攻略
前言
在 Vue 开发中,组件通信是构建复杂应用的基础。随着 Vue 3 的普及,通信方式发生了不少变化(如 defineProps 的引入、EventBus 的退场)。本文将对比 Vue 2 与 Vue 3,带你梳理最常用的 5 种通信方案。
一、 父子组件通信:最基础的单向数据流
这是最常用的通信方式,遵循“Props 向下传递,Emit 向上通知”的原则。
1. Vue 2 经典写法
-
接收:使用
props选项。 -
发送:使用
this.$emit。
2. Vue 3 + TS 标准写法
在 Vue 3 <script setup> 中,我们使用 defineProps 和 defineEmits。
父组件:Parent.vue
<template>
<ChildComponent :id="currentId" @childEvent="handleChild" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import ChildComponent from './Child.vue';
const currentId = ref<string>('001');
const handleChild = (msg: string) => {
console.log('接收到子组件消息:', msg);
};
</script>
子组件:Child.vue
<script setup lang="ts">
// 使用 TS 类型定义 Props
const props = defineProps<{
id: string
}>();
// 使用 TS 定义 Emits,具备更好的类型检查
const emit = defineEmits<{
(e: 'childEvent', args: string): void;
}>();
const sendMessage = () => {
emit('childEvent', '这是来自子组件的参数');
};
</script>
二、 跨级调用:通过 Ref 访问实例
有时父组件需要直接调用子组件的内部方法。
1. Vue 2 模式
直接通过 this.$refs.childRef.someMethod() 调用。
2. Vue 3 模式(显式暴露)
Vue 3 的组件默认是关闭的。如果父组件想访问子组件的方法,子组件必须使用 defineExpose。
子组件:Child.vue
<script setup lang="ts">
const childFunc = () => {
console.log('子组件方法被调用');
};
// 必须手动暴露,父组件才能访问
defineExpose({
childFunc
});
</script>
父组件:Parent.vue
<template>
<Child ref="childRef" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import Child from './Child.vue';
// 这里的类型定义有助于获得代码提示
const childRef = ref<InstanceType<typeof Child> | null>(null);
onMounted(() => {
childRef.value?.childFunc();
});
</script>
三、 非父子组件通信:事件总线 (EventBus)
1. Vue 2 做法
利用一个新的 Vue 实例作为中央调度器。
import Vue from 'vue';
export const EventBus = new Vue();
// 组件 A 发送
EventBus.$emit('event', data);
// 组件 B 接收
EventBus.$on('event', (data) => { ... });
2. Vue 3 重要变更
Vue 3 官方已移除了 $on、$off 和 $once 方法,因此不再支持直接通过 Vue 实例创建 EventBus。
-
官方推荐方案:使用第三方库
mitt或tiny-emitter。 -
补充:如果逻辑简单,可以使用 Vue 3 的
provide/inject实现跨级通信。
provide / inject 示例:
- 祖先组件:提供数据 (
App.vue)
<template>
<div class="ancestor">
<h1>祖先组件</h1>
<p>当前主题:{{ theme }}</p>
<Middle />
</div>
</template>
<script setup lang="ts">
import { ref, provide } from 'vue';
import Middle from './Middle.vue';
// 1. 定义响应式数据
const theme = ref<'light' | 'dark'>('light');
// 2. 定义修改数据的方法(推荐在提供者内部定义,保证数据流向清晰)
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light';
};
// 3. 注入 key 和对应的值/方法
provide('theme', theme);
provide('toggleTheme', toggleTheme);
</script>
-
中间组件:无需操作 (
Middle.vue)中间组件不需要显式接收
theme,直接透传即可 -
后代组件:注入并使用 (
DeepChild.vue)
<template>
<div class="descendant">
<h3>深层子组件</h3>
<p>接收到的主题:{{ theme }}</p>
<button @click="toggleTheme">切换主题</button>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue';
// 使用 inject 获取,第二个参数为默认值(可选)
const theme = inject('theme');
const toggleTheme = inject<() => void>('toggleTheme');
</script>
四、 集中式状态管理:Vuex 与 Pinia
当应用变得庞大,组件间的关系交织成网时,我们需要一个“单一事实来源”。
-
Vuex:Vue 2 时代的标准。基于 Mutation(同步)和 Action(异步)。
-
Pinia:Vue 3 的官方推荐。
- 优势:更完美的 TS 支持、没有 Mutation 的繁琐逻辑、极其轻量。
-
核心:
state、getters、actions。
Pinia 示例:
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
name: '张三',
age: 18
}),
actions: {
updateName(newName: string) {
this.name = newName;
}
}
});
五、 总结与纠错
-
安全性建议:在使用
defineExpose时,尽量只暴露必要的接口,遵循最小暴露原则。 -
EventBus 警示:Vue 3 开发者请注意,不要再尝试使用
new Vue()来做事件总线,应当转向 Pinia 或全局状态。
Vue-从 Vue 2 到 Vue 3:生命周期全图鉴与实战指南
前言
生命周期钩子(Lifecycle Hooks)是 Vue 组件从诞生到销毁的全过程记录。掌握生命周期,不仅能让我们在正确的时间点执行逻辑,更是优化性能、排查内存泄露的关键。
一、 生命周期四大阶段
Vue 的生命周期大体可分为:创建、挂载、更新、销毁。
二、 Vue 2 vs Vue 3 生命周期对比图
在 Vue 3 组合式 API 中,生命周期钩子需要从 vue 中导入,且命名上增加了 on 前缀。
| 阶段 | Vue 2 (选项式 API) | Vue 3 (组合式 API) | 备注 |
|---|---|---|---|
| 创建 |
beforeCreate / created
|
setup() |
Vue 3 中 setup 包含了这两个时期 |
| 挂载 |
beforeMount / mounted
|
onBeforeMount / onMounted
|
常用:操作 DOM、请求接口 |
| 更新 |
beforeUpdate / updated
|
onBeforeUpdate / onUpdated
|
响应式数据变化时触发 |
| 销毁 |
beforeDestroy / destroyed
|
onBeforeUnmount / onUnmounted
|
注意:Vue 3 中命名的变更 |
| 缓存 |
activated / deactivated
|
onActivated / onDeactivated
|
配合 <keep-alive> 使用 |
三、 详细解析与实战场景
1. 创建阶段 (Creation)
-
Vue 2 (
beforeCreate/created) :-
beforeCreate:组件实例刚在内存中被创建,此时还没有初始化好data和methods属性。适合插件开发,注入全局变量。 -
created:实例已创建,响应式数据data、methods已准备好。- 场景:最早可发起异步请求的时机。
-
-
Vue 3 (
setup) :- 在 Vue 3 中,
setup的执行早于beforeCreate,它是组合式 API 的入口。
- 在 Vue 3 中,
2. 挂载阶段 (Mounting)
-
Vue 2 (
beforeMount/mounted) :-
beforeMount:此时已经完成了模板的编译,但是还没有挂载到页面中 -
mounted:此时已经将编译好的模板挂载到了页面指定的容器中,可以访问页面中的dom了-
场景:dom已创建,可用于获取接口数据和dom元素、访问子组件
-
-
-
Vue 3 (
onBeforeMount/onMounted) :-
onBeforeMount:模板编译完成,但尚未渲染到 DOM 树中。 -
onMounted:组件已挂载,可以安全地访问 DOM 元素。- 场景:获取接口数据、初始化第三方插件(如 ECharts)、访问子组件。
-
3. 更新阶段 (Updating)
-
Vue 2 (
beforeUpdate/updated) :-
beforeUpdate:数据状态更新之前执行,此时 data 中的状态值是最新的,但是界面上显示的数据还是旧的,因为此时还没有开始重新渲染DOM节点。- 场景 :此时view层还未更新,可用于获取更新前各种状态。
-
updated:实例更新完毕之后调用,此时 data 中的状态值和界面上显示的数据,都已经完成了更新,界面已经被重新渲染好了。
-
-
Vue 3 (
onBeforeUpdate/onUpdated) :-
onBeforeUpdate:数据已更新,但 DOM 尚未重新渲染。可用于获取更新前的 DOM 状态。 -
onUpdated:DOM 已完成更新。注意:不要在此钩子中修改状态,否则可能导致死循环。
-
4. 销毁阶段 (Unmounting / Destruction)
-
Vue 2 (
beforeDestroy/destroyed) :-
beforeDestroy:实例销毁之前调用。-
场景:清理工作,如 清除定时器 (
setInterval)、解绑全局事件监听、取消订阅。
-
场景:清理工作,如 清除定时器 (
-
destroyed:Vue 实例销毁后调用。组件彻底从 DOM 中移除,所有的指令和事件监听都会被解除。
-
-
Vue 3 (
onBeforeUnmount/onUnmounted) :-
onBeforeUnmount:实例销毁之前调用。 -
onUnmounted:组件彻底从 DOM 中移除,所有的指令和事件监听都会被解除。
-
5. 缓存阶段 (Keep-alive)
如果使用了keep-alive缓存组件会新增两个生命周期函数
-
onActivated:组件进入视野,被重新激活时调用。 -
onDeactivated:组件移出视野,进入缓存状态时调用。
四、 Vue 3 + TypeScript 实战演示
以下是使用 script setup 语法编写的生命周期示例:
<template>
<div ref="container">
<h2>当前计数:{{ count }}</h2>
<button @click="count++">增加</button>
</div>
</template>
<script setup lang="ts">
import {
ref,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount
} from 'vue'
const count = ref<number>(0)
const container = ref<HTMLElement | null>(null)
let timer: number | null = null
// 挂载阶段
onMounted(() => {
console.log('Component Mounted. DOM element:', container.value)
// 模拟一个定时任务
timer = window.setInterval(() => {
console.log('Timer running...')
}, 1000)
})
// 运行阶段
onBeforeUpdate(() => {
console.log('Data updated, but DOM is not yet re-rendered.')
})
onUpdated(() => {
console.log('Data updated and DOM re-rendered.')
})
// 销毁阶段
onBeforeUnmount(() => {
console.log('Cleanup before unmount.')
if (timer) {
clearInterval(timer) // 关键:防止内存泄漏
}
})
</script>
五、 进阶:父子组件生命周期执行顺序
为了清晰起见,我们将顺序拆解为三个主要场景(vue3):
1. 初始挂载阶段
父组件必须等待所有子组件挂载完成后,才能完成自己的挂载逻辑。
-
父
setup(开始创建) -
父
onBeforeMount -
子
setup -
子
onBeforeMount -
子
onMounted(子组件渲染完毕,向上通知) -
父
onMounted(父组件接收到信号,宣布整体挂载完毕)
记忆口诀: 父创 -> 子创 -> 子挂 -> 父挂。
2. 更新阶段
当父组件传递给子组件的 props 发生变化时,更新逻辑如下:
-
父
onBeforeUpdate -
子
onBeforeUpdate -
子
onUpdated -
父
onUpdated
注意: 如果只是父组件自身的私有状态更新,且未影响到子组件,则子组件的更新钩子不会被触发。
3. 销毁阶段
销毁过程同样是“递归”式的,父组件先启动销毁,等子组件销毁完毕后,父组件正式功成身退。
-
父
onBeforeUnmount -
子
onBeforeUnmount -
子
unmounted -
父
onUnmounted
六、 Vue 3 + TS 模拟演示
你可以通过以下代码在控制台直接观察执行逻辑。
父组件 Parent.vue
<script setup lang="ts">
import { onMounted, onBeforeMount } from 'vue'
import Child from './Child.vue'
console.log('1. 父 - setup')
onBeforeMount(() => console.log('3. 父 - onBeforeMount'))
onMounted(() => console.log('8. 父 - onMounted'))
</script>
<template>
<div class="parent">
<h1>父组件</h1>
<Child />
</div>
</template>
子组件 Child.vue
<script setup lang="ts">
import { onMounted, onBeforeMount } from 'vue'
console.log('4. 子 - setup')
onBeforeMount(() => console.log('6. 子 - onBeforeMount'))
onMounted(() => console.log('7. 子 - onMounted'))
</script>
<template>
<div class="child">子组件内容</div>
</template>
📝 总结与避坑
-
接口请求放哪里?
- 如果子组件的渲染依赖父组件接口返回的数据,请在父组件的
created(Vue 2)或setup(Vue 3)中请求。 -
注意:即便你在父组件的
onMounted发请求,子组件此时也已经渲染完成了。
- 如果子组件的渲染依赖父组件接口返回的数据,请在父组件的
-
Refs 访问时机:
- 父组件想通过
ref访问子组件实例,必须在父组件的onMounted之后,因为只有这时子组件才真正挂载完成。
- 父组件想通过
-
异步组件:
- 如果子组件是异步组件(如使用
defineAsyncComponent),顺序会发生变化,父组件可能会先执行onMounted。
- 如果子组件是异步组件(如使用
React-Hooks逻辑复用艺术
前言
在 React 开发中,Hooks 的出现彻底改变了逻辑复用的方式。它让我们能够将复杂的、可复用的逻辑从 UI 组件中抽离,实现真正的“关注点分离”。本文将分享 Hooks 的核心原则,并提供 4 个在真实业务场景中封装的实战案例。
一、 Hooks 核心
1. 概念理解
Hooks 本质上是将组件间共享的逻辑抽离并封装成的特殊函数。
2. 使用“红线”:规则与原理
-
命名规范:必须以
use开头(如useChat),这不仅是约定,也是静态检查工具(ESLint)识别 Hook 的依据。 - 调用位置:严禁在循环、条件判断或嵌套函数中调用 Hook。
底层原理: React 内部并不是通过“变量名”来记录 Hook 状态的,而是通过链表 。每次渲染时,React 严格依赖 Hook 的调用顺序来查找对应的状态。
注意: 如果在
if语句中调用 Hook,一旦条件不成立导致某次渲染跳过了该 Hook,整个链表的指针就会错位,导致状态读取异常。
二、 实战:自定义 Hooks 封装
1. AI 场景:消息点赞/点踩逻辑 (useChatEvaluate)
在 AI 对话系统中,消息评价是通用功能。我们需要处理:状态切换(点赞 -> 取消点赞)、单选逻辑、以及异步接口调用。
import React, { useState } from 'react';
// 模拟接口
const public_evaluateMessage = async (params: any) => ({ data: true });
type EvaluateType = "GOOD" | "BAD" | "NONE";
export const useChatEvaluate = (initialType: EvaluateType = "NONE") => {
const [ratingType, setRatingType] = useState<EvaluateType>(initialType);
const evaluateMessage = async (contentId: number, type: "GOOD" | "BAD") => {
let newEvaluateType: EvaluateType;
// 逻辑:如果点击已选中的类型,则取消选中(NONE);否则切换到新类型
if (type === "GOOD") {
newEvaluateType = ratingType === "GOOD" ? "NONE" : "GOOD";
} else {
newEvaluateType = ratingType === "BAD" ? "NONE" : "BAD";
}
try {
const res = await public_evaluateMessage({
contentId,
ratingType: newEvaluateType,
content: "",
});
if (res.data === true) {
setRatingType(newEvaluateType);
}
} catch (error) {
console.error("评价失败:", error);
}
};
return { ratingType, evaluateMessage };
};
// 使用示例
const ChatMessage: React.FC<{ id: number }> = ({ id }) => {
const { ratingType, evaluateMessage } = useChatEvaluate();
return (
<button onClick={() => evaluateMessage(id, "GOOD")}>
{ratingType === "GOOD" ? "👍 已点赞" : "👍 点赞"}
</button>
);
};
2. 响应式布局:屏幕尺寸监听 (useMediaSize)
在响应式系统中,封装一个能根据窗口宽度自动切换“设备类型”的 Hook,可以极大地简化响应式开发。
import { useState, useEffect, useMemo } from 'react';
export enum MediaType {
mobile = 'mobile',
tablet = 'tablet',
pc = 'pc',
}
const useMediaSize = (): MediaType => {
const [width, setWidth] = useState<number>(globalThis.innerWidth);
useEffect(() => {
const handleWindowResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleWindowResize);
// 记得清理事件监听
return () => window.removeEventListener('resize', handleWindowResize);
}, []);
// 使用 useMemo 避免每次渲染都重新运行计算逻辑
const media = useMemo(() => {
if (width <= 640) return MediaType.mobile;
if (width <= 768) return MediaType.tablet;
return MediaType.pc;
}, [width]);
return media;
};
export default useMediaSize;
3. 性能优化:防抖与节流 Hook
A. 防抖 Hook (useDebounce)
常用于搜索框,防止用户快速输入时频繁触发请求。
import { useState, useEffect } from 'react';
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// 关键:在下一次 useEffect 执行前清理上一次的定时器
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;
B. 节流 Hook (useThrottle)
常用于滚动加载、窗口缩放,确保在规定时间内只执行一次。
import { useState, useEffect, useRef } from 'react';
function useThrottle<T>(value: T, delay: number): T {
const [throttledValue, setThrottledValue] = useState<T>(value);
const lastExecuted = useRef<number>(Date.now());
useEffect(() => {
const now = Date.now();
const remainingTime = delay - (now - lastExecuted.current);
if (remainingTime <= 0) {
// 立即执行
setThrottledValue(value);
lastExecuted.current = now;
} else {
// 设置定时器处理剩余时间
const timer = setTimeout(() => {
setThrottledValue(value);
lastExecuted.current = Date.now();
}, remainingTime);
return () => clearTimeout(timer);
}
}, [value, delay]);
return throttledValue;
}
export default useThrottle;
三、 总结:封装自定义 Hook 的心法
-
抽离状态而非仅逻辑:如果一段逻辑只涉及纯函数计算,不需要 Hook;只有涉及
useState或useEffect等状态管理时,才有必要封装 Hook。 - 保持纯净:自定义 Hook 应该只关心逻辑,而不应该直接操作 DOM。
-
TS 类型保护:利用泛型
<T>增强 Hook 的兼容性,让它能适配各种数据类型。
React-Scheduler 调度器如何掌控主线程?
前言
在 React 18 的并发时代,Scheduler(调度器) 是实现非阻塞渲染的幕后英雄。它不只是 React 的一个模块,更是一个通用的、高性能的 JavaScript 任务调度库。它不仅让 React 任务可以“插队”,还让“长任务”不再阻塞浏览器 UI 渲染。
一、 核心概念:什么是 Scheduler?
Scheduler 是一个独立的包,它通过与 React 协调过程(Reconciliation)的紧密配合,实现了任务的可中断、可恢复、带优先级执行。
主要职责
- 优先级管理:根据任务紧急程度(如用户点击 vs 数据预取)安排执行顺序。
- 空闲时间利用:在浏览器每一帧的空闲时间处理不紧急的任务。
- 防止主线程阻塞:通过“时间片(Time Slicing)”机制,避免长任务导致页面假死。
二、 Scheduler 的完整调度链路
当一个 setState 触发后,Scheduler 内部会经历以下精密流程:
1. 任务创建与通知
当状态更新时,React 不会立即执行 Render。它首先会创建一个 Update对象来记录这次变更,这个对象中包含这次更新所需的全部信息,例如更新后的状态值,Lane车道模型分配的任务优先级.
2. 优先级排序与队列维护
-
任务优先级排序: 创建更新后,react会调用
scheduleUpdateOnFiber函数通知scheduler调度器有个一个新的任务需要调度,这时scheduler会对该任务确定一个优先级,以及过期时间(优先级越高,过期时间越短,表示越紧急) -
队列维护: 接着
scheduler会将该任务放入到循环调度中,scheduler对于任务循环调度在内部维护着两个队列,一个是立即执行队列taskQueue和延迟任务队列timeQueue,新任务会根据优先级进入到相应对列-
timerQueue(延时任务队列) :存放还未到开始时间的任务,按开始时间排序。 -
taskQueue(立即任务队列) :存放已经就绪的任务,按过期时间排序。优先级越高,过期时间越短。
-
3. 时间片的开启:MessageChannel
将任务放入队列后,scheduler会调用requetHostCallback函数去请求浏览器在合适的时机去执行调度,该函数通过 MessageChannel对象中的port.postMessage 方法创建一个宏任务,浏览器在下一个宏任务时机触发 port.onmessage,并在这宏任务回调中启动 workLoop函数。
补充:Scheduler 会调用
requestHostCallback请求浏览器调度。它没有选择setTimeout,而是选择了MessageChannel。
为什么选 MessageChannel?
setTimeout(fn, 0)在浏览器中通常有 4ms 的最小延迟,且属于宏任务中执行时机较晚的。MessageChannel的port.postMessage产生的宏任务执行时机更早,且能更精准地在浏览器渲染帧之间切入。
4. 工作循环:workLoop
-
在宏任务回调中,调度器会进入
workLoop。它会调用performUnitOfWork函数循环地处理Fiber节点,对比新旧节点的props、state,并从队列中取出最紧急的任务交给 React 执行。 -
workLopp中会包含一个shouldYield函数中断检查函数,用于检查当前时间片是否耗尽以及是否有更高优先级的任务执行,如果有的话则会将主线程控制权交还给浏览器,以保证高优先级任务(如用户输入、动画)能及时响应。
5. 中断与恢复:shouldYield 的魔力
在 workLoop 执行过程中,每一项单元工作完成后,都会调用 shouldYield() 函数进行“路况检查”。
-
中断条件:如果当前时间片(通常为 5ms)耗尽,或者检测到有更紧急的用户交互(高优任务插队),
shouldYield返回true。 -
状态保存:此时 React 会记录当前
workInProgress树的位置,将控制权交还给浏览器。 -
任务恢复:Scheduler 会在下一个时间片通过
MessageChannel再次触发,从记录的位置继续执行,从而实现可恢复。
6. 任务插队
如果在执行一个低优先级任务时,有高优先级任务加入(如用户突然点击按钮),Scheduler会中断当前的低优任务并记录该位置,先执行高优任务。等高优任务完成后,再重新执行或继续之前的低优任务
三、 补充
-
执行时机对比:
MessageChannel确实在宏任务中非常快,但在某些极其特殊的情况下(如没有MessageChannel的旧环境),它会回退到setTimeout。 -
饥饿现象防止:如果一个低优先级任务一直被插队怎么办?Scheduler 通过过期时间解决。一旦任务过期,它会从
taskQueue中被提升为同步任务,强制执行。