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

196 阅读24分钟

vue 高频面试题——2026

2026年Vue面试不再慌!本文整理了一线大厂高频考察的Vue核心问题,从响应式底层原理到复杂数据性能优化,每个问题都给出易懂的解答和核心要点。

vue 中怎么实现样式隔离?

  1. 核心方案:scoped 属性

在 Vue 组件的 style 标签上添加 scoped 属性,Vue 会自动为该组件内的所有 DOM 元素添加唯一的属性标识,同时给样式规则加上对应的属性选择器。这样该组件的样式只会作用于自身 DOM,不会影响其他组件,这是最常用、最基础的样式隔离方式。

  1. 使用 CSS 预处理器的作用域功能

借助 Less、Sass/Scss 等预处理器的嵌套特性,给组件样式包裹一个唯一的父类 / ID 命名空间(比如以组件名作为父类),让样式只作用于该命名空间内的元素,通过手动划分作用域实现隔离,是 scoped 属性的补充方案。

  1. 避免全局样式污染

减少或规范全局样式的使用,若必须写全局样式,通过特定的命名规范(如 BEM 命名法)划分模块,避免类名重复;同时利用 scoped 样式的穿透规则(如 ::v-deep),按需修改子组件样式,既保证隔离性,又能灵活调整样式。

说说 Vue 中 css scoped 的原理

Vue 中 scoped 的本质是CSS 作用域隔离,其核心实现思路是:

  1. 编译阶段:Vue 编译器会为带有 scoped 的组件模板中的每个 DOM 元素,自动添加一个唯一的 data-v-xxx 形式的属性(xxx 是随机生成的哈希值)。
  2. 样式转换:同时将该组件内的 scoped CSS 规则,自动追加一个对应的属性选择器 [data-v-xxx],使样式仅匹配当前组件中带有该属性的元素。
  3. 作用域限制:通过这种 “属性匹配” 的方式,让样式只作用于当前组件的 DOM 元素,避免样式泄露到全局或其他组件。

如何打破 scope 对样式隔离的限制?

  • 使用深度选择器(最常用)Vue 提供了专属的深度选择器语法(:deep),能穿透 scoped 的属性隔离,让样式作用到子组件内部的 DOM 元素。不同 Vue 版本 / 预处理器的写法略有差异。

  • 拆分 style 标签(最灵活)在组件中写两个 style 标签:一个加 scoped 用于组件自身样式隔离,另一个不加 scoped 作为全局样式区。不加 scoped 的样式会成为全局样式,可直接作用于子组件、外部元素,既能保留自身样式隔离,又能按需编写全局生效的样式。

  • 使用全局样式文件覆盖在项目的全局样式文件(如 global.css)中编写目标样式,全局样式的优先级高于 scoped 样式,能直接覆盖子组件 / 外部元素的样式。

  • 利用样式优先级强制覆盖通过 CSS 优先级规则(如增加类名层级、使用 !important)提升样式的优先级,突破 scoped 样式的隔离限制。比如给样式规则添加多层父类嵌套,或在样式值后加 !important,让该样式优先于子组件的 scoped 样式生效。

  • 动态绑定样式(运行时调整)不依赖 scoped 样式穿透,而是通过 v-bind:class/v-bind:style 动态给子组件 / 外部元素绑定样式类或行内样式。行内样式的优先级最高,能直接覆盖 scoped 样式。

vue3 中怎么设置全局变量?

  • 通过 app.config.globalProperties 挂载(最常用)创建 Vue 应用实例(app)后,将需要全局访问的变量 / 方法挂载到 app.config.globalProperties 上,所有组件实例中都能直接访问该属性。这是 Vue3 替代 Vue2 中 Vue.prototype 挂载全局变量的官方方案。

  • 利用provide/inject全局注入在根组件通过 provide 提供全局变量,所有子组件(无论层级多深)都能通过 inject 注入并使用。适合需要全局共享、且希望按需注入(而非所有组件都默认可见)的变量。

  • 封装独立模块导出(纯数据类全局变量)新建单独的 JS 文件,定义并导出全局变量,在需要使用的组件中导入即可。这种方式无 Vue 框架耦合,适合静态全局常量(如接口地址、枚举值),也支持通过响应式 API(如 ref/reactive)定义可修改的全局响应式数据。

  • 使用 Pinia/Vuex 管理全局状态(复杂场景)若全局变量是需要跨组件修改、且有状态管理需求的业务数据(如用户信息、全局配置),优先使用 Pinia(Vue3 官方推荐)或 Vuex,通过定义全局仓库实现变量的全局共享和统一修改。

