「2026」高频前端面试题汇总之Vue(下篇)

0 阅读47分钟

vue 高频面试题——2026

2026年Vue面试不再慌!本文整理了一线大厂高频考察的Vue核心问题,从响应式底层原理到复杂数据性能优化,每个问题都给出易懂的解答和核心要点。上一文章我们已经整理了一部分了,接下来我们继续整理后面内容。让你在面试过程中不再迷茫。

vue 路由中,history 和 hash 两种模式有什么区别?

hash 模式

URL 尾部带 # 号,# 后的内容不会包含在 HTTP 请求中,修改 hash 不会触发页面重新加载,核心原理是监听浏览器的 onhashchange() 事件实现路由切换。

history 模式

URL 无 # 号,外观更规范;但刷新 / 直接访问非首页路由时,若前端 URL 与后端请求路径不一致会报 404,需后端配合配置(将所有请求重定向到前端入口页)。

底层基于 HTML5 History API,分为两类操作:

  • 修改历史状态:pushState()replaceState()(新增 / 替换历史记录);
  • 切换历史状态:forward()back()go()(前进 / 后退 / 跳转历史记录)。

Vue 中父组件怎么监听到子组件的生命周期?

一、Vue2 监听子组件生命周期

  1. 子组件主动触发(最常用) :子组件在目标生命周期钩子中,通过 $emit 触发自定义事件;父组件在模板中监听该自定义事件,即可感知子组件的生命周期。
  2. @hook 内置语法(无需子组件修改) :父组件直接在子组件标签上通过 @hook:生命周期名 监听(如 @hook:mounted),无需子组件写任何代码,可直接捕获子组件对应生命周期。

二、Vue3 监听子组件生命周期

  1. 子组件 emit 触发(通用方案) :子组件在 setup 中通过 defineEmits 定义自定义事件,在目标生命周期(如 onMounted)中调用 emit 触发;父组件监听该事件即可。
  2. @hook 语法(兼容沿用) :与 Vue2 一致,父组件直接用 @hook:生命周期名(如 @hook:onMounted)监听,无需修改子组件代码。

扩展:@hook的原理是什么? @hook 是 Vue 内置的生命周期监听语法糖,核心原理是:

  1. 生命周期钩子的底层机制:Vue 组件的每个生命周期(如 mounted、updated),底层都对应一个「回调函数队列」—— 组件初始化时,会把开发者写的生命周期回调(如 onMounted(() => {}))存入对应队列;
  2. @hook 的劫持逻辑:父组件使用 @hook:xxx(如 @hook:mounted)时,Vue 会自动给子组件的 xxx 生命周期回调队列,额外追加一个「事件派发回调」;
  3. 事件自动触发:当子组件执行该生命周期时,除了执行自身的回调,还会触发这个追加的回调,向父组件派发 hook:xxx 事件;父组件监听到该事件后,执行对应的处理函数。

说说 vue 中,key 的原理

Vue 中 key 的本质是虚拟 DOM(VNode)的唯一标识,核心服务于虚拟 DOM 的 Diff 算法,原理拆解如下:

  1. 无 key 的问题:若节点无 key,Vue 会采用 “就地复用” 策略 —— 仅对比节点类型(如都是 <li>),不验证节点对应的数据,易导致节点错误复用(比如列表顺序变化时,DOM 节点未真正移动,引发状态错乱);

  2. 有 key 的 Diff 逻辑:对比新旧 VNode 列表时,Vue 先通过 key 匹配节点:

    • 匹配到相同 key:判定节点可复用,仅更新动态属性(如文本、样式),不重新创建 DOM;
    • 匹配不到相同 key:判定为新节点,创建并插入 DOM;
    • 旧列表有但新列表无的 key:判定为废弃节点,从 DOM 中删除;
  3. key 的取值要求:需用唯一且稳定的值(如数据 ID),不能用索引(索引随列表顺序变化,会失去唯一标识意义)。

Vue 常用的修饰符有哪些?分别有什么应用场景?

1. 事件修饰符

  • .stop:阻止事件冒泡,如嵌套按钮点击时避免触发父元素的点击事件。
  • .prevent:阻止浏览器默认行为,如表单提交阻止页面刷新、a 标签阻止默认跳转。
  • .once:事件仅触发一次,如弹窗确认按钮、抽奖提交按钮,防止重复执行逻辑。
  • .self:仅元素自身触发事件时生效,如父元素点击仅点击自身空白处触发,点击子元素不触发。
  • .capture:事件在捕获阶段触发(默认冒泡阶段),如父元素优先处理子元素的事件。
  • .passive:告知浏览器无需阻止默认行为,如移动端滚动 / 触摸事件,提升滚动流畅度。

2. 按键修饰符

  • .enter:仅按下回车键时触发事件,如输入框按回车提交表单、搜索框按回车执行搜索。

3. 表单修饰符(搭配 v-model 使用)

  • .trim:自动去除输入内容的首尾空格,如用户名、手机号输入框,避免空格导致验证失败。
  • .number:自动将输入内容转为数字类型,如年龄、金额输入框,无需手动转换字符串为数字。
  • .lazy:输入框失去焦点 / 按回车时才更新绑定数据,如长文本、评论输入,减少实时更新的性能消耗。

4. 鼠标修饰符

  • .left/.right:仅鼠标左键 / 右键触发事件,如右键触发自定义操作菜单、区分左键和右键的不同逻辑

vue中组件通信的方式有哪些?

父子组件通信(最基础)

  • 父传子:通过 props 传递数据,父组件在子组件标签上绑定属性,子组件声明接收后即可使用,适用于父组件向子组件传递数据 / 方法。
  • 子传父:通过自定义事件(emit),子组件触发事件并传递数据,父组件监听该事件接收数据,适用于子组件向父组件反馈状态 / 数据。
  • 父访问子:通过 ref 引用子组件实例,直接调用子组件的方法 / 访问数据,适用于父组件主动操作子组件。
  • 子访问父:通过 $parent 访问父组件实例,仅适用于简单场景,不推荐过度使用(易耦合)。

兄弟组件通信

  • 借助父组件中转:子组件先通过 emit 将数据传给父组件,父组件再通过 props 传给另一个子组件,适用于兄弟组件关系简单的场景。
  • 通过事件总线(Event Bus):创建全局事件中心,兄弟组件通过触发 / 监听事件传递数据,适用于中小型项目的非深度嵌套组件。

跨层级 / 任意组件通信

  • Vuex/Pinia(状态管理库):统一管理全局共享状态,任意组件可读取 / 修改状态,适用于中大型项目的全局数据共享。
  • provide/inject:父组件通过 provide 提供数据,所有子孙组件通过 inject 注入使用,适用于深度嵌套的组件通信(如组件库开发)。
  • attrs/listeners:传递父组件未被接收的属性,listeners 传递父组件的事件,适用于跨层级透传属性 / 事件。

为什么 Vue 中的 data 属性是一个函数而不是一个对象?

核心原因:避免组件实例共享数据

  • Vue 中组件可以被多次复用(如多次渲染同一个子组件),每个组件实例需要拥有独立的数据源,而非共用同一个对象。
  • 若 data 是对象,所有组件实例会指向同一个对象的引用,修改其中一个实例的 data,会导致所有实例的 data 同步变化,引发数据污染。
  • 若 data 是函数,每次创建组件实例时,会执行该函数并返回一个新的对象,每个实例都拥有独立的 data 对象,保证数据隔离。

补充逻辑:符合 Vue 组件的设计原则

  • 组件的核心特性是 “可复用性”,独立的数据源是复用的基础,函数返回新对象的方式完美适配这一原则。
  • 根实例(new Vue ())的 data 可以是对象,因为根实例只会被创建一次,不存在复用和数据共享的问题。

谈谈你对 Vue 中 keep-alive 的理解

