阅读视图
Message组件和Vue3 进阶:手动挂载组件与 Diff 算法深度解析
Vue3响应式API全指南:ref/reactive及衍生API的区别与最佳实践
Vue3基于Proxy重构了响应式系统,提供了一套灵活的API矩阵——核心的ref与reactive、浅响应式的shallowRef/shallowReactive、只读封装的readonly/shallowReadonly。这些API看似功能重叠,实则各有适配场景,误用易导致响应式失效或性能冗余。本文将从特性本质、核心区别、代码示例、适用场景四个维度,系统拆解六大API,帮你精准选型、规避踩坑。
一、核心基础:ref 与 reactive
ref和reactive是Vue3响应式开发的基石,均用于创建响应式数据,但针对的数据类型、访问方式有明确边界,是后续衍生API的设计基础。
1. 核心特性与区别
| 维度 | ref | reactive |
|---|---|---|
| 支持类型 | 基本类型(string/number/boolean等)+ 引用类型 | 仅支持引用类型(对象/数组),基本类型传入无响应式效果 |
| 实现原理 | 封装为Ref对象(含.value属性),基本类型靠Object.defineProperty拦截.value,引用类型内部调用reactive | 直接通过Proxy拦截对象的属性读取/修改,天然支持嵌套属性响应式 |
| 操作方式 | 脚本中需通过.value访问/修改,模板中自动解包(无需.value) | 脚本、模板中均直接操作属性(无.value冗余) |
| 解构特性 | 解构后丢失响应式,需用toRefs/toRef转换保留 | 直接解构失效,通过toRefs可将属性转为Ref对象维持响应式 |
| 响应式深度 | 默认深响应式(嵌套对象属性变化触发更新) | 默认深响应式(嵌套对象属性变化触发更新) |
2. 代码示例
import { ref, reactive, toRefs } from 'vue';
// ref使用:基本类型+引用类型
const count = ref(0);
count.value++; // 脚本中必须用.value
console.log(count.value); // 1
const user = ref({ name: '张三', age: 20 });
user.value.age = 21; // 嵌套属性修改,触发响应式
// reactive使用:仅引用类型
const person = reactive({ name: '李四', info: { height: 180 } });
person.name = '王五'; // 直接操作属性
person.info.height = 185; // 嵌套属性深响应式
// 解构处理
const { name, age } = toRefs(user.value); // 保留响应式
name.value = '赵六'; // 触发更新
3. 适用场景
ref:优先用于基本类型响应式(如计数器、开关状态、输入框值);单独维护单个引用类型数据(无需复杂嵌套解构);组合式API中作为默认选择,灵活性更高。
reactive:适用于复杂引用类型(如用户信息、列表数据、表单聚合状态);希望避免.value冗余,追求更直观的属性操作;组件内部状态聚合管理(相关属性封装为一个对象,可读性更强)。
二、性能优化:shallowRef 与 shallowReactive
ref和reactive的深响应式会递归处理所有嵌套属性,对大型对象/第三方实例而言,可能产生不必要的性能开销。浅响应式API仅拦截顶层数据变化,专为性能优化场景设计。
1. 核心特性与区别
| 维度 | shallowRef | shallowReactive |
|---|---|---|
| 支持类型 | 基本类型 + 引用类型(同ref) | 仅引用类型(同reactive) |
| 响应式深度 | 仅拦截.value的引用替换,嵌套属性变化不触发更新 | 仅拦截顶层属性变化,嵌套属性变化无响应式效果 |
| 更新触发 | 需替换.value引用(如shallowRef.value = 新对象);嵌套修改需用triggerRef手动触发更新 | 仅修改顶层属性触发更新,嵌套属性修改完全不拦截 |
| 使用成本 | 嵌套修改需手动触发更新,有额外编码成本 | 无需手动触发,但需牢记仅顶层响应式,易踩坑 |
2. 代码示例
import { shallowRef, shallowReactive, triggerRef } from 'vue';
// shallowRef示例
const shallowUser = shallowRef({ name: '张三', info: { age: 20 } });
shallowUser.value.info.age = 21; // 嵌套修改,无响应式
shallowUser.value = { name: '李四', info: { age: 22 } }; // 替换引用,触发更新
triggerRef(shallowUser); // 手动触发更新(嵌套修改后强制同步)
// shallowReactive示例
const shallowPerson = shallowReactive({
name: '王五',
info: { height: 180 }
});
shallowPerson.name = '赵六'; // 顶层修改,触发更新
shallowPerson.info.height = 185; // 嵌套修改,无响应式
3. 适用场景
shallowRef:引用类型数据仅需整体替换(如大型图表配置、第三方库实例、不可变数据);明确不需要嵌套属性响应式,追求极致性能(避免递归Proxy开销)。
shallowReactive:复杂对象仅需顶层属性响应式(如表单顶层状态、静态嵌套数据的配置对象);大型对象场景下,规避深响应式的性能损耗,且无需频繁修改嵌套属性。
注意:浅响应式API并非“银弹”,仅在明确不需要深层响应式时使用,否则易导致响应式失效问题,增加调试成本。
三、只读防护:readonly 与 shallowReadonly
在父子组件通信、全局常量管理等场景,需禁止数据被修改,此时可使用只读API。它们会拦截修改操作(开发环境抛警告),同时保留原数据的响应式特性(原数据变化时,只读数据同步更新)。
1. 核心特性与区别
| 维度 | readonly | shallowReadonly |
|---|---|---|
| 支持类型 | 引用类型为主(基本类型只读无实际意义) | 引用类型为主(基本类型只读无实际意义) |
| 只读深度 | 深只读:顶层+所有嵌套属性均不可修改 | 浅只读:仅顶层属性不可修改,嵌套属性可正常修改 |
| 修改拦截 | 任何层级修改均被拦截,开发环境抛警告 | 仅顶层修改被拦截,嵌套修改无拦截、无警告 |
| 响应式保留 | 保留深响应式:原数据任意层级变化,只读数据同步更新 | 保留浅响应式:原数据变化(无论层级),只读数据同步更新 |
2. 代码示例
import { readonly, shallowReadonly, reactive } from 'vue';
// 原始响应式数据
const original = reactive({
name: '张三',
info: { age: 20 }
});
// readonly示例
const readOnlyData = readonly(original);
readOnlyData.name = '李四'; // 顶层修改,被拦截(抛警告)
readOnlyData.info.age = 21; // 嵌套修改,被拦截(抛警告)
original.name = '李四'; // 原数据变化,只读数据同步更新
console.log(readOnlyData.name); // 李四
// shallowReadonly示例
const shallowReadOnlyData = shallowReadonly(original);
shallowReadOnlyData.name = '王五'; // 顶层修改,被拦截(抛警告)
shallowReadOnlyData.info.age = 22; // 嵌套修改,正常执行(无警告)
console.log(shallowReadOnlyData.info.age); // 22
3. 适用场景
readonly:完全禁止修改的响应式数据(如全局常量配置、接口返回的不可变数据);父子组件通信的Props(Vue内部默认对Props做readonly处理,防止子组件修改父组件状态);需要严格防护数据完整性的场景。
shallowReadonly:仅需禁止顶层属性修改,嵌套属性允许微调(如父组件传递给子组件的复杂对象,子组件可修改嵌套细节但不能替换整体);追求性能优化,避免深只读的递归拦截开销(大型对象场景更明显)。
四、API选型总指南与避坑要点
1. 快速选型流程图
- 明确需求:是否需要响应式?→ 不需要则直接用普通变量;需要则进入下一步。
- 数据类型:基本类型→只能用ref;引用类型→进入下一步。
- 修改权限:需要禁止修改→readonly(深防护)/shallowReadonly(浅防护);允许修改→进入下一步。
- 响应式深度:仅需顶层响应式→shallowRef/shallowReactive;需要深层响应式→ref/reactive。
- 操作习惯:避免.value→reactive;接受.value或基本类型→ref。
2. 常见坑点规避
- ref解构丢失响应式:务必用toRefs/toRef转换,而非直接解构。
- reactive传入基本类型:无响应式效果,需改用ref。
- 浅响应式嵌套修改失效:shallowRef需用triggerRef手动触发,shallowReactive避免依赖嵌套属性更新。
- readonly修改原数据:只读API仅拦截对自身的修改,原数据仍可修改,需注意数据溯源。
- ref嵌套对象修改:无需额外处理,内部已转为reactive,直接修改.value.属性即可。
五、总结
Vue3的响应式API设计围绕“灵活性”与“性能”两大核心:ref/reactive构建基础响应式能力,适配绝大多数日常场景;shallow系列API针对性优化性能,降低大型数据的响应式开销;readonly系列API保障数据安全性,适配只读场景。
核心原则是“按需选型”——无需为简单场景引入复杂API,也无需为性能牺牲开发效率。掌握各API的响应式深度、修改权限、操作方式,就能在项目中精准运用,打造高效、健壮的响应式系统。
Vue3 多主题/明暗模式切换:CSS 变量 + class 覆盖的完整工程方案(附开源代码)
文章简介
之前逛 V 站的时候刷到一个讲 JSON 格式化工具信息泄漏的帖子,有条评论说:“V 站不是人手一个工具站吗?”受此感召,我给自己做了一个工具站。
在搭建工具站的时候有做多主题、亮/暗主题切换,于是有了这篇文章。
备注:工具站当前支持的工具还不多,但已开源,也有部署在 Github page 中,文中介绍的主题切换源码也在其中,感兴趣的朋友可随意取用,后续我也会将自己要用的、感兴趣的工具集成进去。
再备注:此处介绍的多主题、模式切换是在 vue3 中实现,其他环境请感兴趣的朋友自行实现。
工具站源码地址
工具站地址:the-wind-is-rising-dev.github.io/endless-que…
实现原理
主题切换使用了 CSS 变量和 class 覆盖两种特性。
- class 覆盖特性,后加载的 class 样式会覆盖之前加载的 class 样式,变量也会被覆盖。
- CSS 变量定义时以 -- 开头,如下:
:root {
/* ========== 品牌主色调 ========== */
--brand-primary: #4f46e5; /* 主色:靛蓝 */
--brand-secondary: #0ea5e9; /* 次要色:天蓝 */
--brand-accent: #8b5cf6; /* 强调色:紫色 */
}
实现思路
- 首先在 :root 伪 class 下定义所有需要用到的变量,然后定义拥有相同变量的不同主题 class
- 切换主题时通过 document 直接设置对应主题的 class
- 跟随系统主题可以通过监听 (prefers-color-scheme: dark) 来切换
:root 伪 class 定义
源码在 src/themes/index.css 文件内,此处只贴出部分变量
:root {
/* 背景与表面色 */
--bg-primary: #f8fafc; /* 主背景 */
--bg-secondary: #ffffff; /* 次级背景/卡片 */
--bg-tertiary: #f1f5f9; /* 工具栏/三级背景 */
--bg-sidebar: #e2e8f0; /* 侧边栏背景 */
}
默认主题明亮模式 class 定义
源码在 src/themes/default/light.css 文件内,此处只贴出部分变量
html.theme-default {
/* 背景与表面色 */
--bg-primary: #f8fafc; /* 主背景 */
--bg-secondary: #ffffff; /* 次级背景/卡片 */
--bg-tertiary: #f1f5f9; /* 工具栏/三级背景 */
--bg-sidebar: #e2e8f0; /* 侧边栏背景 */
}
默认主题暗夜模式 class 定义
源码在 src/themes/default/dark.css 文件内,此处只贴出部分变量
html.theme-default.dark {
/* 背景与表面色 */
--bg-primary: #0f172a; /* 主背景 */
--bg-secondary: #1e293b; /* 次级背景/卡片 */
--bg-tertiary: #334155; /* 工具栏/三级背景 */
--bg-sidebar: #1e293b; /* 侧边栏背景 */
}
主题切换源码
源码位置:src/themes/theme.ts
切换主题后会将当前主题保存至本地,下次打开站点时会自动加载上次设置的主题
- 对象定义
- Theme:用来定义主题信息
- ThemeModel:用来定义当前模式(明亮/暗夜),以及是否跟随系统
- ThemeConfig:用来定义当前主题与模式
- 函数定义
- isDarkMode:用来判断当前系统是否为暗夜模式
- applyTheme:用来应用主题与模式
- initializeTheme:初始化主题,用来加载之前设置的主题与模式
- getCurrentThemeConfig:获取当前主题配置(主题与模式)
- addDarkListener:添加暗夜模式监听
- removeDarkListener:移除暗夜模式监听
- changeThemeMode:切换主题模式(亮/暗模式)
- changeTheme:切换主题,默认主题、星空主题、海洋主题等
- getThemeList:获取支持的主题列表 备注:主题初始化、暗夜模式监听/移除监听函数需要在主页面加载时调用、设置
// 存储主题配置的键
const THEME_STORAGE_KEY = "custom-theme";
// 主题
export interface Theme {
name: string; // 主题名称
className: string; // 对应的 CSS 类名
}
// 模式
export interface ThemeModel {
name: string; // 模式名称
followSystem: boolean; // 是否跟随系统
value: "light" | "dark"; // 模式值
}
// 主题配置
export interface ThemeConfig {
theme: Theme; // 主题
model: ThemeModel; // 默认主题模式
}
/**
* 检测当前系统是否启用暗黑模式
*/
function isDarkMode() {
return (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
);
}
/**
* 应用主题
* @param themeConfig 主题配置
*/
function applyTheme(themeConfig: ThemeConfig) {
const className = themeConfig.theme.className;
const mode = themeConfig.model;
// 移除旧的主题类
const classes = document.documentElement.className.split(" ");
const themeClasses = classes.filter(
(c) => !c.includes("theme-") && c !== "dark"
);
document.documentElement.className = themeClasses.join(" ");
// 添加新的主题类
document.documentElement.classList.add(className);
// 判断是否启用暗黑模式
if (mode.value === "dark") {
document.documentElement.classList.add("dark");
}
// 存储当前主题配置
localStorage.setItem(THEME_STORAGE_KEY, JSON.stringify(themeConfig));
}
/**
* 初始化主题
*/
export function initializeTheme() {
// 获取当前主题配置并应用
const themeConfig = getCurrentThemeConfig();
// 初始化当前主题类型
if (themeConfig.model.followSystem) {
themeConfig.model.value = isDarkMode() ? "dark" : "light";
}
applyTheme(themeConfig);
}
/**
* 获取当前主题配置
* @returns 主题配置
*/
export function getCurrentThemeConfig(): ThemeConfig {
let theme: any = localStorage.getItem(THEME_STORAGE_KEY);
return theme
? JSON.parse(theme)
: {
theme: getThemeList()[0], // 默认主题
model: {
name: "跟随系统",
followSystem: true,
value: isDarkMode() ? "dark" : "light",
},
};
}
/**
* 添加暗黑模式监听
*/
export function addDarkListener() {
// 监听暗黑模式变化, auto 模式动态切换主题
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
const themeConfig = getCurrentThemeConfig();
if (!themeConfig.model.followSystem) return;
changeThemeMode(themeConfig.model);
});
}
/**
* 移除暗黑模式监听
*/
export function removeDarkListener() {
window
.matchMedia("(prefers-color-scheme: dark)")
.removeEventListener("change", () => {});
}
/**
* 切换主题模式
* @param mode 模式
*/
export function changeThemeMode(themeModel: ThemeModel) {
const themeConfig = getCurrentThemeConfig();
themeConfig.model = themeModel;
if (themeModel.followSystem) {
themeConfig.model.value = isDarkMode() ? "dark" : "light";
}
applyTheme(themeConfig);
}
/**
* 切换主题
* @param theme 主题
*/
export function changeTheme(theme: Theme) {
const themeConfig = getCurrentThemeConfig();
themeConfig.theme = theme;
applyTheme(themeConfig);
}
/**
* 获取主题列表
* @returns 主题列表
*/
export function getThemeList(): Theme[] {
return [
{
name: "默认",
className: "theme-default",
},
{
name: "星空",
className: "theme-starry",
},
{
name: "海洋",
className: "theme-ocean",
},
];
}
主题、模式手动切换组件
源码位置:src/themes/Theme.vue
组件内会自动加载站点支持的主题与模式,也会根据系统模式变化自动切换状态信息,源码内有注释,此处不赘述
<script setup lang="ts">
import { SettingOutlined, BulbFilled } from "@ant-design/icons-vue";
import { onMounted, onUnmounted, ref } from "vue";
import {
Theme,
getThemeList,
getCurrentThemeConfig,
changeTheme,
changeThemeMode,
} from "./theme";
const themeList = ref<Theme[]>(getThemeList());
const currentTheme = ref<Theme>(getCurrentThemeConfig().theme);
const followSystem = ref<boolean>(getCurrentThemeConfig().model.followSystem);
const isLightModel = ref<boolean>(
getCurrentThemeConfig().model.value == "light"
);
// 切换主题
function onChangeTheme(theme: Theme) {
currentTheme.value = theme;
changeTheme(theme);
}
// 切换跟随系统
function onFollowSystemChange() {
followSystem.value = !followSystem.value;
let themeConfig = getCurrentThemeConfig();
themeConfig.model.followSystem = followSystem.value;
changeThemeMode(themeConfig.model);
}
// 切换主题模式
function onChangeThemeModel(value: boolean) {
isLightModel.value = value;
let themeConfig = getCurrentThemeConfig();
themeConfig.model.value = value ? "light" : "dark";
changeThemeMode(themeConfig.model);
}
// 添加主题模式监听
let interval: NodeJS.Timeout | null = null;
onMounted(() => {
// 定时更新主题信息
interval = setInterval(() => {
const themeConfig = getCurrentThemeConfig();
currentTheme.value = themeConfig.theme;
followSystem.value = themeConfig.model.followSystem;
isLightModel.value = themeConfig.model.value == "light";
}, 200);
});
onUnmounted(() => {
// 移除定时更新主题信息
interval && clearInterval(interval);
});
</script>
<template>
<div class="theme-root center">
<a-dropdown placement="bottom">
<div class="theme-btn center">
<SettingOutlined />
</div>
<template #overlay>
<a-menu>
<div
class="theme-item"
v-for="theme in themeList"
:key="theme.className"
@click="onChangeTheme(theme)"
>
<div class="row">
<div
style="width: var(--space-xl); font-size: var(--font-size-sm)"
>
<BulbFilled
class="sign"
v-if="theme.className == currentTheme.className"
/>
</div>
<div>{{ theme.name }}-主题</div>
</div>
</div>
<div class="theme-model-item row">
<a-radio
v-model:checked="followSystem"
@click="onFollowSystemChange()"
>🖥️</a-radio
>
<a-switch
checked-children="☀️"
un-checked-children="🌑"
v-model:checked="isLightModel"
:disabled="followSystem"
@change="onChangeThemeModel"
/>
</div>
</a-menu>
</template>
</a-dropdown>
</div>
</template>
<style scoped>
.theme-root {
padding: var(--space-lg);
}
.theme-btn {
padding: var(--space-xs) var(--space-lg);
font-size: var(--font-size-2xl);
color: var(--brand-primary);
}
.theme-item {
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-sm);
color: var(--text-primary);
user-select: none;
cursor: pointer;
.sign {
color: var(--brand-accent);
}
&:hover {
background: var(--brand-secondary);
color: var(--text-inverse);
}
&:active {
background: var(--brand-primary);
color: var(--text-inverse);
.sign {
color: var(--text-inverse);
}
}
}
.theme-model-item {
padding: var(--space-sm) var(--space-md);
color: var(--text-primary);
user-select: none;
}
</style>
vue main.js 文件内容
源码位置:src/main.js
该文件内需引入 "src/themes/index.css" 文件,如下
import { createApp } from "vue";
import Antd from "ant-design-vue";
import "./themes/index.css";
import App from "./App.vue";
createApp(App).use(Antd).mount("#app");
主题初始化、模式监听
源码位置:src/App.vue
src/App.vue 文件是 vue 所有的页面基础,在此处初始化主题信息、监听模式变化比较合适。
- 初始化主题样式只需要调用 src/themes/theme.ts 内的 initializeTheme() 函数即可
- 监听模式变化需要在组件挂载之后,在 onMounted 函数内调用 addDarkListener() 函数即可
- 移除监听需要在组件卸载之后,在 onUnmounted 函数内调用 removeDarkListener() 函数即可
src/App.vue 文件内 script 块部分源码如下
function initialize() {
// 初始化主题样式
initializeTheme();
}
initialize();
// 组件生命周期钩子
onMounted(() => {
initialize();
// 添加暗黑模式监听器
addDarkListener();
});
onUnmounted(() => {
// 移除暗黑模式监听器
removeDarkListener();
});
仓库地址:
这 10 个 Vue3 性能优化技巧很实用,但很多项目都没用上
今天来分享 10 个 Vue3 的性能优化技巧。
核心原则:
减少不必要的响应式追踪
避免无谓的 DOM 操作
按需加载资源
咱也不要为了优化而优化!小项目用默认写法完全没问题,优化应在性能瓶颈出现后进行。
这些技巧不难,但都非常关键。 看完你会发现:原来 Vue3 还能这么写。
1. 使用 shallowReactive 替代 reactive
问题:reactive 会让对象里每一层都变得“敏感”——哪怕你只改了最里面的某个小字段,Vue 也会花力气去追踪它。数据一大,性能就变慢。
解决方案:
对不需要深层响应的数据,使用 shallowReactive,只让最外层变成响应式的。
示例:
import { shallowReactive } from 'vue';
const data = shallowReactive({
list: [],
meta: { total: 0 }
});
适用场景:
当你从后端拿到一大坨只读数据(比如表格列表、API 响应),且不会修改嵌套属性时。
2. 用 toRefs 解构响应式对象
问题:
如果你直接从 reactive 对象里解构变量(如 const { name } = state),这个 name 就变成普通变量了,修改它不会触发页面更新。
解决方案:
使用 toRefs 解构,保持每个属性的响应性。
示例:
const state = reactive({ name: 'Vue', age: 3 });
const { name, age } = toRefs(state); // name 和 age 依然是响应式的!
好处:
在模板中可以直接写 {{ name }},不用写 {{ state.name }},代码更清爽。
3. 优先使用 watchEffect 而非 watch
区别:
-
watch:你要手动指定监听谁(比如watch(count, ...))。 -
watchEffect:你只写逻辑,Vue 自动分析里面用了哪些响应式变量,并监听它们。
示例:
watchEffect(() => {
// Vue 自动发现 count.value 被用了 → 只要 count 变,这段就执行
localStorage.setItem('count', count.value);
});
适合场景:
保存用户输入到本地缓存、根据筛选条件自动请求数据、同步状态到 URL 等。
4. 利用 <Suspense> 优雅处理异步组件
问题:
动态加载组件(如通过 import())时,页面可能白屏几秒,用户体验差。
解决方案:
用 <Suspense> 包裹异步组件,显示 loading 提示。
示例:
<Suspense>
<template #default>
<UserProfile /> <!-- 必须是异步组件 -->
</template>
<template #fallback>
<div>加载中,请稍候…</div>
</template>
</Suspense>
注意:
仅适用于异步组件(即用 defineAsyncComponent 或 () => import(...) 定义的组件)。
5. 使用 <Teleport> 解决模态框层级问题
问题:
弹窗写在组件内部,可能被父级的 overflow: hidden 或 z-index 限制,导致显示不全或盖不住其他内容。
解决方案:
用 <Teleport> 把组件“传送”到 <body> 底部,脱离当前 DOM 树。
示例:
<Teleport to="body">
<Modal v-if="show" />
</Teleport>
类比:
就像你在客厅写了个气球,但它实际飘到了天空——不受房间天花板限制。
常用目标:to="body" 是最常见用法。
6. 自定义指令封装高频操作(如复制)
问题:
复制文本、防抖点击、自动聚焦……这些功能到处都要用,每次都写一堆代码很麻烦。
解决方案:
写一个自定义指令,一次定义,处处使用。
示例:
app.directive('copy', {
mounted(el, binding) {
el.addEventListener('click', () => {
navigator.clipboard.writeText(binding.value);
});
}
});
使用:
<button v-copy="'要复制的内容'">点我复制</button>
好处:逻辑集中、复用性强、模板干净。
7. 用 Pinia 插件扩展 store 能力
问题:
每个 store 都想加个“重置”功能?手动一个个写太重复。
解决方案:
通过 Pinia 插件,一次性给所有 store 添加 $reset() 方法。
正确实现:
pinia.use(({ store }) => {
// 保存初始状态快照(深拷贝)
const initialState = JSON.parse(JSON.stringify(store.$state));
store.$reset = () => {
store.$state = initialState;
};
});
使用:
const userStore = useUserStore();
userStore.$reset(); // 恢复初始状态
适用场景:表单重置、清除缓存、统一日志等。
注意:不能直接用
store.$patch(store.$state),因为$state是当前状态,不是初始状态!
8. v-memo 优化大型列表渲染
问题:
列表有上千项,哪怕只改了一行的状态,Vue 默认会重新比对整张表,浪费性能。
解决方案:
用 v-memo 告诉 Vue:“只有这些值变了,才需要重新渲染这一行”。
示例:
<li v-for="item in list" :key="item.id" v-memo="[item.id, item.status]">
{{ item.name }} —— 状态:{{ item.status }}
</li>
注意事项:
- 适合内容稳定、更新频率低的大列表。
- 不要和
<transition-group>一起用(会失效)。 - 高频变动的列表慎用,可能适得其反。
v-memo是 Vue 3.2+ 的功能。
9. 虚拟滚动(Virtual Scrolling)
问题:
渲染 10,000 条消息?浏览器直接卡死!
解决方案:
只渲染“当前可见区域”的内容,滑动时动态替换,内存和性能都省下来。
推荐库(Vue 3 兼容):
-
vueuc(轻量、活跃维护) vue-virtual-scroll-grid
安装 & 示例(以 vueuc 为例):
npm install vueuc
<script setup>
import { VirtualList } from 'vueuc';
</script>
<template>
<VirtualList :items="messages" :item-height="60" :bench="10">
<template #default="{ item }">
<MessageItem :msg="item" />
</template>
</VirtualList>
</template>
类比:
就像微信聊天记录——你往上滑,旧消息才加载;不滑的时候,几千条其实没真画出来。
10. 路由与组件懒加载 + 图片优化
组件懒加载
原理:不是一打开网页就加载所有页面,而是“用到哪个才加载哪个”。
写法:
{ path: '/about', component: () => import('./views/About.vue') }
好处:首屏加载更快,节省流量和内存。
图片优化
- 用 WebP 格式:比 JPG/PNG 小 30%~50%,清晰度不变(现代浏览器都支持)。
- 图片懒加载:屏幕外的图先不加载,滑到附近再加载。
- 关键图预加载:首页 Banner 图提前加载,避免白块。
简单懒加载(原生支持):
<img src="image.jpg" loading="lazy" alt="示例图" />
兼容性提示:
loading="lazy"在 Chrome/Firefox/Edge 支持良好,但 Safari 15.4 以下和 IE 不支持。若需兼容旧环境,建议搭配IntersectionObserver或第三方库(如lazysizes)。
总结
| 技巧 | 解决什么问题 | 关键词 |
|---|---|---|
shallowReactive |
大对象响应式开销大 | 浅响应 |
toRefs |
解构丢失响应性 | 保持链接 |
watchEffect |
手动监听麻烦 | 自动追踪 |
<Suspense> |
异步组件白屏 | 加载提示 |
<Teleport> |
弹窗被遮挡 | 脱离 DOM |
| 自定义指令 | 重复逻辑多 | 一键复用 |
| Pinia 插件 | store 功能重复 | 全局增强 |
v-memo |
大列表重渲染 | 按需更新 |
| 虚拟滚动 | 上万条卡顿 | 只渲染可见 |
| 懒加载 + 图片优化 | 首屏慢、流量大 | 按需加载 |
先写出清晰可维护的代码,再根据实际性能问题选择合适的优化手段!
本文首发于公众号:程序员大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
解锁Vue新姿势:5种定义全局方法的实用技巧,让你的代码更优雅!
解锁Vue新姿势:5种定义全局方法的实用技巧,让你的代码更优雅!
无论你是Vue新手还是有一定经验的开发者,相信在工作中都遇到过这样的场景:多个组件需要用到同一个工具函数,比如格式化日期、权限验证、HTTP请求等。如果每个组件都单独引入,不仅代码冗余,维护起来也让人头疼。
今天我就为大家分享5种定义全局方法的实用方案,让你轻松解决这个问题!
🤔 为什么需要全局方法?
先来看一个真实的例子。假设你的项目中有三个组件都需要格式化日期:
// UserProfile.vue
methods: {
formatDate(date) {
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}
}
// OrderList.vue
methods: {
formatDate(date) {
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}
}
// Dashboard.vue
methods: {
formatDate(date) {
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}
}
发现了问题吗?同样的代码写了三遍! 这就是我们需要全局方法的原因。
📝 方案一:Vue.prototype(最经典的方式)
这是Vue 2时代最常用的方法,直接扩展Vue的原型链:
// main.js 或 plugins/global.js
import Vue from 'vue'
// 定义全局方法
Vue.prototype.$formatDate = function(date) {
const dayjs = require('dayjs')
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}
Vue.prototype.$checkPermission = function(permission) {
const user = this.$store.state.user
return user.permissions.includes(permission)
}
// 在组件中使用
export default {
mounted() {
console.log(this.$formatDate(new Date()))
if (this.$checkPermission('admin')) {
// 执行管理员操作
}
}
}
优点:
- • 使用简单,直接通过
this调用 - • 广泛支持,兼容性好
缺点:
- • 污染Vue原型链
- • 方法多了难以管理
- • TypeScript支持需要额外声明
🎯 方案二:全局混入(适合通用逻辑)
如果你有一组相关的全局方法,可以考虑使用混入:
// mixins/globalMethods.js
export default {
methods: {
$showSuccess(message) {
this.$message.success(message)
},
$showError(error) {
this.$message.error(error.message || '操作失败')
},
$confirmAction(title, content) {
return this.$confirm(content, title, {
type: 'warning'
})
}
}
}
// main.js
import Vue from 'vue'
import GlobalMixin from './mixins/globalMethods'
Vue.mixin(GlobalMixin)
// 组件中使用
export default {
methods: {
async deleteItem() {
try {
await this.$confirmAction('确认删除', '确定删除该记录吗?')
await api.deleteItem(this.id)
this.$showSuccess('删除成功')
} catch (error) {
this.$showError(error)
}
}
}
}
适合场景: UI反馈、确认对话框等通用交互逻辑。
🏗️ 方案三:独立模块 + Provide/Inject(Vue 3推荐)
Vue 3提供了更优雅的解决方案:
// utils/globalMethods.js
export const globalMethods = {
// 防抖函数
debounce(fn, delay = 300) {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
},
// 深度拷贝
deepClone(obj) {
return JSON.parse(JSON.stringify(obj))
},
// 生成唯一ID
generateId() {
return Math.random().toString(36).substr(2, 9)
}
}
// main.js
import { createApp } from 'vue'
import { globalMethods } from './utils/globalMethods'
const app = createApp(App)
// 通过provide提供给所有组件
app.provide('$global', globalMethods)
// 组件中使用
import { inject } from 'vue'
export default {
setup() {
const $global = inject('$global')
const handleInput = $global.debounce((value) => {
console.log('搜索:', value)
}, 500)
return { handleInput }
}
}
这是Vue 3的推荐方式,保持了良好的类型推断和代码组织。
📦 方案四:插件化封装(企业级方案)
对于大型项目,建议采用插件化的方式:
// plugins/globalMethods.js
const GlobalMethodsPlugin = {
install(app, options) {
// 添加全局方法
app.config.globalProperties.$http = async (url, config) => {
try {
const response = await fetch(url, config)
return await response.json()
} catch (error) {
console.error('请求失败:', error)
throw error
}
}
app.config.globalProperties.$validate = {
email(email) {
return /^[^\s@]+@[^\s@]+.[^\s@]+$/.test(email)
},
phone(phone) {
return /^1[3-9]\d{9}$/.test(phone)
}
}
// 添加全局属性
app.config.globalProperties.$appName = options?.appName || 'My App'
// 添加自定义指令
app.directive('focus', {
mounted(el) {
el.focus()
}
})
}
}
// main.js
import { createApp } from 'vue'
import GlobalMethodsPlugin from './plugins/globalMethods'
const app = createApp(App)
app.use(GlobalMethodsPlugin, {
appName: '企业管理系统'
})
// 组件中使用
export default {
mounted() {
// 使用全局方法
this.$http('/api/users')
// 使用验证
if (this.$validate.email(this.email)) {
// 邮箱有效
}
// 访问全局属性
console.log('应用名称:', this.$appName)
}
}
🌟 方案五:Composition API方式(最现代)
如果你使用Vue 3的Composition API,可以这样组织:
// composables/useGlobalMethods.js
import { readonly } from 'vue'
export function useGlobalMethods() {
// 定义所有全局方法
const methods = {
// 金额格式化
formatCurrency(amount) {
return '¥' + Number(amount).toFixed(2)
},
// 文件大小格式化
formatFileSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB']
let size = bytes
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
return `${size.toFixed(1)} ${units[unitIndex]}`
},
// 复制到剪贴板
async copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text)
return true
} catch {
// 降级方案
const textArea = document.createElement('textarea')
textArea.value = text
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
return true
}
}
}
return readonly(methods)
}
// main.js
import { createApp } from 'vue'
import { useGlobalMethods } from './composables/useGlobalMethods'
const app = createApp(App)
// 挂载到全局
app.config.globalProperties.$globalMethods = useGlobalMethods()
// 组件中使用
import { getCurrentInstance } from 'vue'
export default {
setup() {
const instance = getCurrentInstance()
const $global = instance?.appContext.config.globalProperties.$globalMethods
// 或者在setup中直接引入
// const $global = useGlobalMethods()
return { $global }
},
mounted() {
console.log(this.$global.formatCurrency(1234.56))
}
}
📊 5种方案对比总结
| 方案 | 适用版本 | 优点 | 缺点 | 推荐指数 |
|---|---|---|---|---|
| Vue.prototype | Vue 2 | 简单直接 | 污染原型链 | ⭐⭐⭐ |
| 全局混入 | Vue 2/3 | 逻辑分组 | 可能造成冲突 | ⭐⭐⭐ |
| Provide/Inject | Vue 3 | 类型安全 | 使用稍复杂 | ⭐⭐⭐⭐ |
| 插件封装 | Vue 2/3 | 功能完整 | 配置复杂 | ⭐⭐⭐⭐⭐ |
| Composition API | Vue 3 | 现代灵活 | 需要Vue 3 | ⭐⭐⭐⭐⭐ |
💡 最佳实践建议
- 1. 按功能分类组织
// 不推荐:把所有方法堆在一个文件
// 推荐:按功能模块拆分
utils/
├── formatters/ # 格式化相关
├── validators/ # 验证相关
├── http/ # 请求相关
└── ui/ # UI交互相关
- 2. 添加TypeScript支持
// global.d.ts
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$formatDate: (date: Date) => string
$checkPermission: (permission: string) => boolean
}
}
- 3. 注意性能影响
- • 避免在全局方法中执行重逻辑
- • 考虑使用懒加载
- • 及时清理不再使用的方法
- 4. 保持方法纯净
- • 一个方法只做一件事
- • 做好错误处理
- • 添加详细的JSDoc注释
🎁 福利:一个实用的全局方法库
我整理了一些常用的全局方法,你可以直接使用:
// utils/essentials.js
export const essentials = {
// 下载文件
downloadFile(url, filename) {
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},
// 获取URL参数
getUrlParam(name) {
const params = new URLSearchParams(window.location.search)
return params.get(name)
},
// 休眠函数
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
// 对象转FormData
objectToFormData(obj) {
const formData = new FormData()
Object.keys(obj).forEach(key => {
formData.append(key, obj[key])
})
return formData
}
}
✨ 结语
掌握全局方法的定义和使用,能够让你的Vue项目更加模块化、可维护、高效。不同的方案适用于不同的场景和需求,关键是要根据项目实际情况选择最合适的方式。
记住:好的代码不是写出来的,而是设计出来的。
希望今天的分享对你有帮助!如果你有更好的方案或实践经验,欢迎在评论区留言分享。
vscode 中找settings.json 配置
在VSCode中查找和配置settings.json,最快捷的方式是通过命令面板直接打开,具体操作如下:
一、快速打开settings.json的方法
方法1:命令面板(推荐)
- 按
Ctrl + Shift + P(Windows/Linux)或Cmd + Shift + P(macOS) - 输入"Preferences: Open Settings (JSON)"并回车
- 系统会直接打开当前生效的
settings.json文件(通常是用户全局设置)
方法2:设置界面跳转
- 按
Ctrl + ,打开设置UI界面 - 点击右上角的"打开设置(JSON)"图标(文件图标)
- 自动跳转到对应的JSON文件
方法3:文件路径访问
- 用户全局设置:在资源管理器中输入对应路径(Windows:
%APPDATA%\Code\User\settings.json) - 工作区设置:项目根目录下的
.vscode/settings.json(需先创建文件夹)
二、配置示例(针对"不换行整理"需求)
在打开的settings.json文件中,添加或修改以下配置:
{
// 全局格式化设置
"editor.formatOnSave": true,
"editor.wordWrap": "off",
// Prettier配置(如使用Prettier)
"prettier.printWidth": 200,
"prettier.proseWrap": "never",
// 各语言默认格式化器
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features",
"html.format.wrapLineLength": 0
}
}
三、配置注意事项
-
保存生效:修改后按
Ctrl + S保存,配置立即生效 - JSON格式:确保文件是合法的JSON格式(逗号、引号正确)
- 优先级:工作区设置(项目内)会覆盖用户全局设置
- 扩展依赖:如果配置了Prettier等格式化器,需确保已安装对应扩展
四、验证配置是否生效
- 打开一个代码文件
- 按
Ctrl + S保存,观察是否按预期格式化(不自动换行) - 或手动按
Shift + Alt + F格式化,检查效果
如果配置后仍自动换行,可能是其他扩展或配置冲突,建议检查:
- 是否安装了多个格式化扩展
- 通过命令面板"Format Document With..."查看当前使用的格式化器
- 在状态栏右下角查看当前文件使用的格式化工具
核心提示:日常使用建议通过命令面板(Ctrl+Shift+P)快速打开,这是最直接且不易出错的方式。配置时注意JSON语法正确性,保存后即可生效。
Vue项目中使用xlsx库解析Excel文件
项目中有个需求是上传Excel实现批量导入,但是解析Excel的需要前端来实现,所以用到了xlsx库
xlsx 库是一个强大的 JavaScript 库,用于处理 Excel 文件,支持:
- 读取
.xls和.xlsx格式 - 写入 Excel 文件
- 解析工作表数据
- 支持多种数据格式转换
在项目中安装 xlsx 库:
npm install xlsx
# 或者使用 yarn
yarn add xlsx
# 或者使用 pnpm
pnpm add xlsx
核心 API
import * as XLSX from 'xlsx';
// 主要方法
XLSX.read(data, options) // 读取 Excel 数据
XLSX.readFile(filename) // 从文件读取
XLSX.utils.sheet_to_json() // 工作表转 JSON
XLSX.utils.sheet_to_csv() // 工作表转 CSV
XLSX.utils.sheet_to_html() // 工作表转 HTML
Excel 文件读取与解析
1. 使用 FileReader 读取文件
在浏览器环境中,我们需要使用 FileReader API 来读取用户上传的文件:
const readExcelFile = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
// 读取文件内容
const data = new Uint8Array(e.target.result);
resolve(data);
} catch (error) {
reject(new Error('文件读取失败'));
}
};
reader.onerror = () => {
reject(new Error('文件读取失败'));
};
// 以 ArrayBuffer 格式读取文件
reader.readAsArrayBuffer(file);
});
};
2. 解析 Excel 文件
使用 XLSX.read() 方法解析 Excel 数据:
const parseExcelData = (data) => {
// 读取 Excel 工作簿
const workbook = XLSX.read(data, { type: 'array' });
// 获取所有工作表名称
const sheetNames = workbook.SheetNames;
console.log('工作表名称:', sheetNames);
// 获取第一个工作表
const firstSheetName = sheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
// 将工作表转换为 JSON
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
return {
workbook,
worksheet,
jsonData,
sheetNames
};
};
3. 不同数据格式的转换
// 转换为 JSON 对象(带表头)
const jsonWithHeaders = XLSX.utils.sheet_to_json(worksheet);
// 转换为 JSON 数组(不带表头)
const jsonArray = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
// 转换为 CSV 字符串
const csvString = XLSX.utils.sheet_to_csv(worksheet);
// 转换为 HTML 表格
const htmlString = XLSX.utils.sheet_to_html(worksheet);
表头验证与数据提取
1. 验证表头格式
在实际应用中,我们通常需要验证 Excel 文件的表头是否符合预期格式:
const validateExcelHeaders = (jsonData, requiredHeaders) => {
if (jsonData.length === 0) {
throw new Error('Excel文件为空');
}
// 获取表头行(第一行)
const headers = jsonData[0].map(header =>
header ? header.toString().trim() : ''
);
// 检查必需表头
const missingHeaders = requiredHeaders.filter(header =>
!headers.includes(header)
);
if (missingHeaders.length > 0) {
throw new Error(`缺少必需表头: ${missingHeaders.join(', ')}`);
}
return headers;
};
2. 提取数据行
const extractDataRows = (jsonData, headers) => {
// 跳过表头行(第一行)
const dataRows = jsonData.slice(1);
return dataRows.map((row, rowIndex) => {
const rowData = {};
headers.forEach((header, colIndex) => {
rowData[header] = row[colIndex] || '';
});
return {
...rowData,
_rowNumber: rowIndex + 2 // Excel 行号(从1开始,表头为第1行)
};
}).filter(row => {
// 过滤空行(所有单元格都为空)
return Object.values(row).some(value =>
value !== '' && value !== undefined && value !== null
);
});
};
3. 数据验证与清洗
const validateAndCleanData = (dataRows, validationRules) => {
const errors = [];
const cleanedData = [];
dataRows.forEach((row, index) => {
const rowErrors = [];
// 检查每个字段
Object.keys(validationRules).forEach(field => {
const value = row[field];
const rules = validationRules[field];
// 必填验证
if (rules.required && (!value || value.toString().trim() === '')) {
rowErrors.push(`${field} 不能为空`);
}
// 类型验证
if (value && rules.type) {
if (rules.type === 'number' && isNaN(Number(value))) {
rowErrors.push(`${field} 必须是数字`);
}
if (rules.type === 'email' && !isValidEmail(value)) {
rowErrors.push(`${field} 格式不正确`);
}
}
// 枚举值验证
if (value && rules.enum && !rules.enum.includes(value)) {
rowErrors.push(`${field} 必须是以下值之一: ${rules.enum.join(', ')}`);
}
});
if (rowErrors.length === 0) {
cleanedData.push(row);
} else {
errors.push({
row: row._rowNumber,
errors: rowErrors
});
}
});
return { cleanedData, errors };
};