ts 中 type 和 interface的区别【TypeScript】

  1. 定义范围

    • type:能定义所有类型(基本类型、对象、联合 / 交叉类型等)
    • interface:只能定义对象 / 函数类型
  2. 扩展方式

    • type:用 & 交叉组合
    • interface:用 extends 继承
  3. 重复定义

    • type:不支持,重复会报错
    • interface:支持,同名自动合并
  4. 类实现

    • type:仅纯对象类型可被 implements(不推荐)
    • interface:原生支持 implements

示例 1:interface + implements(推荐、常用)

// 1. 定义接口(契约):规定必须有 run 方法
interface IRun {
  run(): void;
}

// 2. 类实现这个接口 → 必须写 run 方法,否则报错
class Person implements IRun {
  // 必须实现 IRun 里的 run 方法
  run() {
    console.log("人在跑步");
  }
}

// ✅ 正确:类满足了接口的所有要求
const p = new Person();
p.run(); // 输出:人在跑步

// ❌ 错误示例:如果类不实现 run 方法,TS 会直接报错
// class Student implements IRun {} 
// 报错:Class 'Student' incorrectly implements interface 'IRun'. Property 'run' is missing in type 'Student' but required in type 'IRun'.

示例 2:type + implements(仅纯对象类型可行,但不推荐)

// 1. 定义纯对象类型的 type(仅包含属性/方法,无联合/交叉)
type RunType = {
  run(): void;
};

// 2. 类实现这个 type → 语法上可行,但语义不贴合
class Dog implements RunType {
  run() {
    console.log("狗在跑");
  }
}

// ❌ 错误示例:如果 type 包含联合类型,无法被 implements
type RunOrJump = { run(): void } | { jump(): void };
// class Cat implements RunOrJump {}
// 报错:A class can only implement an object type or intersection of object types with statically known members.
  1. 高级类型

    • type:支持映射、条件类型
    • interface:不支持

[Vue] watch 和 computed 的区别和理解

