1 描述对vue生命周期的理解

# 1 描述对vue生命周期的理解

Vue 生命周期是指一个组件从 创建 → 挂载 → 更新 → 销毁 的过程

在这个过程中,Vue 提供了不同的钩子函数,方便我们在合适的阶段写逻辑

具体分为几个阶段:

  1. 创建阶段
    • beforeCreate:实例刚初始化,datamethods 还没挂载。
    • created:实例创建完成,datamethods 可以访问,但 DOM 还没生成。 👉 常用场景:发起请求获取数据。
  2. 挂载阶段
    • beforeMount:模板编译完成,但还没挂载到页面。
    • mounted:组件 DOM 已经渲染到页面。 👉 常用场景:操作 DOM、使用第三方插件。
  3. 更新阶段
    • beforeUpdate:数据更新前,此时可以获取更新前的 DOM。
    • updated:视图更新完成,可以获取更新后的 DOM。 👉 常用场景:对比前后数据/DOM,执行一些需要 DOM 已经更新的操作。
  4. 销毁阶段
    • beforeDestroy:实例销毁前,仍然可以访问数据和方法。
    • destroyed:实例销毁后,所有绑定解除,事件监听清理。 👉 常用场景:清理定时器、解绑事件,防止内存泄漏。

# 2 双向数据绑定是什么

双向数据绑定就是 数据和视图保持同步

数据变化更新视图

视图变化更新数据

监听器

解析器

双向绑定的原理

在 Vue2 中主要通过 数据劫持 + 发布订阅模式

使用 Object.defineProperty 对数据的 getter/setter 进行劫持。

当数据变化时,触发 setter,通知依赖(Watcher),进而更新视图。

如何实现双向绑定

new Vue初始化,对数据data执行响应化处理,这个过程发现

监听

解析

然后vue3改用了proxy代理对象,实现响应式系统

那双向数据绑定有什么优缺点?

优点

  1. 减少手动 DOM 操作,开发效率高。
  2. 数据和视图实时保持一致,代码更简洁。

缺点

  1. 大型项目里,双向绑定的数据流比较隐蔽,不如单向数据流(React)清晰,调试会比较困难。
  2. 频繁的绑定可能带来性能开销。

所以在 Vue3 中,也推荐尽量用单向数据流,v-model 只在表单等场景下使用。

# 3 Vue组件之间的通信方式都有哪些

Vue 的组件通信方式主要取决于 组件之间的关系

  1. 父子组件通信
  • props:父组件通过 props 向子组件传值。
  • $emit:子组件通过 $emit 向父组件传递事件或数据。 👉 常见于表单输入组件。
  1. 兄弟组件通信
  • Event Bus(事件总线):在 Vue2 中常用一个空的 Vue 实例作为事件中心。
  • 在 Vue3 中则可以用 mitt(轻量事件库)代替。
  1. 跨层级通信
  • provide / inject:祖先组件用 provide 提供数据,子孙组件用 inject 注入。 👉 适合多层嵌套的场景,比如主题色、全局配置共享。
  1. 全局状态管理
  • Vuex(Vue2/3 通用)、Pinia(Vue3 推荐)。 👉 适合大型应用,管理全局共享数据。
  1. 其它方式
  • $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的优先级是什么?

  1. v-for的优先级高于v-if,所以避免将他们两个同时用在同一个元素上,会带来性能方面的浪费

  2. 一般都是外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后内部v-for循环

  3. 如何条件出现在循环内部,可以通过计算属性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>

这里如果不加 nextTickoffsetHeight 拿到的是切换之前的值。

那你觉得 $nextTicksetTimeout 有什么区别?能不能用 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) 实现响应式;
  • 初始化 propscomputedwatchermethods

二 接下来进入编译阶段:

  • 如果写了 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 更偏向函数式、模块化,结构更清晰、性能更好。

那你能说说 mountedcreated 有什么区别吗?

可以。created 是在组件实例创建完毕后立即调用,此时数据已经初始化,但 DOM 还没挂载。常用于请求数据、初始化变量等。

mounted 是 DOM 挂载完毕后执行的,这时候可以访问 $el,适合进行 DOM 操作,比如初始化图表、滚动区域等。

# 13 你了解vue的diff算法吗?

diff算法是一种通过同层的树节点进行比较的高效算法

diff 算法,它的核心是在更新视图时比对新旧虚拟 DOM 树,通过最小化的 DOM 操作来提升性能。

特点:

  • 比较只会在同层级进行,不会跨层级比较(深度优先)
  • 在diff比较的过程中,循环从两边向中间比较
  • 根据 key 快速定位可复用节点

