Vue scoped、keep-alive、插槽、过滤器
涵盖 scoped 编译原理、CSS Modules 对比、keep-alive 缓存机制、插槽三种类型、Vue3 移除过滤器与迁移方案。原文件四个独立章节合并到本文。
第一部分:Vue scoped 原理
标准面试回答(1 分钟)
Scoped 是 Vue 实现组件样式隔离的机制,通过在编译阶段给组件元素添加唯一属性(如 data-v-xxxx),并重写 CSS 选择器来匹配该属性,确保样式只在当前组件内生效。
具体原理分三步:
第一步,编译时添加唯一标识。当 <style scoped> 被解析时,Vue 的编译器会为当前组件生成一个唯一哈希值,比如 data-v-7ba5bd90。然后遍历模板中的每个元素,为该元素添加这个自定义属性。
第二步,CSS 选择器重写。编译器同时会处理 CSS 规则,将原本的类选择器或元素选择器改写成带属性选择器的形式。比如 .title { color: red; } 会被重写成 .title[data-v-7ba5bd90] { color: red; }。这样 CSS 规则只会匹配带有该特定属性的元素,实现了样式隔离。
第三步,子组件处理。如果父组件使用了 scoped,子组件的根元素会被父组件的 scope 标记影响(带上父组件的 data 属性),但子组件内部元素不会被父组件样式影响,除非使用深度选择器(:deep 或 >>>)穿透。
这种实现方式的优点是编译时完成,运行时零性能损耗,且开发体验好,不需要手动维护命名空间。缺点是某些动态生成的 DOM 或第三方组件需要穿透样式时,需要用深度选择器。
20 秒极简版
Scoped 通过在编译时给组件元素添加唯一 data-v-xxx 属性,并重写 CSS 选择器匹配该属性,实现样式隔离。编译时完成,运行时无性能损耗,保证组件样式不互相污染。
追问 1:样式穿透怎么实现?
样式穿透使用 :deep()(Vue 3)或 >>>、/deep/(Vue 2)来实现,作用是让父组件的 scoped 样式能够影响到子组件内部的元素。
原理:正常情况下,scoped 的 CSS 选择器会被编译成 .parent-class[data-v-xxx],限制在当前组件内。使用 :deep(.child-class) 后,编译结果变成 [data-v-xxx] .child-class,把属性选择器挪到父级,不再限制子元素必须带有该属性,从而可以匹配子组件内部的元素。
实际开发中,:deep 主要用于修改第三方 UI 库的样式,比如调整 Element Plus 或 Ant Design Vue 组件的内部样式。使用时要谨慎,过度穿透会破坏样式隔离,增加维护成本。
追问 2:Scoped 和 CSS Modules 的区别?
两者都是解决样式冲突的方案,但实现方式和心智模型不同。
Scoped 是 Vue 特有的编译时机制,通过添加属性选择器实现隔离。优点是上手简单、心智负担小,直接写 CSS 就行;缺点是全局样式和穿透场景需要额外处理,且对动态类名支持不友好。
CSS Modules 是 CSS 标准化的模块化方案,在 Vue、React 中都能用。它通过编译时将类名转换成哈希字符串(如 .title → ._title_7ba5bd90),通过 JS 对象引入。优点是彻底隔离、支持组合、类型友好;缺点是需要在模板中写 $style.title 这种语法,学习成本稍高。
选择:简单组件、快速迭代用 Scoped;大型组件库、需要强类型约束或团队有 React 背景时倾向 CSS Modules。两者不冲突,可以混用。
追问 3:Scoped 样式在动态生成的 DOM 上会失效吗?
会失效。Scoped 样式是在编译时静态分析模板添加属性的,如果通过 v-html 动态插入 DOM,或者在 created 里用 document.createElement 动态添加元素,这些元素不会被添加上组件的 data-v-xxx 属性,因此 scoped 样式无法匹配。
解决方案:
- 用非 scoped 样式:在同一个组件里再加一个不带 scoped 的
<style>标签,专门给动态内容写样式,但要手动保证命名唯一 - 用 CSS Modules:CSS Modules 的类名在运行时通过 JS 绑定,动态元素可以手动设置对应的类名,不受影响
- 用内联样式或 JS 动态添加:如果动态内容简单,直接用
:style绑定;或者通过 JS 给动态生成的 DOM 手动添加类名
实际开发中,如果动态内容复杂且样式需求多,我会避免用 v-html,改用组件化方式,让每个子项都有独立的作用域。
易错点
- 只说"给元素加属性"不说"重写选择器"
- 误以为 scoped 是在运行时通过 JS 控制样式
- 对深度选择器的版本差异不清楚:Vue 2 用
>>>或/deep/(有废弃警告),Vue 3 统一用:deep() - 混淆 scoped 和 CSS Modules 的适用场景
- 忽略 scoped 对性能的影响:编译后选择器权重增加,且属性选择器比类选择器慢一丢丢
- 不知道动态 DOM 的场景:
v-html里怎么用 scoped
第二部分:Vue keep-alive
标准面试回答(1 分钟)
keep-alive 是 Vue 内置的抽象组件,用于缓存不活动的组件实例,避免组件被频繁销毁和重建,从而提升性能和保留组件状态。它最典型的应用场景是路由缓存(比如列表页切到详情页再返回,保留滚动位置和筛选条件)和动态组件缓存。
核心原理是:keep-alive 内部维护一个缓存对象(key 是组件的 name 或 key),当组件被切换掉时,不会调用 unmounted,而是将组件实例缓存在内存中,再次切回时直接从缓存中取出复用,触发 activated 钩子而不是重新走 created 到 mounted 的完整流程。
使用上,keep-alive 提供了两个常用属性:include(只有匹配的组件会被缓存)、exclude(匹配的组件不会被缓存),可以传字符串、正则或数组。还提供了 max 属性限制最大缓存实例数,超出后会按照 LRU(最近最少使用)策略淘汰。
生命周期上,被 keep-alive 缓存的组件会多出两个钩子:activated(组件被激活时调用)和 deactivated(组件被缓存时调用)。注意首次进入时,activated 在 mounted 之后执行;再次进入时,只触发 activated,不会触发 created 和 mounted。
Vue 2 和 Vue 3 中 keep-alive 的基本用法一致,但 Vue 3 中结合 RouterView 使用时语法略有调整,需要通过插槽方式包裹。
20 秒极简版
keep-alive 是 Vue 的缓存组件,用于保留组件状态,避免重复销毁和创建。常用于路由缓存(如列表页保留筛选条件)、动态组件缓存。提供 include、exclude、max 属性控制缓存范围,缓存的组件会多出 activated 和 deactivated 生命周期钩子。
追问 1:缓存策略与 max 淘汰
keep-alive 内部使用一个 LRU(Least Recently Used,最近最少使用)缓存策略。它维护一个缓存对象(存储组件实例)和一个 keys 数组(记录缓存的顺序)。
当组件被缓存时,如果缓存数量未达到 max,直接存入;如果已达到 max,会淘汰掉 keys 数组中最早缓存的组件(即最近最少使用的那个),然后存入新组件。
当缓存组件被再次访问时,会将其在 keys 数组中的位置移到末尾,表示"最近使用过"。
这种策略保证了高频访问的组件不会被淘汰,缓存空间利用率更高。比如设置 max=5,当第 6 个组件被缓存时,最早缓存的组件(最久没用到的)会被销毁。
追问 2:activated 和 mounted 的执行顺序
首次进入:created → mounted → activated。activated 在 mounted 之后执行。
再次进入(从缓存中恢复):只触发 activated,不会触发 created 和 mounted。
使用场景:
- 需要在每次进入组件时都执行的逻辑(比如重新拉取数据、刷新滚动位置、重置某些状态),放在
activated中 - 只需要在组件首次创建时执行一次的逻辑(比如初始化监听、一次性数据加载),放在
mounted或created中
常见误区:有人把所有初始化逻辑都放 mounted,结果从缓存回来时数据没更新;也有人把接口请求放 activated,但首次加载时 activated 在 mounted 之后,数据出现"先展示旧数据再刷新"的闪烁,需要配合 loading 状态解决。
追问 3:在 Vue Router 中怎么用?
Vue 2 中,直接在 <router-view> 外层包裹 <keep-alive>,所有路由组件都会被缓存。如果只想缓存部分路由,有两种方式:
方式一:用 include 属性配合路由组件的 name
<keep-alive :include="['List', 'Detail']">
<router-view />
</keep-alive>路由组件需要在组件中定义 name 选项,Vue 3 中 <script setup> 需要用 defineOptions 或单独 <script> 定义。
方式二:在路由 meta 中配置
<keep-alive>
<router-view v-slot="{ Component }">
<component :is="Component" v-if="$route.meta.keepAlive" />
</router-view>
</keep-alive>路由配置中添加 meta: { keepAlive: true },通过条件判断决定是否包裹缓存。
Vue 3 中的差异:Vue Router 4 中,<router-view> 推荐用插槽方式:
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>这种方式更灵活,可以在插槽里做更多条件控制。
易错点
- 混淆"缓存组件"和"缓存数据":
keep-alive缓存的是组件实例(包括 DOM 状态、数据、滚动位置) - 不知道 include/exclude 匹配的是 name:很多人以为匹配的是组件文件路径或路由 path
- 生命周期答错:说"
activated在mounted之前执行"是常见错误 - 忽略 Vue 2 和 Vue 3 的路由写法差异
- LRU 淘汰策略说不清楚:只提"超过 max 会淘汰最早的",但说不出"最近最少使用"这个核心概念
- 忘记 deactivated 钩子
- 场景举例太简单:能结合真实项目经验更有说服力
第三部分:Vue 插槽
标准面试回答(1 分钟)
插槽是 Vue 实现组件内容分发的机制,让父组件可以向子组件传递模板内容,核心价值是提高组件的复用性和灵活性。Vue 提供了三种插槽:默认插槽、具名插槽、作用域插槽。
默认插槽是最基础的,子组件用 <slot> 标签占位,父组件传入的内容会替换这个位置。如果没有传入内容,会显示 <slot> 标签内的备用内容。
具名插槽用于多个插槽的场景,给 <slot> 加 name 属性区分,父组件用 v-slot:header 或 #header 的语法指定内容放到哪个插槽。常见于布局组件,比如头部、主体、底部。
作用域插槽是插槽的进阶用法,子组件可以把数据通过 <slot> 标签的 props 传递给父组件,父组件拿到数据后可以自定义渲染。核心场景是数据逻辑在子组件,渲染样式由父组件控制,比如表格组件,子组件负责数据获取和循环,父组件决定每一列怎么展示。
使用上,Vue 2 和 Vue 3 有细微差别:Vue 2 中 v-slot 只能用在 <template> 上,Vue 3 中支持在普通元素上使用简化语法。另外 Vue 3 中作用域插槽的性能也做了优化,父子组件渲染解耦更好。
20 秒极简版
插槽是 Vue 的内容分发机制,分三种:默认插槽占位、具名插槽区分位置、作用域插槽让子组件传数据给父组件自定义渲染。核心价值是组件复用性和灵活性,常用于布局组件、表格列自定义等场景。
追问 1:作用域插槽和普通插槽的本质区别?
本质区别在于数据作用域和编译时机。
普通插槽(默认插槽和具名插槽)是在父组件编译时就已经确定了内容,数据作用域是父组件,相当于父组件把一段模板直接"放"到子组件的指定位置。子组件只是提供一个占位,不参与内容的数据处理。
作用域插槽则不同:子组件在 <slot> 上通过 props 把数据暴露出去,父组件拿到这些数据后决定怎么渲染。这个过程是子组件提供数据,父组件提供渲染方式,数据源在子组件,但渲染逻辑在父组件。
编译时机上,作用域插槽的模板内容在父组件编译,但数据是从子组件运行时传递上来的,通过函数参数的形式实现。理解起来可以认为:子组件把数据作为参数传给父组件定义的一个渲染函数,父组件返回 VNode。
追问 2:怎么用插槽实现可复用的弹窗组件?
弹窗组件提供固定的结构(遮罩层、容器、关闭按钮等),用插槽来暴露可变内容。
通常至少需要三个插槽:一个默认插槽或具名插槽 body 放主体内容,一个具名插槽 footer 放底部按钮区,还可以加一个 header 插槽支持自定义标题。
同时会提供 open、close 方法和 visible 状态,这些在弹窗组件内部管理。如果有些弹窗需要回调(比如确定时执行某些逻辑),通过 @confirm 这种自定义事件暴露给父组件。
这样封装的弹窗,基础结构一致,但内容完全由使用者控制,复用性很好。实际开发中还会考虑用 Teleport 把弹窗挂载到 body,避免样式层级问题。
追问 3:Vue 2 和 Vue 3 中插槽的实现有什么变化?
第一,语法统一。Vue 3 中 v-slot 可以直接用在普通元素上(不限于 <template>),而且 v-slot 的简写 # 更统一,不用区分默认插槽和具名插槽的写法差异。
第二,作用域插槽的性能优化。Vue 2 中,父组件重新渲染时,作用域插槽的内容会跟着重新渲染,即使子组件数据没变也会执行。Vue 3 通过编译时的优化,将插槽内容编译为独立的函数,子组件更新时只有依赖了子组件数据的插槽才会重新执行,实现了更细粒度的更新,减少了不必要的渲染开销。
第三,底层实现重构。Vue 3 的插槽本质是函数,父子组件的渲染更独立,配合 Block Tree 和 PatchFlags 的优化,整体性能比 Vue 2 好不少。
易错点
- 混淆"作用域插槽"和"具名插槽":经常有人说"作用域插槽就是带名字的",这是概念错误
- 不清楚作用域插槽的数据流向:正确理解是"数据在子组件,父组件用这些数据返回 VNode,子组件负责渲染最终结果"
- 只说用法不说场景
- 遗漏备用内容:默认插槽的备用内容是个实用细节
- 忽略 Vue 2 和 Vue 3 的差异:面试 Vue 3 岗位还讲 Vue 2 的
slot和slot-scope旧语法,会显得知识陈旧 - 不解释编译时机差异
第四部分:Vue 过滤器
标准面试回答(1 分钟)
Vue 过滤器是用于文本格式化的函数,通过管道符 | 在模板中使用,主要做数据展示前的轻量级转换,比如日期格式化、金额千分位、文本截断等。但在 Vue 3 中过滤器已被移除,官方推荐用计算属性或方法替代。
Vue 2 中的过滤器
可以全局注册(Vue.filter)或局部注册(filters 选项),在模板中用 {{ value | format }} 或 v-bind 中使用。支持串联和传参,比如 {{ date | formatDate('YYYY-MM-DD') | uppercase }}。它的特点是专用于模板中的文本格式化,不改变原始数据。
Vue 3 中的变化
Vue 3 移除了模板过滤器语法。迁移指南强调它需要额外的非标准表达式语法,而方法调用已经能完成同样工作;官方推荐改用计算属性或方法:
- 简单格式化用计算属性,依赖明确、有缓存
- 复杂或需要传参的场景用方法(methods),更灵活、类型友好
实际迁移经验:Vue 2 项目里如果大量用过滤器,迁移到 Vue 3 时可以统一封装成工具函数,在组件里通过计算属性或方法调用。比如原来 {{ price | currency }} 改成 {{ formattedPrice }} 计算属性,或者 {{ formatCurrency(price) }} 方法。
20 秒极简版
Vue 过滤器是模板中做文本格式化的函数,Vue 2 常用,支持全局/局部注册和管道符。Vue 3 已移除,官方推荐用计算属性或方法替代,因为性能和组合式 API 设计上的考虑。
追问 1:Vue 3 为什么移除过滤器?
核心原因是过滤器的管道符属于自定义模板语法,并不是 JavaScript 表达式的一部分,而普通方法调用能够覆盖其格式化能力。改成标准方法/计算属性后,表达式语义和类型工具也更统一。性能会受具体格式化逻辑与调用频率影响,但不应把“过滤器必然破坏 PatchFlags”当作官方删除理由。
追问 2:计算属性和方法替代过滤器,怎么选?
选型原则:有依赖且需要缓存的用计算属性,需要实时传参或用 v-for 中的用方法。
计算属性适合依赖固定的场景,比如 createdAt 转成 YYYY-MM-DD 格式,这个转换只依赖 createdAt,计算属性会自动缓存,性能更好。而且计算属性在模板中语义清晰,看起来就像数据属性一样。
方法适合需要传参的场景,比如 {{ formatPrice(price, 'USD') }},因为参数可能变化,没法用计算属性缓存。另外在 v-for 循环中如果格式化逻辑简单且无额外依赖,也可以直接用方法。但要注意,方法每次渲染都会执行,如果循环体很大或格式化逻辑重,可能影响性能。
实际开发中,更倾向于把格式化函数抽成独立工具函数,然后在组件里根据场景封装成计算属性或方法,保持统一性和可测试性。
追问 3:Vue 2 项目过滤器多,迁移到 Vue 3 怎么处理?
第一步,统一抽取工具函数。把所有过滤器逻辑抽到独立的 utils 文件中,比如 formatDate、formatCurrency、truncateText 等。这一步在 Vue 2 项目里就可以做,让过滤器变成工具函数的薄薄一层封装。
第二步,批量替换。迁移时,用正则或 AST 工具扫描模板中的过滤器用法,转换成计算属性或方法调用。比如 {{ date | formatDate }} 改成 {{ formattedDate }},同时在组件里定义 formattedDate 计算属性。如果是 v-for 中传参的,改成方法调用。
第三步,必要时提供全局格式化函数作为过渡。Vue 3 插件可以通过 app.config.globalProperties 暴露函数,减少逐组件导入,但模板仍必须改成函数调用;普通插件无法恢复已从编译器移除的 | 过滤器语法。若要自动迁移,应使用 AST/codemod 修改模板。
核心思路是把格式化逻辑从模板迁移到 JS 侧,保持数据处理的统一性和可维护性。
易错点
- 忽略 Vue 3 已经移除过滤器:说"Vue 3 也有过滤器"会被直接判定知识陈旧
- 说"过滤器可以处理复杂逻辑":过滤器设计初衷就是轻量级文本格式化
- 混淆过滤器和计算属性的使用场景
- 不知道 Vue 3 的替代方案
- 误以为 Vue 2 的过滤器性能好:在大型列表中也有性能开销
- 迁移方案只说"改成计算属性":还应根据是否传参选择方法,并可用工具函数与 codemod 统一迁移;不要声称普通兼容插件能恢复管道语法
关联文档
- 01-Vue 响应式与双向绑定原理
- 03-Vue 组件与生命周期(keep-alive 钩子)
- 04-Vue 组件通信
- 07-Vue Router 与前端路由(keep-alive + Router)