1. 核心用途(一句话分清)

  • computed(计算属性) :用已有数据算出新值(比如:用 age 算出 是否成年
  • watch(监听器) :盯着某个数据做操作(比如:age 变了就发请求、弹提示)

2. 他们之间的区别

对比维度computed(计算属性)watch(监听器)
核心定位基于已有响应式数据,计算并返回新的派生值,侧重 “计算结果”监听指定响应式数据的变化,触发对应的操作逻辑,侧重 “数据变化后的行为”
是否支持异步❌ 不支持。异步操作会导致计算结果无法被缓存,破坏依赖追踪机制✅ 支持。专门用于处理异步 / 开销大的操作(如发请求、定时器、读写本地文件)
数据可修改性只读属性。直接修改 computed 的值会报错,只能通过修改依赖的原响应式数据触发重新计算无 “只读” 限制。监听的数据可以手动修改,回调里也能修改其他数据
执行时机(懒计算)懒执行:首次访问时才计算,后续访问直接读取缓存;仅当依赖数据变化时才重新计算非懒执行:默认初始化时不执行(可通过 immediate: true 开启立即执行),监听数据变化时立即触发回调
返回值必须有返回值,返回值作为计算结果供模板 / 代码使用不需要返回值,回调函数的作用是执行操作(如修改数据、调用方法)
适用场景1. 简单数据推导(如姓名拼接、金额格式化)2. 依赖多个数据计算一个值3. 频繁访问且计算逻辑稳定的场景1. 数据变化后执行异步操作(如搜索关键词变化发请求)2. 数据变化后执行复杂业务逻辑(如表单校验、弹窗提示)3. 监听对象 / 数组的深度变化

[Vue] 在 v-for 时给每项元素绑定事件需要用事件代理吗?为什么?

  1. 不是必须使用事件代理,但推荐在特定场景下使用
  • Vue 本身对 v-for 绑定的事件做了底层优化,不会像原生 JS 那样直接在每个 DOM 元素上挂载独立事件处理器,而是通过组件级的事件委托机制管理,因此少量循环项(比如几十项内)直接绑定 @click 等事件完全可行,无需额外做事件代理。
  1. 需要使用事件代理的核心原因:优化性能(针对大量循环项场景)
  • 当 v-for 渲染的列表项数量极大(比如几百、上千项)时,即使 Vue 有优化,每个项绑定独立事件仍会产生一定的内存开销(每个事件处理器都会占用少量内存);

  • 事件代理的核心逻辑是将事件绑定到列表的父容器上,而非每个子项,无论子项数量多少,都只存在一个事件处理器,能显著减少内存占用,降低浏览器的渲染和事件管理压力。

  1. 无需使用事件代理的场景与原因
  • 列表项数量少(比如几十项以内):此时直接绑定事件的性能损耗可以忽略,且代码更直观、易维护,无需额外做代理;

  • 事件逻辑与单个列表项强关联(比如需要频繁获取当前项的 $eventindex 或数据):直接在项上绑定事件,能通过 @click="handleClick(item, index)" 快速获取上下文,而事件代理需要手动通过 event.target 查找对应项,代码复杂度会增加。

Vue 模板是如何编译的

vue 中的模板 template 无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML 语法,所有需要将template 转化成一个 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML 元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。 模板编译又分三个阶段,解析parse优化optimize生成 generate,最终生成可执行函数 render

  • 解析阶段:使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树AST
  • 优化阶段:遍历 AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化runtime的性能。
  • 生成阶段:将最终的AST转化为 render 函数字符串。

vue3 相比较于 vue2,在编译阶段有哪些改进?

  1. 静态提升Vue3 会把模板里不会变化的静态节点提升到渲染函数外部,避免每次渲染都重新创建,从而减少渲染开销。

  2. PatchFlags 补丁标记Vue3 在编译时会给动态节点打上标记,比如文本更新、class 更新、属性更新等。运行时 diff 时只更新有标记的部分,实现精准更新,提高性能。

  3. 事件缓存优化对于事件绑定,Vue3 会缓存事件处理函数,避免每次渲染都生成新的函数,减少不必要的虚拟 DOM 对比。

  4. 静态节点复用连续的静态节点会被合并成静态片段,渲染时直接复用,而不是逐个创建,提升渲染效率。

  5. 更友好的 Tree-shakingVue3 的编译产物结构更模块化,打包工具可以更好地剔除未使用的代码,减小最终打包体积。

  6. v-if 和 v-for 优先级调整Vue3 让 v-if 优先级高于 v-for,避免无效循环,同时编译阶段会给出更明确的提示,减少开发者错误。

  7. 内联事件优化内联事件会被编译成缓存函数,而不是每次渲染都生成新函数,减少虚拟 DOM 的差异对比。

  8. 编译架构重构Vue3 的编译过程被拆分为解析、转换、生成三个独立阶段,扩展性更强,也更容易维护。

为什么 Vue 中的 v-if 优先级高于v-for?

  1. 规避无效性能消耗

Vue2 中 v-for 优先级更高时,即便 v-if 条件为 false,仍会先遍历所有数据生成节点,再隐藏这些节点,造成无意义的计算和渲染开销。Vue3 把 v-if 优先级调高后,会先判断条件,若为 false 则直接跳过 v-for 遍历,从根源上避免无效循环,提升性能。

  1. 符合编程直觉逻辑

日常编程中遵循 “先判断是否执行,再执行循环” 的逻辑,Vue3 调整优先级后,模板逻辑与开发者的代码编写习惯保持一致,降低理解和使用成本,减少逻辑混乱。

  1. 减少变量访问错误
  • Vue2 里同一元素写 v-for 和 v-if 时,若数据源是空的(比如 undefined),在 v-if 里用 v-for 的变量会直接报错;

  • Vue3 把 v-if 优先级调高后,会要求开发者把 v-for 写到外层标签里,就不会出现变量没定义就使用的错误了。

Vue 3.0 中 Treeshaking 特性是什么,并举例进行说明?

  1. Treeshaking定义

核心定义Tree-shaking 直译是 “摇树”,在 Vue3 中是指打包工具(如 Vite、Webpack)能精准识别并剔除项目中未被使用的 Vue 框架代码,只保留实际用到的部分,最终减小打包后的文件体积。Vue2 因代码架构问题,无法做到如此精准的剔除,即便只用一小部分功能,也会引入整个 Vue 核心库。

  1. 实现原理

实现原理Vue3 采用 ES 模块(ESM)规范重构了代码架构,将核心功能(如响应式、指令、内置组件等)拆分成独立的小模块,每个功能都是可单独导入 / 导出的单元。打包工具会分析代码的导入导出关系,标记出未被引用的模块,打包时直接忽略这些模块,只打包用到的部分。

  1. 具体示例说明
  • 示例 1:仅使用 Vue3 的响应式功能若你的项目只用到 ref reactive 等响应式 API,未使用 v-show 等功能,Tree-shaking 会剔除这些未使用的代码,最终打包文件中只包含响应式相关逻辑,而非整个 Vue 库。

  • 示例 2:仅使用基础模板渲染若项目只做简单的模板渲染,未使用 vue-router 集成、provide/inject 依赖注入、Suspense 组件等功能,打包时这些未用到的模块会被全部剔除,大幅减少打包体积。

  • 示例 3:自定义指令未使用Vue3 内置了 v-model v-bind 等基础指令,同时保留了 v-memo 等进阶指令,若项目未使用 v-memo,Tree-shaking 会自动剔除该指令的代码,不会将其打入最终包中。

  1. 与 Vue2 的核心差异

Vue2 的代码是整体式架构,即便只用到 Vue 构造函数,也会引入包括编译器、所有内置指令、组件在内的完整代码;而 Vue3 借助 Tree-shaking,能实现 “用多少、打包多少”,这也是 Vue3 打包体积比 Vue2 小约 40% 的核心原因之一。

Vue 中的 v-show 和 v-if 有什么区别

  • 渲染机制:v-show 是通过 CSS 控制显示隐藏,元素始终在 DOM 中;v-if 是条件为 true 才渲染,false 则从 DOM 移除。

  • 性能:v-show 切换开销小,适合频繁切换的场景;v-if 初始化开销小,切换开销大,适合条件少变的场景。另外,v-show 不支持 template 和 v-else,而 v-if 支持。”

谈谈对 Vue 中双向绑定的理解

Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

首先对data执行响应化处理,在Observe中劫持所有属性,同时对complie中对模板进行编译,找到动态绑定的数据,从data中获取并且初始化视图,同时定义watcher和更新函数,通常data中会有多个watcher,因此定义一个Dep来管理这些watcher,当监听到Observe中有数据变化时会通知Dep,然后由Dep来通知所有的watcher去触发更新函数,最终更新视图。

image.png

MVVM、MVC、MVP 的区别

MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。

MVVM

  • MVVM 是一种架构模式,把代码分成 Model(数据)、View(视图)、ViewModel(桥梁)三部分。

  • Model 负责数据,View 负责展示,ViewModel 负责让数据和视图自动同步。

  • Vue 实现 MVVM 的方式是:通过响应式系统监听数据变化,通过虚拟 DOM 和 Diff 算法更新视图,通过指令(如 v-model)处理用户输入并同步回数据。

  • 这样开发者只需要关注数据,不需要手动操作 DOM,实现了数据驱动视图。

image.png

MVC

MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。

  • View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关View 层更新页面。
  • Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对Model的修改,然后 Model 层再去通知 View 层更新。

MVP

MVP 模式与 MVC 唯一不同的在于 Presenter 和Controller。在MVC 模式中使用观察者模式,来实现当Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过使用Presenter来实现对 View 层和 Model 层的解耦。MVC 中的Controller 只知道Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此可以在Presenter中将Model 的变化和 View 的变化绑定在一起,以此来实现View和Model 的同步更新。这样就实现了对View 和Model 的解耦,Presenter 还包含了其他的响应逻辑。

说说 vue 中的 diff 算法

在新老虚拟 DOM 对比时:

  • 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
  • 如果为相同节点,进行 patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children 没有子节点,将旧的子节点移除)。
  • 比较如果都有子节点,则进行 updateChildren,判断如何对这些新老节点的子节点进行操作(diff 核心)。
  • 匹配时,找到相同的子节点,递归比较子节点在 diff 中,只对同层的子节点进行比较,放弃跨级的节点比较,降低时间复杂度,也就是说,只有当新旧children都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。