Vue diff 的执行步骤(以 v-for 为例):

  1. 先按顺序头部比较:旧前 vs 新前(相同就 patch,然后指针右移)
  2. 再比较尾部:旧后 vs 新后
  3. 交叉比较:旧前 vs 新后、旧后 vs 新前(用于处理节点被“移动”到新位置)
  4. key 匹配:遍历中间未匹配部分,用 key 建立索引映射,找出复用节点;
  5. 新增和删除处理:新列表有但旧列表没有的就创建,旧列表有但新列表没有的就删除。

你有没有遇到过 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 操作但不能提炼成组件的情况?你是怎么决定用指令的?

有的,比如我之前做一个内容管理系统,要求点击任意图片支持全屏预览功能。这类功能不能直接写到组件里,因为:

  1. 图片分布在不同组件中,逻辑相同但 DOM 结构不一致;
  2. 不希望每个组件都手动加逻辑,而是统一处理。

所以我写了一个 v-preview 指令,只要绑定到图片元素上,就可以在点击时触发弹窗预览

你刚才提到懒加载,有考虑过和 IntersectionObserver 结合使用吗?怎么封装到指令里?

是的,其实 Vue 懒加载指令最常见的优化就是用 IntersectionObserver 替代传统的 scroll 监听。

  1. 在指令 mounted 阶段创建 IntersectionObserver
  2. 当图片出现在视口中,就加载真实 src 并解绑监听
  3. el.dataset.src 存储真实图片地址,防止图片提前加载
  4. unmounted 阶段 disconnect observer

# 17 Vue中的过滤器了解吗?过滤器的应用场景有哪些?

过滤器(fileter) 是输送介质管道上不可缺少的一种装置

过滤器主要用于对绑定的数据进行格式化处理,比如时间格式化、金额千分位处理、首字母大写等。它们通常在模板中使用,语法是 ,能让模板更加简洁清晰。

ps: vue3中已经废弃了filter,原因是过滤器不够灵活且不利于代码逻辑等,使用方法或者计算属性替代

# 18 说说你对solt插槽的理解?slot使用场景有哪些?

简单来讲其实就是一个占位符,是vue实现组件内容分发的一种机制,当时使用该组件标签的时候,组件标签里面的内容会自动填充替换掉slot所在的位置

vue中有三种插槽

  1. 默认插槽:用于传入默认内容
  2. 具名插槽:用过name属性来指定位置
  3. 作用域插槽:父组件可以访问子组件传来的数据,父组件在使用中通过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,你的思路会是什么样的?能写个基本的结构吗?

  1. 定义虚拟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, '段落')
    ]);
    
    
  2. 将虚拟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;
    }
    
    
  3. 使用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,用来统一处理请求逻辑。主要封装了这几个方面:

  1. 基础配置

    首先对 Axios 实例做了统一配置:

    • baseURL:根据开发/测试/生产环境自动切换;

    • timeout:设置请求超时时间;

    • withCredentials:是否携带跨域 cookie;

    • 统一设置请求头,比如默认的 Content-Typeapplication/json

    const service = axios.create({
        baseURL: import.meta.env.VITE_API_BASE_URL,
        timeout: 10000,
        headers: {
            'content-Type': 'application/json'
        }
    })
    
  2. 请求拦截器

    • 自动注入 token(从 localStorage 或 pinia/store 拿);

    • 加载动画(如开启全局 loading 状态);

    • 自定义请求日志(可选);

    service.interceptors.request.use(config => {
      const token = localStorage.getItem('token');
      if (token) config.headers.Authorization = `Bearer ${token}`;
      return config;
    });
    
    
  3. 响应拦截器

    • 统一处理状态码(如 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);
      }
    );
    
    
  4. 封装通用请求函数

    比如统一的 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项目中的错误的?

我会将错误处理分为三个层级去做

  1. 请求级别错误处理(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);
      }
    );
    
    
  2. 运行时错误处理(全局 errorHandler)

    vue 提供了全局错误处理钩子,我们在 main.tsmain.js 中设置:

    app.config.errorHandler = (err, vm, info) => {
      console.error('Vue runtime error:', err);
      // 也可以上传到日志服务,比如 Sentry 或企业内部监控平台
      ElMessage.error('系统开小差了,请稍后再试');
    };
    
    

    这个可以捕获组件内部的运行时错误,防止整个页面崩掉。

  3. 异步未捕获错误(window.onerror / unhandledrejection)

    对于一些未捕获的 Promise 错误,我们也做了监听:

    window.addEventListener('error', (e) => {
      console.error('window error:', e);
    });
    
    window.addEventListener('unhandledrejection', (e) => {
      console.error('Unhandled Promise rejection:', e.reason);
    });
    
    
  4. 后端返回 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>

