阅读视图
闭包实际项目中应用场景有哪些举例
什么是闭包&&举例
在编程中,闭包(Closure) 是指一个函数能够 “记住” 其定义时所处的作用域(即使该作用域已经销毁),并可以访问和操作该作用域中的变量。简单来说,闭包是 “函数 + 其捆绑的周边状态(词法环境)” 的组合。
核心特点:
- 函数嵌套:闭包通常由嵌套函数实现,内部函数引用了外部函数的变量。
- 作用域保留:外部函数执行结束后,其作用域不会被销毁(因为内部函数仍在引用其中的变量)。
- 变量私有化:外部函数的变量可以被内部函数访问,但无法被外部直接修改,实现了 “私有变量” 的效果。
举个例子:
function outer() {
let count = 0; // 外部函数的变量
// 内部函数,引用了外部的count
function inner() {
count++;
return count;
}
return inner; // 返回内部函数
}
// 调用外部函数,得到闭包(inner函数 + 其捆绑的count)
const closure = outer();
console.log(closure()); // 1(count从0变为1)
console.log(closure()); // 2(count从1变为2)
console.log(closure()); // 3(count持续被保留和修改)
在这个例子中:
-
outer函数执行后,理论上其作用域(包括count)应被销毁,但由于inner函数引用了count,outer的作用域被 “保留” 了下来。 -
closure变量持有inner函数,每次调用closure()时,都能操作outer中定义的count,这就是闭包的效果。
在 Vue 项目中, 闭包的应用场景非常广泛,核心是利用其 “保存词法环境(状态)并允许外部访问内部变量” 的特性,解决状态隔离、逻辑封装、异步上下文保持等问题。 以下是具体场景及示例:
1. 组件生命周期与异步操作中的状态留存
组件的生命周期钩子(如 mounted、beforeDestroy)和异步操作(定时器、接口请求)中,闭包用于留存组件实例或局部变量,确保异步回调能正确访问上下文。
示例:组件内定时器的清理
vue
<template>
<div>{{ time }}</div>
</template>
<script>
export default {
data() { return { time: 0 } },
mounted() {
// 定时器回调是闭包,保存了组件实例 this
const timer = setInterval(() => {
this.time++; // 访问组件 data 中的 time
}, 1000);
// beforeDestroy 钩子回调是闭包,保存了 timer 变量
this.$on('hook:beforeDestroy', () => {
clearInterval(timer); // 组件销毁时清理定时器
});
}
};
</script>
- 闭包确保异步回调(定时器、销毁钩子)能访问
this(组件实例)和timer(局部变量),避免状态丢失。
2. 事件处理与防抖 / 节流逻辑
事件处理函数(如 @click、@input)中,闭包可封装防抖、节流等逻辑,保存中间状态(如定时器 ID),避免污染组件数据。
示例:输入框防抖
vue
<template>
<input @input="handleInput" placeholder="搜索...">
</template>
<script>
export default {
methods: {
handleInput() {
let timer; // 闭包保存定时器状态
return (e) => {
clearTimeout(timer);
timer = setTimeout(() => {
console.log('搜索:', e.target.value); // 防抖后执行
}, 500);
};
}() // 立即执行,返回闭包函数作为事件处理函数
}
};
</script>
- 闭包将
timer隔离在事件处理逻辑内部,避免多个输入框共享状态冲突。
3. 自定义指令的私有状态管理
自定义指令中,闭包用于保存指令实例的私有状态(如绑定值、临时变量),确保多个指令实例间状态隔离。
示例:权限控制指令
vue
<script>
export default {
directives: {
permission: {
inserted(el, binding) {
const requiredPerm = binding.value; // 闭包保存当前指令需要的权限
// 检查权限的函数(闭包访问 requiredPerm)
const checkPerm = () => {
if (!hasPermission(requiredPerm)) { // 假设 hasPermission 是权限工具
el.style.display = 'none'; // 无权限则隐藏元素
}
};
checkPerm();
// 监听权限变化(闭包确保能访问 checkPerm 和 requiredPerm)
el._permWatcher = window.addEventListener('permChange', checkPerm);
},
unbind(el) {
// 清理监听(闭包访问 el._permWatcher)
window.removeEventListener('permChange', el._permWatcher);
}
}
}
};
</script>
<template>
<button v-permission="'delete'">删除按钮</button>
</template>
- 闭包使指令的
inserted和unbind钩子能共享requiredPerm和checkPerm,且每个指令实例的状态独立。
4. Vue 3 Composition API 与组合式函数
Vue 3 的 setup 函数和组合式函数(如 useXXX)重度依赖闭包封装响应式状态和逻辑,实现逻辑复用且状态隔离。
示例:封装表单验证逻辑
vue
<script setup>
import { ref } from 'vue';
// 组合式函数:闭包封装表单状态和验证逻辑
function useFormValidator(initialValues) {
const form = ref(initialValues);
const errors = ref({});
const validate = () => {
errors.value = {};
// 验证逻辑(闭包访问 form 和 errors)
if (!form.value.name) errors.value.name = '必填';
return Object.keys(errors.value).length === 0;
};
return { form, errors, validate }; // 暴露闭包中保存的状态和方法
}
// 组件中使用:两个表单实例状态完全隔离
const userForm = useFormValidator({ name: '' });
const addressForm = useFormValidator({ city: '' });
</script>
- 每次调用
useFormValidator时,内部的form、errors与validate形成闭包,不同实例的状态互不干扰。
5. 状态管理(Vuex/Pinia)中的 getter 与 action
Vuex/Pinia 的 getter 函数通过闭包访问 state,action 则通过闭包维护异步操作中的上下文(如 commit、dispatch)。
示例:Pinia 的 getter 与 action
javascript
运行
// store/user.js
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({ token: null, info: null }),
getters: {
isLogin(state) {
// getter 是闭包,访问外部 state
return !!state.token;
}
},
actions: {
async fetchUserInfo() {
// action 闭包访问 state 和 this(store 实例)
const res = await api.getUser(this.token); // 用当前 token 发请求
this.info = res.data; // 更新状态
}
}
});
- getter 和 action 本质是闭包,确保能访问最新的
state和 store 实例方法。
6. 高阶函数与逻辑复用
通过闭包创建高阶函数(返回函数的函数),封装通用逻辑(如权限校验、参数预设),实现代码复用。
示例:封装带 loading 状态的请求
vue
<script setup>
import { ref } from 'vue';
// 高阶函数:闭包保存 loading 状态
function withLoading(apiFn) {
const loading = ref(false);
const wrappedFn = async (...args) => {
loading.value = true;
try {
return await apiFn(...args); // 闭包调用传入的接口函数
} finally {
loading.value = false;
}
};
return { wrappedFn, loading }; // 暴露闭包中的状态和包装函数
}
// 使用:获取用户列表(自带 loading 状态)
const { wrappedFn: fetchUsers, loading } = withLoading(api.getUsers);
</script>
<template>
<button @click="fetchUsers" :disabled="loading">
{{ loading ? '加载中' : '获取用户' }}
</button>
</template>
- 闭包使
wrappedFn能访问loading状态,且每个withLoading调用的状态独立。
总结
闭包在 Vue 中的核心作用是:
- 隔离状态:如组合式函数、指令实例的独立状态;
- 保存上下文:确保异步操作、事件回调能访问正确的组件 / Store 实例;
- 封装逻辑:将相关状态与方法捆绑,避免全局污染,提升复用性。
所以上面的几个举例只是日常中大家经常见到的,肯定不止这些例子,不管什么项目,只要体现出函数内部调用函数外部的常量就行,确保常量不被修改,体现出闭包的特性就行
使用时需注意:闭包可能导致变量长期驻留内存,需及时清理(如组件销毁时清除定时器、解绑事件),避免内存泄漏。
函数组件和异步组件
“函数式组件” 和 “异步组件” 是 Vue 中两种不同定位的组件形态,前者通过 “无状态、无实例” 精简渲染流程,后者通过 “按需加载” 减少初始资源体积,二者从不同维度优化性能,具体解析如下:
一、函数式组件:无状态、无实例的 “轻量渲染器”
1. 核心定义
函数式组件是 仅接收 props 和 context 作为参数、无自身状态(无 data/reactive)、无组件实例(无 this)、无生命周期钩子 的组件,本质是一个 “纯函数”—— 输入 props 后直接返回虚拟 DOM,不参与组件实例的创建和挂载流程。
在 Vue 2 中需通过 functional: true 声明,Vue 3 中则直接用 “无 <script setup> 的单文件组件” 或 “返回虚拟 DOM 的函数” 实现,例如:
组件UserCard
<!-- Vue 3 函数式组件:仅渲染,无状态 -->
<template functional>
<div class="user-card">
<img :src="props.avatar" alt="用户头像" />
<div>{{ props.name }}</div>
</div>
</template>
<script>
// 也可通过 JS 定义:接收 props,返回虚拟 DOM
export default function UserCard(props) {
return h('div', { class: 'user-card' }, [
h('img', { src: props.avatar, alt: '用户头像' }),
h('div', props.name)
]);
}
</script>
2. 函数组件≠jsx
函数组件可以用jsx写,jsx只是一种语法,函数组件的强调在状态和实例
举个例子:
// 用 JSX 写的普通组件(有状态,不是函数组件)
import { ref } from 'vue';
export default () => {
// 有自身状态(count),不符合函数组件“无状态”特征
const count = ref(0);
// 有自身事件处理逻辑
const handleAdd = () => {
count.value++;
};
// JSX 渲染,但组件是普通组件
return (
<div>
<span>计数:{count.value}</span>
<button onClick={handleAdd}>+1</button>
</div>
);
};
2. 性能提升原理:跳过 “组件实例创建” 流程
Vue 普通组件的渲染需经历 “创建组件实例 → 初始化状态 → 执行生命周期 → 渲染虚拟 DOM” 等完整流程,而函数式组件会 跳过 “实例创建” 和 “状态初始化” 步骤,直接根据 props 生成虚拟 DOM,减少内存占用和渲染耗时。
3. 适用场景:纯展示、高频复用的轻量组件
仅当组件满足 “无状态、仅渲染” 时,用函数式组件才能提升性能,典型场景:
- 列表项组件:如表格行、列表项(
v-for循环渲染几十上百个的场景,减少实例数量); - 纯展示组件:如标签(Tag)、头像(Avatar)、按钮组(ButtonGroup)等无交互或仅触发父组件事件的组件;
- 高阶组件包装:如用于封装逻辑、生成新组件的 “容器组件”(无自身状态,仅转发
props)。
注意:若组件需状态(如 ref/reactive)、生命周期(如 onMounted)或复杂交互(如内部事件处理),则不适合用函数式组件 —— 强行使用会导致代码复杂度上升,反而抵消性能优势。
二、异步组件:按需加载的 “延迟渲染组件”
1. 核心定义
异步组件是 不在初始渲染时加载,而是在 “需要时”(如路由跳转、条件渲染触发)才动态加载组件代码 的组件,本质是通过 “代码分割” 将组件打包成独立的 chunk 文件,避免初始包体积过大。
Vue 3 中通过 defineAsyncComponent 声明,Vue 2 中通过 “返回 Promise 的函数” 声明,例如:
// Vue 3 异步组件:路由跳转时才加载 UserDetail 组件
import { defineAsyncComponent } from 'vue';
import { Loading, ErrorComponent } from './components';
// 定义异步组件,指定加载函数、加载中/加载失败占位组件
const UserDetail = defineAsyncComponent({
loader: () => import('./UserDetail.vue'), // 动态导入,打包成独立 chunk
loadingComponent: Loading, // 加载中显示的组件
errorComponent: ErrorComponent, // 加载失败显示的组件
delay: 200, // 延迟 200ms 显示加载组件(避免闪屏)
timeout: 3000 // 3 秒加载超时则显示错误组件
});
// 路由配置中使用:访问 /user/:id 时才加载 UserDetail
const routes = [
{ path: '/user/:id', component: UserDetail }
];
2. 性能提升原理:减少初始资源体积
普通组件会被打包到主包(如 app.js)中,若项目包含大量组件(如几十个页面组件),会导致初始包体积过大(如超过 2MB),首屏加载时间变长;而异步组件会 被单独打包成小 chunk(如 UserDetail.[hash].js) ,初始加载时仅下载主包,需要时再通过网络请求加载组件 chunk,从而减少首屏加载时间和初始内存占用。
三、核心差异
| 维度 | 函数式组件 | 异步组件 |
|---|---|---|
| 核心特性 | 无状态、无实例、同步渲染 | 有状态 / 无状态均可、异步加载、延迟渲染 |
| 性能优化点 | 减少组件实例创建开销,提升渲染速度 | 减少初始包体积,提升首屏加载速度 |
| 适用组件类型 | 纯展示、高频复用的轻量组件 | 非首屏、条件触发的重量级组件 |
| 代码分割 | 不涉及代码分割,组件代码在主包中 | 强制代码分割,组件代码在独立 chunk 中 |
怎么理解函数式组件会 跳过 “实例创建” 和 “状态初始化” 步骤,呢?
Vue 中普通组件和函数式组件的渲染流程差异,本质是 “是否创建组件实例” 导致的流程分支。下面从源码的角度去拆分下这个过程:
一、普通组件的完整渲染流程(包含实例创建)
普通组件的渲染是一个 “从组件定义到 DOM 挂载” 的完整生命周期,可分为 5 个核心步骤,每一步都和 “组件实例” 强绑定:
1. 解析组件定义,准备创建实例
当 Vue 解析到模板中的组件标签(如 <UserForm>)时,会先读取该组件的选项定义(data/methods/computed 等),然后调用 Vue 内部的 createComponentInstance 方法,初始化一个组件实例对象(VNode 组件实例) 。这个实例对象会包含:
- 基础属性:
uid(唯一标识)、vnode(虚拟 DOM 节点)、parent(父实例)等; - 状态容器:
ctx(上下文,用于存放data/props等)、setupState(组合式 API 的状态)等; - 方法引用:
emit方法、生命周期钩子队列等。
2. 初始化组件状态(实例的核心工作)
实例创建后,Vue 会执行 initComponent 方法,为实例 “注入” 状态和能力:
-
处理 props:将父组件传入的
props解析后挂载到实例的ctx中(如this.props.name); -
初始化 data:执行
data()函数,将返回的对象通过reactive转为响应式数据,挂载到实例(如this.count); - 绑定 computed/watch:将计算属性和监听器与实例关联,依赖收集时绑定到实例的更新逻辑;
-
处理生命周期:将
mounted/updated等钩子函数添加到实例的钩子队列,等待触发时机。
3. 执行初始化生命周期钩子
实例状态准备好后,Vue 会按顺序执行初始化阶段的生命周期钩子:
-
beforeCreate:此时props和data尚未挂载到实例,无法访问; -
created:props和data已初始化,可通过this访问,但 DOM 尚未生成。
4. 渲染虚拟 DOM(VNode)
初始化完成后,Vue 调用实例的 render 方法(模板会被编译为 render 函数),生成组件的虚拟 DOM 树(VNode)。这个过程中,render 函数通过 this 访问实例上的 props/data(如 this.name),最终生成描述 DOM 结构的 VNode 对象(包含标签名、属性、子节点等信息)。
5. 挂载真实 DOM,执行挂载生命周期
-
虚拟 DOM 转真实 DOM:Vue 调用
patch方法,将 VNode 转换为真实 DOM 节点,并插入到父组件的 DOM 中; -
执行挂载钩子:触发
beforeMount→ 真实 DOM 挂载完成 → 触发mounted; -
实例关联 DOM:实例的
el属性(Vue 2)或vnode.el(Vue 3)指向真实 DOM,方便后续更新。
二、函数式组件的渲染流程(跳过实例创建)
函数式组件因为 “无状态、无实例”,流程被大幅简化,直接跳过 “实例创建” 和 “状态初始化”,仅保留 “输入 props → 输出 VNode” 的核心步骤:
1. 解析组件定义,确认函数式标识
当 Vue 解析到函数式组件(如 <template functional> 或返回 VNode 的函数)时,会识别其 “函数式” 标识(Vue 3 中通过 functional: true 或无状态函数判断),直接进入轻量渲染流程。
2. 直接接收 props 和上下文(无实例,无初始化)
-
函数式组件没有实例,所以不需要创建
componentInstance对象; -
父组件传入的
props和上下文(slots/emit等)会被直接打包成参数,传递给渲染函数(模板或 JSX 函数)。例如:- 模板式函数组件中,通过
props.xxx直接访问数据,context.emit触发事件; - JSX 函数组件中,函数参数直接接收
props和context((props, context) => { ... })。
- 模板式函数组件中,通过
3. 生成虚拟 DOM(直接渲染,无生命周期)
函数式组件的 “渲染函数”(模板编译后的函数或 JSX 函数)会直接使用 props 和 context 生成 VNode,过程中:
- 不需要访问
this(因为没有实例); - 不需要处理响应式数据初始化(
props由父组件传入,已在父组件中完成响应式处理); - 没有生命周期钩子(无需执行
created/mounted等)。
4. 挂载真实 DOM(复用父组件的挂载流程)
生成的 VNode 会直接进入父组件的 patch 流程,和父组件的其他节点一起被转换为真实 DOM。因为没有实例,所以 DOM 挂载后也不会触发任何生命周期钩子,完成渲染后即结束。
三、一句话总结
普通组件的渲染是 “先创建一个‘管理者(实例)’,由管理者统筹状态、生命周期和渲染”,流程完整但冗余;函数式组件的渲染是 “无管理者,直接用输入数据生成输出结果”,跳过所有和实例相关的步骤,因此更轻量、更快。