Vue 有了数据响应式,为何还要 diff ?

  • 数据响应式的核心是感知数据变化:通过 Object.defineProperty(Vue2)/Proxy(Vue3)监听数据改动,标记需要更新的视图依赖,但它只解决 “知道数据变了要更视图” 的问题,不负责 “怎么高效更视图”。

  • Diff 算法的核心是高效更新视图:直接操作真实 DOM 性能成本高,Diff 算法通过对比新旧虚拟 DOM 的差异,只更新变化的部分,避免全量渲染 DOM;同时能批量合并更新操作,减少浏览器重排重绘。

  • 二者分工协作:响应式系统负责 “触发更新”,Diff 算法负责 “优化更新过程”,共同实现数据驱动视图的高效渲染。

说说Vue 页面渲染流程

首先在导入引入Vue 时,会对 Vue 框架进行初始化 然后创建 Vue 实例,管理生命周期(初始化 —— 模板编译 —— 挂载 —— 销毁) 在第二个阶段:

  1. new Vue() 进行实例的创建(这个时候会调用_init(),相当于程序的入口),
  2. 挂载组件($mount),进行实例化
  3. 构建 VNode (_render() 方法 、createElement() 返回 VNode )通过createElement来创建虚拟节点VNode,将 VNode 渲染成 DOM。
  4. patch() 对比新旧 VNode,createElm()生成(/更新)真实 DOM 节点树 (递归)。最终将整个 DOM 树插入到页面中,再移除旧的根节点(初始化渲染实际上是新的根节点代替旧的根节点)
  5. 节点插入生命周期钩子函数

