vue前端面试题——记录一次面试当中遇到的题(2)
1.vue2和vue3的区别
一、核心机制与响应式原理(最根本的区别)
Vue 2:使用 Object.defineProperty
-
原理: 通过遍历对象的所有属性,并使用
Object.defineProperty
将它们转换为getter/setter
来实现响应式。 -
局限性:
-
无法检测属性的添加或删除: 必须使用
Vue.set
或this.$set
来确保新属性是响应式的。 -
无法检测数组索引和长度的变化: 通过索引设置数组项 (
arr[index] = newValue
) 或修改数组长度 (arr.length = newLength
) 不会被检测到。Vue 2通过重写数组的7个变更方法(如push
,pop
,splice
等)来 hack 解决。 -
性能问题: 对于深层嵌套的对象,需要递归遍历所有属性,初始化性能较差。
-
Vue 3:使用 Proxy
- 原理: 通过ES6的
Proxy
来代理整个对象,创建一个对象的“拦截层”。 - 优势:
-
支持更多数据结构: 原生支持
Map
,Set
,WeakMap
,WeakSet
等。 -
性能提升: 只在
Proxy
层面进行拦截,无需递归初始化所有属性,只有在访问到嵌套对象时才会递归处理,延迟了计算,提升了初始化性能。 -
更好的数组支持: 天然支持数组索引和长度的直接操作。
-
全面拦截: 可以检测到所有属性的增、删、改、查,包括动态添加的属性,无需使用
Vue.set
。
-
二、组合式 API (Composition API) vs 选项式 API (Options API)
Vue 2:选项式 API
-
组织方式: 将代码按照逻辑关注点(如数据、方法、计算属性、生命周期等)分散在不同的选项(
data
,methods
,computed
,mounted
等)中。 -
缺点: 当一个组件的逻辑复杂时,同一功能的代码会被拆分到不同选项,导致组件难以阅读和维护。尤其是在使用
mixin
共享逻辑时,容易发生命名冲突和数据来源不清晰的问题。
Vue 3:组合式 API (主要新增,选项式 API 仍支持)
-
组织方式: 允许你将与同一逻辑功能相关的代码(状态、方法、生命周期等)组织在一起。
-
核心:
setup
函数和一系列响应式API(如ref
,reactive
,computed
,watch
等)。 -
优势:
-
更好的逻辑复用与代码组织: 可以将相关的逻辑抽取到一个独立的“组合函数”(Composable)中,实现高度可复用的逻辑。
-
更灵活的代码组织: 代码按功能而非选项进行组织,使得复杂组件更易于理解和维护。
-
更好的 TypeScript 支持: 消除了
this
的类型推断问题,因为setup
中几乎没有this
。
-
三、生命周期变化
生命周期钩子被重新命名以更好地配合组合式 API。
-
beforeCreate
-> 使用setup()
-
created
-> 使用setup()
-
beforeMount
->onBeforeMount
-
mounted
->onMounted
-
beforeUpdate
->onBeforeUpdate
-
updated
->onUpdated
-
beforeDestroy
->onBeforeUnmount
-
destroyed
->onUnmounted
-
新增:
onErrorCaptured
(捕获子孙组件的错误)
注意:Vue 3 中
beforeDestroy
和destroyed
已被重命名为onBeforeUnmount
和onUnmounted
,但旧名称在 Vue 3 中仍作为别名使用。
四、性能与打包体积优化
-
更小的体积: Vue 3 通过 Tree-shaking 优化,使得很多不常用的 API(如
transition
,v-model
指令等)可以被摇掉,最终打包体积比 Vue 2 小很多。 -
虚拟 DOM 重写: 优化了虚拟 DOM 的 diff 算法,在编译时提供了更多的提示信息,减少了运行时比较的开销。
-
编译器优化: 如静态节点提升(Hoist Static)、Patch Flags 等,减少了不必要的更新操作。
2.vue2和vue3 性能方面的区别
一、打包体积(Bundle Size)
-
Vue 2: 无论我们用到哪些功能,最终打包的整个 Vue 核心库都会被打进来。例如,即使用不到
transition
组件,它的代码也无法被移除。 -
Vue 3: 引入了 Tree-shaking(摇树优化)机制。Vue 3 的绝大多数全局 API 和内部组件(如
v-model
、transition
等)都设计为可以通过 ES Module 的import
语法按需引入。-
原理: 打包工具(如 Webpack、Vite)在打包时会静态分析代码,只将项目中实际使用到的 API 和模块打包到最终产物中。
-
结果: 对于一个只使用了 Vue 核心功能的项目,Vue 3 的打包体积可以比 Vue 2 小 40%+。项目依赖的功能越少,体积优化效果越明显。
-
二、编译时优化(Compiler Optimizations)
Vue 3 的模板编译器变得更“智能”,它会在编译模板时进行分析,并生成更优化的渲染函数代码。
-
静态节点提升(Hoist Static)
-
Vue 2: 在每次重新渲染时,无论元素是否是静态的(如
<div>Hello World</div>
),都会创建新的 VNode 节点,并进行完整的 Diff 比较。 -
Vue 3: 编译器会识别出纯静态的节点和子树,并将其“提升”到渲染函数之外。
-
结果: 这些静态节点只会在首次渲染时创建一次,后续更新时会直接复用,完全跳过 Diff 过程。这大大减少了 VNode 创建和比较的开销。
-
-
-
Patch Flags(补丁标志)
-
Vue 2: 在对比两个 VNode 时,由于不知道具体是哪个部分会变化,因此需要递归地比较所有属性、子节点等,这是一项相对昂贵的操作。
-
Vue 3: 编译器会在动态节点上打上一个
PatchFlag
标记(一个数字),标识出这个节点需要更新的具体内容类型,例如1
代表文本
变化,2
代表class
变化,4
代表style
变化等。-
结果: 在运行时,Diff 算法可以直接根据这些标志进行定向更新。比如一个节点只有
class
会变,那么运行时只需要检查class
而无需关心其他属性,极大地提升了 Diff 效率。
-
-
-
缓存事件处理函数(Cache Handler)
-
问题: 在 Vue 2 中,内联的事件处理函数(如
@click="() => {...}"
)在每次父组件更新时都会被视作一个新的函数,导致子组件不必要的更新。 -
Vue 3: 编译器会自动缓存内联事件处理函数。除非事件依赖的响应式数据发生变化,否则处理函数会被缓存起来,避免子组件的无效重渲染
-
三、响应式系统(Reactive System)
这是性能提升最关键的一环,从 Object.defineProperty
切换到 Proxy
。
-
Vue 2 (
Object.defineProperty
):-
初始化性能: 在初始化时,需要递归地遍历数据对象的所有属性,并使用
Object.defineProperty
进行转换。对于深层嵌套的大对象,这个过程会比较慢。 -
内存开销: 需要为每个对象的每个属性维护一个依赖关系列表(Dep),内存占用相对较高。
-
-
Vue 3 (
Proxy
):-
初始化性能:
Proxy
是代理整个对象,无需递归遍历所有属性。它只在属性被访问时才会递归将其转换为响应式,这是一种“惰性”处理,初始化速度更快。 -
内存开销:
Proxy
不需要为每个属性创建 Dep,它只在getter中动态地追踪依赖,内存效率更高。 -
组件更新粒度: Vue 3 的响应式系统与组件的渲染关联更精确,可以减少不必要的组件更新
-
四、虚拟 DOM 与 Diff 算法
虽然 Vue 3 依然使用虚拟 DOM,但其内部的 Diff 算法经过了彻底的重写。
-
Vue 2: 采用的是一个全面的、递归的 Diff 算法。当组件更新时,会从根节点开始,递归地比较新旧两棵 VNode 树。
-
Vue 3:
-
得益于编译时的
Patch Flags
,运行时可以跳过大量的无意义比较。 -
实现了更高效的更新类型推断,例如,在对比子节点列表时,采用了更快速的双端比较算法,并优先处理常见的更新模式(如头尾节点的增删)
-
3.兼容性的问题之前遇到过吗?是怎么解决的?
1. 浏览器兼容性
这是最常见的一类,尤其是需要支持旧版浏览器(如 IE)时。
-
问题场景:
-
使用了 ES6+ 新特性(如
Promise
,Array.prototype.includes
, 箭头函数,const/let
)。 -
使用了现代浏览器才支持的 Web API(如
fetch
,IntersectionObserver
)。 -
CSS 属性在某些浏览器中需要前缀(如
-webkit-
,-moz-
)。
-
-
解决方案:
-
语法转换与 Polyfill:
-
工具: 使用 Babel 将 ES6+ 代码转换为 ES5 语法。
-
配置: 在
babel.config.js
或.browserslistrc
文件中指定需要兼容的浏览器范围,Babel 会根据这个范围决定需要转换哪些特性。 -
Polyfill: 对于 Babel 无法转换的 API(如
Promise
,fetch
),需要引入 core-js 和 regenerator-runtime 来模拟实现这些功能。
javascript
// 在项目入口文件 (如 main.js) 中引入 import 'core-js/stable' import 'regenerator-runtime/runtime'
-
-
自动化前缀:
-
工具: 使用 PostCSS 配合 Autoprefixer 插件,它会根据
.browserslistrc
配置自动为 CSS 属性添加供应商前缀。
-
-
条件依赖与动态加载:
-
对于一些非核心的、只在现代浏览器中性能更好的库(如
IntersectionObserver
),可以采用动态import()
或通过条件判断来加载 Polyfill 或降级方案。
-
-
2. Vue 2 到 Vue 3 的迁移兼容性
这是在升级框架版本时遇到的最典型问题。
-
问题场景:
-
被移除的 API: 如
$on
,$off
,$once
事件总线 API、Filters
(过滤器)、Vue.extend
等。 -
行为改变的 API: 如
v-model
的用法、keyCode
修饰符、$listeners
被合并到$attrs
等。 -
生命周期钩子名称变化:
destroyed
->unmounted
。
-
-
解决方案:
-
使用官方迁移指南和工具:
-
第一步: 仔细阅读 Vue 3 迁移指南,这是最重要的参考资料。
-
第二步: 使用官方提供的 迁移构建版本 (Migration Build)。它在 Vue 3 运行时中提供了 Vue 2 兼容行为,并在你使用已移除/改变的 API 时发出警告。这让你可以逐步修改代码,而不是一次性重写。
-
第三步: 运行 vue-upgrade 工具,它可以自动检测代码库中需要修改的地方。
-
-
渐进式重构:
-
对于大型项目,一次性迁移风险太高。可以采用“渐进式迁移”策略:
-
将项目构建工具(如 Webpack)配置为同时支持 Vue 2 和 Vue 3 包。
-
新写的组件使用 Vue 3 的 Composition API。
-
旧的 Vue 2 组件暂时保留,逐步重构。
-
-
-
替代方案:
-
事件总线: 用
mitt
或tiny-emitter
这类第三方库替代$on/$off
。 -
过滤器: 用计算属性或方法替代。
-
全局 API 变更: 将
Vue.prototype
的修改改为使用app.config.globalProperties
。
-
-
3. 第三方库兼容性
-
问题场景:
-
项目依赖的某个第三方库(如 UI 组件库、工具库)尚未支持 Vue 3。
-
-
解决方案:
-
检查官方仓库: 首先去该库的 GitHub 或官网查看是否有支持 Vue 3 的版本。很多流行库(如 Element Plus, Vant, Ant Design Vue)都提供了 Vue 3 版本。
-
寻找替代品: 如果原库不再维护,寻找一个功能相似且支持 Vue 3 的替代库。
-
自己封装或降级: 如果是小功能,可以考虑自己实现。如果暂时无法替换,可以考虑在 Vue 3 项目中使用 vue-demi 来尝试兼容,但这通常是临时方案。
-
举例说明(让回答更具体)
“让我举一个具体的例子。在我们将项目从 Vue 2 升级到 Vue 3 的过程中,遇到了一个典型问题:我们之前大量使用了 Event Bus
进行组件间通信。
-
问题: Vue 3 移除了
$on
,$off
这些实例方法,导致所有通过new Vue()
创建的 Event Bus 全部失效,控制台报错。 -
排查: 我们运行了官方的迁移构建版本,它清晰地警告了我们正在使用已移除的 API。然后我们通过全局搜索
$on
和$off
,定位了所有使用 Event Bus 的文件。 -
解决:
-
我们评估了通信场景,对于一些简单的跨组件通信,我们用 Vue 3 的
provide/inject
进行了重构。 -
对于更复杂的、需要事件监听和发射的场景,我们引入了官方推荐的轻量级库
mitt
。 -
我们将原来的
const bus = new Vue()
替换为const bus = mitt()
。 -
将
bus.$on('event', handler)
改为bus.on('event', handler)
。 -
将
bus.$off('event', handler)
改为bus.off('event', handler)
。
-
-
结果: 通过这种方式,我们以最小的代价平滑地解决了这个兼容性问题,并且代码更加现代化和清晰。”
总结与预防
“通过处理这些兼容性问题,我总结出的经验是:
-
预防优于解决: 在项目启动时,就通过
.browserslistrc
和package.json
的engines
字段明确目标环境。 -
依赖管理: 定期更新依赖,关注核心库(如 Vue、React)的发布动态和破坏性更新预告。
-
工具化: 善用官方提供的迁移工具和林格(ESLint)规则,它们能提前发现大部分潜在问题。
-
渐进策略: 对于大型项目的升级,不要追求一步到位,采用渐进式、可回滚的方案更为稳妥。
4.在pc当中上传图片,在小程序当中进行展示,假设在小程序当中图片所呈现的大小比例都固定,如何处理pc的图片,在小程序当中显示是不会被拉伸的
方案一:使用 CSS object-fit
属性(推荐)
-
object-fit: cover
(CSS) /mode="aspectFill"
(小程序):-
保持图片原始比例缩放
-
确保图片完全覆盖容器
-
可能会裁剪掉图片的边缘部分
-
适用场景: 头像、商品主图、 banner 等,确保容器被填满且视觉重点在中央的情况
-
-
object-fit: contain
(CSS) /mode="aspectFit"
(小程序):-
保持图片原始比例缩放
-
确保整个图片都在容器内可见
-
容器可能会有留白
-
适用场景: 需要完整展示图片内容,如证件照、详细产品图等
-
.container {width: 300px; /* 容器固定宽度 */height: 200px; /* 容器固定高度 (3:2比例) */overflow: hidden;
}.adaptive-image {width: 100%;height: 100%;object-fit: cover; /* 关键属性 */
}
<!-- 在小程序WXML中 -->
<view class="container"><image class="adaptive-image" src="{{imageUrl}}" mode="aspectFill"></image>
</view>
方案二:背景图方式(灵活控制)
.image-container {width: 300px;height: 200px;background-position: center center;background-repeat: no-repeat;background-size: cover; /* 或 contain */
}
<view class="image-container" style="background-image: url({{imageUrl}})"
></view>
方案三:服务端预处理(终极解决方案)
// 服务端处理示例 (Node.js + Sharp库)
const sharp = require('sharp');async function processImageForMiniProgram(inputPath, outputPath) {// 定义小程序展示的标准尺寸const TARGET_WIDTH = 750;const TARGET_HEIGHT = 500; // 3:2比例await sharp(inputPath).resize(TARGET_WIDTH, TARGET_HEIGHT, {fit: 'cover', // 覆盖模式position: 'center' // 从中心开始裁剪}).toFile(outputPath);
}
方案四:动态计算 + 定位(精确控制)
// 在小程序JS中计算图片位置
Page({data: {imageStyle: ''},onImageLoad(e) {const { width, height } = e.detail;const containerRatio = 3/2; // 容器宽高比const imageRatio = width / height;let style = '';if (imageRatio > containerRatio) {// 图片较宽,需要水平居中裁剪const displayWidth = height * containerRatio;const offsetX = (width - displayWidth) / 2;style = `width: auto; height: 100%; margin-left: -${offsetX/width*100}%`;} else {// 图片较高,需要垂直居中裁剪 const displayHeight = width / containerRatio;const offsetY = (height - displayHeight) / 2;style = `width: 100%; height: auto; margin-top: -${offsetY/height*100}%`;}this.setData({ imageStyle: style });}
})
<view class="container"><image src="{{imageUrl}}" style="{{imageStyle}}"bindload="onImageLoad"></image>
</view>
实际项目中的综合策略
"在实际项目中,我通常会采用分层策略:
-
前端基础保障: 使用
object-fit: cover
或小程序的mode="aspectFill"
作为基础方案 -
重要图片服务端预处理: 对于商品主图、用户头像等重要图片,在上传时进行智能裁剪和优化
-
用户体验优化:
-
添加加载状态和错误处理
-
使用CDN加速图片加载
-
实现懒加载减少初始加载压力
-
// 上传时的处理建议
const uploadStrategies = {// 用户头像:严格正方形裁剪avatar: { width: 200, height: 200, fit: 'cover' },// 商品主图:3:2比例product: { width: 750, height: 500, fit: 'cover' },// 详情图片:保持原比例,宽度固定detail: { width: 750, height: null, fit: 'inside' }
};