前端面试题之:Vue2基础篇

130 阅读10分钟

哈喽,大家好,我是一个学前端的前端,来来来,小板凳准备好,划重点~,今天来聊聊关于我的面试总结,希望对你有帮助


我:你好,面试官,我是......

面试官:你来说说......

最后:没有最后,啊哈哈哈哈哈


1. 面试官:谈一下你对Vue响应式原理的理解?

这是Vue最核心的特性。Vue 2主要通过Object.defineProperty实现。

  • 核心过程:

    1. 数据劫持:初始化时,Vue会遍历data中的属性,用Object.defineProperty将它们转为getter/setter,在属性被访问和修改时拦截。
    2. 依赖收集:在getter中,会将当前正在计算的Watcher(如组件的render函数)添加为该属性的订阅者。
    3. 派发更新:在setter被触发时,会通知所有相关的Watcher,Watcher再触发组件的重新渲染。
  • 注意事项:Vue 2无法检测到对象属性的直接添加或删除(需用Vue.set/Vue.delete),也无法监听数组索引和长度变化(需使用数组变异方法如push)。

2. 面试官:Vue 2中如何进行数组的响应式更新?

Vue 2对数组做了特殊处理。

  • 原理:它重写了数组的7个变更方法(push, pop, shift, unshift, splice, sort, reverse)。当你调用这些方法时,Vue除了执行原始操作,还会触发视图更新。
  • 注意事项:直接通过索引修改数组元素(arr[index] = newValue)或直接修改数组长度(arr.length = newLength)不会触发更新。解决方案是使用Vue.set(arr, index, newValue)或splice方法。

3.面试官: Vue的虚拟DOM是什么?请描述Diff算法过程。

虚拟DOM是一个用JS对象(VNode)描述的DOM树副本。

  • 作用:通过新旧VNode比较(Diff),计算出最小的DOM操作,提升性能。

  • Diff过程(同层级比较):

    1. 节点比较:新旧节点不同(tag、key等),直接替换。

    2. 相同节点比较:

      • 更新属性:对比并更新节点的class、style等属性。
      • 比较子节点(核心):

        • Vue使用双端比较算法,通过新旧子节点的首尾指针进行4次快速比对(新前-旧前,新后-旧后,新后-旧前,新前-旧后)。
        • 若都未命中,则通过key建立旧子节点的索引图,用新节点的key去查找可复用的旧节点,然后进行移动或创建。

4. 面试官:Vue的生命周期有哪些?created和mounted的区别?

Vue实例从创建到销毁的过程。

  • 主要阶段:

    • 创建阶段:beforeCreate(实例初始化前,无法访问data/methods) -> created(实例创建完成,可访问data/methods,常用于异步请求) -> beforeMount(挂载前) -> mounted(DOM挂载完成,可操作DOM)。
    • 更新阶段:beforeUpdate(数据更新,DOM未更新) -> updated(DOM已更新)。
    • 销毁阶段:beforeDestroy(实例销毁前,常用于清理定时器、解绑事件) -> destroyed(实例销毁后)。
  • 核心区别:

    • created:数据观测已完成,DOM未生成,不能操作DOM。
    • mounted:DOM已挂载,可以操作DOM。

5.面试官:为什么v-for要绑定key?为什么不能用index作为key?

  • key的作用:帮助Vue在虚拟DOM Diff过程中高效地识别节点的身份和复用关系。没有key时,Vue会采用“就地更新”策略,可能导致错误的节点被复用,带来状态更新问题。
  • 为何避免用index:因为index不具有稳定性和唯一性。当列表顺序发生变化(如增、删、排序)时,同一个index指向的数据项已经改变。Vue会误以为可以复用旧的DOM节点,导致更新错误(如本该更新的节点没有更新,或节点状态错乱)。

6.面试官:computed和watch的区别与使用场景?

  • computed(计算属性):

    特点:是基于响应式依赖进行缓存的计算值。只有依赖发生变化,才会重新计算。不支持异步操作。

    场景:适合进行同步计算,并将结果作为属性使用,如:fullName = firstName + lastName。

  • watch(侦听器):

    特点:监听一个特定响应式数据的变化,并在变化时执行异步或开销较大的操作。支持配置immediate(立即执行)和deep(深度监听)。

    场景:适合在数据变化时执行异步请求、复杂逻辑或副作用操作,如:搜索建议、表单验证。