keep-alive 是什么

  • keep-alive 是 Vue 提供的一个抽象组件,用来包裹动态组件或路由组件,使组件在切换时不会被销毁,而是被缓存起来。

它的核心作用

  • 保留组件状态,比如表单输入内容、列表滚动位置等。
  • 避免重复渲染,提升页面切换时的性能。
  • 常用于频繁切换的组件,如标签页、详情页、列表页。

使用 keep-alive 后的生命周期变化

  • 组件不再触发 mounted 和 unmounted。
  • 新增 activated(组件被激活时)和 deactivated(组件被缓存时)两个钩子。

常用属性

  • include:指定需要缓存的组件。
  • exclude:指定不需要缓存的组件。
  • max:限制缓存的组件数量,避免占用过多内存。

简单总结

  • keep-alive 通过缓存组件实例来保留状态并提升性能,是 Vue 中优化组件切换的重要方式。

vue 中 computed 和 watch 区别

核心定位

  • computed:计算属性,核心是「基于已有数据生成新值」,侧重 “算结果”,是声明式(告诉 Vue 要计算什么)。
  • watch:监听器,核心是「监听数据变化并执行逻辑」,侧重 “做操作”,是命令式(告诉 Vue 数据变了要做什么)。

使用场景

  • computed:适用于同步、简单的计算场景,比如姓名拼接、商品总价计算、列表过滤、状态判断(如是否显示按钮)。
  • watch:适用于数据变化后执行异步 / 复杂逻辑,比如监听输入框变化请求搜索接口、监听路由变化刷新页面数据、监听表单变化保存草稿。

关键特性

  • computed:有缓存,依赖数据不变时,多次访问只会计算一次;必须返回值;支持自定义 setter(手动修改计算结果时反向更新依赖)。
  • watch:无缓存,数据变化就触发;无需返回值;支持立即执行(页面初始化就触发)、深度监听(监听对象 / 数组内部属性变化)。

使用形式

  • computed:以属性形式使用(如 {{fullName}}),不用加括号。
  • watch:以函数形式定义,数据变化时执行函数体里的逻辑。

vue中 ref和reactive有什么区别

适用数据类型不同

  • ref 可以用于基本类型(数字、字符串、布尔值),也可以用于对象和数组。
  • reactive 只能用于对象和数组,不能用于基本类型。

访问方式不同

  • ref 定义的数据在脚本里必须通过 .value 访问或修改,模板里可以省略。
  • reactive 定义的对象直接访问属性,不需要 .value

响应式原理不同

  • ref 通过一个包含 .value 的包装对象实现响应式。
  • reactive 通过 Proxy 代理整个对象实现响应式。

解构时的表现不同

  • ref 解构后不会丢失响应式(因为它本身是一个对象)。
  • reactive 直接解构属性会丢失响应式,需要使用 toRefs 或 toRef 处理。

使用场景不同

  • ref 适合管理单个简单数据,比如计数器、开关、输入框值等。
  • reactive 适合管理复杂对象或一组相关数据,比如用户信息、表单数据、列表数据。

重置数据的方式不同

  • ref 可以直接重新赋值(例如 count.value = 0)。
  • reactive 不能直接给整个对象重新赋值,否则会失去响应式,需要用 Object.assign 或替换属性。

Vue 中的 $nextTick 有什么作用?

  1. 核心问题背景:Vue 里修改数据后,DOM 不会立刻更新(Vue 会把多次数据更新缓存起来,一次性更新 DOM 以提升性能),如果此时直接操作 DOM,拿到的还是更新前的旧状态。
  2. $nextTick 的作用:它是一个异步方法,能让你的代码「等 DOM 更新完成后再执行」,确保操作的是最新的 DOM 元素。
  3. 典型使用场景:比如修改输入框绑定的数值后,想立即聚焦这个输入框;或者列表数据更新后,想获取最新的列表滚动高度,这些场景都需要用 $nextTick 包裹操作逻辑。
  4. 简单原理:$nextTick 会把回调函数放到「DOM 更新完成后的下一个微任务」中执行,保证时机精准,且优先用 Promise 实现,兼容场景下会降级为 setTimeout。

vue 中 route 和 router 有什么区别?

本质定义不同

  • $route当前激活的路由信息对象,它只针对当前页面,包含该页面路由的所有详情(比如路径、参数、名称、元信息等),是对 “当前路由状态” 的描述。
  • $routerVue Router 的全局实例对象,是整个应用的路由控制中心,包含了路由跳转、导航守卫、动态路由管理等核心方法,是用来 “操作路由” 的工具。

核心用途不同

  • $route:主要是「读取」当前页面的路由数据,比如获取路由参数($route.params.id)、查询参数($route.query.key)、判断当前路由名称($route.name)等。
  • $router:主要是「操作」路由跳转,比如点击按钮跳转到首页($router.push('/home'))、返回上一页($router.back())、设置路由拦截($router.beforeEach)等。

作用范围不同

  • $route 是 “页面级” 的,每个页面的 $route 内容都不同,只对应自身的路由信息;
  • $router 是 “全局级” 的,整个 Vue 应用只有一个 $router 实例,所有页面共享这个实例来控制路由。