说说 vue3 中的响应式设计原理

  • Vue3 的响应式是基于 ES6 Proxy 实现的,它可以代理整个对象或数组,能够监听属性的读取、修改、新增和删除,以及数组的索引和长度变化,比 Vue2 的 Object.defineProperty 更强大、更完整。

  • 当组件首次渲染时,访问响应式数据会触发 Proxy 的 “读取拦截”,Vue 会记录当前组件或副作用函数为依赖,这个过程叫依赖收集

  • 当数据发生变化时,Proxy 的 “修改拦截” 会被触发,Vue 会通知所有依赖该数据的组件或副作用函数重新执行,这个过程叫触发更新

  • Vue3 还提供了 ref 和 reactive 两种方式来创建响应式数据,分别用于基础类型和复杂对象 / 数组,让响应式更灵活、更高效。

Vuex 的原理

  • Vuex 是专为 Vue 设计的全局状态管理库,核心原理基于 单向数据流 + 集中式存储:将所有组件共享的状态(数据)集中存储在一个全局唯一的 “仓库(store)” 中,避免组件间状态混乱,同时通过严格的规则保证状态修改可追踪。

  • 核心机制:单向数据流

    • 状态存储:所有全局状态统一存放在 store 的 state 中,组件可读取 state不允许直接修改
    • 触发修改:组件需通过提交 mutation(同步操作)来修改 statemutation 是修改状态的唯一入口,且必须是同步函数;
    • 异步处理:若有异步操作(如接口请求),需先触发 action,由 action 完成异步逻辑后,再提交 mutation 修改状态;
    • 派生数据:通过 getterstate 中派生新数据(类似组件的 computed),实现数据的复用和缓存。
  • 响应式保障Vuex 的 state 本质是借助 Vue 自身的响应式系统实现的 ——state 会被 Vue 转化为响应式数据,因此当 mutation 修改 state 后,所有依赖该状态的组件会自动更新视图,保证状态与视图的同步。

  • 模块化设计(解决复杂场景)为避免单一 store 过于臃肿,Vuex 支持 module 模块化:将不同业务的状态拆分为独立模块,每个模块拥有自己的 statemutationactiongetter,最终通过命名空间隔离,既保持全局统一管理,又避免命名冲突。

为什么 Vuex 的 mutation 中不能做异步操作? Vuex 中所有的状态更新的唯一途径都是mutation,异步操作通过Action 来提交 mutation 实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。 每个 mutation 执行完成后都会对应到一个新的状态变更,这样devtools就可以打个快照存下来,然后就可以实现time-travel了。 如果 mutation 支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。

vuex 中的辅助函数怎么使用?

  • Vuex 的辅助函数就是用来简化组件访问 store 的写法,不用每次都写 this.$store.state.xxx 这种长路径。

  • 常用的有四个:

    • mapState:把 store 的 state 映射成组件的计算属性。
    • mapGetters:把 store 的 getters 映射成组件的计算属性。
    • mapMutations:把 store 的 mutations 映射成组件的方法。
    • mapActions:把 store 的 actions 映射成组件的方法。
  • 使用方式很统一:

    • 先从 Vuex 导入辅助函数;
    • 在组件里用扩展运算符把它们 “展开” 到对应位置(计算属性或 methods);
    • 然后在模板或方法里直接用映射后的名字即可。
  • 如果是模块化的 store,需要加上命名空间,让辅助函数知道要从哪个模块取数据或方法。

Vuex 和单纯的全局对象有什么区别?