7.面试官:简述Vue的组件通信方式。

常用方式如下,适用于不同场景。

方式通信方向说明使用场景
Props / $emit父 <-> 子父通过props传数据给子,子通过$emit触发事件给父最常用、最基础的父子通信
$refs / $parent / $children父 <-> 子直接访问组件实例操作子组件方法/数据,但耦合度高
$attrs / $listeners父 <-> 子 (跨级)attrs:接收非Props属性;attrs:接收非Props属性;listeners:接收事件创建高级组件(如二次封装UI组件)
Event Bus任意组件创建一个空的Vue实例作为中央事件总线小型项目、简单场景的跨级/兄弟通信
provide / inject祖先 -> 后代祖先组件provide提供数据,后代组件inject注入库/高阶组件开发,有明确层级关系
Vuex任意组件全局状态管理库,集中式存储管理中大型项目,复杂数据流管理

8.面试官:$nextTick的原理和作用是什么?

  • 作用:将回调函数延迟到下次DOM更新循环之后执行。用于确保能获取到数据更新后的最新DOM。

  • 原理:

    1. Vue的DOM更新是异步队列。当你修改数据后,视图不会立即更新,而是将更新作为一个微任务(在Vue 2中优先使用Promise.then,降级到setImmediate或setTimeout)推入队列。
    2. $nextTick接受一个回调函数,也会被推入同一个队列。当同步代码执行完毕,会依次清空这个微任务队列,先执行DOM更新任务,再执行$nextTick的回调,从而拿到更新后的DOM。

9.面试官:说一下Vue 2和Vue 3在响应式实现上的主要区别?

特征vue2vue3
实现方式Object.defineProperty (ES5)Proxy (ES6)
监听范围对对象的每个属性进行劫持直接代理整个对象
数组监听需要重写数组方法,无法直接监听索引变化可直接监听数组索引和长度变化
动态新增属性默认无法监听,需用Vue.set可以直接监听
性能与初始化需要递归遍历初始数据,性能开销大惰性代理,访问时才进行响应式转换,性能更优

10.面试官:Vue中如何实现一个自定义指令?

自定义指令的核心是定义一组生命周期钩子函数。

  • 注册方式:

    1. 全局注册:Vue.directive('focus', { ... })
    1. 局部注册:在组件选项中定义directives: { focus: { ... } }
  • 常用钩子函数:

    1. bind:指令第一次绑定到元素时调用(只调用一次)。
    1. inserted:被绑定元素插入父节点时调用(不一定已插入文档流)。
    1. update:所在组件的VNode更新时调用,可能发生在子VNode更新之前。
    1. componentUpdated:所在组件的VNode及其子VNode全部更新后调用。
    1. unbind:指令与元素解绑时调用(只调用一次)。
  • 示例(实现一个输入框自动聚焦指令):
Vue.directive('focus', {
  inserted: function (el) {
    el.focus()
  }
})
<input v-focus>

11.面试官:Vue.mixin 的使用场景、实现原理与潜在问题

Vue.mixin 是一种分发组件可复用功能的灵活方式,用于全局混入代码逻辑。

  • 使用场景:适用于需要给大量组件注入相同逻辑的情况,例如统一的埋点方法、公共的工具函数或特定的生命周期处理。

  • 实现原理:

    • 在组件初始化时,Vue会将mixin对象与组件自身的选项进行合并。
    • 对于数据对象(如data),会进行递归合并,以组件数据优先。
    • 对于同名钩子函数(如created),会被合并成一个数组,混入的钩子先执行。
    • 值为对象的选项(如methods, components)会被合并为同一个对象,键名冲突时以组件优先。
  • 潜在问题:

    • 来源不明确:混入的属性/方法在多个组件中使用时,难以快速定位其定义位置,增加维护和调试成本。
    • 命名冲突:多个混入或与组件自身选项容易产生命名冲突,可能导致意外覆盖。
    • 滥用导致关系复杂:过度使用会使组件逻辑变得不透明,数据流难以追踪。
  • 替代方案:对于可复用的业务逻辑,更推荐使用组合式函数(在Vue 2中可通过独立模块模拟)或基于Vue.extend的高阶组件。

