1 描述对vue生命周期的理解
# 1 描述对vue生命周期的理解
Vue 生命周期是指一个组件从 创建 → 挂载 → 更新 → 销毁 的过程
在这个过程中,Vue 提供了不同的钩子函数,方便我们在合适的阶段写逻辑
具体分为几个阶段:
- 创建阶段
beforeCreate:实例刚初始化,data和methods还没挂载。created:实例创建完成,data和methods可以访问,但 DOM 还没生成。 👉 常用场景:发起请求获取数据。
- 挂载阶段
beforeMount:模板编译完成,但还没挂载到页面。mounted:组件 DOM 已经渲染到页面。 👉 常用场景:操作 DOM、使用第三方插件。
- 更新阶段
beforeUpdate:数据更新前,此时可以获取更新前的 DOM。updated:视图更新完成,可以获取更新后的 DOM。 👉 常用场景:对比前后数据/DOM,执行一些需要 DOM 已经更新的操作。
- 销毁阶段
beforeDestroy:实例销毁前,仍然可以访问数据和方法。destroyed:实例销毁后,所有绑定解除,事件监听清理。 👉 常用场景:清理定时器、解绑事件,防止内存泄漏。
# 2 双向数据绑定是什么
双向数据绑定就是 数据和视图保持同步:
数据变化更新视图
视图变化更新数据
监听器
解析器
双向绑定的原理
在 Vue2 中主要通过 数据劫持 + 发布订阅模式:
使用 Object.defineProperty 对数据的 getter/setter 进行劫持。
当数据变化时,触发 setter,通知依赖(Watcher),进而更新视图。
如何实现双向绑定
new Vue初始化,对数据data执行响应化处理,这个过程发现
监听
解析
然后vue3改用了proxy代理对象,实现响应式系统
那双向数据绑定有什么优缺点?
优点:
- 减少手动 DOM 操作,开发效率高。
- 数据和视图实时保持一致,代码更简洁。
缺点:
- 大型项目里,双向绑定的数据流比较隐蔽,不如单向数据流(React)清晰,调试会比较困难。
- 频繁的绑定可能带来性能开销。
所以在 Vue3 中,也推荐尽量用单向数据流,v-model 只在表单等场景下使用。
# 3 Vue组件之间的通信方式都有哪些
Vue 的组件通信方式主要取决于 组件之间的关系
- 父子组件通信
- props:父组件通过
props向子组件传值。 - $emit:子组件通过
$emit向父组件传递事件或数据。 👉 常见于表单输入组件。
- 兄弟组件通信
- Event Bus(事件总线):在 Vue2 中常用一个空的 Vue 实例作为事件中心。
- 在 Vue3 中则可以用 mitt(轻量事件库)代替。
- 跨层级通信
- provide / inject:祖先组件用
provide提供数据,子孙组件用inject注入。 👉 适合多层嵌套的场景,比如主题色、全局配置共享。
- 全局状态管理
- Vuex(Vue2/3 通用)、Pinia(Vue3 推荐)。 👉 适合大型应用,管理全局共享数据。
- 其它方式
$attrs / $listeners:父组件传递属性,子组件透传给子孙组件(Vue3 合并为$attrs)。$parent / $children / ref:通过组件实例直接访问,不过不推荐,耦合度高。
那你在实际项目中,最常用的是哪几种?
- 简单父子组件 →
props / emit就够了。 - 跨层级/兄弟组件 → 小项目用
provide / inject或 mitt,大项目用 Vuex/Pinia。 - 全局性配置 → 用状态管理,比如登录信息、主题。
# 4 为什么在 Vue 组件里,data 属性必须是一个函数,而不是一个对象?
这是因为 组件是可以复用的。
- 如果
data是一个对象,那么多个组件实例会 共享同一个对象引用,修改一个实例的数据会影响所有实例,造成数据污染。 - 如果
data是一个函数,每次创建组件实例时,都会执行一次函数并返回一个新的对象,保证 每个组件实例都有自己独立的数据作用域。
// 错误写法(共享同一个对象)
data: {
count: 0
}
// 正确写法(每个组件实例都有自己的 count)
data() {
return {
count: 0
}
}
这样每个组件实例互不干扰,才符合组件化的设计思想。
那为什么在 new Vue() 根实例中可以写成对象呢?
根实例只会被创建一次,不存在复用的问题,所以可以直接写成对象。
Vue3 里用 setup 定义数据是不是也解决了这个问题?
Vue3 setup 每次实例化都会执行一遍,所以不会出现数据共享问题。但如果把变量定义在 setup 外部,就会被多个组件共享。
# 5 动态给vue的data添加一个新的属性时会发生什么?怎么解决?
Vue2 动态添加属性不会响应,需要用 Vue.set;Vue3 用 Proxy 实现响应式,天然支持动态属性。
# 6 v-if和v-for的优先级是什么?
v-for的优先级高于v-if,所以避免将他们两个同时用在同一个元素上,会带来性能方面的浪费
一般都是外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后内部v-for循环
如何条件出现在循环内部,可以通过计算属性computed提前过滤掉哪些不需要显示的项
# 7 v-show和v-if有什么区别?使用场景分别是什么?
都能控制元素在页面是否显示
当表达式为true时,都会占据页面的位置,为false都不会占据页面的位置
区别:
控制手段不同:v-show是设置css-display:none,dom元素依旧还在,v-if显示隐藏是将dom元素整个添加或者删除
编译过程不同:v-if切换有一个局部编译/卸载的过程,css只是简单的基于css切换
编译条件不同:v-if是真正的条件渲染,确保切换过程中条件块内的事件监听器子组件等都被销毁或者重建,只有真才操作渲染假的话就是不做操作
原理:
v-show 不管初始条件是什么,元素总是会被渲染 判断是否有transition,有则执行,没有就直接设置dispaly属性
v-if 他会返回一个node节点然后根据render函数同表达式的值来决定是否生成DOM
使用场景:
需要频繁地切换元素使用v-show比较好
运行条件很少改变,使用v-if比较好
# 8 你知道vue中key的原理吗?说说你对它的理解
key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key更准确,更快的找到对应的vnode节点
具体来说,当我们使用 v-for 渲染一组元素时,Vue 在更新时会通过对比新旧虚拟 DOM 来决定哪些元素需要复用、哪些需要创建或销毁。而这个过程中,key 的作用就是 唯一标识每个节点,让 Vue 能精确判断两个节点是否是同一个。
设置key和不设置key的区别
如果不写 key,Vue 默认会采用“就地复用”的策略,也就是说它会尝试复用相同位置的元素,这可能在某些情况下导致渲染错误,比如列表项顺序改变的时候。
举个例子,如果我们有一个输入框列表,用户在输入内容,这时候列表项顺序变化了,如果没加 key,Vue 可能会错误地复用了 DOM,导致输入框的值对不上。
实际开发中我通常会用唯一的 id 来做 key,而不是 index,除非顺序不会改变
# 9 你说说看,对 Vue 的 mixin 怎么理解?你用过吗?它一般适合在哪些场景用?
mixin 我有用过,在我理解里,它是 Vue 提供的一种复用组件逻辑的方式,我们可以把多个组件之间通用的逻辑提取出来,写在一个 mixin 对象里,然后通过 mixins: [] 的方式混入组件中。
混入可以分为局部混入和全局混入,插件的话一般是全局混入
封装重复的请求逻辑:比如多个组件在加载时要发起分页请求,可以把分页逻辑、数据处理、loading 状态抽出来做成 mixin。在 Vue 2 中,组合式 API 还没出现之前,mixin 是比较主要的逻辑复用方式。
如果是 Vue 3 项目,我会优先考虑 Composition API 的方式替代 mixin
那你觉得 mixin 和组合式 API 相比,各有什么优势?
mixin 的上手成本低,对于新手或者小项目来说写法直观;
但组合式 API 逻辑归属清晰,参数传递更灵活如果是多人协作或者维护大型项目,组合式 API 会更适合,模块化程度更高。
# 10 vue常用的修饰符有哪些什么应用场景
修饰符处理了许多DOM事件的细节,是用于限定类型以及类型成员的声明的一种符号
这个我平时开发中经常用,Vue 中的修饰符主要是为了增强指令的功能,常见的修饰符有:
👉 v-on 事件修饰符:
.stop:阻止事件冒泡,比如一个嵌套组件中有点击事件,防止外层一起触发;.prevent:阻止默认行为,像表单提交或 a 标签跳转常用;.capture:用事件的捕获模式;使事件触发从包含这个元素的顶层开始往下触发.self:只在事件源是当前元素本身时触发(避免被子元素触发);.once:事件只触发一次;.passive:告诉浏览器这个事件不会阻止默认行为,提升滚动性能,常见于touchstart;
👉 v-model 修饰符:
.lazy:把默认的input事件改成change事件,适合用户输入完成后再更新;.number:自动将输入值转换为数字,适合输入表单中需要数字的场景;.trim:去除输入的首尾空格,比如用户填写用户名或邮箱时很实用;
👉 v-bind 修饰符:
.async: 能对props进行一个双向绑定.prop:绑定 DOM 原生属性而不是 HTML attribute,比如 input 的value;.camel:把 kebab-case 的属性名转成 camelCase,适用于绑定驼峰属性名(尤其是自定义组件)。
鼠标键盘修饰符:
- left right middle
- enter tab delete space esc up ctrl shift
那你能具体说说 .stop 和 .self 的区别吗?有没有用过它们?
举一个弹窗组件的例子
比如点击背景层关闭弹窗,但点击弹窗内容区不关闭:
两者的使用动机不同:.stop 是主动阻止冒泡,.self 是只响应自己。
两者可以结合使用:
.self 常用于模态框点击遮罩层关闭,而 .stop 常用于阻止子元素冒泡上去。
那你在项目中有没有遇到过用 .prevent 或 .passive 的实际场景?
有的,.prevent 最典型的就是处理表单或 a 标签:这样可以避免页面跳转或刷新。
至于 .passive,我之前在一个移动端滑动场景中遇到,页面卡顿严重:
你觉得 .number 和直接用 Number(inputValue) 有什么区别?你会推荐哪个?
.number 是语法糖,写起来简单它会在绑定时自动把字符串转换成数字。好处是简洁、统一。
而 Number(value) 是手动转,适合处理更复杂或条件性的逻辑
所以如果只是简单的输入框收集数字,.number 更方便。如果输入逻辑复杂、有格式要求,我会用手动转换结合校验。
# 11 vue中的$nextTick有什么作用?
官方定义:在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个办法,获取更新后的DOM
简单来说就是在 DOM 更新完成之后执行回调函数,也就是说,如果我们在修改数据之后,立即去访问 DOM,可能访问的是“旧 DOM”,这时候就需要 $nextTick 等 Vue 异步更新 DOM 完成之后再操作。
这是因为 Vue 的更新是异步的,它会把数据变更后的 DOM 更新推到事件循环的下一个“tick”中去执行,以进行批量优化。如果我们需要确保 DOM 已经更新,比如要手动操作 DOM、获取元素高度、滚动定位等,就需要放在 $nextTick 里。
能不能举个你实际用过的场景?
可以。比如我在做一个展开/收起动画的折叠面板时:
<template>
<div ref="content" v-show="showContent">内容</div>
</template>
<script setup>
import { ref, nextTick } from 'vue';
const showContent = ref(false);
const content = ref(null);
function toggle() {
showContent.value = !showContent.value;
nextTick(() => {
// v-show 切换完了,现在 DOM 显示出来了
const height = content.value.offsetHeight;
console.log("内容高度:", height);
// 可以用于动画处理或滚动定位等
});
}
</script>
这里如果不加 nextTick,offsetHeight 拿到的是切换之前的值。
那你觉得 $nextTick 和 setTimeout 有什么区别?能不能用 setTimeout 替代?
这两个不太一样。setTimeout(fn, 0) 是把任务放到浏览器的宏任务队列,而 $nextTick 是 Vue 内部实现的,通常基于微任务(如 Promise.then),所以它执行的时机更快,也更可靠地保证 DOM 更新完成。
而且 $nextTick 更明确是等待 Vue 的 DOM 更新完成,而 setTimeout 不一定能精确卡到这个时机,容易出现时序 bug。
如果我写了多次 nextTick,它们会执行几次?
这也是 Vue 的优化点之一:同一轮事件循环中多次调用 nextTick,Vue 会把它们合并成一次微任务来执行,也就是只等一轮 DOM 更新,节省性能。
# 12 vue实例挂载的过程
整体可以分为 初始化 → 模板编译 → 渲染 → 挂载 四个阶段。
一 当我们调用 new Vue({...}) 的时候,Vue 会执行初始化逻辑:
- 初始化生命周期、事件中心等
- 调用
initData()初始化响应式数据,使用Object.defineProperty(Vue2) 或Proxy(Vue3) 实现响应式; - 初始化
props、computed、watcher、methods;
二 接下来进入编译阶段:
如果写了
template,Vue 会用模板编译器将其编译成 render 函数;如果直接写了
render()函数,就跳过这步;编译过程会把模板转成 AST,再生成 render 函数。
三 调用 $mount(el) 后,Vue 会将 render 函数生成的虚拟 DOM 渲染到页面中:
进入
beforeMount生命周期钩子;调用
render()函数生成 虚拟 DOM;执行
patch函数,把虚拟 DOM 渲染成真实 DOM;挂载到指定的 DOM 元素上(比如
#app);最后执行
mounted()钩子。
四 更新机制(响应式 + diff)
一旦数据发生改变:
- 响应式系统会触发依赖的更新;
- 重新执行
render()得到新的虚拟 DOM; - 和旧的虚拟 DOM 做 diff;
- 找出差异,通过最小化的 DOM 操作更新视图。
这也是 Vue 性能优化的核心机制之一。
那 Vue2 和 Vue3 挂载过程有什么区别?
- 响应式从
Object.defineProperty改成了Proxy,性能更好; - 生命周期钩子从
beforeCreate/created等变成了setup函数; - 模板编译支持静态提升、事件缓存等优化,提升 runtime 性能;
- Vue3 的编译尽量在构建阶段完成(利用 vite、rollup),Vue2 是 runtime 编译。
总体来看,Vue3 更偏向函数式、模块化,结构更清晰、性能更好。
那你能说说 mounted 和 created 有什么区别吗?
可以。created 是在组件实例创建完毕后立即调用,此时数据已经初始化,但 DOM 还没挂载。常用于请求数据、初始化变量等。
而 mounted 是 DOM 挂载完毕后执行的,这时候可以访问 $el,适合进行 DOM 操作,比如初始化图表、滚动区域等。
# 13 你了解vue的diff算法吗?
diff算法是一种通过同层的树节点进行比较的高效算法
diff 算法,它的核心是在更新视图时比对新旧虚拟 DOM 树,通过最小化的 DOM 操作来提升性能。
特点:
- 比较只会在同层级进行,不会跨层级比较(深度优先)
- 在diff比较的过程中,循环从两边向中间比较
- 根据 key 快速定位可复用节点
Vue diff 的执行步骤(以 v-for 为例):
- 先按顺序头部比较:旧前 vs 新前(相同就 patch,然后指针右移)
- 再比较尾部:旧后 vs 新后
- 交叉比较:旧前 vs 新后、旧后 vs 新前(用于处理节点被“移动”到新位置)
- key 匹配:遍历中间未匹配部分,用 key 建立索引映射,找出复用节点;
- 新增和删除处理:新列表有但旧列表没有的就创建,旧列表有但新列表没有的就删除。
你有没有遇到过 key 没写好导致 diff 出问题的情况?
有遇到过。比如我在做一个动态列表表单时,有时候用了 index 作为 key,结果当我插入一项的时候,Vue 复用了错误的 DOM 节点,导致输入框错位,或者 v-model 对不上数据。
后来我改成使用数据库返回的唯一 id 作为 key,问题就解决了。这也说明 diff 过程是依赖 key 来定位节点的,key 不稳定就会影响 Vue 的判断。
# 14 vue中组件和插件有什么区别?
组件(Component)——用于构建 UI
组件更偏向于视图层的复用单位;
它通常包含 模板(template)+ 逻辑(script)+ 样式(style);
举个例子:像 Element Plus 的 <el-button />、你自己封装的分页组件、图表组件,都是组件。
插件(Plugin)——用于扩展全局功能
插件更偏向于全局功能的封装和扩展;
它不一定有界面,核心是通过
app.use(MyPlugin)安装;插件可以添加全局方法(比如
$message)、全局指令、全局组件,或者提供全局 mixin、注入配置等;
举个例子:Vue Router、Vuex、Element Plus 都是插件;我们也可以写一个自定义插件,在其中注册一个全局指令或者添加 $notify 方法。
那你有没有自己写过插件?写插件和写组件最大的差异是什么?
我有写过简单的插件,比如封装一个全局 $toast 方法,让所有组件都能调用。
最大的差异是:组件主要关注模板和交互,插件则关注全局功能扩展。
写插件时我会暴露一个 install(app) 方法,在里面用 app.config.globalProperties 添加全局方法,或者注册全局组件、指令等。例如:
export default {
install(app) {
app.config.globalProperties.$toast = function (msg) {
alert(msg);
};
}
}
然后通过 app.use(MyPlugin) 注册。
那组件能作为插件的一部分吗?
可以的,一个插件完全可以包含一个或多个组件,在 install 方法中通过 app.component() 把它们注册为全局组件。
比如 Element Plus 就是一个典型的插件,它的 install 方法里注册了大量全局组件,使用时只需要 app.use(ElementPlus)。
所以插件是更高层的封装,组件可以作为插件的一部分输出。
# 15 Vue项目中你是如何解决跨域的呢?
跨域本质就是浏览器基于同源策略的一种安全手段,是对浏览器的限制,一般的postman啥的请求是可以请求到接口的
同源策略。是一种约定,是浏览器最核心最基本的安全功能,所谓同源(即在同一个域):
- 协议相同
- 主机相同
- 端口相同
反之就是非同源,三者一不同就会产生跨域
解决跨域方法:
- JSONP
- nginx 反向代理
- CORS
- Proxy
vue中一般使用CORS和Proxy进行
开发环境:使用 Vue 的代理配置解决跨域
在 vue.config.js 中配置 devServer.proxy,将本地请求代理到目标服务器,实现“看似同源”的效果:
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://backend.example.com',
changeOrigin: true, // 会把请求头中的 Origin 改成目标地址,实现伪同源
pathRewrite: { '^/api': '' }
}
}
}
}
// 这样在开发环境下,我请求 /api/user 实际会被代理成 http://backend.example.com/user
生产环境:服务端处理跨域(CORS)
生产环境不能依赖前端代理,因此跨域问题一般由后端处理,比如在 Node.js/Express 中设置 CORS 头:
// Node.js 示例
res.setHeader('Access-Control-Allow-Origin', '*');
你知道 CORS 的原理吗?
CORS(Cross-Origin Resource Sharing ,跨域资源共享)是一个系统,由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端js代码获取跨域请求的响应
它会在跨域请求时先发一个“预检请求”(OPTIONS 请求),询问目标服务器是否允许实际请求的方法和头部。
如果服务端返回了类似这些头:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type
浏览器才会发起真正的请求。否则会被拦截。
# 16 你有写过自定义指令吗?自定义指令的应用场景有哪些?
指令系统
vue中提供一种为数据驱动视图更为方便的操作,这些操作被称为指令系统
v- 开头的行内属性都是指令
指令使用的几种方式:
// 会实例化一个指令,但这个指令没有参数
v-xxx
// -- 将值传到指令中
v-xxx = value
// -- 将字符串传入指令中
v-xxx="'string'"
// -- 传参数(arg) ,就像v-bind:class = "className"
v-xxx:arg="value"
// -- 使用修饰符
v-xxx:arg.modifier = "value"
有写过,我在实际项目中主要用自定义指令来处理一些 DOM 层级较低但复用性较强的行为逻辑,比如:
- 实现点击元素自动聚焦(
v-focus) - 图片懒加载(
v-lazy) - 拖拽(
v-drag) - 复制文本(
v-copy)
这些场景通常只涉及对 DOM 元素的直接操作,用组件实现会显得过于重,指令更加轻便且职责单一。
// 自动聚焦指令 v-focus.ts
// 功能:当元素插入 DOM 后,自动获取焦点
// 使用:<input v-focus />
export default {
mounted(el: HTMLElement) {
// el 是绑定该指令的 DOM 元素
el.focus()
}
}
// 图片懒加载指令 v-lazy.ts
// 功能:只有当图片进入视口时才加载真实的图片 src
// 使用:<img v-lazy="'https://xxx.jpg'" />
export default {
mounted(el: HTMLImageElement, binding: any) {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
el.src = binding.value // 设置图片地址
observer.unobserve(el) // 加载完成后取消观察
}
})
observer.observe(el) // 开始观察当前图片元素
}
}
// 拖拽指令 v-drag.ts
// 功能:让绑定的元素支持鼠标拖动
// 使用:<div v-drag>拖我</div>
export default {
mounted(el: HTMLElement) {
el.style.position = 'absolute' // 设置定位,便于移动
el.onmousedown = e => {
// 记录鼠标按下时的偏移
const disX = e.clientX - el.offsetLeft
const disY = e.clientY - el.offsetTop
// 鼠标移动时设置元素的位置
const move = (e: MouseEvent) => {
el.style.left = e.clientX - disX + 'px'
el.style.top = e.clientY - disY + 'px'
}
// 鼠标抬起时移除事件监听
const up = () => {
document.removeEventListener('mousemove', move)
document.removeEventListener('mouseup', up)
}
document.addEventListener('mousemove', move)
document.addEventListener('mouseup', up)
}
}
}
// 复制文本指令 v-copy.ts
// 功能:点击元素,将绑定的内容复制到剪贴板
// 使用:<button v-copy="'Hello World'">点击复制</button>
export default {
mounted(el: HTMLElement, binding: any) {
el.style.cursor = 'pointer' // 鼠标样式提示可点击
el.addEventListener('click', () => {
navigator.clipboard.writeText(binding.value).then(() => {
console.log('复制成功:', binding.value)
}).catch(err => {
console.error('复制失败:', err)
})
})
}
}
统一注册插件 directives/index.ts
// 指令注册插件 index.ts
// 功能:统一注册所有自定义指令,供 main.ts 使用 app.use(directives)
import { App } from 'vue'
import vFocus from './v-focus'
import vLazy from './v-lazy'
import vDrag from './v-drag'
import vCopy from './v-copy'
// 所有指令统一收集
const directives = {
focus: vFocus,
lazy: vLazy,
drag: vDrag,
copy: vCopy
}
export default {
install(app: App) {
// 遍历指令并注册到 Vue 实例中
for (const [name, directive] of Object.entries(directives)) {
app.directive(name, directive)
}
}
}
在 main.ts 中注册插件
// main.ts
// 功能:注册全局指令插件
import { createApp } from 'vue'
import App from './App.vue'
import directives from './directives' // 引入自定义指令插件
const app = createApp(App)
app.use(directives) // 一次性注册所有指令
app.mount('#app')
使用方式(组件中)
<template>
<input v-focus />
<img v-lazy="'https://example.com/image.jpg'" width="200" />
<div v-drag style="width: 100px; height: 100px; background: #eee;">拖我</div>
<button v-copy="'Hello from Vue!'">点我复制</button>
</template>
那有没有在项目中遇到组件内复用 DOM 操作但不能提炼成组件的情况?你是怎么决定用指令的?
有的,比如我之前做一个内容管理系统,要求点击任意图片支持全屏预览功能。这类功能不能直接写到组件里,因为:
- 图片分布在不同组件中,逻辑相同但 DOM 结构不一致;
- 不希望每个组件都手动加逻辑,而是统一处理。
所以我写了一个 v-preview 指令,只要绑定到图片元素上,就可以在点击时触发弹窗预览
你刚才提到懒加载,有考虑过和 IntersectionObserver 结合使用吗?怎么封装到指令里?
是的,其实 Vue 懒加载指令最常见的优化就是用 IntersectionObserver 替代传统的 scroll 监听。
- 在指令
mounted阶段创建IntersectionObserver - 当图片出现在视口中,就加载真实
src并解绑监听 - 用
el.dataset.src存储真实图片地址,防止图片提前加载 - 在
unmounted阶段 disconnect observer
# 17 Vue中的过滤器了解吗?过滤器的应用场景有哪些?
过滤器(fileter) 是输送介质管道上不可缺少的一种装置
过滤器主要用于对绑定的数据进行格式化处理,比如时间格式化、金额千分位处理、首字母大写等。它们通常在模板中使用,语法是 ,能让模板更加简洁清晰。
ps: vue3中已经废弃了filter,原因是过滤器不够灵活且不利于代码逻辑等,使用方法或者计算属性替代
# 18 说说你对solt插槽的理解?slot使用场景有哪些?
简单来讲其实就是一个占位符,是vue实现组件内容分发的一种机制,当时使用该组件标签的时候,组件标签里面的内容会自动填充替换掉slot所在的位置
vue中有三种插槽
- 默认插槽:用于传入默认内容
- 具名插槽:用过name属性来指定位置
- 作用域插槽:父组件可以访问子组件传来的数据,父组件在使用中通过v-shot:(简称:#)获取子组件的信息在内容中使用
应用场景举例
- 负责的表单组件,表格这些
- 组件复用但是灵活
- 自定义卡片、布局
你刚才提到作用域插槽可以“让父组件访问子组件的数据”,能详细解释一下机制吗?
作用域插槽本质上是将子组件内部的数据“暴露”出来,让父组件在插入插槽内容时能使用这些数据。
slot 和 props 的区别你能简单对比一下吗?
| 对比点 | props | slot |
|---|---|---|
| 传递方向 | 父组件 → 子组件 | 父组件 → 子组件中“占位符” |
| 内容控制权 | 子组件定义内容和结构 | 父组件定义结构 |
| 适用场景 | 控制数据/参数 | 控制结构/布局 |
| 是否可嵌套结构 | 一般不可 | 插槽内容可以包含任意 HTML/Vue 结构 |
所以两者不是对立的,而是互补的:props 控制行为逻辑,slot 控制展示结构。
# 19 什么是虚拟DOM?
虚拟DOM使用js来模拟DOM树的结构,它是一个轻量级的中间层,避免直接频繁操作真实DOM,从而提升性能
在vue或者react中,每当状态(数据)发生变化的时候,框架会重新生成一棵新的虚拟DOM树,然后用diff算法对比前后两棵虚拟DOM树,找出最小的差异,然后只将差异部分更新到真实DOM,这个过程叫patch
假设我们有一个 <ul> 列表,修改了一个 <li> 项,虚拟 DOM 会只更新那一项,而不是整个列表,这样比直接操作 DOM 快得多,尤其是在大量节点时效果更明显。
那你要是自己实现一个简单的虚拟 DOM,你的思路会是什么样的?能写个基本的结构吗?
定义虚拟DOM的结构(VNode)
一个虚拟节点是一个js对象,至少有以下3个属性
function h(tag, props, children) { return { tag, // 节点类型,例如 'div' props, // 属性或事件监听,如 { id: 'foo' } children // 子节点,可以是文本或 VNode 数组 }; } // 使用 const vnode = h('div', { id: 'app' }, [ h('h1', null, '标题'), h('p', null, '段落') ]);将虚拟DOM渲染成真实DOM
function render(vnode) { const el = document.createElement(vnode.tag); // 设置属性 for (let key in vnode.props) { el.setAttribute(key, vnode.props[key]); } // 递归处理 children if (typeof vnode.children === 'string') { el.textContent = vnode.children; } else if (Array.isArray(vnode.children)) { vnode.children.forEach(child => { el.appendChild(render(child)); }); } return el; }使用Diff算法
简化版的
patch(oldVNode, newVNode)函数,通过递归比较两个虚拟 DOM 树:- 如果节点类型不同,直接替换;
- 如果相同,再比较属性和 children;
- 对文本节点做直接替换判断。
那你觉得使用虚拟 DOM 一定比操作真实 DOM 快吗?有没有例外?
不一定,静态页面变化少可能真实操作更快,并且虚拟DOM有性能开销,而且是在有框架做了许多处理之后才这么好用的
# 20 vue项目中有封装过axios吗?主要是封装哪方面的?
基本的使用:
axios({
url:'xxx', // 设置请求的地址
method:"GET", // 设置请求方法
params:{ // get请求使用params进行参数凭借
type:'',
page:1
}
}).then(res => {
// res为后端返回的数据
console.log(res)
})
既然提供了API为啥还要自己封装呢?你不觉得每次发起请求都要写一堆东西其实挺麻烦的吗?所以为了复用提升代码的质量,应该要在项目中二次封装一下axios再使用
有的在实际项目中我有封装 Axios,用来统一处理请求逻辑。主要封装了这几个方面:
基础配置
首先对 Axios 实例做了统一配置:
baseURL:根据开发/测试/生产环境自动切换;timeout:设置请求超时时间;withCredentials:是否携带跨域 cookie;统一设置请求头,比如默认的
Content-Type为application/json;
const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000, headers: { 'content-Type': 'application/json' } })请求拦截器
自动注入 token(从 localStorage 或 pinia/store 拿);
加载动画(如开启全局 loading 状态);
自定义请求日志(可选);
service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) config.headers.Authorization = `Bearer ${token}`; return config; });响应拦截器
统一处理状态码(如 token 失效跳转登录、403、500 等);
自动提示错误信息(结合 Element Plus 的
ElMessage);对响应数据结构统一格式处理(比如
res.data.data解构)
service.interceptors.response.use( res => { if (res.data.code === 200) { return res.data.data; } else { ElMessage.error(res.data.message || '请求失败'); return Promise.reject(res.data); } }, error => { ElMessage.error(error.message || '网络异常'); return Promise.reject(error); } );封装通用请求函数
比如统一的
get/post/put/delete方法:export const get = (url, params) => service.get(url, { params }); export const post = (url, data) => service.post(url, data);
你说做了环境配置,环境变量是怎么切换的?不同环境用的 baseURL 是怎么处理的?
我们用的是 Vite + Vue 3 项目,所以我使用了 .env 文件
.env.development .env.production
然后在 axios 中通过 import.meta.env 读取环境变量:
baseURL: import.meta.env.VITE_API_BASE_URL
// Vite 会自动根据 mode 加载对应 .env 文件,非常方便。
如果你项目中多个接口要加不同的 header,比如有些需要 token,有些不需要,怎么处理?
我会通过请求拦截器判断是否需要 token,比如设置一个自定义字段:
config.headers._needToken = false;
然后在拦截器中判断:
if (config.headers._needToken !== false) {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
}
# 21 是怎么处理vue项目中的错误的?
我会将错误处理分为三个层级去做
请求级别错误处理(axios 拦截器)
在封装 axios 的时候,会在响应拦截器中统一处理接口的网络异常和业务错误
axios.interceptors.response.use( response => { if (response.data.code !== 200) { // 业务错误,比如 token 过期、权限不足 ElMessage.error(response.data.message || '业务错误'); return Promise.reject(response.data); } return response.data.data; }, error => { // 网络层错误处理,如断网、超时等 ElMessage.error(error.message || '请求失败,请稍后重试'); return Promise.reject(error); } );运行时错误处理(全局 errorHandler)
vue 提供了全局错误处理钩子,我们在
main.ts或main.js中设置:app.config.errorHandler = (err, vm, info) => { console.error('Vue runtime error:', err); // 也可以上传到日志服务,比如 Sentry 或企业内部监控平台 ElMessage.error('系统开小差了,请稍后再试'); };这个可以捕获组件内部的运行时错误,防止整个页面崩掉。
异步未捕获错误(window.onerror / unhandledrejection)
对于一些未捕获的 Promise 错误,我们也做了监听:
window.addEventListener('error', (e) => { console.error('window error:', e); }); window.addEventListener('unhandledrejection', (e) => { console.error('Unhandled Promise rejection:', e.reason); });后端返回 code 或 message 错误格式不统一?
做了兼容处理:定义统一的
errorAdapter函数,处理不同接口返回的字段格式,比如有的用code/message,有的用status/msg。
你提到可以上传错误到日志平台,你项目中用过什么异常监控工具?
用过 Sentry,接入非常方便:
- 支持自动捕捉 Vue 的报错;
- 支持 sourcemap 回溯源码位置;
- 可以记录用户设备、路由、浏览器等上下文;
- 对上线后的线上异常定位非常有帮助。
# 22 你了解axios的原理吗?如何实现一个建议的axios呢?有看过axios的源码吗
回顾一下axios的基本使用:
// utils/request.js
import axios from 'axios'
// 创建 Axios 实例
const instance = axios.create({
baseURL: 'https://api.example.com', // 请求基础路径
timeout: 10000, // 超时时间,单位:ms
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
instance.interceptors.request.use(
config => {
// ✅ 每次请求前做一些处理,如添加 token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
console.log('[Request]', config)
return config
},
error => {
// 请求错误时
console.error('[Request Error]', error)
return Promise.reject(error)
}
)
// 响应拦截器
instance.interceptors.response.use(
response => {
// ✅ 接收到响应数据后处理
console.log('[Response]', response)
return response.data // 统一返回 data 字段
},
error => {
// ❌ 处理响应错误(如 401, 500)
console.error('[Response Error]', error)
if (error.response) {
const status = error.response.status
if (status === 401) {
alert('未授权,请重新登录')
} else if (status === 500) {
alert('服务器异常,请稍后再试')
}
}
return Promise.reject(error)
}
)
export default instance
// ---- 使用 Axios 发送请求(GET、POST)
// 在组件或 API 模块中使用
import request from '@/utils/request'
// GET 请求
export const getUserInfo = () => {
return request.get('/user/info')
}
// POST 请求
export const login = (data) => {
return request.post('/auth/login', data)
}
// ---取消请求(使用 AbortController)
// 使用方式:手动取消请求
import axios from 'axios'
const controller = new AbortController()
// 发起请求并绑定 signal
axios.get('https://api.example.com/data', {
signal: controller.signal
})
.then(res => {
console.log('请求成功', res.data)
})
.catch(err => {
if (axios.isCancel(err)) {
console.warn('请求已取消')
} else {
console.error('请求错误', err)
}
})
// 取消请求(例如在组件卸载或用户点击取消时)
controller.abort()
实现一个简易的axios
//拦截器机制
//请求方法封装
//Promise 接口风格
class MyAios{
constructor(config) {
this.default = config
this.interceptors = {
request: [],
response: []
}
}
request(config){
// 合并默认配置
config = { ...this.defaults, ...config}
// 创建拦截器链
let chain = []
// 添加请求拦截器
this.interceptors.request.forEach(interceptor => {
chain.unshift(interceptor) // 先执行请求拦截器
})
// 添加核心请求方法
chain.push(this.dispatchRequest)
// 添加响应拦截器
this.interceptors.response.forEach(interceptor => {
chain.push(interceptor) // 后执行响应拦截器
})
// 执行 promise 链
let promise = Promise.resolve(config);
while(chain.length) {
const then = chain.shift()
promise = promise.then(then.onFulfilled, then.onRejected)
}
return promise
}
dispatchRequest(config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(config.method || 'GET', config.url);
// 设置 header
for (let key in config.headers || {}) {
xhr.setRequestHeader(key, config.headers[key]);
}
xhr.onload = () => {
resolve({
data: JSON.parse(xhr.responseText),
status: xhr.status,
config
});
};
xhr.onerror = () => reject(new Error('Request failed'));
xhr.send(config.data || null);
});
}
get(url, config = {}) {
return this.request({ ...config, method: 'GET', url });
}
post(url, data, config = {}) {
return this.request({ ...config, method: 'POST', url, data });
}
// 拦截器注册方法
useRequestInterceptor(onFulfilled, onRejected) {
this.interceptors.request.push({ onFulfilled, onRejected });
}
useResponseInterceptor(onFulfilled, onRejected) {
this.interceptors.response.push({ onFulfilled, onRejected });
}
}
// 使用示例
const axios = new MyAxios({
baseURL: '/api',
headers: { 'Content-Type': 'application/json' }
});
axios.useRequestInterceptor(config => {
console.log('请求拦截:', config);
return config;
});
axios.useResponseInterceptor(response => {
console.log('响应拦截:', response);
return response;
});
axios.get('/test').then(res => {
console.log('最终结果:', res);
});
axios 中拦截器是怎么实现“顺序执行”的?
用一个链式数组模拟中间件队列,先执行请求拦截器(先进后出),再执行核心请求逻辑,然后是响应拦截器(先进先出),最终通过 Promise.then() 链式连接。
axios 怎么实现取消请求的?
在旧版本中使用 CancelToken 构造函数;
新版本推荐用 AbortController,可用于 fetch/XHR。
# 23 vue要做权限管理该怎么做?
权限是对特定资源的访问许可,所谓权限控制,就是确保用户只能访问到被分配的资源
而前端权限归根是请求的发起权
发起请求的方式:
- 页面加载触发
- 页面按钮等事件触发
最终实现的目标是:
路由权限(页面权限)
核心机制:通过 Vue Router 的导航守卫 beforeEach() 实现
基本逻辑:
- 登录后获取用户的角色或权限列表(通常从后端接口返回)
- 根据角色过滤出可访问的路由表(动态路由)
- 如果访问的是未授权的页面,跳转到 403 或登录页
router.beforeEach(async (to, from, next) => {
const token = getToken();
if (!token && to.path !== '/login') {
return next('/login');
}
const hasRoles = store.getters.roles.length > 0;
if (!hasRoles) {
const roles = await store.dispatch('user/getUserInfo');
const accessRoutes = await store.dispatch('permission/generateRoutes', roles);
accessRoutes.forEach(route => router.addRoute(route));
next({ ...to, replace: true });
} else {
next();
}
});
//路由配置中我们会在 meta 中设置角色:
{
path: '/admin',
component: AdminPage,
meta: { roles: ['admin'] }
}
按钮权限(功能权限)
应用场景:用户角色不同,某些按钮(如“删除”、“审核”)不应该出现。
实现方式:通过自定义指令 v-permission 实现 DOM 层级的权限控制。
app.directive('permission', {
mounted(el, binding) {
const roles = store.getters.roles;
const allowed = binding.value; // ['admin']
const hasPermission = roles.some(role => allowed.includes(role));
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el);
}
}
});
///模板中使用:
<el-button v-permission="['admin']">删除</el-button>
菜单权限
接口权限(数据权限)靠后端兜底
前端虽然可以隐藏按钮或页面,但安全性主要依赖后端接口权限判断。
- 所有请求都要带上用户 token;
- 后端根据 token 解析出用户身份;
- 判断用户是否有权限访问该接口或数据;
- 返回 401 / 403 给前端处理。
前端接收到 403 响应后,一般会跳转到提示页或弹出权限不足的提示。
# 24 说说你对keep-alive的理解是什么?
Vue 中的 keep-alive 是一个内置组件,主要用于缓存组件的状态,避免重复渲染,提高性能。
也就是:包裹动态组件时,缓存已经渲染过的组件实例,下次切换回来时不会重新销毁和创建,而是从缓存中读取;它自身是一个抽象组件不会渲染一个DOM原生也不会出现在父组件链中
原理:通过 activated / deactivated 生命周期管理组件的挂载与恢复,并利用 LRU 缓存策略维护组件实例;
<keep-alive>
<component :is="currentView"></component>
</keep-alive>
// 或者路由级缓存:
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
使用场景(常见的面试加分点)
- 多标签页切换:如“订单列表”与“订单详情”之间反复切换,列表组件的数据不想重新加载;
- 表单填写中途切换:比如填表中点开选择项,回来后保留填写状态;
- 缓存分页数据:列表页翻页或操作后切换回来保留之前的分页状态、筛选条件等;
- 移动端 Web App 页面切换缓存。
使用 keep-alive 的组件,不再触发 created / mounted,而是用:
activated():组件从缓存中被激活时触发deactivated():组件被缓存(切换出去)时触发
配合 include/exclude 精细控制
- include属性是包含的意思,值为字符串或者正则或者数组,只有组件的名称与include的值相同的时候才会被缓存
- exclude就是反义词了,除了,指定哪些组件不被缓存
可以通过以下属性控制缓存哪些组件:
<keep-alive include="User,Home" exclude="Login">
<router-view></router-view>
</keep-alive>
你有没有遇到过 keep-alive 的缓存失效问题?
有,比如动态组件使用 v-if 条件包裹时,可能会导致组件被销毁不再缓存;
我们一般建议配合 v-show 或条件判断在 router-view 外部实现控制。
keep-alive 是怎么做缓存的?
它内部维护一个基于组件名的缓存对象(VNode 缓存表),通过组件名判断是否命中缓存,并保留已创建的实例。如果超出 max 限制,则采用 LRU 缓存策略进行淘汰。
LRU缓存:当缓存满了,需要淘汰数据时,优先移除最近最少被使用的那个数据
能不能控制缓存数量?
可以,keep-alive 提供 max 属性,例如:
<keep-alive :max="5">...</keep-alive>
# 25 你对SPA单页面的理解,它的优缺点分别是什么?如何实现SPA应用呢?
SPA就是整个网站只有一个HTML页面,所有页面内容和视图切换,都通过前端js进行动态完成,而不是每次切换都从服务端加载新的HTML
换句话说,路由的切换时前端路由,页面局部刷新,而不是整个页面刷新
在 Vue 项目中,典型的 SPA 应用结构如下:
<body>
<div id="app"></div>
</body>
前端通过 Vue Router 控制页面切换,实现组件的“伪页面跳转”。
vue的url模式采取的时哈希模式,基于 URL 的 # 号后的部分,不会被浏览器刷新,靠 hashchange 事件监听。
https://example.com/#/home
History 模式:基于 HTML5 的 history.pushState(),没有 #,需要服务端做 URL 重写。
https://example.com/home
SPA 的优点 ✅
- 用户体验好:页面切换时不需要刷新整个页面,速度快、过渡平滑;
- 前后端分离:前端只负责视图渲染,后端只提供接口,适合团队协作开发;
- 可以做前端缓存、懒加载、权限控制等高级功能;
- 更适合 PWA 和移动端场景。
SPA 的缺点 ❌
- 首次加载慢:需要下载整个项目的 JS、CSS 和组件,初始白屏时间较长;
- SEO 不友好:因为内容是 JS 渲染出来的,对搜索引擎不友好(不过可以配合 SSR 或预渲染优化);
- 前进后退(浏览器历史)处理复杂;
- 页面数据状态管理更复杂:组件生命周期、缓存、销毁等要自行管理。
Vue 实现 SPA 的基本方式:
页面不刷新,只是动态替换 <router-view /> 中的组件。
你在项目中有没有优化过 SPA 的缺点?
首次加载慢:我使用了 路由懒加载 + 分包策略(webpack 分 chunk);
SEO 不友好:我们用过
prerender-spa-plugin进行预渲染,或者配合 Nuxt 做 SSR;白屏:添加了 骨架屏 和 loading 动画;
浏览器导航问题:Vue Router 有
scrollBehavior管理滚动、历史状态。
如何给SPA做SEO?
SSR服务端渲染
将组件或者页面通过服务器生成html,再返回给浏览器,如:nuxt.js
静态化
程序将动态页面抓取并保存为静态页面或者通过web服务器的URL Rewrite的方式
使用Phantomjs针对爬虫处理
# 26 SPA首屏加载速度慢的怎么解决?
SPA 首屏加载慢主要是因为初次访问需要一次性加载大量 JS/CSS 资源。我主要从以下几个方面做了优化:
路由懒加载(按需加载组件)
资源预加载 / 预获取(预热下一个页面)
拆包优化(合理分包、动态加载)
使用工具压缩js/css体积
使用 CDN 加速静态资源加载
使用 Skeleton 骨架屏 / Loading 动画
首屏重要内容内联 + SSR 预渲染
存优化(缓存 CDN、缓存资源)
Q:你用过 webpack-bundle-analyzer 吗?怎么分析打包体积?
Q:你们有没有对首页图片资源做过优化?
Q:为什么懒加载能提升首屏速度?有没有可能反而让体验变差?
Q:你能说说服务端渲染和客户端渲染的区别吗?分别适合什么场景?你项目中用过 SSR 吗?
| 项目 | 客户端渲染(CSR) | 服务端渲染(SSR) |
|---|---|---|
| 渲染时机 | 首次页面加载时由浏览器执行 JS 动态生成 DOM | 首次请求时由服务器直接返回完整 HTML |
| 首屏速度 | 较慢(需要下载 JS 后才能渲染) | 较快(直接返回 HTML) |
| SEO 友好性 | 差,爬虫抓不到动态内容 | 好,爬虫可以抓取完整内容 |
| 技术实现 | Vue + Vue Router,前端完全控制 | 使用 Nuxt(Vue)/ Next(React) 或手写 SSR |
| 用户体验 | 页面空白时间长,适合 WebApp | 页面可快速显示内容,适合内容类站点 |
# 27 vue项目本地开发完成后部署到服务器后报404是什么原因呢?
因为vue是前后端分离的项目,所以前后端都是独立部署的,前端只需要将最后的构建物dist上传服务器的web指定的静态目录即可
使用nginx来运行,,,
是的,我在项目部署过程中遇到过这个问题。Vue 项目部署后访问子路由页面比如 /about 报 404,一般是因为使用了 Vue Router 的 history 模式,但服务器没有做 URL 重写配置。
解决办法就是:配置服务器的“URL重定向”只要把所有路径重定向到 index.html 就可以了。
告诉服务器:
无论用户访问什么路径,都返回
index.html,让前端来接管路由。
Q:为什么 hash 模式不会报错?
因为 hash 模式下的路由不需要服务器参与解析路径。
使用 nginx 主要是因为它稳定、高性能、配置灵活,尤其适合部署前端静态资源;
Q:你们部署是放到 nginx 还是 node 服务器?
我们项目通常会将前端构建好的静态文件(HTML、JS、CSS)部署到 Nginx 上。
Q:有没用过 CI/CD 自动部署?(可答 GitHub Action / Docker)
# 28 SSR解决了什么问题?有做过SSR吗?你是怎么做的?
是的,我做过 SSR 项目,用的是 Vue + Nuxt 框架。SSR 主要是用来解决前端单页面应用中的首屏加载慢和SEO 不友好的问题。
# 29 你用过 Vue3 吗?和 Vue2 相比,它有什么不同?有哪些新特性?
首先,从响应式原理来说:Vue2 是基于
Object.defineProperty实现的,不能很好地处理对象新增属性、数组变更等问题,深层响应也有一些限制。而 Vue3 改用了Proxy,它可以完整地监听对象的读写、添加、删除操作,性能也更好。组件写法:Vue2 是 Options API,把
data、methods、computed分开放。但在大型项目中,这种结构不太利于逻辑复用。Vue3 引入了 Composition API,把逻辑都集中在setup()函数里,变量使用ref、reactive声明,配合watch、computed做响应式处理。这样逻辑聚合更清晰,也方便抽离成 composable 方法进行复用。性能和打包:Vue3 默认支持 Tree Shaking,内部结构更扁平,按需导入能减少打包体积。另外,Vue3 的虚拟 DOM diff 算法也做了重写,首次渲染和更新都更快。
# 30 vue的响应式机制
# vue2
1 用了什么
Object.defineProperty:对对象的属性进行“劫持”(getter/setter),从而在读写属性时触发依赖收集和更新。- 发布-订阅模式(观察者模式):依赖收集(Dep)+ 订阅(Watcher)。
2 核心机制
- 初始化数据
- Vue 在初始化时会遍历
data中的所有属性,用Object.defineProperty给每个属性添加 getter/setter。(只能读写) - getter:依赖收集(哪个组件用到了这个属性,就把这个组件的 watcher 收集起来)。
- setter:触发依赖(当属性变化时,通知对应的 watcher 更新)。
- Vue 在初始化时会遍历
- 依赖收集
- 每次渲染组件时,会访问到响应式数据,触发 getter,把当前的渲染函数 watcher 记录到依赖中。
- 派发更新
- 当数据变化时,setter 会触发,Dep 通知相关 watcher,进而重新执行渲染更新。
局限性
- 无法监听对象的新增/删除属性
需要
Vue.set/Vue.delete才能生效。 - 无法监听数组的索引和长度变化 Vue2 通过 重写数组原型方法(push、pop、splice...)来实现“曲线救国”。
- 性能开销大:初始化时要遍历所有数据做
defineProperty劫持。
# vue3
1 用了什么
ES6 Proxy:可以拦截对对象的所有操作(读、写、删、遍历),比
Object.defineProperty更强大。Proxy是 ES6 引入的一个新特性,可以 代理整个对象,拦截对象上的各种操作。let obj = { name: "mmx", age: 20 } let proxy = new Proxy(obj, { get(target, key) { console.log("读取属性", key) return target[key] }, set(target, key, value) { console.log("设置属性", key, "为", value) target[key] = value return true }, deleteProperty(target, key) { console.log("删除属性", key) return delete target[key] } }) console.log(proxy.name) // 拦截 get proxy.age = 22 // 拦截 set delete proxy.name // 拦截 deletePropertyProxy能拦截的操作远比 getter/setter 多,有更多的操作Reflect:配合 Proxy 使用,保证操作的默认行为。
2 核心机制
- 创建响应式对象
- Vue3 用
reactive、ref创建响应式对象时,会返回一个 Proxy 包装对象。
- Vue3 用
- 依赖收集
- 访问属性时,Proxy 的
get拦截触发,调用track()函数收集依赖(effect)。
- 访问属性时,Proxy 的
- 派发更新
- 修改属性时,Proxy 的
set拦截触发,调用trigger()函数通知依赖更新。
- 修改属性时,Proxy 的
优势
- 能监听对象新增/删除属性:Proxy 能直接捕获
in、deleteProperty等操作。 - 能监听数组索引和 length 变化。
- 性能更好:不需要初始化时遍历所有属性,只有访问时才收集依赖(惰性)。
- 更灵活:
effect、computed、watch都是基于 Proxy 响应式系统的能力实现的。
# 31 vue3中的Compostion APi与vue2的Options API有什么不同?
Vue2:Options API
- 通过
data、methods、computed、watch等 配置项 来组织代码。 - 一个功能点的逻辑,可能分散在不同的选项里,不是很聚合。
Vue3:Composition API
- 使用
setup()函数,直接在函数内部通过组合 API(ref、reactive、computed、watch)来组织逻辑。 - 一个功能点的逻辑可以聚合在一起,更加模块化、函数化。
代码对比
差别:
- Vue2 是把逻辑“拆开写”到不同选项里;
- Vue3 是把逻辑“聚合写”到一个作用域里。
# 32 vue3的设计目标是什么?做了哪些优化?
目标肯定为了解决业务通点,变得更小更快更友好
更小:vue3移除了一些不常用的API,引入tree-shaking,可以将无用模块剪辑掉然后仅打包需要的使整体体积变小了
更快:diff算法优化,静态提升,SSR优化、事件监听缓存
更友好:组合式API,更好支持ts
# 33 用vue3写过组件吗?如果想实现要给Modla你会怎么设计?
我会把 Modal 设计成结构清晰、功能独立、可插槽、可复用的组件,
# 34 vue3的性能提升主要是哪几方面体现的?
① 响应式系统升级:Proxy 替代 Object.defineProperty,更快、能力更强、性能更优,特别是深层嵌套对象和大数据量场景。
② 编译优化:静态提升 + PatchFlag,减少无用的 diff、虚拟 DOM 创建与 patch,提高渲染效率。
③ Fragment(多根节点)支持,Vue3 支持组件返回多个根节点;
避免额外的包裹元素,减少 DOM 层级,提高性能。构更简洁、DOM 操作更少。
④ Tree-Shaking 更彻底,
⑤ 更快的 SSR 支持
# 35 vue3里为什么要用Proxy API替代defineProperty API?
Vue3 选择 Proxy 替代 defineProperty,是为了更完整的响应式能力(支持新增/删除属性、数组索引和 length 变化),同时提升性能和可维护性,但代价是放弃了对 IE 的支持。
# 36 说说vue3中Treeshaking特性?举例说明一下?
Vue3 模块化导出 API,支持 Tree-shaking,使得未使用的代码不会打包进去,从而显著减小构建体积,提高性能。
Tree Shaking 就是消除未使用的代码,减小打包体积。
赖 ESM(ES Modules)的静态分析和打包工具分析依赖树
那是不是所有的 API 都能被 Tree-shaking?
不是的。Vue3 对核心 API(如响应式、生命周期钩子、Composition API)做了模块化导出,适合 Tree-shaking。
但像 createApp 这种入口方法、运行时需要的指令、渲染逻辑等,还是会被保留的。
# 37 解释一下hook、composable
Hook 是 React 的概念,Composable 是 Vue3 的概念,它们的目标类似:用函数的方式复用和组合逻辑。
那你平时在项目里写过哪些 Composable?
写过一些,比如:
useFetch:封装 axios 请求逻辑,支持 loading / error 状态。useAuth:封装登录用户的权限校验。