响应式保障不同

  • 单纯全局对象:普通 JS 对象无响应式,修改数据后组件不会自动更新视图,需手动触发刷新;
  • Vuex:state 基于 Vue 响应式系统构建,数据修改后依赖该状态的组件会自动更新,契合 Vue 数据驱动视图的逻辑。

状态修改规则不同

  • 单纯全局对象:可在任意地方直接修改,修改行为无约束、不可追踪,复杂项目中易出现状态混乱;
  • Vuex:强制通过 mutation(同步)/action(异步)修改状态,所有状态变更可被记录、调试(如 Vue Devtools),能精准定位修改来源。

适配 Vue 生态不同

  • 单纯全局对象:与 Vue 组件生命周期、钩子无联动,无法感知组件挂载 / 卸载;
  • Vuex:深度适配 Vue 组件体系,支持模块化、命名空间、getter 缓存、严格模式等,能处理复杂业务场景下的全局状态管理(如多组件共享、异步更新)。

作用域与隔离性不同

  • 单纯全局对象:无内置隔离机制,属性名易冲突,无法拆分不同业务的状态;
  • Vuex:支持模块化拆分,不同业务的状态可封装在独立模块中,通过命名空间隔离,避免命名冲突,便于维护。

双向数据绑定的原理是什么?

Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

首先对data执行响应化处理,在Observe中劫持所有属性,同时对complie中对模板进行编译,找到动态绑定的数据,从data中获取并且初始化视图,同时定义watcher和更新函数,通常data中会有多个watcher,因此定义一个Dep来管理这些watcher,当监听到Observe中有数据变化时会通知Dep,然后由Dep来通知所有的watcher去触发更新函数,最终更新视图。

说说你对 slot 的理解?slot 使用场景有哪些?

一、对 slot 的核心理解

  1. slot 本质是 Vue 组件的内容分发机制,可以把它理解成组件内部预留的 “占位符 / 容器”;
  2. 父组件可以往这个 “容器” 里填充任意内容(文本、标签、甚至其他组件),子组件负责渲染这些内容;
  3. 核心价值是让组件更灵活、可复用—— 组件的结构框架固定,但内部部分内容可由使用方自定义。

二、slot 的核心使用场景

  1. 基础定制化场景:组件主体逻辑 / 样式固定,仅部分内容需要自定义。比如通用的 “卡片组件”,卡片标题、底部按钮可通过插槽由父组件自定义,卡片的边框、内边距等样式固定;
  2. 多内容区定制场景:组件有多个可自定义区域,用具名插槽(name slot)区分。比如 “弹窗组件”,预留header(弹窗标题)、body(弹窗内容)、footer(弹窗按钮)三个插槽,分别定制不同区域;
  3. 带数据交互的场景:用作用域插槽(scope slot),子组件把内部数据传递给父组件,父组件基于这些数据自定义渲染内容。比如 “列表组件”,子组件负责请求列表数据,父组件通过作用域插槽自定义列表项的展示样式(如文字、图片、按钮组合);
  4. 组件嵌套场景:父组件需要往子组件内部嵌入其他自定义组件时,比如在 “导航栏组件” 的右侧插槽中嵌入 “搜索组件” 或 “用户头像组件”。

刷新浏览器后,Vuex 的数据是否存在?如何解决?

一、核心答案:刷新后数据不存在

Vuex 的数据是存储在内存中的,浏览器刷新时页面会重新加载,内存中的数据会被清空,所以 Vuex 里的状态会丢失。

二、解决方法(核心思路:把数据持久化到本地存储)

  1. 手动同步(简单场景)

    • 存储:在 Vuex 修改 state 时,同步把数据存入 localStorage/sessionStorage(前者永久保存,后者仅当前会话有效);
    • 恢复:在 Vuex 初始化(如 store 创建时),从本地存储读取数据并赋值给 state。
  2. 使用插件(复杂场景,推荐)

    用成熟的第三方插件(如 vuex-persistedstate),只需简单配置,就能自动将指定的 Vuex 数据持久化到本地存储,刷新后自动恢复,无需手动写同步逻辑。

vue2和vue3的区别

响应式原理不同

  • Vue2 用 Object.defineProperty,只能监听属性,不能监听数组下标和新增属性。
  • Vue3 用 Proxy,可以监听整个对象和数组,功能更强、性能更好。

API 写法不同

  • Vue2 用 Options API,按 data、methods、computed 分开写。
  • Vue3 用 Composition API,通过 setup 按逻辑组织代码,更方便复用。