12.面试官:Vue 组件中 data 为什么必须是函数?

这是为了确保每个组件实例拥有独立的数据副本,防止数据污染。

  • 根本原因:组件是可复用的。如果data直接是一个对象,那么所有使用这个组件的实例将共享引用同一个数据对象。修改其中一个实例的数据,会直接影响所有其他实例。
  • 函数的作用:当data是一个函数时,Vue在创建每个组件实例时都会调用这个函数,返回一个全新的数据对象,从而实现了实例间数据的隔离。
  • 源码层面的简化解释:在组件初始化过程中,Vue会执行mergeOptions策略。对于data选项,会判断其是否为函数,如果是,则通过调用getData来获取独立的数据对象。

13.面试官:Vue.extend 与 Vue.component 的区别及用法

两者都用于组件注册,但层级和用途不同。

Vue.extend(options)

  • 作用:创建一个Vue组件的“子类”或“构造器”。它接收一个组件选项对象,返回一个可复用的组件构造函数(VueComponent)。
  • 用法:常用于编程式地创建组件实例。例如,在需要动态挂载一个弹窗组件到body时:
const MessageConstructor = Vue.extend(MessageComponent)
const instance = new MessageConstructor({ propsData: { text: 'Hello' } })
instance.$mount() // 可以挂载到指定DOM元素
document.body.appendChild(instance.$el)

Vue.component(id, [definition])

  • 作用:注册或获取一个全局组件。它让组件可以在任何Vue实例的模板中通过标签名使用。
  • 用法:definition可以直接是一个选项对象(Vue会在内部自动调用Vue.extend),也可以是一个已经由Vue.extend创建好的构造函数。
// 方式一:传入选项对象
Vue.component('my-button', { template: '<button>Click</button>' })
// 方式二:传入构造器
const MyButton = Vue.extend({ ... })
Vue.component('my-button', MyButton)

14.面试官:如何在Vue 2项目中实现性能优化?

Vue项目性能优化需多维度考虑。

优化方 向具体措施原理与效果
代码层面1. 路由懒加载:使用import()动态导入组件。 2. 第三方库按需引入:如lodash的lodash-es。 3. 合理使用v-ifv-show减少初始包体积,提升首屏加载速度。
运行时层面1. 避免v-forv-if联用。 2. 精细化v-forkey,避免用index。 3. 使用Object.freeze冻结大数据。减少不必要的计算和DOM操作,提升渲染效率。
构建层面1. 利用Webpack的SplitChunks拆包。 2. 使用uglifyjs-webpack-plugin或terser-webpack-plugin压缩混淆。 3. 配置Gzip/Brotli压缩。优化资源加载和传输效率。
高级特性1. 使用keep-alive缓存不活跃组件。 2. 对于纯展示组件,使用functional函数式组件。减少组件创建和销毁开销,提升复用。

15.面试官:解释一下 Vue 的模板编译过程

Vue的模板(.vue文件中的<template>或template选项)会经历一个从字符串到渲染函数的编译过程,主要分为三步:

  1. 解析(Parse):使用正则和词法分析,将模板字符串解析成一个抽象语法树(AST)。AST是一个用JavaScript对象描述的节点树,记录了标签名、属性、父子关系等所有模板信息。
  2. 优化(Optimize):静态标记。遍历AST,找出所有静态节点(即不依赖于响应式数据的节点,如<div>静态文本</div>)和静态根节点,并为它们打上标记。在后续的patch(虚拟DOM对比)过程中,Vue会跳过这些静态节点,从而提升性能。
  3. 代码生成(Generate):将优化后的AST转换(编译)成可执行的渲染函数(render function)的代码字符串。这个渲染函数在被调用时会返回虚拟DOM(VNode)。

最终产物:一个render函数,形式大致如下:

with(this) {
  return _c('div', { attrs: { id: 'app' } }, [
    _c('p', [_v('Hello ' + _s(name))]) // _c创建元素, _v创建文本, _s转字符串
  ])
}