什么是路由守卫

  1. 全局路由守卫

    • beforeEach:前置守卫(路由跳转前触发)
    • beforeResolve:路由解析守卫(导航被确认前、所有组件内守卫和异步路由组件解析完成后触发)
    • afterEach:后置守卫(路由跳转完成后触发)
  1. 组件内路由守卫

    • beforeRouteEnter:路由进入组件之前触发(此时组件实例未创建,无法直接访问 this
    • beforeRouteUpdate:路由更新时触发(如动态路由参数变化,但组件被复用的场景)
    • beforeRouteLeave:路由离开组件之前触发(常用于表单未保存时的拦截提示)
  1. 路由独享守卫

    • 写在路由配置里,仅对当前路由生效
    • beforeEnter:路由进入之前触发

核心逻辑与使用场景

这些钩子函数都包含一个回调,回调里有 to(目标路由)、from(当前路由)、next(导航控制方法)三个核心参数。

典型场景

  • 页面鉴权:用户登录后,将后端返回的 token 和用户信息存入 Vuex / 本地存储。
  • 跳转拦截:在路由守卫中获取 token,存在则调用 next() 允许进入目标页面;不存在则调用 next('/login') 重定向到登录页。

vue 中,推荐在哪个生命周期发起请求?

Vue2 中:优先用 mounted(最推荐)
  1. 核心原因

    • mounted 阶段组件已挂载完成,DOM 结构已生成,此时发起请求能保证:① 可在请求回调中操作 DOM(如渲染数据到页面);② 避免请求过早触发导致的生命周期混乱。
    • 不推荐 created:虽能发起请求(实例已创建),但 DOM 未挂载,若请求回调中操作 DOM 会报错;仅当请求无需操作 DOM 时可临时用。
    • 绝对避免 beforeCreate/beforeMount:实例 / DOM 未就绪,易引发逻辑混乱,且不利于后续维护。
2. Vue3 中:优先在 setup 中配合 onMounted(组合式 API)
  1. 核心原因

    • Vue3 组合式 API 中,setup 执行时机等同于 beforeCreate + created,若直接在 setup 中发起请求,需注意:① 可发起但无法立即操作 DOM;② 推荐将请求逻辑封装在 onMounted 钩子中,对齐 Vue2 mounted 的语义,更符合 “挂载后请求” 的最佳实践。
    • 若使用 <script setup> 语法,直接写在脚本中的请求等同于 setup 阶段执行,需确保回调中操作 DOM 时已挂载(可配合 nextTick)。

computed 计算值为什么还可以依赖另外一个 computed 计算值?

computed 本质是基于依赖追踪的响应式计算属性,它的设计天然支持多层依赖:

  1. 响应式依赖追踪机制Vue 会自动收集 computed 函数中用到的所有响应式数据(包括其他 computed)作为依赖。当底层依赖变化时,会触发所有依赖它的 computed 重新计算。

  2. 计算属性的缓存特性每个 computed 都有缓存,只有当它的直接依赖发生变化时才会重新求值。如果一个 computed A 依赖另一个 computed B,只有 B 的值变化时,A 才会重新计算,避免了冗余计算,保证性能。

  3. 数据流的清晰性允许 computed 之间相互依赖,可以把复杂的计算拆分成多个小的、单一职责的 computed,让代码更模块化、更易维护。例如:

    • 先通过 totalPrice 计算商品总价
    • 再通过 discountPrice 依赖 totalPrice 计算折扣价
    • 最后通过 finalPrice 依赖 discountPrice 计算最终价格这种分层逻辑比写在一个大函数里更清晰。

computed 怎么实现的缓存

computed 缓存的实现原理

  1. 依赖收集:computed 首次执行时,Vue 会记录它依赖的响应式数据(如 data、其他 computed)。

  2. 脏值标记 + 结果缓存

    • 为 computed 维护 dirty 标记和缓存值,首次计算后把 dirty 置为「无需重新计算」,并缓存结果;
    • 后续访问时,若 dirty 为「无需计算」,直接返回缓存值;
  3. 触发更新:只有依赖的响应式数据变化时,才把 dirty 置为「需要计算」,下次访问时重新计算并更新缓存。

一句话总结

computed 靠「依赖收集 + 脏值标记」实现缓存:依赖不变直接返缓存,依赖变了才重新算。

说下 Vite 的原理

  1. 利用浏览器原生 ES 模块(ESM)

    浏览器直接通过 <script type="module"> 加载代码,不需要像 Webpack 那样先把所有文件打包成一个大 bundle。

  2. 按需编译(开发环境)

    只有浏览器请求某个文件时,Vite 才实时编译它。

    项目再大,启动也很快,因为只编译当前页面需要的代码。

  3. Esbuild 预构建依赖

    第三方库(如 Vue、React)用 Esbuild 提前编译成 ESM,速度比 Webpack 快很多。

  4. 生产环境用 Rollup 打包

    因为 Rollup 对代码优化、Tree-Shaking 更成熟,能生成更小的生产包。

一句话总结

Vite 开发时不打包,靠浏览器 ESM 按需加载 + Esbuild 预构建依赖,实现极速启动;生产时用 Rollup 打包优化。

Vue 实例挂载的过程中发生了什么?

Vue 实例的挂载过程主要分为三个核心阶段:模板编译、生成 VNode、渲染真实 DOM

  1. 模板编译(Compile)阶段

    • 解析:用正则解析 template,生成 AST(抽象语法树)。
    • 优化:遍历 AST,标记静态节点(后续更新可跳过 diff)。
    • 生成:将 AST 转化为 render 函数。
  2. 生成 VNode(Render)阶段

    • 执行上一步生成的 render 函数,结合当前的 data 数据,生成虚拟 DOM(VNode)树。
  3. 渲染与挂载(Patch/Mount)阶段

    • 调用 __patch__ 方法,将 VNode 转换为真实的 DOM 元素。
    • 将真实 DOM 插入到页面的 el 挂载点中。
    • 触发 mounted 生命周期钩子,此时组件挂载完成。

挂载完成后,Vue 会建立数据(Data)与视图(DOM)之间的响应式关联。当数据发生变化时,会再次执行 render 函数生成新的 VNode,通过 diff 算法对比新旧 VNode,最终只更新变化的部分(Patch),实现视图的响应式更新。

Vue 中的双向绑定和单向数据流原则是否冲突?

结论:不冲突。

单向数据流的定义:数据只能从父组件通过 props 流向子组件,子组件不能直接修改 props,只能通过触发事件通知父组件修改。

双向绑定的本质:Vue 中的 v-model 只是一个语法糖,它的底层实现是 v-bind(数据流向视图)加上 v-on(视图事件流向数据)。

为什么不冲突

  • 父组件通过 props 将数据传给子组件(单向向下)。
  • 子组件内部不直接修改 props,而是通过触发 input 事件将新值传递给父组件。
  • 父组件监听事件并更新自己的数据,数据变化后再通过 props 流向子组件。
  • 整个过程数据流向依然是单向的,只是写法上看起来是双向的。

说说你对 vue 的理解?

核心定位:Vue 是一套渐进式 JavaScript 前端框架,“渐进式” 指可按需使用核心功能(如视图渲染)或扩展生态(如路由、状态管理),无需一次性引入所有内容,适配从小型组件到大型应用的不同场景。

核心设计思想

  • 数据驱动视图:基于响应式系统,数据变化自动触发视图更新,无需手动操作 DOM,核心是通过 Object.defineProperty(Vue2)/ Proxy(Vue3)实现数据劫持,建立数据与视图的关联;
  • 组件化开发:将页面拆分为独立、可复用的组件,每个组件拥有自己的模板、逻辑和样式,降低耦合度,提升代码复用性和维护性。

核心特性

  • 双向绑定(语法糖):v-model 封装了 v-bind(数据→视图)和 v-on(视图→数据),本质仍遵循单向数据流原则;
  • 虚拟 DOM:用 JS 对象描述真实 DOM 结构,通过 diff 算法对比新旧虚拟 DOM,仅更新变化的部分,减少真实 DOM 操作,提升性能;
  • 指令系统:内置 v-if/v-for/v-bind 等指令,封装常用 DOM 操作,也支持自定义指令扩展底层 DOM 操作逻辑;
  • 生命周期:提供从组件创建、挂载、更新到销毁的完整钩子函数,方便开发者在不同阶段执行逻辑(如 mounted 处理 DOM 初始化、beforeDestroy 清理定时器)。

版本核心差异(Vue2 vs Vue3)

  • 响应式底层:Vue2 用 Object.defineProperty(仅支持对象 / 数组,无法监听新增属性),Vue3 用 Proxy(支持所有数据类型,监听更全面);
  • 组件写法:Vue2 以 Options API 为主(按选项组织代码),Vue3 新增 Composition API(按逻辑组织代码,更适合复杂组件);
  • 性能与体积:Vue3 重写虚拟 DOM,体积更小、编译效率更高,还支持 Tree-Shaking,按需打包减少体积。

生态与适用场景

  • 核心生态:搭配 Vue Router 实现路由管理、Pinia/Vuex 实现状态管理、Vite 实现快速构建,形成完整的前端开发体系;

vue 是如何识别和解析指令的?

识别阶段(模板编译) :Vue 用正则遍历模板字符串,筛选出 v- 开头的属性(指令),解析指令的参数、修饰符、值,并挂载到 AST 节点上。

解析阶段(代码生成) :遍历含指令的 AST 节点,将不同指令(如 v-if/v-model)转化为对应的渲染函数逻辑代码。

执行阶段(运行时) :组件挂载 / 更新时执行渲染函数,触发指令逻辑;自定义指令则在对应生命周期钩子(如 mounted)中执行 DOM 操作,数据变化时重新执行指令逻辑更新视图。

你是怎么处理 vue 项目中的错误的?

全局错误捕获

  • Vue.config.errorHandler 捕获组件内的渲染、生命周期等错误,统一记录日志(如上报到监控平台);
  • window.onerror/window.unhandledrejection 捕获全局 JS 错误、未处理的 Promise 异常。

组件内错误处理

  • Vue3 用 onErrorCaptured 钩子、Vue2 用 errorCaptured 钩子,捕获当前组件及子组件的错误,可阻止错误向上传播;
  • 关键逻辑(如接口请求)用 try/catch 包裹,针对性处理异常。

异步 / 接口错误处理

  • 接口请求(Axios)统一配置拦截器,捕获响应错误,做通用处理(如 401 跳转登录、500 提示服务器错误);
  • 异步操作(Promise)必须加 catch,避免未捕获的 Promise 异常。

错误兜底与用户体验

  • componentIsolation(Vue3)或自定义全局组件,给出错组件设置兜底 UI(如 “加载失败,请重试”);
  • 生产环境隐藏具体错误信息,仅展示友好提示,开发环境保留详细错误便于调试。

说说你对 vue 的 mixin 的理解,以及有哪些应用场景?

mixin 是什么?

  • 本质:一个装了「重复代码」的对象(比如方法、生命周期、数据),是 Vue 早期的「逻辑复用工具」;
  • 用法:组件通过 mixins: [xxxMixin] 引入,mixin 里的代码会和组件自身代码合并;
  • 核心规则:① 同名方法 / 数据 → 组件自己的生效(覆盖 mixin);② 同名生命周期 → mixin 的先执行,组件的后执行。

mixin 的缺点

  • 命名冲突:多个 mixin 或组件与 mixin 重名时,容易覆盖且难排查;
  • 溯源困难:组件里的方法可能来自 mixin,后期维护不知道代码来源;
  • Vue3 中已被 Composition API 替代(更清晰、灵活)。

mixin 的应用场景

  • 简单通用方法复用:比如多个组件都需要的「时间格式化」「防抖 / 节流」方法;
  • 通用生命周期逻辑:比如所有页面挂载时都要「初始化埋点」「监听窗口尺寸变化」;
  • 基础列表逻辑复用:比如不同列表组件都需要的「分页请求」「刷新数据」逻辑。

什么是初始化埋点

初始化埋点就是页面加载后,自动执行的统计代码。

它的作用是记录用户进入页面的行为,比如访问时间、页面名称等。

因为很多页面都需要这一步,所以把这段统计代码抽成 mixin,让所有页面复用,不用重复写。

Vue 中组件和插件有什么区别?

核心定位不同

  • 组件:是 Vue 中可复用的视图单元(比如按钮、弹窗、列表),聚焦 “页面 UI / 局部逻辑” 的复用,本质是带模板、样式、逻辑的独立视图模块;
  • 插件:是 Vue 中扩展框架功能的工具(比如 VueRouter、Vuex、ElementUI 的指令),聚焦 “全局能力增强”,本质是给 Vue 添新功能的代码集合。

使用方式不同

  • 组件:通过 import 引入后,在模板中以标签形式使用(如 <MyButton />),可局部 / 全局注册;
  • 插件:通过 Vue.use(插件名)(Vue2)或 app.use(插件名)(Vue3)全局注册,注册后整个项目都能使用其功能(如全局指令、全局方法)。

作用范围不同

  • 组件:作用于局部视图,只在引入 / 注册的地方生效,可嵌套、传参(props)、触发事件;
  • 插件:作用于全局,注册后所有组件都能访问(如全局过滤器、原型链上的方法 this.$http)。

你写过什么插件全局埋点统计插件

  • 功能:封装埋点 SDK(如友盟 / 百度统计),通过 Vue.use() 全局注册后,所有组件可通过 this.$track() 上报用户行为(如点击、页面访问);
  • 核心逻辑:插件内部封装初始化埋点、事件上报方法,挂载到 Vue 原型上,支持全局配置埋点前缀、环境区分(开发 / 生产)。

全局指令插件

  • 功能:封装防抖 / 节流指令(如 v-debounce/v-throttle),全局注册后所有组件可直接在按钮上使用(如 <button v-debounce="handleClick">);
  • 核心逻辑:插件内定义自定义指令,接收指令参数(如防抖时间),处理事件绑定与防抖逻辑,支持全局默认配置。

全局请求封装插件

  • 功能:基于 Axios 封装请求插件,全局注册后通过 this.$http 调用,内置请求 / 响应拦截器(如统一加 token、处理 401/500 错误);
  • 核心逻辑:插件内部创建 Axios 实例,配置通用拦截器,暴露 get/post 等方法挂载到 Vue 原型,支持全局配置基础 URL、超时时间。

全局工具类插件

  • 功能:封装常用工具方法(如时间格式化、数据校验、深拷贝),全局注册后所有组件可通过 this.$utils 调用;
  • 核心逻辑:插件将工具方法集合挂载到 Vue 原型,支持按需引入子方法,避免全局污染。

Vue 项目中有封装过 axios 吗?怎么封装的?

整体封装逻辑

  • 核心是把「Axios 通用请求逻辑」和「业务 API 接口」完全分开,前者处理全局请求规则,后者只管理接口地址和参数,做到解耦和统一维护。

具体实现方式

  • 第一步:封装 Axios 基础层

    先创建独立的 Axios 实例,配置基础请求地址、超时时间,再设置请求拦截器和响应拦截器;请求拦截器主要给所有请求统一添加 token 等通用参数,响应拦截器统一处理 401 跳转登录、500 服务器错误等通用异常,同时简化返回的数据结构。

  • 第二步:单独抽离 API 层

    按业务模块划分(比如用户模块、订单模块、商品模块),每个模块对应一个文件,文件里只定义该模块下的所有接口地址、请求方式和所需参数,全部基于第一步封装好的 Axios 基础层来编写。

  • 第三步:组件调用方式

    组件里不需要写任何请求相关的逻辑,直接引入对应业务模块的 API 方法,传入参数就能调用,不用关心底层的请求拦截、错误处理等细节。

总结

  • 完整的 Axios 封装是「基础层 + API 层」分离,你把 API 单独抽离是符合实际开发最佳实践的;
  • 基础层管通用规则(拦截、错误、配置),API 层管业务接口(地址、参数、请求方式);
  • 组件仅调用 API 方法,解耦性强,是企业开发中最常用的封装方式。

Vue 项目如何进行部署?是否有遇到部署服务器后刷新 404 问题?

Vue 项目常规部署流程

  • 第一步:打包构建。在本地执行npm run build(Vue2)/npm run build:prod(Vue3),生成 dist 文件夹(包含编译后的静态资源:HTML/CSS/JS/ 图片);
  • 第二步:上传静态资源。将 dist 文件夹上传到服务器的 Web 服务目录(如 Nginx 的 html 目录、Apache 的 www 目录);
  • 第三步:配置 Web 服务器。以 Nginx 为例,配置端口、域名、静态资源路径,确保能访问到 dist 里的 index.html;
  • 第四步:测试访问。启动 Web 服务后,通过域名 / 服务器 IP + 端口访问项目,验证功能正常。

部署后刷新 404 问题的原因

  • 核心原因:Vue 单页应用(SPA)使用前端路由(如 VueRouter),刷新时浏览器会直接向服务器请求当前路由路径(如/user/1),但服务器没有该路径的物理文件,仅存在 index.html,因此返回 404。

刷新 404 问题的解决方法(Nginx 为例)

  • 方法一:Nginx 配置重定向。在 Nginx 的 location 配置中,添加try_files $uri $uri/ /index.html;,意思是服务器找不到对应文件时,自动跳转到 index.html,由前端路由处理;
  • 方法二:VueRouter 修改模式。将路由的history模式改为hash模式(URL 带 #),hash 模式的路由不会向服务器发起请求,自然不会出现 404,但 URL 不够美观;
  • 方法三:后端配置兜底。若项目部署在 Tomcat 等 Java 服务器,可配置 web.xml,将所有请求转发到 index.html。

说一下 vm.$set 原理

  1. 核心作用vm.$set(Vue2)是为了解决Object.defineProperty无法监听新增属性、数组索引修改的问题,手动给目标对象 / 数组添加响应式属性并触发视图更新。

  2. 底层原理(分场景)

    • 场景 1:操作数组本质是调用数组的splice方法(Vue 已重写的变异方法),因为splice会触发依赖更新,从而实现索引修改后的视图更新;
    • 场景 2:操作对象① 先判断属性是否已存在,存在则直接赋值(已有响应式);② 不存在则调用Object.defineProperty为对象新增属性,劫持get/set;③ 手动触发依赖收集和更新,通知视图重新渲染。
  3. 关键补充:Vue3 中无需$set,因为Proxy能直接监听对象新增属性、数组索引修改,天然支持这些场景。

总结

  1. $set核心是手动补全响应式:数组靠重写的splice,对象靠Object.defineProperty+ 手动触发更新;
  2. 解决的是 Vue2 响应式的天然缺陷(Object.defineProperty监听不到新增 / 索引修改);
  3. Vue3 的Proxy从底层解决了该问题,无需$set

vue 组件里写的原生 addEventListeners 监听事件,要手动去销毁吗?为什么?

  1. 核心结论:必须手动销毁!除非监听的是组件自身 DOM 元素且事件是普通浏览器事件(如 click),否则会导致内存泄漏。

  2. 需要销毁的原因

    • 内存泄漏风险:Vue 组件销毁(如页面跳转)时,对应的 DOM 可能被移除,但addEventListener绑定的事件不会自动解绑,事件回调仍持有组件实例引用,导致组件实例无法被 GC(垃圾回收),长期运行会占用内存;
    • 无效逻辑执行:即使组件已销毁,事件触发时(如 window.resize、document.click),回调函数仍会执行,可能导致报错(如访问已销毁组件的this.xxx)或无效逻辑。
  3. 常见场景与销毁方式

    • 场景 1:监听 window/document/ 非组件 DOM → 必须销毁销毁时机:在组件beforeUnmount(Vue3)/beforeDestroy(Vue2)钩子中调用removeEventListener
    • 场景 2:监听组件自身 DOM(如this.$el)→ 可不用手动销毁原因:组件销毁时自身 DOM 会被移除,浏览器会自动解绑该 DOM 上的事件监听。

总结

  1. 监听全局对象(window/document)或非组件 DOM 的事件,必须手动销毁,否则内存泄漏 + 无效执行;
  2. 仅监听组件自身 DOM 的事件,可不用手动销毁,DOM 移除时浏览器自动解绑;
  3. 销毁时机统一在组件卸载钩子中,用removeEventListener匹配解绑。

谈谈你对渐进式框架的理解

  1. 核心定义

    渐进式框架是 Vue 最核心的设计理念,核心是「按需使用、分层集成」—— 你不需要一开始就掌握或引入 Vue 的所有功能,而是可以根据项目的实际需求,从简单到复杂逐步叠加功能,全程保持对项目的掌控力,且扩展过程不会对原有代码造成侵入。

  2. 具体落地体现

    • 入门级使用:仅引入 Vue 核心库,只用声明式渲染(如{{}}插值、v-bind绑定属性、v-on绑定事件),就能快速实现数据和视图的双向绑定,满足简单页面(如表单、静态页动态化)的需求,像使用轻量库一样灵活;
    • 进阶级使用:当项目需要组件化拆分时,再引入 Vue 的组件系统,封装复用按钮、弹窗等通用组件;需要路由跳转时,集成 VueRouter;需要全局状态管理时,引入 Pinia/Vuex;这些功能都是可选的 “插件式” 集成,不依赖彼此;
    • 企业级使用:对于大型项目,可进一步集成 Vue CLI/Vite(工程化构建)、TypeScript(类型校验)、Vue 的过渡动画、自定义指令等高级特性,构建完整的单页应用(SPA)。
  3. 核心优势

    • 学习成本低:新手不用一次性掌握所有知识点,可分阶段学习,先搞定核心渲染,再逐步学习路由、状态管理等,降低入门门槛;
    • 项目适配性强:小到简单的页面改造,大到大型企业应用,都能通过 “按需加功能” 适配,避免小项目引入冗余代码,大项目功能不足;
    • 代码可控性高:扩展功能时是 “增量式” 的,而非重构式的,原有代码逻辑不受影响,便于项目迭代维护。

总结

  1. 渐进式的核心是 “按需扩展、分层集成”,核心逻辑是 “能用、好用、可扩展”;
  2. Vue 的渐进式体现在从基础渲染到组件、路由、状态管理等功能,可根据项目规模灵活添加;
  3. 这一特性让 Vue 既适合新手快速上手,也能支撑大型复杂项目的开发,是其广泛应用的重要原因。

Vue2 动态给 data 添加一个新的属性时会发生什么

  1. 核心现象:动态给 data 中的对象新增属性(如this.user.age = 20)时,属性会被添加到对象上,但不会触发视图更新,且该新增属性不具备响应式能力 —— 后续修改这个属性的值,也无法同步到界面。

  2. 底层原因:Vue2 通过Object.defineProperty对 data 中的数据做响应式处理,这个过程仅在组件初始化时执行:

    • 初始化阶段:Vue 会遍历 data 中已声明的对象属性,为每个属性添加get/set劫持(用于依赖收集和触发更新);
    • 动态新增属性时:Object.defineProperty无法主动为新属性添加get/set,因此该属性脱离了 Vue 的响应式系统,既不会收集依赖,也无法触发视图更新。
  3. 补充细节

    • 新增属性本身是存在的:控制台打印this.user能看到新增的age属性,只是不触发视图;
    • 若新增的是嵌套对象的属性(如this.user.info.addr = '北京'),同样无响应式,因为嵌套对象的属性也仅在初始化时被劫持;
    • 对比数组:数组索引修改和对象新增属性原理类似,都是Object.defineProperty的天然短板。

总结

  1. Vue2 动态新增 data 属性,属性会存在但无响应式,无法触发视图更新;
  2. 核心原因是初始化时仅劫持已有属性的get/set,新增属性未被劫持;
  3. 解决办法:用this.$set手动添加响应式,或提前声明属性初始值。

Vue 中给对象添加新属性时,界面不刷新怎么办?

  1. 问题核心原因:Vue2 基于Object.defineProperty实现响应式,该 API 只能劫持对象已存在的属性,新增属性时无法自动添加get/set劫持,因此无法触发依赖更新和视图刷新;Vue3 虽用Proxy天然支持新增属性,但若项目仍兼容 Vue2 写法,也可参考下述方案。

  2. 具体解决方法(按优先级排序)

    • **方法 1:使用vm.$set/Vue.set(推荐)**这是 Vue 官方推荐的方式,可手动给对象新增响应式属性并触发视图更新。用法:this.$set(目标对象, 新增属性名, 属性值),例:this.$set(this.user, 'age', 20);原理:内部会为新增属性添加get/set,并手动触发依赖更新。
    • **方法 2:替换整个对象(浅拷贝 / 深拷贝)**通过重新赋值的方式让 Vue 感知到对象变化,例:this.user = { ...this.user, age: 20 };原理:新对象会被重新劫持响应式,覆盖原对象后触发视图更新(注意:浅拷贝仅适用于单层对象,多层对象需用深拷贝如JSON.parse(JSON.stringify()))。
    • **方法 3:手动触发更新(不推荐)**用this.$forceUpdate()强制组件重新渲染,虽能刷新界面,但会跳过 Vue 的响应式系统,可能导致性能浪费,仅临时应急使用。
  3. 提前规避方案

    • 定义对象时提前声明所有可能用到的属性(哪怕赋初始值undefined),例:data() { return { user: { name: '', age: undefined } } }
    • Vue3 项目直接使用Proxy特性,新增属性无需额外处理,天然响应式。

总结

  1. 核心原因是 Vue2 的Object.defineProperty无法监听新增属性,Vue3 的Proxy无此问题;
  2. 最优解是用$set手动添加响应式属性,或通过对象替换触发更新;
  3. 提前声明属性可从根源避免该问题,是开发中的最佳实践。

Vue2.0 为什么不能检查数组的变化,该怎么解决?

  1. 核心原因:Vue2 基于Object.defineProperty实现响应式,但该 API 存在天然局限:

    • 无法监听数组索引的修改(如arr[0] = 新值)和长度的修改(如arr.length = 0),因为数组索引本质是属性,但 Vue 不会遍历数组所有索引做get/set劫持(数组索引可能成千上万,遍历会导致严重性能问题);
    • 从设计层面,Vue 认为直接操作数组索引并非最佳实践,因此未对数组索引做响应式处理。
  2. Vue2 的兜底方案(能检测的数组操作) :Vue2 对数组的 7 个变异方法做了重写(push/pop/shift/unshift/splice/sort/reverse),调用这些方法时,会先执行原生逻辑,再手动触发依赖更新,因此能检测到变化并刷新视图。

  3. 解决数组无法检测变化的具体方法

    • **方法 1:使用重写的变异方法(推荐)**索引修改:用splice替代直接索引赋值,例:arr.splice(0, 1, 新值);清空数组:用splice(0)替代arr.length = 0,例:arr.splice(0)
    • **方法 2:使用Vue.set/$set**手动给数组指定索引添加响应式值,例:this.$set(arr, 索引, 新值)(底层仍是调用splice);
    • **方法 3:替换整个数组(浅拷贝)**通过生成新数组触发响应式,例:arr = arr.map((item, idx) => idx === 0 ? 新值 : item)
    • **方法 4:临时方案(不推荐)**用this.$forceUpdate()强制组件重新渲染,跳过响应式系统,仅应急使用。

总结

  1. Vue2 无法检测数组索引 / 长度修改,核心是Object.defineProperty性能代价高 + 设计取舍;
  2. 官方兜底:重写 7 个变异方法,调用这些方法可检测变化;
  3. 最优解:用splice$set修改数组,避免直接操作索引 / 长度。

vue-loader 做了哪些事情?

  1. 核心思路vue-loader 是处理 .vue 单文件组件的核心加载器,核心作用是把单文件组件(SFC)拆解、编译成浏览器能识别的代码,并整合到 Webpack 构建流程中。
  2. 具体处理逻辑
    • 拆分单文件组件结构将 .vue 文件里的 <template><script><style> 三个模块拆分出来,分别交给对应的解析器处理,实现结构、逻辑、样式的分离解析。
    • 编译模板部分<template> 里的 Vue 模板语法(如插值、指令)编译成渲染函数,让模板最终能转换成可执行的 JS 代码。
    • 处理脚本部分<script> 中的代码做兼容性处理(如转译 ES6+ 语法),识别并处理 Vue 组件的导出逻辑,同时支持 <script setup> 语法的解析(Vue3 场景)。
    • 处理样式部分解析 <style> 标签的 scoped、module 等特性:scoped 会给样式添加作用域标识,避免样式污染;module 则实现 CSS 模块化。同时将样式交给 css-loader、style-loader 等处理,最终整合到构建产物中。
    • 整合编译结果把模板、脚本、样式的编译结果重新组合,生成一个符合 Vue 组件规范的 JS 模块,让 Webpack 能正常打包,且组件能被 Vue 实例识别和挂载。
    • 支持热更新在开发环境下实现组件的热模块替换(HMR),修改 .vue 文件后无需刷新页面,仅更新当前组件,提升开发效率。

总结

  • vue-loader 核心是拆解并编译 .vue 单文件组件的三大模块,适配 Webpack 构建流程;
  • 关键能力包括模板编译、样式作用域处理、脚本转译、热更新;
  • 是 Vue 项目中连接单文件组件和 Webpack 的核心桥梁。

Vue.observable 是什么?

  1. 核心思路vue-loader 是处理 .vue 单文件组件的核心加载器,核心作用是把单文件组件(SFC)拆解、编译成浏览器能识别的代码,并整合到 Webpack 构建流程中。
  2. 具体处理逻辑
  • 拆分单文件组件结构将 .vue 文件里的 <template><script><style> 三个模块拆分出来,分别交给对应的解析器处理,实现结构、逻辑、样式的分离解析。
  • 编译模板部分<template> 里的 Vue 模板语法(如插值、指令)编译成渲染函数,让模板最终能转换成可执行的 JS 代码。
  • 处理脚本部分<script> 中的代码做兼容性处理(如转译 ES6+ 语法),识别并处理 Vue 组件的导出逻辑,同时支持 <script setup> 语法的解析(Vue3 场景)。
  • 处理样式部分解析 <style> 标签的 scoped、module 等特性:scoped 会给样式添加作用域标识,避免样式污染;module 则实现 CSS 模块化。同时将样式交给 css-loader、style-loader 等处理,最终整合到构建产物中。
  • 整合编译结果把模板、脚本、样式的编译结果重新组合,生成一个符合 Vue 组件规范的 JS 模块,让 Webpack 能正常打包,且组件能被 Vue 实例识别和挂载。
  • 支持热更新在开发环境下实现组件的热模块替换(HMR),修改 .vue 文件后无需刷新页面,仅更新当前组件,提升开发效率。

总结

  • vue-loader 核心是拆解并编译 .vue 单文件组件的三大模块,适配 Webpack 构建流程;
  • 关键能力包括模板编译、样式作用域处理、脚本转译、热更新;
  • 是 Vue 项目中连接单文件组件和 Webpack 的核心桥梁。

vue 的响应式开发比命令式有哪些优势?

  1. 核心思路

响应式开发是「数据驱动视图」,只需关注数据变化,Vue 自动同步视图;命令式开发是「手动操控 DOM」,每改一次数据都要写代码修改 DOM,两者核心差异是 “DOM 更新的责任主体不同”。

  1. 具体优势
  • 开发效率更高:不用手写查找 DOM、修改 DOM 内容 / 样式的代码,仅需处理数据逻辑,数据修改后 Vue 自动更新视图,大幅减少重复代码,开发速度提升;
  • 代码更易维护:数据和视图解耦,业务逻辑不再混杂 DOM 操作,后期修改需求(比如调整按钮显示规则),只需改数据相关逻辑,不用逐行修改 DOM 操作代码;
  • 降低出错概率:Vue 内置 DOM 更新时机处理逻辑,规避了手动操作 DOM 时 “DOM 未加载就修改”“重复操作 DOM” 等常见问题,减少低级错误;
  • 适配复杂场景更轻松:面对多数据联动(如表单输入实时筛选列表),只需绑定数据关系,Vue 自动触发所有关联视图更新,无需手动编写多层 DOM 修改逻辑。

Vue 中,created 和 mounted 两个钩子之间调用时间差值受什么影响?

核心思路

  • created 和 mounted 的调用时间差,本质是 Vue 完成数据初始化后,将模板渲染为真实 DOM 并挂载到页面的耗时,仅受 DOM 渲染相关因素影响,且两者执行顺序固定为 created 先、mounted 后。

主要影响因素

  • 组件模板 / DOM 结构复杂度

    • 模板包含多层嵌套子组件、大量 DOM 节点(如长列表、复杂表格)时,Vue 解析模板、生成真实 DOM 的耗时增加,时间差变大;
    • 模板结构简单(少量标签、无嵌套组件)时,DOM 渲染耗时极短,时间差可忽略。
  • 数据初始化复杂度

    • data 中包含大量深层嵌套的响应式数据、或 created 钩子内执行了数据格式化等耗时逻辑时,会延迟 DOM 渲染的启动时间,间接拉大时间差;
    • 数据结构简单、created 内无耗时操作时,对时间差影响极小。

核心结论

  • 时间差的核心影响因素是模板 / DOM 复杂度,其次是数据初始化的耗时;
  • 两者仅体现 DOM 渲染耗时差异,不会改变 “created 先执行、mounted 后执行” 的固定顺序。

如果使用 Vue3.0 实现一个 Modal,你会怎么进行设计?

核心功能与实现逻辑

  • 基础显隐控制

    • 利用v-model绑定布尔值(如v-model:visible)实现弹窗显隐的双向绑定,内部通过监听该值控制 DOM 渲染;
    • 显隐切换时添加过渡动画(使用 Vue3 的<Transition>组件),提升交互体验,如淡入淡出、滑入滑出效果。
  • 内容自定义

    • 通过<slot>插槽实现内容灵活定制:默认插槽承载弹窗主体内容,具名插槽(如headerfooter)分别定义弹窗头部(标题)、底部(确认 / 取消按钮);
    • 支持通过 props 传入标题文本,满足简单场景的快速使用,复杂标题则通过插槽自定义。
  • 交互与关闭逻辑

    • 支持点击弹窗遮罩层、右上角关闭按钮、底部取消按钮触发关闭,关闭逻辑封装为内部方法,通过emit触发父组件更新visible值;
    • 可选配置closeOnClickModal(是否点击遮罩关闭)、closeOnEsc(是否按 ESC 键关闭),提升灵活性。
  • 样式与挂载方式

    • 使用scoped样式 + CSS 变量实现样式隔离,支持通过 props 传入自定义 class 或样式变量(如弹窗宽度、背景色),适配不同业务场景;
    • 采用teleport(传送门)将 Modal 挂载到body节点下,避免被父组件的overflow: hiddenz-index影响,保证弹窗层级正确。
  • 逻辑封装与复用

    • 将显隐控制、关闭逻辑、键盘事件监听等核心逻辑抽离到setup中,通过refwatch管理状态,保证代码简洁;
    • 支持函数式调用(如Modal.open({ title: '提示', content: '内容' })),通过创建组件实例并手动挂载到 DOM,满足非模板调用场景。

核心结论

  • 基于 Vue3 的组合式 API+Teleport + 插槽实现,核心解决显隐控制、内容自定义、样式隔离三大问题;
  • 兼顾基础场景的易用性和复杂场景的扩展性,同时通过过渡动画提升交互体验。

SSR 是什么?

  1. 页面的 HTML 在服务器端生成,浏览器直接拿到完整的 DOM 结构,而不是空页面加 JS。
  2. 首屏加载更快:浏览器直接渲染服务器返回的 HTML,不需要等待 JS 下载、解析、执行后再渲染;
  3. SEO 更友好:搜索引擎能直接读取完整页面内容,而不是只看到空壳;
  4. 服务器压力更大;

总结:SSR 就是页面在服务器端渲染成完整 HTML,首屏更快、SEO 更好,然后客户端再激活成 SPA。

SPA(单页应用)首屏加载速度慢怎么解决?/ 单页应用如何提高加载速度?

  • 减小打包体积

    • 路由懒加载:将非首屏路由对应的组件拆分为独立代码块,仅访问时加载,降低首屏需加载的 JS 体积;
    • 按需引入第三方库:UI 库、工具库只引入使用的部分,避免全量打包(如 Vue 的 UI 库按需引入按钮、表格等组件);
    • 代码压缩与 Tree Shaking:开启生产环境代码压缩,剔除未使用的代码,减小 JS/CSS 文件体积;
    • 替换轻量依赖:用体积更小的库替代重量级库(如 dayjs 替换 moment.js)。
  • 提升资源加载效率

    • 静态资源 CDN 部署:将 JS、CSS、图片等放到 CDN,利用就近节点加速资源获取;
    • 开启 Gzip/Brotli 压缩:服务器对传输的文件压缩,通常可减小 50% 以上体积;
    • 合理利用缓存:给静态资源加哈希值,配置强缓存 / 协商缓存,避免重复加载不变资源;
    • 预加载 / 预解析:对首屏关键资源用<link rel="preload">预加载,提升加载优先级。
  • 优化渲染逻辑

    • 首屏骨架屏:先渲染骨架屏占位,待数据 / 资源加载完成后替换,降低用户感知的等待时间;
    • 数据请求优化:首屏接口提前请求、合并请求,减少 HTTP 请求次数;
    • 降级渲染方案:对低性能设备 / 弱网环境,简化首屏渲染内容,优先展示核心信息;
    • 服务端渲染(SSR)/ 预渲染:对首屏要求高的场景,用 SSR 在服务端生成完整 HTML,或预渲染生成静态 HTML,替代客户端动态渲染。

什么是虚拟 DOM? 如何实现一个虚拟 DOM? 说说你的思路

1. 什么是虚拟 DOM?
  • 核心定义

    • 虚拟 DOM(Virtual DOM)是用 JS 对象模拟真实 DOM 结构的抽象层,它映射真实 DOM 的层级、属性和内容,且不依赖浏览器 DOM 环境;
    • 核心作用是减少真实 DOM 的频繁操作:通过对比新旧虚拟 DOM 的差异(diff 算法),只把变化的部分更新到真实 DOM,而非重绘整个页面,提升渲染性能。
  • 核心特性

    • 轻量:JS 对象操作远快于真实 DOM 操作;
    • 跨平台:可在浏览器、Node.js 等环境使用(如 Vue 的 SSR、小程序适配);
    • 可复用:虚拟 DOM 节点可被缓存、复用,降低渲染开销。
2. 实现虚拟 DOM 的核心思路
  • 第一步:定义虚拟 DOM 的结构

    • 用 JS 对象描述真实 DOM 节点,核心包含三类属性:

      • 节点类型(type):如元素节点('div'/'button')、文本节点('text');
      • 节点属性(props):如 class、style、onClick 等;
      • 子节点(children):数组形式,包含子虚拟 DOM 节点(元素 / 文本)。
  • 第二步:创建虚拟 DOM(h 函数)

    • 封装h函数,接收typepropschildren参数,返回符合上述结构的 JS 对象(即虚拟 DOM 节点);
    • 处理边界场景:如 children 为文本时,自动转为文本类型的虚拟 DOM 节点。
  • 第三步:将虚拟 DOM 渲染为真实 DOM(mount 函数)

    • 封装mount函数,接收虚拟 DOM 节点和挂载容器:

      • 若为元素节点:创建对应真实 DOM 元素,设置props(如绑定事件、添加 class),递归处理children并挂载到当前元素;
      • 若为文本节点:创建Text节点,设置文本内容;
      • 最终将生成的真实 DOM 挂载到容器中。
  • 第四步:对比新旧虚拟 DOM,计算差异(diff 算法)

    • 封装diff函数,递归对比新旧虚拟 DOM 的typepropschildren

      • 类型不同:直接替换整个节点;
      • 类型相同:对比props(更新新增 / 修改的属性,移除废弃属性),对比children(按 key 匹配、增删改子节点);
      • 核心优化:只对比同层级节点,忽略跨层级移动(降低算法复杂度)。
  • 第五步:将差异更新到真实 DOM(patch 函数)

    • 封装patch函数,接收差异结果和真实 DOM 节点,只把差异部分更新到真实 DOM,而非重绘整个 DOM 树。

大型项目中,Vue 项目怎么划分结构和划分组件比较合理呢?

项目整体结构划分

  • 基础目录分层(按功能 / 类型)

    src/核心目录拆分:

    • assets/:存放全局静态资源(图片、字体、全局样式),按类型再分images/styles/(含全局变量、重置样式);
    • components/:全局通用组件(如按钮、输入框、分页器),与业务解耦,可跨页面复用;
    • views/:页面级组件,对应路由路径,每个页面单独建文件夹(如User/),内含页面核心逻辑和专属子组件;
    • router/:路由配置,按业务模块拆分路由文件(如modules/user.js),支持动态路由、路由守卫;
    • store/:状态管理(Vuex/Pinia),按业务模块划分(如user/order/),仅存放全局共享数据;
    • api/:接口请求封装,按业务模块拆分(如user.jsorder.js),统一管理接口地址和请求参数;
    • utils/:工具函数(如时间格式化、权限判断),按功能拆分文件,避免单一文件过大;
    • directives/:全局自定义指令(如权限指令、防抖指令);
    • hooks/:Vue3 组合式 API 封装,抽离通用业务逻辑(如表单校验、列表加载)。
  • 业务模块隔离

    • 复杂业务(如订单、商品)在views/api/store/中按相同模块名对齐,形成「模块闭环」,便于定位和维护;
    • 非核心业务(如埋点、日志)封装为独立插件,通过plugins/目录管理,按需引入。

组件划分策略

  • 按粒度分层

    • 全局通用组件(原子级):粒度最小,无业务逻辑(如ButtonInput),放在components/,统一设计规范;
    • 业务通用组件(分子级):基于原子组件组合,含少量通用业务逻辑(如SearchFormTableList),放在components/business/
    • 页面专属组件(页面级):仅当前页面使用,放在页面目录下(如views/User/components/),不对外暴露;
    • 页面组件(根级):views/下的核心组件,负责整合页面内子组件、处理页面级逻辑(如接口请求、路由跳转)。
  • 划分核心规则

    • 单一职责:一个组件只做一件事(如「订单列表」组件只负责渲染列表,筛选逻辑抽离为独立筛选组件);
    • 粒度适中:避免组件过大(如一个页面拆成多个子组件),也避免过度拆分(如单个按钮拆成独立组件);
    • 数据隔离:组件内部数据优先私有化,跨组件通信通过 props/emit、全局状态管理,避免直接修改父组件数据;
    • 复用优先:出现 2 次及以上的 UI / 逻辑,立即抽离为通用组件 /hooks。

核心结论

  • 项目结构按「基础功能 + 业务模块」分层,目录命名统一、职责清晰,适配多人协作;
  • 组件按「原子→分子→页面」粒度划分,遵循单一职责,通用逻辑抽离复用;
  • 核心是让每个文件 / 组件的职责可预判,降低大型项目的维护成本。

自定义指令是什么?有哪些应用场景?

自定义指令的核心定义

  • Vue 的自定义指令是对 DOM 元素进行底层操作的扩展方式,允许开发者封装可复用的 DOM 操作逻辑,通过指令语法(如v-directive)绑定到 DOM 元素上,补充 Vue 内置指令(v-if、v-for 等)无法覆盖的 DOM 操作场景。
  • 核心特性:分为全局自定义指令(注册后全项目可用)和局部自定义指令(仅当前组件可用),支持钩子函数(如绑定、更新、卸载),可监听元素的生命周期并执行对应逻辑。

核心应用场景

  • 防抖 / 节流:封装v-debounce/v-throttle指令,绑定到按钮 / 输入框,控制点击 / 输入事件的触发频率,避免频繁请求或操作;
  • 图片懒加载:封装v-lazy指令,监听元素滚动位置,仅当图片进入视口时才加载,减少首屏资源请求。
  • 点击复制:封装v-copy指令,点击时动态创建 textarea,赋值并调用execCommand('copy'),成功后移除元素并提示。
  • 输入框聚焦:封装v-focus指令,页面加载或满足条件时自动让输入框获取焦点;
  • 表单校验:封装v-validate指令,绑定表单元素,实时校验输入内容并提示错误;

vue3 的响应式库是独立出来的,如果单独使用是什么样的效果?

  1. 核心结论

    • Vue3 的@vue/reactivity响应式库可脱离 Vue 组件独立使用,核心效果是给普通 JS 对象 / 数组添加响应式能力,实现 “数据变化自动触发回调”,但无 Vue 组件的模板渲染、生命周期等能力。
  2. 单独使用的具体效果

    • 数据响应式化

      • 可通过reactive/ref将普通 JS 对象、基本类型数据转为响应式数据,修改数据时会自动追踪依赖;
      • 支持computed创建缓存计算属性,依赖数据变化时自动更新计算结果。
    • 数据变化监听

      • 可通过watch/watchEffect监听响应式数据变化,数据修改时自动触发回调函数,无需手动编写监听逻辑;
      • 监听逻辑与 Vue 组件内完全一致,支持深度监听、立即执行、取消监听等特性。
    • 无 Vue 组件关联能力

      • 仅提供数据层面的响应式,无法自动更新 DOM / 模板(无 Vue 的渲染器);
      • 无组件生命周期、指令、插槽等 Vue 专属能力,仅聚焦 “数据 - 回调” 的响应式逻辑。
  3. 典型使用场景

    • 非 Vue 项目(如纯 JS 工具、Node.js 服务)需要响应式数据管理;
    • 跨框架复用响应式逻辑(如 React/Angular 项目中嵌入 Vue 的响应式能力);
    • 封装独立的业务逻辑库,需数据变化自动触发处理逻辑(如表单校验、状态管理)。
  4. 核心结论

    • 单独使用仅保留数据响应式核心能力,剥离了 Vue 的视图渲染层;
    • 本质是 “纯数据层面的响应式工具”,可适配任何需要数据监听的 JS 场景。

什么是watch什么是watchEffect,他们有什么区别

核心定义

  • watch:是 Vue3 中显式监听指定数据源的响应式 API,需明确指定要监听的变量 / 计算值,仅当数据源发生变化时执行回调。
  • watchEffect:是 Vue3 中隐式监听依赖的响应式 API,无需指定监听目标,自动收集回调内部用到的响应式数据作为依赖,依赖变化时触发回调。

核心区别

  • 监听方式

    • watch:显式指定监听源(如 ref、reactive 对象的属性、计算属性),仅监听指定的数据源;
    • watchEffect:隐式收集依赖,回调执行时用到的所有响应式数据都会被自动监听,无需手动指定。
  • 执行时机

    • watch:默认首次不执行(可通过immediate: true配置立即执行),仅在监听源变化时触发;
    • watchEffect:首次渲染时立即执行(收集依赖),之后依赖变化时自动触发。
  • 参数获取

    • watch:回调可获取新值、旧值(如监听 ref 时,回调参数为 newVal、oldVal),能明确对比数据变化前后的状态;
    • watchEffect:回调无参数,无法直接获取旧值,仅能感知依赖变化并执行逻辑。
  • 使用场景适配

    • watch:适合需要精准监听特定数据、关注数据变化前后值、按需触发的场景(如监听表单提交状态、路由参数变化);
    • watchEffect:适合依赖较多且无需区分新旧值、需要立即执行的场景(如根据多个数据自动请求接口、实时更新 DOM 样式)。

核心结论

  • watch 是 “精准监听、按需执行”,适合明确数据源且关注值变化的场景;
  • watchEffect 是 “自动收集、立即执行”,适合依赖分散且只需触发逻辑的场景。

Vue 怎么实现权限管理?控制到按钮级别的权限怎么做?

  1. 核心思路

    • 遵循「前端鉴权 + 后端兜底」原则,前端负责控制界面显示,后端负责校验接口权限,防止通过 URL 或调试工具绕过;
    • 权限控制分为三个层级:路由级(进不去)、页面级(看不见模块)、按钮级(看不见按钮)。
  2. 路由级权限控制

    • 全局前置守卫

      • router.beforeEach中拦截路由跳转,获取当前用户的权限列表;
      • 对比目标路由的meta中配置的权限要求,若无权限则跳转到 403 页面或登录页。
    • 动态路由注册

      • 登录成功后,根据后端返回的权限菜单数据,动态生成路由配置;
      • 调用router.addRoute将可访问的路由添加到路由实例中,实现 “没有权限的路由根本不存在”。
  3. 按钮级权限控制(核心)

    • 方案一:自定义指令(推荐)

      • 封装全局指令(如v-permission),绑定在按钮上并传入权限标识;
      • 指令内部逻辑:获取全局存储的用户权限列表,判断当前标识是否存在;
      • 处理结果:存在则正常渲染,不存在则从 DOM 中移除该元素(或设置disabled),避免无效占位。
    • 方案二:权限组件(封装)

      • 封装一个<Auth>组件,接收perms属性和默认插槽;
      • 组件内部进行权限判断,有权限时渲染插槽内容,无权限则返回空;
      • 适用于不希望操作 DOM 底层,或需要复杂逻辑判断的场景。
  4. 核心结论

    • 路由权限靠守卫拦截动态添加,按钮权限靠自定义指令控制 DOM 显隐;
    • 前端权限只是为了用户体验,后端必须对每一个接口进行权限校验,这是安全底线。