生命周期不同

  • Vue3 的钩子前面加 on,比如 onMounted。
  • setup 会在组件创建前执行,替代了 beforeCreate 和 created。

模板语法不同

  • Vue3 支持多个根节点。
  • 插槽必须用 v-slot。
  • v-if 优先级高于 v-for。
  • 去掉了过滤器 filter。

性能和新特性

  • Vue3 支持 Tree-shaking,打包更小。
  • 新增 Teleport(传送门)和 Suspense(异步组件)。

可以简单说一下 Teleport(传送门)吗?

  • 核心作用:Teleport 能把组件的 DOM 结构,“瞬移” 到页面任意指定的 DOM 节点下(比如 body 标签),但组件的逻辑(数据、方法、事件)仍和原组件保持关联。
  • 解决的问题:比如写弹窗组件时,若嵌套在多层父组件里,会受父组件的样式(如 overflow:hidden)、z-index 影响导致显示异常,用 Teleport 把弹窗 DOM 移到 body 下,就能规避这些问题。
  • 核心特点:DOM 位置变了,但组件的上下文(父子关系、props、事件)没变,逻辑还归原组件管。

什么是Suspense(异步组件) Suspense 是 Vue3 专属,搭配异步组件(defineAsyncComponent) 使用; 核心是 “加载中占位 + 加载完成渲染”,解决异步组件加载的空白问题; 异步组件的本质是 “按需加载”,只有用到时才会加载该组件的代码,减少初始打包体积。

Vue3.0里为什么要用 Proxy API 替代defineProperty APl ?

  • Object.defineProperty 无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
  • Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。
  • Proxy 可以劫持整个对象,并返回一个新的对象。
  • Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

Vue3.0里为什么要用 Proxy API 替代defineProperty APl ?

数组监听更完善defineProperty 监听不到数组下标 / 方法(如 push)的变化,Proxy 能原生监听整个数组的所有变动;

劫持效率更高defineProperty 要逐个劫持对象属性(嵌套对象还需深度遍历),Proxy 直接劫持整个对象,不用遍历;

支持动态属性defineProperty 监听不到对象新增 / 删除的属性,Proxy 可以原生支持,无需手动处理。

vue3 为什么要引入 Composition API?

解决逻辑复用难题:Vue2 的 Options API 只能通过 mixins、extends 复用逻辑,易出现命名冲突、逻辑来源不清晰的问题;Composition API 可将相关逻辑封装成独立函数,复用更灵活。

优化代码组织:Options API 按 data/methods/computed 等选项拆分代码,复杂组件中同一块业务逻辑的代码会分散在不同选项里,阅读和维护成本高;Composition API 按业务逻辑聚合代码,比如 “表单验证逻辑” 的所有代码集中在一处,结构更清晰。

适配复杂场景:对于大型项目(如中台、管理系统),Composition API 支持更好的类型推导(适配 TypeScript),也更方便拆分和管理复杂的组件逻辑,开发体验更优。

vue3 为什么不需要时间分片?

1. 编译阶段:Patch Flags(补丁标记)实现「精准更新」

Vue3 在编译 template 模板时,会对每个动态节点(如绑定了变量的文本、v-if/v-for 节点、动态属性等)打上 Patch Flags 标记,标记内容包含「该节点哪部分是动态的」(比如文本内容、class、style 等)。

  • Vue2 缺陷:更新时会全量遍历虚拟 DOM 树,即使只有一个节点的文本变化,也会遍历整个树的所有节点做对比,若节点数量多,极易产生长任务;
  • Vue3 优化:更新时只遍历带有 Patch Flags 标记的动态节点,且仅对比标记指定的「动态部分」(比如只对比文本内容,而非整个节点),单次更新的遍历节点数从「全量」降到「少量动态节点」,计算量大幅减少。
2. 响应式系统:Proxy 实现「精准依赖追踪」

Vue3 的响应式基于 Proxy 重构,从「属性级劫持」升级为「对象级代理」,实现了更精准的依赖收集与触发:

  • Vue2 缺陷:基于 Object.defineProperty 劫持属性,依赖收集粒度较粗,修改一个数据可能触发多个无关组件的更新(比如父组件数据变化,子组件即使没用到该数据也可能被触发更新),增加了无效的计算开销;
  • Vue3 优化:每个组件的渲染函数都会被精准收集为「对应数据的依赖」,修改数据时,只会触发真正使用该数据的组件更新,完全避免无效的组件重渲染,从源头减少了更新的总工作量。