使用场景(常见的面试加分点)

  1. 多标签页切换:如“订单列表”与“订单详情”之间反复切换,列表组件的数据不想重新加载;
  2. 表单填写中途切换:比如填表中点开选择项,回来后保留填写状态;
  3. 缓存分页数据:列表页翻页或操作后切换回来保留之前的分页状态、筛选条件等;
  4. 移动端 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 的优点 ✅

  1. 用户体验好:页面切换时不需要刷新整个页面,速度快、过渡平滑;
  2. 前后端分离:前端只负责视图渲染,后端只提供接口,适合团队协作开发;
  3. 可以做前端缓存、懒加载、权限控制等高级功能
  4. 更适合 PWA 和移动端场景

SPA 的缺点 ❌

  1. 首次加载慢:需要下载整个项目的 JS、CSS 和组件,初始白屏时间较长;
  2. SEO 不友好:因为内容是 JS 渲染出来的,对搜索引擎不友好(不过可以配合 SSR 或预渲染优化);
  3. 前进后退(浏览器历史)处理复杂
  4. 页面数据状态管理更复杂:组件生命周期、缓存、销毁等要自行管理。

Vue 实现 SPA 的基本方式:

页面不刷新,只是动态替换 <router-view /> 中的组件。

你在项目中有没有优化过 SPA 的缺点?

  • 首次加载慢:我使用了 路由懒加载 + 分包策略(webpack 分 chunk)

  • SEO 不友好:我们用过 prerender-spa-plugin 进行预渲染,或者配合 Nuxt 做 SSR;

  • 白屏:添加了 骨架屏loading 动画

  • 浏览器导航问题:Vue Router 有 scrollBehavior 管理滚动、历史状态。

如何给SPA做SEO?

  1. SSR服务端渲染

    将组件或者页面通过服务器生成html,再返回给浏览器,如:nuxt.js

  2. 静态化

    程序将动态页面抓取并保存为静态页面或者通过web服务器的URL Rewrite的方式

  3. 使用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,把 datamethodscomputed 分开放。但在大型项目中,这种结构不太利于逻辑复用。Vue3 引入了 Composition API,把逻辑都集中在 setup() 函数里,变量使用 refreactive 声明,配合 watchcomputed 做响应式处理。这样逻辑聚合更清晰,也方便抽离成 composable 方法进行复用。

  • 性能和打包:Vue3 默认支持 Tree Shaking,内部结构更扁平,按需导入能减少打包体积。另外,Vue3 的虚拟 DOM diff 算法也做了重写,首次渲染和更新都更快。

# 30 vue的响应式机制

# vue2

1 用了什么

  • Object.defineProperty:对对象的属性进行“劫持”(getter/setter),从而在读写属性时触发依赖收集和更新。
  • 发布-订阅模式(观察者模式):依赖收集(Dep)+ 订阅(Watcher)。

2 核心机制

  1. 初始化数据
    • Vue 在初始化时会遍历 data 中的所有属性,用 Object.defineProperty 给每个属性添加 getter/setter。(只能读写)
    • getter:依赖收集(哪个组件用到了这个属性,就把这个组件的 watcher 收集起来)。
    • setter:触发依赖(当属性变化时,通知对应的 watcher 更新)。
  2. 依赖收集
    • 每次渲染组件时,会访问到响应式数据,触发 getter,把当前的渲染函数 watcher 记录到依赖中。
  3. 派发更新
    • 当数据变化时,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         // 拦截 deleteProperty
    
    

    Proxy 能拦截的操作远比 getter/setter 多,有更多的操作

  • Reflect:配合 Proxy 使用,保证操作的默认行为。

2 核心机制

  1. 创建响应式对象
    • Vue3 用 reactiveref 创建响应式对象时,会返回一个 Proxy 包装对象。
  2. 依赖收集
    • 访问属性时,Proxy 的 get 拦截触发,调用 track() 函数收集依赖(effect)。
  3. 派发更新
    • 修改属性时,Proxy 的 set 拦截触发,调用 trigger() 函数通知依赖更新。

优势

  • 能监听对象新增/删除属性:Proxy 能直接捕获 indeleteProperty 等操作。
  • 能监听数组索引和 length 变化
  • 性能更好:不需要初始化时遍历所有属性,只有访问时才收集依赖(惰性)。
  • 更灵活effectcomputedwatch 都是基于 Proxy 响应式系统的能力实现的。

# 31 vue3中的Compostion APi与vue2的Options API有什么不同?

Vue2:Options API

  • 通过 datamethodscomputedwatch配置项 来组织代码。
  • 一个功能点的逻辑,可能分散在不同的选项里,不是很聚合。

Vue3:Composition API

  • 使用 setup() 函数,直接在函数内部通过组合 API(refreactivecomputedwatch)来组织逻辑。
  • 一个功能点的逻辑可以聚合在一起,更加模块化、函数化。

代码对比

差别

  • 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:封装登录用户的权限校验。