3. 虚拟 DOM 优化:静态提升(Static Hoisting)减少重复创建

Vue3 编译时会将「静态节点 / 静态属性」(如 <div class="title">标题</div>)从 render 函数中抽离,提升到函数外部:

  • Vue2 缺陷:每次组件更新时,即使是静态节点,也会重新创建虚拟 DOM 节点,产生无意义的内存开销和计算量;
  • Vue3 优化:静态节点只会在组件首次渲染时创建一次,后续更新时直接复用,不再重复创建,进一步降低了单次更新的耗时。
4. 编译优化:缓存事件处理函数与静态树
  • 事件处理函数缓存:Vue3 会缓存 @click="handleClick" 这类事件处理函数,避免每次更新时重新创建函数引用,减少不必要的虚拟 DOM 对比;
  • 静态树提升:对于完全静态的节点树(如多个嵌套的静态 div),会整体提升为一个常量,更新时直接跳过整个树的对比。

说说你对 Vue 生命周期的理解

不管是 Vue2 还是 Vue3,组件生命周期都围绕 4 个核心阶段展开,只是钩子名称 / 写法有差异:

  1. 初始化阶段:组件实例被创建,完成数据响应式绑定、初始化事件 / 属性,但还未挂载到 DOM。

    • 核心作用:初始化数据、配置全局事件、提前处理一些非 DOM 相关的逻辑。
  2. 挂载阶段:组件模板编译成 DOM 并插入到页面中。

    • 核心作用:操作 DOM(比如获取节点、初始化第三方插件)、发起异步请求(接口数据获取)。
  3. 更新阶段:组件依赖的数据发生变化,触发模板重新渲染。

    • 核心作用:监听数据更新后的 DOM 变化、处理更新后的业务逻辑(避免重复请求)。
  4. 销毁阶段:组件实例被销毁,从 DOM 中移除。

    • 核心作用:清理副作用(比如清除定时器、取消接口请求、解绑自定义事件),防止内存泄漏。

一、Vue2 生命周期钩子(Options API)

按执行顺序梳理核心钩子,分 4 个阶段:

  1. 初始化阶段beforeCreate(实例创建前,无数据 / 方法)→ created(实例创建完成,可访问数据 / 方法,DOM 未挂载)
  2. 挂载阶段beforeMount(模板编译完成,DOM 未挂载)→ mounted(DOM 挂载完成,可操作 DOM、请求接口)
  3. 更新阶段beforeUpdate(数据更新,DOM 未重新渲染)→ updated(DOM 更新完成)
  4. 销毁阶段beforeDestroy(实例销毁前,仍可访问数据 / 方法)→ destroyed(实例销毁完成,清理副作用)
  • 补充:还有 activated/deactivated(keep-alive 组件激活 / 失活)、errorCaptured(捕获子组件错误)

二、Vue3 生命周期钩子(Composition API)

核心分两种写法,核心阶段和 Vue2 一致,仅命名 / 触发时机调整:

  1. 组合式 API(setup 中使用,主流)

    • 初始化阶段:无 beforeCreate/created,由 setup 替代(setupbeforeCreate 前执行)
    • 挂载阶段:onBeforeMountonMounted
    • 更新阶段:onBeforeUpdateonUpdated
    • 销毁阶段:onBeforeUnmount(替代 Vue2 的beforeDestroy)→ onUnmounted(替代 Vue2 的destroyed
    • 补充:onActivated/onDeactivatedonErrorCaptured 用法和 Vue2 一致

vue2和vue3生命周期核心阶段(挂载 / 更新 / 销毁)的执行时机完全一致,只是写法 / 命名不同。

扩展

父组件和子组件生命周期钩子执行顺序:

Vue2 父子组件生命周期执行顺序

加载渲染过程:父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

更新过程:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

销毁过程:父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyedVue3(组合式 API)父子组件生命周期执行顺序

Vue3(组合式 API)父子组件生命周期执行顺序

加载渲染过程:父 setup -> 父 onBeforeMount -> 子 setup -> 子 onBeforeMount -> 子 onMounted -> 父 onMounted

更新过程:父 onBeforeUpdate -> 子 onBeforeUpdate -> 子 onUpdated -> 父 onUpdated

销毁过程:父 onBeforeUnmount -> 子 onBeforeUnmount -> 子 onUnmounted -> 父 onUnmounted