Vue高级面试题

466 阅读44分钟

总共35道

vue的核心是什么?

Vue 的核心设计理念可以概括为以下 5 个关键要素,这些要素共同构成了 Vue 的独特优势:

一、响应式系统(Reactivity System)
二、单文件组件
三、虚拟 DOM(Virtual DOM)
四、模板编译系统
五、渐进式框架(Progressive Framework)

Vue实例挂载的过程 🌟🌟

Vue 实例挂载的过程是将 Vue 实例与 DOM 元素关联起来,并将虚拟 DOM 渲染为真实 DOM 的一系列步骤。下面详细介绍 Vue 2.x 中实例挂载的主要过程。


一、挂载流程

1. 初始化实例

当创建一个 Vue 实例时,会调用 Vue 构造函数,在这个阶段会进行一系列的初始化操作

const vm = new Vue({
  el: '#app',      // 挂载目标
  data: { message: 'Hello Vue' },
  template: '<div>{{ message }}</div>' // 或者 render: h => h(App)    
});
  • 选项合并:合并全局配置(如Vue.config)与实例选项
  • 生命周期初始化:初始化beforeCreatecreated阶段的内部状态
2. 初始化响应式系统
  • 初始化顺序:propsmethodsdata,并挂载到组件实例 this 上,但此时 DOM 还未生成,数据仅存在于内存中,修改不会触发 DOM 渲染。

  • 示例:created() 中 this.count = 1 能修改数据,但无 DOM 可更新。

// 内部执行
initProps(vm)       // 处理props
initMethods(vm)     // 绑定方法
initData(vm)        // 数据劫持
initComputed(vm)    // 计算属性初始化
initWatch(vm)       // 监听器设置
  • 使用Object.defineProperty(Vue2)或Proxy(Vue3)实现数据劫持
  • 建立DepWatcher的依赖收集关系

3. 编译模板(仅运行时+编译器版本需要)
// 编译过程伪代码
const ast = parse(template)   // 生成抽象语法树
optimize(ast)                 // 静态节点标记
const code = generate(ast)    // 生成渲染函数
  • 输入template字符串(如<div>{{message}}</div>

  • 输出:可执行的render函数(如_c('div', [_v(_s(message))])

    问题

    • Vue 挂载时,templaterender 和 el 的关系是什么?
    • 如果同时提供了 template 和 render,优先级如何?

    关键点

    • 优先级:render > template > el 内部的 HTML。
4. 生成渲染函数
// 生成的render函数示例
function render() {
  with(this) {
    return _c('div', [_v(_s(message))])
  }
}
  • _c:createElement的简写,用于创建虚拟节点
  • _v:创建文本节点
  • _s:将值转换为字符串
5. 生成虚拟DOM
const vnode = vm._render()  // 执行render函数生成虚拟节点
  • 生成轻量级的JS对象描述DOM结构
  • 包含标签名、属性、子节点等信息
6. 挂载真实DOM
vm._update(vnode)  // 将虚拟DOM转化为真实DOM
  • patch过程:对比新旧虚拟DOM(首次挂载时直接创建)
  • DOM操作通过createElm等原生方法创建实际节点

三、挂载方法差异

1. 自动挂载
new Vue({
  el: '#app'  // 自动触发$mount('#app')
})
2. 手动挂载
const vm = new Vue({...})
vm.$mount('#app')  // 或 vm.$mount(document.getElementById('app'))
3. 运行时版本限制

运行时版(vue.runtime.js)无法直接编译模板,需使用预编译后的 render 函数。

// 需要预编译模板(避免运行时编译)
new Vue({
  render: h => h(App)  // 直接提供渲染函数
})
问题
  • Vue 实例挂载的目标(el)必须满足哪些条件?
  • 如果指定的 el 元素不存在,会发生什么?
  • 如何动态挂载 Vue 实例?
关键点
  • el 必须是一个已存在的 DOM 元素或 CSS 选择器。
  • 如果 el 不存在,Vue 会创建一个空的 DOM 元素作为挂载点。

四、性能优化策略

问题
  • Vue 的挂载过程如何影响性能?
  • 有哪些优化挂载过程的方法?
  • 如何避免挂载过程中出现白屏问题?
关键点
  • 使用 v-cloak 避免模板渲染前的白屏:

    [v-cloak] {
      display: none;
    }
    
  • 优化挂载性能:

    • 减少初始数据体积,或者冻结非响应式数据

    • 延迟挂载不必要的组件(按需加载)。

    • 使用预渲染或服务端渲染(SSR)。

    • 延迟挂载
      setTimeout(() => {
        vm.$mount('#app')  // 在关键内容加载后挂载
      }, 2000)
      

五、常见问题排查

问题
  • 如果挂载后视图未正常渲染,你会如何排查问题?
  • 在项目中是否遇到过挂载相关的问题?如何解决?
  • 如何挂载多个 Vue 实例到不同的 DOM 节点?
关键点
  • 排查挂载问题:

    • 检查 el 是否存在。
    • 确认 data 或模板中是否有语法错误。
    • 是否自动化或者手动挂载了
  • 多实例挂载:

    new Vue({ el: '#app1' });
    new Vue({ el: '#app2' });
    

六、 Vue 3 的挂载变化

问题
  • Vue 3 的挂载过程与 Vue 2 有何不同?
  • Vue 3 的 createApp 方法如何取代 Vue 2 的 new Vue
  • Vue 3 是否支持多个应用实例共存?
关键点
  • Vue 3 使用 createApp 创建实例:

    import { createApp } from 'vue';
    const app = createApp(App);
    app.mount('#app');
    
  • 支持多个应用实例共存,实例之间彼此独立


七、 自定义指令与挂载的关系

问题
  • 自定义指令(如 v-focus)如何与 Vue 挂载过程关联?
  • 在自定义指令中,哪个生命周期适合操作 DOM?
关键点
  • 自定义指令的生命周期(Vue 2 和 3):

    • bind/beforeMount:绑定时触发。
    • inserted/mounted:元素插入 DOM 后触发,适合操作 DOM。

八、 开放性问题

问题
  • 如何在挂载过程中注入异步数据?

    异步数据注入:优先在 created/ 路由守卫提前请求,Vue3 用 Suspense 更优雅;

  • Vue 的挂载机制是否有改进空间?你会如何设计?

关键点
  • 展现候选人对 Vue 框架的深度理解,以及解决问题的能力。

面试总结

通过考察 Vue实例挂载,可以了解候选人对 Vue 生命周期、响应式系统、模板编译、性能优化等核心机制的掌握情况,特别是在实际项目中如何应用这些知识以解决问题。优秀的候选人应具备扎实的理论基础,并能结合实际经验深入阐述。

为什么 vue2中能在methods,watch,等中访问全局的data数据

methods 能访问 data 数据的本质是:Vue 主动将 data 代理到组件实例,并将 methods 方法的 this 绑定到实例,二者通过 this 形成了 “访问链路”。

Vue3 相比 Vue2 有哪些核心改进?⭐️⭐️

    • 响应式系统重构:用 Proxy 替代 Object.defineProperty,支持监听数组索引、对象新增 / 删除属性。
    • Composition API:替代 Options API,按逻辑组织代码,解决复杂组件的 “逻辑碎片化” 问题。Vue3 的 Composition API 允许将相关逻辑封装到函数中,大幅提高代码复用性
    • 性能优化:渲染速度提升 55%+,内存占用减少 54%,得益于重写的虚拟 DOM 和编译时优化(静态节点标记)
    • 更小的体积:支持 Tree-shaking,未使用的功能会被打包工具剔除。生命周期、响应式 API 等均可按需导入,未使用的功能不会被打包(Tree-shaking 支持)
    • 更好的 TypeScript 支持:源码用 TS 重写,原生支持类型推导,开发体验更优
    • 新增特性:Teleport(组件瞬移)、Suspense(异步加载)、生命周期钩子变化多根节点模板等。

Vue3.0性能提升主要是通过哪几方面体现的?⭐️⭐️

  1. 响应式系统优化 用 Proxy 替代 Vue2 的 Object.defineProperty

    • 原生支持监听对象新增 / 删除属性、数组索引变化,无需手动调用 Vue.set,减少额外代码开销。
    • 只在访问属性时才递归触发响应式(懒代理),避免 Vue2 中初始化时对所有属性递归劫持的性能损耗,尤其适合大对象。
  2. 编译阶段优化 编译器对模板进行静态分析,生成更高效的渲染代码:Vue3 的渲染速度比 Vue2 提升约 55%,

    • 静态节点标记:识别并标记纯静态内容(如固定文本),运行时跳过比对,直接复用。
    • 动态节点区块化:将模板拆分为 “区块”,每个区块只包含动态节点,更新时仅遍历动态部分,减少虚拟 DOM 比对范围。
    • 事件缓存对无内联表达式的事件(如 @click="handleClick")缓存函数引用,避免每次渲染创建新函数导致的虚假更新。
  3. 体积优化(Tree-shaking 支持) 采用 ES 模块语法,支持 Tree-shaking:

    • 只打包项目中实际使用的 API(如未用 Teleport 则不打包相关代码),核心库体积比 Vue2 减少约 40%。

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

Vue3 在编译阶段的改进主要围绕提升渲染性能优化代码体积展开,通过对模板的静态分析和编译优化,减少运行时的计算开销。以下是核心改进:

1. 静态节点标记与复用
  • Vue2无论节点是否静态(内容不变),每次更新都会参与虚拟 DOM 的比对,产生不必要的性能消耗。
  • Vue3:编译时会标记静态节点(如纯文本、无动态绑定的元素),生成 _hoisted_ 前缀的常量,运行时直接复用这些节点,跳过比对过程。例:<div>静态文本</div> 会被标记为静态节点,更新时不重新渲染
2. 动态节点区块化(Block Tree)
  • Vue2:虚拟 DOM 比对时需遍历整个节点树,效率低
  • Vue3:编译时将模板拆分为 “区块(Block)”,每个区块内只包含动态节点(有 v-bindv-if 等动态绑定的节点)。运行时更新时,只需遍历区块内的动态节点无需处理整个树,大幅减少比对次数。例:一个包含多个静态元素和少量动态数据的列表,只会对动态数据所在节点进行比对。
3. 按需生成代码(Tree-shaking 友好)
  • Vue2:编译器会生成固定结构的渲染函数,包含所有可能用到的运行时 API,即使某些功能未使用也会打包。
  • Vue3:编译时根据模板中使用的功能(如是否有 v-forv-model 等)按需生成代码,未使用的功能相关代码会被 Tree-shaking 剔除,减小打包体积。
4. 静态提升(Static Hoisting)
  • 对于多次出现的静态内容(如重复的静态文本、样式),Vue3 编译时会将其提升为全局常量,避免在每次渲染时重复创建,减少内存占用。
5. 事件缓存(Event Caching)
  • Vue2:模板中的事件处理函数(如 @click="handleClick")每次渲染都会创建新的函数引用,导致虚拟 DOM 认为节点变化,触发不必要的更新。
  • Vue3:编译时对无内联表达式的事件(如 @click="handleClick")进行缓存,复用同一个函数引用,避免虚假更新。
总结

Vue3 编译阶段的核心改进是 “编译时优化运行时”:通过静态分析模板,标记静态内容、隔离动态节点、缓存重复逻辑,让运行时的虚拟 DOM 比对和渲染过程更高效,最终实现性能提升(渲染速度提升 55%+)和体积优化。

Vue2的diff算法 🌟🌟🌟

Vue 的 Diff 算法(差异算法)是虚拟 DOM 的核心优化机制,用于高效更新真实 DOM

其核心思想是 通过对比新旧虚拟 DOM 树的差异,找到最小变更并批量更新


一、Diff 算法的核心原理

1. 同层比较

Vue 的 Diff 算法仅在同层级节点之间比较,深度优先,不会跨层级递归。如果发现节点层级变化(如父节点不同),则直接销毁旧节点并创建新节点。

image.png

2. 双端比较策略

在对比子节点时,Vue 采用 双端指针(头尾指针) 策略,通过四次快速对比减少遍历次数

  1. 头头对比:新旧头节点对比。
  2. 尾尾对比:新旧尾节点对比。
  3. 旧头与新尾对比:若匹配,将旧头节点移动到尾部。
  4. 旧尾与新头对比:若匹配,将旧尾节点移动到头部。

比较的过程中,循环从两边向中间收拢 具体详情见pdf文档 image.png

3. Key标识节点

通过 key 复用相同节点, 避免不必要的销毁和重建


二、Diff 算法的具体步骤

1. 节点类型不同

若新旧节点标签名不同(如 div vs span,直接替换整个节点及其子树

// 伪代码
if (oldVnode.tag !== newVnode.tag) {
  replaceNode(oldVnode, newVnode);
}
2. 节点类型相同

若标签名相同,比较属性和子节点差异

2.1 属性更新

对比新旧节点的属性差异(如 classstyle),仅更新变化的属性。

// 伪代码
patchProps(oldVnode.props, newVnode.props);
2.2 子节点对比
  • 无子节点直接清空旧节点的子节点
  • 有子节点:进入双端对比流程。

关键问题与最佳实践

1. 为什么 Diff 算法是 O(n) 复杂度?
  • 通过同层比较和双端策略,避免 O(n³) 的传统树差异算法。
2. 何时会触发全量替换?
  • 新旧节点类型不同(如 div 变为 span)。
  • 跨层移动节点(如将子节点提升为父节点)。

总结:Vue Diff 的核心逻辑

步骤操作优化目的
同层比较仅对比同一层级的节点减少递归深度
双端对比头头、尾尾、旧头新尾、旧尾新头四次匹配快速定位可复用节点
Key 标识通过唯一 Key 识别节点避免错误复用
静态标记(Vue3)跳过静态节点对比减少遍历范围

通过 Diff 算法,Vue 在保证性能的同时,最小化真实 DOM 操作,这也是其高效渲染的核心原因。


Vue 2 vs Vue 3 的 Diff 优化 🌟🌟

1. Vue 2 的 Diff 特点
  • 全量对比所有子节点。
  • 依赖 key 标识节点复用
  • 性能瓶颈在长列表更新
2. Vue 3 的 Diff 优化
  • 静态标记(Patch Flags):通过标记静态节点,跳过未变化的节点对比。
  • Block Tree将动态节点提取为区块,减少遍历范围
  • 最长递增子序列(LIS):优化移动节点的逻辑,减少 DOM 操作次数。
// Vue3 源码片段(简化)
const patchChildren = (n1, n2, container) => {
  if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    // 使用快速 Diff 算法(基于 LIS)
    patchKeyedChildren(n1, n2, container);
  }
};

vue2和vue3diff算法的区别是什么

你想了解 Vue2 和 Vue3 的 diff 算法核心区别,简单来说:Vue3 的 diff 算法通过 “静态标记 + 非全量对比” 大幅减少了对比次数,性能远优于 Vue2 的全量对比,下面用通俗的语言拆解核心差异:

一、核心差异对比

1. Vue2 的 diff 算法:全量对比,无区分
  • 核心逻辑:Vue2 采用 “双端比较法”,对新旧虚拟 DOM 树进行全量递归对比,不管节点是静态(如纯文本、固定标签)还是动态(如带 {{}}、v-bind),都会逐一对比;
  • 问题:即使是完全不会变化的静态节点(比如<div>首页</div>),也会被反复对比,造成性能浪费;
  • 对比单位:以 “节点” 为单位,对比时不区分节点类型,所有节点一视同仁。
2. Vue3 的 diff 算法:静态标记 + 按需对比
  • 核心优化:① 静态提升 + PatchFlags(补丁标记) :编译阶段就给节点打标记 —— 静态节点标记为HOISTED(提升),动态节点标记具体的动态类型(如TEXT(文本变化)、CLASS(类名变化)、PROPS(属性变化)等);② 非全量对比:diff 阶段只对比带 PatchFlags 的动态节点,完全跳过静态节点;③ 最长递增子序列:对比列表节点(v-for)时,用 “最长递增子序列” 算法减少 DOM 移动操作(Vue2 是简单的头尾对比,列表变动时 DOM 移动次数更多);
  • 对比单位:以 “动态节点” 为单位,只处理有变化可能的节点,静态节点直接复用,无需对比。

二、举个通俗例子

  • Vue2 场景:页面有 100 个节点,其中 90 个静态、10 个动态,diff 时会把 100 个节点全对比一遍;
  • Vue3 场景:编译时给 10 个动态节点打标记,diff 时只对比这 10 个,90 个静态节点直接跳过,对比次数骤减。

总结

  1. 核心优化点:Vue3 新增 PatchFlags 静态标记,只对比动态节点,Vue2 无标记、全量对比;
  2. 列表对比优化:Vue3 用最长递增子序列减少 DOM 移动,Vue2 列表对比效率较低;
  3. 性能结果:Vue3 diff 算法在大型列表 / 复杂页面中,对比次数减少约 90%,渲染性能提升显著。

三、使用场景与表现差异

  • Vue2 在组件层级不深、节点结构稳定时表现良好;

  • Vue3 在结构复杂、更新频繁的页面中性能更优,尤其适合:

    • 动态长列表;
    • 表格拖拽重排;
    • Fragment、Teleport 场景。

四、常见误区

  • ❌ 认为 Vue2 与 Vue3 的 Diff 算法本质一样(Vue3 是全新设计);
  • ❌ 误以为 Vue3 不再需要 key(实际上依然推荐使用 key 明确标识);
  • ❌ 忽视了编译优化与运行时性能的协同配合;
  • ❌ 把 PatchFlag 误认为是手动添加的,实际是 Vue 编译器自动生成;

答题要点

  • 说明 Diff 的本质作用及常见策略;
  • 明确 Vue2 和 Vue3 在 Diff 实现上的关键区别;
  • 能结合 PatchFlagLIS 等术语深入讲解 Vue3 的优化点;
  • 补充对比场景与实际开发性能收益;

Vue3中reactive 与ref 的区别?为何需要ref包装基本类型 ⭐️⭐️

考察点

  • 理解 Vue3 响应式核心API reactive 和 ref 的区别与设计初衷
  • 掌握响应式数据的实现机制及底层原理
  • 理解基本类型响应式的特殊处理原因及解决方案
  • 能结合实际项目场景,说明两者的使用时机和注意点

参考答案

一、reactive() 与 ref() 的核心区别
  • reactive()

    • 用于将一个对象(包括数组、普通对象)包装成响应式对象,会将对象的所有嵌套属性递归转换为响应式(深层响应式)。 通过 Proxy 实现深度响应式,拦截所有属性访问和修改
    • 访问属性时直接操作响应式对象,无需 .value 访问
    • 不支持基本类型(如字符串、数字、布尔)作为参数,传入基本类型会原样返回,不具备响应式
    • 不能直接赋值为新对象(会丢失响应性,需用 Object.assign 更新)reactive 直接解构会丢失响应性,推荐使用 toRefs 保留响应性
  • ref()

    • 用于包装单一基本类型或对象返回一个包含 .value 属性的响应式引用
    • 通过内部对象包装,实现对基本类型的响应式支持,对对象是浅层包裹,内部对象不会自动递归为响应式
    • 使用时需要通过 .value 访问或赋值
    • 对象类型作为参数时,ref 只做浅包装,不会递归转换为响应式对象,第一层才有响应式,传入对象时,内部会自动转为 reactive 代理
  • 底层实现差异

    • ref 是对值的响应式容器封装内部通过 Object.defineProperty 定义 .value并调用 reactive 对对象自动包裹
    • reactive 则是直接对目标对象进行 Proxy 包裹,返回一个代理对象,重写 get / set 等操作符;
    • 两者都基于 Vue 3 的响应式系统(effecttracktrigger),但入口形式和封装语义不同。

选择原则:基础类型用 ref,复杂对象用 reactive或统一用 ref(更简单)

二、为何需要 ref 包装基本类型
  • 基本类型(如字符串、数字、布尔值)是不可被 Proxy 直接代理的reactive 是基于 Proxy 的对象代理机制,无法监听基本类型的变化

  • 基本类型(数字、字符串、布尔值)是值类型,不是对象,无法被 Proxy 拦截,他是拦截对象的,也无法用 Object.defineProperty 拦截其本身

  • ref() 的解决方案是:将基本类型包装成一个单属性的对象 {value: 原始值} ,然后通过 Object.defineProperty 拦截这个 value 属性的 get/set,从而实现响应式。

补充说明
  • 模板中自动解包:在 Vue 模板中使用 ref 时,不需要写 .value(如 <div>{{ num }}</div>)—— Vue 编译器会自动识别 __v_isRef 标记,帮你读取 .value
  • 脚本中必须写 .value:在 <script setup> 或组合式 API 中,必须通过 .value 访问 / 修改,因为这是原生 JavaScript 代码,Vue 无法自动解包。
二、为何需要 ref 包装基本类型

答题要点

  • reactive 基于 Proxy 实现,深度代理对象,实现复杂对象响应式
  • ref 用于包装基本类型,实现响应式的包装对象,访问需用 .value
  • 基本类型无法用 reactive 代理,必须用 ref 包装
  • 组合式API中,ref 适合单值管理,reactive 适合对象管理
  • 设计目的是保证响应式系统能统一追踪和更新数据变化

Proxy 是如何递归地处理嵌套对象的?

  1. 核心机制:Proxy 本身无递归能力,Vue 3 采用**惰性递归(懒代理)**实现深度响应式 —— 仅在读取(get 拦截)嵌套对象属性时,才对该嵌套对象动态创建 Proxy 代理,而非初始化时一次性递归所有层级。

  2. 执行流程

    • 初始化仅代理根对象
    • 读取嵌套对象(如 obj.b)时,触发根 Proxy 的 get,检测到值为对象则递归调用 reactive() 创建嵌套对象的 Proxy;
    • 深层嵌套(如 obj.b.d)会重复上述逻辑,最终所有嵌套对象都被 Proxy 拦截。
  3. 性能优化

    • 惰性代理避免初始化时递归大对象的性能损耗;
    • 通过 WeakMap 缓存已代理对象,防止重复创建 Proxy
    • 提供 shallowReactive() 可关闭递归,仅代理根对象

什么是双向绑定?

什么是双向绑定

双向绑定(Two-way Data Binding)是数据模型(Model)和视图(View)之间可以实现自动同步:当数据变化时,视图自动更新;当用户操作视图(如输入内容)时,数据也会自动更新。 最常见的场景是表单输入(如 <input>),用户输入内容会直接修改数据,而数据的变化也会实时反映在输入框中。

双向绑定由三个重要部分构成

  • 数据层(Model): 应用的数据
  • 视图层(View): 应用的展示效果,各类UI组件
  • 业务逻辑层(ViewModel): 框架封装的核心,它负责将数据与视图关联起来

而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是“数据双向绑定”。

理解ViewModel

它的主要职责就是:

  • 数据变化后更新视图
  • 视图变化后更新数据

当然,它还有两个主要部分组成

  • 监听器(Observer): 对所有数据的属性进行监听
  • 解析器(Compiler): 利用 compile 对象解析模板页面。每解析一个表达式(非事件指令)都会创建一个对应的watcher对象, 并建立watcher 与 dep 的关系,complie 与 watcher 关系: 一对多的关系

双向绑定的核心是 数据劫持 + 发布-订阅模式,通过监听数据变化和视图操作,实现数据与视图的自动同步。现代框架(如 Vue、Angular)已内置此功能,

流程

  • 模板编译(Compile)
  • 数据劫持(Observer)
  • 发布的订阅(Dep)
  • 观察者(Watcher)

双向绑定的原理?🌟🌟🌟

Vue2 双向数据绑定基于 Object.defineProperty(),分 3 步

  1. 数据劫持:beforeCreate 后初始化响应式系统,通过 Object.defineProperty 对 data 对象的属性设置 getter/setter监听属性读写。这个过程发生Observe中。

  2. 依赖收集:编译模板时,为每个指令 / 插值表达式创建 Watcher,触发 getter 将 Watcher 存入 Dep依赖管理器)。这个过程发生在Compile中。

  3. 触发更新:数据变更时触发 setterDep 通知所有 WatcherWatcher 更新对应 DOM。

    注:由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher。

    graph TD
      A[数据对象] --> B[创建 Observer 实例]
      B --> C{属性类型}
      C -->|对象| D[递归劫持所有属性]
      C -->|数组| E[重写数组原型方法]
      D --> F[定义 getter/setter]
      F --> G[依赖收集Dep]
      G --> H[Watcher 触发更新]

image.png

双向绑定的核心原理

双向绑定基于以下两种技术实现:

1. 数据劫持(Data Observation)

通过拦截对数据的读写操作,监听数据变化。

  • Vue 2 使用 Object.defineProperty 只能遍历对象属性进行劫持。
  • Vue 3 改用 Proxy(更强大,支持监听数组和对象增删)。直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的
2. 发布-订阅模式(Pub-Sub)

当数据变化时,通知所有依赖该数据的视图进行更新。

  • 依赖收集:在数据读取时,记录哪些视图依赖于该数据。
  • 触发更新:在数据修改时,通知所有依赖的视图更新。

如何实现双向绑定

在 Vue 中,双向绑定通过 v-model 指令实现,其本质是语法糖: 双向数据绑定=单向数据绑定 + input 监听

<input v-model="message">
<!-- 等价于 -->
<input 
  :value="message" 
  @input="message = $event.target.value"
>

自定义组件中的 v-model,需要使用 model 选项或 prop + emit 组合

Vue 如何实现依赖收集?Watcher 和 Dep 的角色是什么?

一、先理解核心概念:Vue 依赖收集的本质

依赖收集是 Vue 响应式系统的核心,其本质是:当响应式数据被读取时,记录下 “谁在使用这个数据”(收集依赖);当数据被修改时,通知 “使用这个数据的人” 进行更新(触发更新)

二、Vue 2 中的依赖收集:Watcher & Dep

1. Dep(依赖管理器)

Dep 是 "Dependency" 的缩写,它的核心作用是管理一组 Watcher,本质是一个 “订阅者容器”:

  • 每个响应式数据(如 data 中的某个属性)都会对应一个 Dep 实例。
  • 提供 depend() 方法:收集当前活跃的 Watcher 到自己的订阅列表。
  • 提供 notify() 方法:当数据变化时,遍历订阅列表,通知所有 Watcher 执行更新。
2. Watcher(观察者)

Watcher 是 “依赖的载体”,每个需要响应数据变化的对象(组件、计算属性、watch 侦听器)都会对应一个 Watcher 实例:

  • 组件渲染 Watcher:每个组件实例对应一个,负责组件的重新渲染。
  • 计算属性 Watcher:懒执行,只有依赖变化且被访问时才更新。
  • watch 侦听器 Watcher:监听指定数据,触发自定义回调。

关键逻辑解释

  1. 初始化组件时,会创建一个组件渲染 Watcher,执行 get() 方法。
  2. get() 方法将当前 Watcher 赋值给 Dep.target(全局唯一),然后执行组件的渲染函数。
  3. 渲染函数中会读取响应式数据,触发数据的 get 方法,调用 dep.depend(),将 Dep.target(当前 Watcher)加入 Dep 的订阅列表。
  4. 当数据被修改时,触发 set 方法,调用 dep.notify(),遍历所有订阅的 Watcher,执行 update() 方法,重新执行渲染函数,更新视图。

在 Vue 3 中,如何通过 Effect 函数收集依赖?

Vue 3 抛弃了 Object.defineProperty,改用 Proxy 实现响应式,同时用 Effect 替代了 Vue 2 的 Watcher,核心逻辑更简洁。

1. 核心角色
  • Reactive:通过 Proxy 包装数据,拦截数据的读取(get)和修改(set)。
  • Effect:副作用函数,相当于 Vue 2 的 Watcher,代表 “依赖响应式数据的一段逻辑”(如组件渲染、watch 回调)。
  • Track(跟踪) :对应 Vue 2 的 dep.depend(),收集 Effect 到依赖映射表。
  • Trigger(触发) :对应 Vue 2 的 dep.notify(),触发依赖的 Effect 重新执行。
2. Vue 3 Effect 收集依赖的简化实现

核心逻辑解释

  1. 调用 effect(fn) 时,创建 ReactiveEffect 实例,执行 run() 方法。
  2. run() 方法将 activeEffect 设为当前 Effect,然后执行传入的 fn(如组件渲染函数)。
  3. fn 中访问响应式数据时,触发 Proxy 的 get 拦截器,调用 track(),将 activeEffect 加入该数据属性对应的 Effect 集合。
  4. 当数据被修改时,触发 Proxy 的 set 拦截器,调用 trigger(),遍历该属性对应的所有 Effect,执行 run() 方法,重新执行副作用函数(更新视图)。

关键补充

  • Vue 会对更新做优化:Vue 2 中 Watcher 的更新会被推入异步队列,批量执行;Vue 3 中 Effect 也会通过 queueEffect 实现异步批处理,避免频繁更新 DOM。
  • 组件级别的依赖隔离:每个组件的 Watcher/Effect 只收集自己渲染所需的数据依赖,数据变化时只会触发对应组件的更新,不会影响整个应用。

Vue 如何跟踪组件 / 视图对响应式数据的依赖

Vue 跟踪依赖的核心是 “精准关联”,核心机制如下:

1. 数据层面:为每个响应式属性绑定依赖容器
  • Vue 2:每个响应式属性对应一个 Dep 实例,存储所有依赖该属性的 Watcher。
  • Vue 3:通过 targetMap(WeakMap)建立 “对象 -> 属性 -> Effect 集合” 的映射,精准记录每个属性的依赖。
2. 组件层面:组件实例与 Watcher/Effect 绑定
  • 每个组件实例对应一个渲染 Watcher/Effect,该 Watcher/Effect 只负责当前组件的渲染。
  • 组件渲染时,所有被访问的响应式数据都会将该组件的 Watcher/Effect 加入自己的依赖列表。
  • 数据变化时,只会通知依赖该数据的组件 Watcher/Effect 执行更新,实现组件级别的精准更新
3. 依赖清理:避免无效依赖
  • Vue 2:Watcher 在更新前会清理旧的依赖(如组件卸载、数据不再使用时)。
  • Vue 3:Effect 会记录自己依赖的所有 dep,当 Effect 失效时(如组件卸载),会从所有 dep 中移除自己,避免内存泄漏和无效更新。

总结

  1. 核心角色:Vue 2 中 Dep 管理依赖列表、Watcher 代表依赖的逻辑;Vue 3 中用 track/trigger 替代 DepEffect 替代 Watcher,核心都是 “收集依赖 - 触发更新”。
  2. 依赖收集触发时机:组件初始化渲染时,执行渲染函数访问响应式数据,触发数据的读取拦截,将当前组件的 Watcher/Effect 加入依赖列表。
  3. 视图更新联动:数据修改触发写入拦截,通知所有依赖的 Watcher/Effect 重新执行渲染函数,通过 VNode 对比(patch)更新真实 DOM,且 Vue 会通过异步批处理优化更新性能。
  4. 跟踪机制:通过 “数据属性 -> 依赖容器 -> 组件 Watcher/Effect” 的精准映射,实现组件级别的依赖跟踪和更新,避免全局无效渲染。

Vue3 响应式系统的实现原理是什么?与 Vue2 有何不同?⭐️⭐️

Vue 3 的响应式系统主要包括以下几个核心概念:

  1. Proxy:使用 Proxy 对象来拦截对象的读取和修改操作,通过定义 get 和 set 方法来实现对数据变化的自动追踪。
  2. Reflect:通过 Reflect API 来实现对对象属性的读取和修改操作,提供了与 Object.defineProperty 类似的功能,但更加强大和灵活。
  3. activeEffect:一个全局变量,用于保存当前正在执行的 effect 函数,以便在追踪依赖时使用。
  4. ref:将普通值转换为响应式可变的 ref 对象,其值可以通过 .value 属性访问和修改。

原理

  • Vue2:基于 Object.defineProperty 劫持对象单个属性的 getter/setter,存在局限性:

    • 无法监听数组索引变化(如 arr[0] = 1
    • 无法监听对象新增 / 删除属性(需用 Vue.set/Vue.delete
  • Vue3:基于 Proxy 代理对象,直接拦截对象的读取、设置、删除等操作,解决了 Vue2 的局限性

    • 天然支持数组索引、对象新增 / 删除属性的监听。
    • 无需手动调用 set/delete,用法更自然。

总结

Vue2 基于 Object.defineProperty 的响应式机制,受限于其只能拦截「已定义属性」的特性,无法原生支持对象属性的新增 / 删除和数组索引的直接修改;而 Vue3 基于 Proxy,能够拦截对象 / 数组的所有操作行为(包括新增、删除、索引修改等),从根本上解决了 Vue2 的局限性,因此可以自然支持这些操作的监听。

Vue 2 中的响应式不能监控数组索引和对象新增/删除属性?

一、核心原因:Object.defineProperty 的先天局限性

Vue2 基于 Object.defineProperty 实现响应式,这个 API 只能对已存在的属性做劫持(添加 get/set 拦截),且对数组场景有性能和逻辑上的取舍,最终导致无法监听数组索引、对象新增 / 删除属性。

二、分场景拆解

1. 无法监听数组索引修改
  • 性能代价:若对数组每个索引都加 get/set,当数组长度很大(如几千条)时,遍历劫持会造成严重的性能损耗,Vue2 为了性能放弃了这种方式;

  • 逻辑适配:因此 Vue2 没有对数组索引进行拦截,而是通过重写数组原型上的方法(如 pushpopsplice 等)来监听数组的变更。但这种方式只能覆盖通过这些方法修改数组的场景直接修改索引(例如 arr[0] = 1 或 arr.length = 0)无法被拦截,因此不会触发响应式更新。

  • Vue3 的改进:Vue3 的 Proxy 可以直接代理数组,并且能拦截数组的索引赋值操作

    • 当通过索引修改数组元素时(例如 arr[0] = 1),Proxy 的 set 陷阱会被触发(数组的索引被视为属性名),Vue3 可以在此处检测到变更并通知更新。
    • 同时,Proxy 也能拦截数组的长度修改arr.length = 0)和原型方法(如 push)的调用,实现了对数组所有变更方式的统一监听。
2. 无法监听对象新增 / 删除属性
  • Object.defineProperty 只能劫持初始化时已存在的属性:组件 data 初始化时,Vue2 会遍历对象的已有属性并绑定 get/set
  • 新增属性时,没有为新属性绑定 get/set,自然无法监听;删除属性时,也没有对应的拦截机制触发响应式更新。

三、补充(Vue2 兜底方案)

  • 数组索引修改:用 Vue.set(arr, index, val) 或 splice
  • 对象新增 / 删除属性:新增用 Vue.set(obj, key, val),删除用 Vue.delete(obj, key)

核心总结

根源是 Object.defineProperty 只能劫持已有属性,且数组索引劫持性价比低;Vue3 改用 Proxy 可天然监听数组索引和对象新增 / 删除属性,解决了该问题。

vm.$set 原理

vm.$set 是 Vue 中用于在对象上设置属性并确保新属性是响应式的方法。其实现原理可以简化为以下几个步骤:

  1. 处理数组情况: 如果目标是数组,并且键是有效的数组索引,使用 splice 方法添加新元素以保持响应性。
  2. 处理已有属性: 如果属性已经存在于对象中,直接赋值。
  3. 处理新属性: 如果目标对象不是响应式对象,直接赋值新属性。
  4. 添加响应式新属性: 如果目标对象是响应式的,通过 defineReactive 方法将新属性定义为响应式。这包括定义 getter 和 setter。
  5. 通知依赖更新: 调用 ob.dep.notify() 通知所有依赖于该对象的 watchers 执行更新。

defineReactive 简要实现

defineReactive 方法定义对象属性为响应式,主要步骤:

  • 依赖管理: 创建一个 Dep 实例管理依赖。
  • 递归观察: 使用 observe 递归地将属性值转化为响应式。
  • 定义 getter 和 setter: 使用 Object.defineProperty 定义属性的 getter 和 setter。在 getter 中收集依赖,在 setter 中通知依赖更新。

总结

vm.$set 使得在运行时动态添加的新属性能够响应数据变化,从而保持 Vue 的响应式特性。

Proxy 如何避免了 Vue 2 中的深度递归问题?

一、先明确核心差异:Vue2 的 “预递归” vs Vue3 的 “懒代理

Vue2 用 Object.defineProperty 实现响应式时,会初始化就深度递归遍历对象所有层级的属性,逐个绑定 get/set;而 Vue3 的 Proxy 是惰性代理,只代理当前层级,访问深层属性时才动态代理,从根源避免了深度递归的性能问题。

二、Proxy 避免深度递归的具体逻辑

1. Vue2 的问题(深度递归开销大)
// Vue2 伪代码:初始化就递归所有层级
function defineReactive(obj) {
  // 遍历当前层所有属性
  for (let key in obj) {
    let val = obj[key];
    // 若属性值是对象/数组,递归劫持(性能瓶颈)
    if (typeof val === 'object' && val !== null) {
      defineReactive(val); 
    }
    // 为当前属性绑定 get/set
    Object.defineProperty(obj, key, {
      get() { /* 依赖收集 */ },
      set(newVal) { /* 触发更新 */ }
    });
  }
}
// 初始化就递归完整个对象,大对象时极慢
defineReactive(bigObj); 
2. Vue3 的解决(懒代理,按需递归)
// Vue3 伪代码:Proxy 惰性代理
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const val = target[key];
      // 只有访问深层属性时,才动态代理该层级(按需递归)
      if (typeof val === 'object' && val !== null) {
        return reactive(val); // 访问时才代理,而非初始化就递归
      }
      /* 依赖收集 */
      return val;
    },
    set(target, key, newVal) {
      /* 触发更新 */
      target[key] = newVal;
      return true;
    }
  });
}
// 初始化只代理第一层,无递归开销
const proxyObj = reactive(bigObj);
// 只有访问深层属性时,才代理对应层级
proxyObj.a.b.c; // 此时才代理 a → a.b → a.b.c

三、核心总结

  1. Vue2:初始化时全量深度递归,不管属性是否被访问,都提前绑定 get/set,大对象 / 深层级时性能差;
  2. Vue3:Proxy 实现惰性递归,仅在「访问某层级属性」时才代理该层级,未访问的深层属性不会被处理,彻底避免了初始化阶段的深度递归开销。

为什么 Vue 3 使用 Proxy 代替了 Object.defineProperty

你想知道 Vue 3 选择用 Proxy 替代 Object.defineProperty 的核心原因,本质上是为了解决后者在响应式系统设计上的先天缺陷,让响应式能力更完整、更高效。

核心原因解析

我会从「缺陷对比」的角度,把两者的核心差异讲清楚,你就能直观理解为什么要替换:

1. 监控范围的局限性(最核心原因)
  • Object.defineProperty:只能监听对象已有属性的「读取 / 修改」,且需要逐个属性遍历定义。

    • 无法监听新增属性(比如 obj.newKey = 123 不会触发响应);
    • 无法监听删除属性(比如 delete obj.key 不会触发响应);
    • 无法监听数组的原型方法(比如 arr.push()arr.splice()arr.length = 0 这类操作)。所以 Vue 2 不得不通过 Vue.set/this.$setVue.delete 等 API 手动触发响应,还需要重写数组的 7 个原型方法(push/pop/shift/unshift/splice/sort/reverse)来兼容,属于「补丁式」解决方案。
  • Proxy:直接代理整个对象,而非单个属性,能监听对象的「所有操作行为」:

    • 天然支持新增 / 删除属性的监听;
    • 天然支持数组的所有方法(push、splice、修改 length 等);
    • 无需逐个遍历属性,初始化性能更好。
2. 对嵌套对象的处理效率
  • Object.defineProperty:需要「深度递归」遍历对象的所有层级,初始化时就把所有嵌套属性都转为响应式,即使某些属性暂时用不到,也会提前消耗性能。
  • Proxy:可以实现「懒代理」—— 只有当访问到嵌套对象的属性时,才会递归为这个嵌套对象创建 Proxy,做到「按需响应式」,初始化性能更优。并支持更细粒度的依赖追踪。
3. 语法设计的灵活性
  • Object.defineProperty:本质是「修改对象属性的描述符」,只能拦截 get(读取)和 set(修改)两种行为,能力单一。
  • Proxy:支持 13 种拦截操作(比如 has 拦截 in 运算符、deleteProperty 拦截删除、apply 拦截函数调用等),能覆盖更复杂的响应式场景(比如监听 in 判断、函数调用),为 Vue 3 的响应式系统拓展了更多可能性。

(补充:Proxy 唯一的小缺点是不兼容 IE 浏览器,但 Vue 3 已放弃 IE 支持,这也让 Proxy 的使用无后顾之忧。)

Vue 3 中,get 和 set 方法如何拦截数据的读写?

你想了解 Vue 3 里响应式系统中 get 和 set 拦截方法是如何具体工作的,核心就是通过 Proxy 的这两个拦截器,在数据被读取时收集依赖、被修改时触发更新,从而实现响应式。

get 和 set 拦截数据读写的核心逻辑

Vue 3 的响应式底层基于 Proxy,其中 get 负责拦截数据读取set 负责拦截数据修改 / 新增,两者配合完成「依赖收集」和「触发更新」的核心流程,我用通俗的方式拆解给你:

1. get 方法:拦截「读取」,核心做「依赖收集」

当你访问响应式对象的属性(比如 obj.name)时,会触发 Proxy 的 get 拦截器,它的核心工作分两步:

  • 第一步:递归处理嵌套对象(懒代理)。如果读取的属性值是对象 / 数组,先把这个嵌套值也转为响应式(避免嵌套对象无法监听);
  • 第二步:收集当前的「依赖」。Vue 会记录 “哪个组件 / 副作用函数正在读取这个属性”,把这个依赖关系存到「依赖映射表」中(比如 target -> key -> effect)。
2. set 方法:拦截「修改 / 新增」,核心做「触发更新」

当你修改 / 新增响应式对象的属性(比如 obj.name = '新值' 或 obj.age = 18)时,会触发 Proxy 的 set 拦截器,核心工作分三步:

  • 第一步:判断值是否真的变化。如果新值和旧值完全一样(比如 obj.name = obj.name),直接返回,避免无效更新;
  • 第二步:更新目标对象的属性值。把新值赋值给原对象的对应属性;
  • 第三步:触发依赖更新。从「依赖映射表」中找到该属性对应的所有依赖(组件 / 副作用函数),逐个执行这些依赖,让页面 / 逻辑响应式更新。

关键细节补充

  • Reflect 的作用:Vue 3 没有直接用 target[key] 读写,而是用 Reflect.get/set,因为 Reflect 能保证操作的规范性(比如处理继承属性、严格遵循返回值规则),适配 Proxy 的拦截上下文;
  • 懒代理get 里只在读取嵌套对象时才转为响应式,而非初始化时递归所有层级,提升性能;
  • set 返回值:必须返回 true,否则在严格模式下会报错,这是 Proxy set 拦截器的规范要求。

总结

  1. get 拦截器:读取数据时,先收集 “谁在用这个数据”(依赖收集),再返回数据,同时处理嵌套对象的懒代理;
  2. set 拦截器:修改 / 新增数据时,先判断值是否真变化,再更新数据,最后通知 “用到这个数据的地方” 更新(触发更新);
  3. 两者配合是 Vue 3 响应式的核心,相比 Vue 2 的 Object.defineProperty能天然支持新增属性、嵌套对象等场景

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

Vue 的数据响应式系统和虚拟 DOM 的 diff 算法是两个不同的概念,解决的是不同的问题:

  1. 数据响应式:Vue 的响应式系统通过 Object.defineProperty(Vue 2.x)或 Proxy(Vue 3.x)实现,它使得数据的变化能够自动通知视图更新。这种响应式机制确保了数据和视图的一致性,但并不直接处理 DOM 的更新。
  2. 虚拟 DOM 和 diff 算法:虚拟 DOM 是 Vue 用来高效更新实际 DOM 的机制。每次数据发生变化时,Vue 会生成一个新的虚拟 DOM 树,并与上一次的虚拟 DOM 树进行比较(diffing)。这一步骤帮助 Vue 识别哪些部分的 DOM 需要更新,从而减少不必要的 DOM 操作,提高性能。

总结来说,数据响应式确保数据变化能够被捕获,而 diff 算法确保 DOM 的更新是高效的。两者配合使用,提升了 Vue 应用的性能和用户体验。

你有看过vue的源码吗?如果有那就说说看 🌟⭐️


Vue 源码核心模块解析

1. 响应式系统(Reactivity)
  • Vue 2(Object.defineProperty)
    src/core/observer 目录实现,核心流程:

    关键源码:

    // 对象属性劫持
    function defineReactive(obj, key) {
      const dep = new Dep()
      let val = obj[key]
      Object.defineProperty(obj, key, {
        get() {
          if (Dep.target) {
            dep.depend() // 收集当前 Watcher
          }
          return val
        },
        set(newVal) {
          val = newVal
          dep.notify() // 通知所有 Watcher
        }
      })
    }
    
  • Vue 3(Proxy)
    packages/reactivity 模块重构,实现更精细的依赖追踪

    const reactiveMap = new WeakMap()
    
    function reactive(target) {
      const existingProxy = reactiveMap.get(target)
      if (existingProxy) return existingProxy
    
      const proxy = new Proxy(target, {
        get(target, key, receiver) {
          track(target, key) // 追踪依赖
          return Reflect.get(target, key, receiver)
        },
        set(target, key, value, receiver) {
          const result = Reflect.set(target, key, value, receiver)
          trigger(target, key) // 触发更新
          return result
        }
      })
    
      reactiveMap.set(target, proxy)
      return proxy
    }
    
2. 虚拟 DOM(Virtual DOM)
  • 创建与 Diff 算法
    位于 src/core/vdom
    // 简化版 diff 逻辑
    function patch(oldVnode, vnode) {
      if (sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode)
      } else {
        // 替换整个节点
        const parent = oldVnode.parentNode
        parent.replaceChild(createElm(vnode), oldVnode)
      }
    }
    
    function patchVnode(oldVnode, vnode) {
      const elm = vnode.elm = oldVnode.elm
      const oldCh = oldVnode.children
      const ch = vnode.children
    
      if (!vnode.text) {
        if (oldCh && ch) {
          updateChildren(elm, oldCh, ch) // 核心 Diff 逻辑
        }
      } else if (oldVnode.text !== vnode.text) {
        elm.textContent = vnode.text
      }
    }
    
3. 组件系统
  • 组件初始化流程
    src/core/instance/init.js
    export function initMixin(Vue) {
      Vue.prototype._init = function(options) {
        const vm = this
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        )
        initLifecycle(vm)
        initEvents(vm)
        initRender(vm)
        callHook(vm, 'beforeCreate')
        initInjections(vm)
        initState(vm) // 初始化 props/data/methods
        initProvide(vm)
        callHook(vm, 'created')
    
        if (vm.$options.el) {
          vm.$mount(vm.$options.el) // 挂载组件
        }
      }
    }
    
4. 模板编译
  • 编译阶段
    src/compiler 目录实现三步转换:
    graph LR
      A[原始模板] --> B[解析器生成 AST]
      B --> C[优化器标记静态节点]
      C --> D[代码生成器输出渲染函数]
    
    关键优化代码:
    function markStatic(root) {
      root.children.forEach(node => {
        if (node.type === 1) { // 元素节点
          if (isStatic(node)) {
            node.static = true
          } else {
            markStatic(node)
          }
        }
      })
    }
    
5. 异步更新队列
  • 批量更新机制
    src/core/observer/scheduler.js
    const queue = []
    let waiting = false
    
    function queueWatcher(watcher) {
      if (!queue.includes(watcher)) {
        queue.push(watcher)
        if (!waiting) {
          waiting = true
          nextTick(flushSchedulerQueue)
        }
      }
    }
    
    function flushSchedulerQueue() {
      queue.sort((a, b) => a.id - b.id) // 保证父组件优先更新
      queue.forEach(watcher => watcher.run())
      resetSchedulerState()
    }
    
6. 性能优化设计
  • 静态节点提升(Vue 3)
    编译时标记静态内容,减少运行时比对:
    // 输入模板
    <div>
      <h1>Static Title</h1>
      <p>{{ dynamicText }}</p>
    </div>
    
    // 编译后代码
    const _hoisted_1 = /*#__PURE__*/_createVNode("h1", null, "Static Title")
    
    function render() {
      return (_openBlock(),
        _createBlock("div", null, [
          _hoisted_1,
          _createVNode("p", null, _toDisplayString(_ctx.dynamicText), 1 /* TEXT */)
        ])
      )
    }
    

源码学习建议

  1. 调试技巧

    # 克隆源码
    git clone https://github.com/vuejs/core.git
    # 安装依赖
    pnpm install
    # 构建开发版本
    pnpm run dev
    

    通过 examples 目录创建测试用例逐步调试

  2. 关键断点位置

    • 响应式追踪:reactivity/src/effect.ts 第 170 行
    • 虚拟 DOM 创建:runtime-core/src/vnode.ts 第 156 行
    • 组件挂载:runtime-core/src/renderer.ts 第 1350 行

总结

Vue 源码通过以下设计实现高效开发:

  • 响应式系统:自动追踪依赖关系
  • 虚拟 DOM:最小化 DOM 操作成本
  • 组件化架构:高内聚低耦合的代码组织
  • 编译优化:在构建时提前优化运行时性能

深入源码可帮助开发者:

  1. 精准定位复杂问题
  2. 编写高性能 Vue 代码
  3. 定制特殊功能(如自定义指令)
  4. 理解框架设计哲学(如渐进式理念)

建议结合官方源码解析文档(如 Vue Mastery 的源码课程)系统学习,逐步掌握各模块协作机制。

什么是render函数吗?有什么好处? 🌟

在 Vue.js 中,Render 函数是用于生成虚拟 DOM(Virtual DOM)的底层 JavaScript 函数它是 Vue 模板的替代方案允许开发者通过编程方式直接描述组件的视图结构,而不是依赖模板语法。

一、Render 函数的核心概念

1. 基本定义
  • 作用:Render 函数接收一个 createElement 方法(通常简写为 h,通过 JavaScript 代码动态创建虚拟 DOM 节点
  • 语法
    render(h) {
      return h('div', { attrs: { id: 'app' } }, 'Hello World');
    }
    
    等效于模板:
    <div id="app">Hello World</div>
    
2. 与模板的关系
  • 模板的本质:Vue 模板在编译阶段会被转换为 Render 函数(通过 vue-template-compiler)。
  • 直接使用 Render 函数跳过模板编译步骤,直接控制虚拟 DOM 的生成

二、Render 函数的优势

1. 更高的灵活性
  • 动态生成结构:根据条件或数据动态创建任意层级的 DOM 结构,无需模板中的 v-if/v-for
    render(h) {
      return h('div', 
        this.items.map(item => 
          h('p', { key: item.id }, item.text)
        )
      );
    }
    
  • JSX 支持:结合 JSX 语法,编写更直观的嵌套结构(需配置 Babel 插件)
    render() {
      return (
        <div>
          {this.items.map(item => <p key={item.id}>{item.text}</p>)}
        </div>
      );
    }
    
2. 更高的性能
  • 性能优化:直接操作虚拟 DOM,避免模板编译的开销(在复杂场景下可能更高效)。
  • 细粒度控制:手动优化渲染逻辑(如跳过不必要的子组件更新)。
3. 跨文件类型支持
  • 无单文件组件依赖:可在纯 JavaScript 文件中定义组件,无需 .vue 文件
    // 纯 JS 文件
    export default {
      render(h) {
        return h('div', 'This component has no template');
      }
    };
    
4. 解决模板的局限性
  • 动态组件名/属性名:灵活处理动态标签或属性名。
    render(h) {
      return h(this.dynamicTag, { class: this.dynamicClass }, '内容');
    }
    
  • 复杂逻辑渲染:处理模板中难以表达的嵌套逻辑(如递归组件)。

三、Render 函数的使用场景

1. 高阶组件(HOC)

封装通用逻辑,动态生成组件结构

function withLoading(WrappedComponent) {
  return {
    render(h) {
      return h(WrappedComponent, {
        props: { ...this.$props, isLoading: this.loading }
      });
    }
  };
}
2. 动态路由或布局

根据权限或配置生成不同的页面结构:

render(h) {
  return h(this.currentLayout, {}, this.$slots.default);
}
3. 可视化工具开发

动态渲染流程图、思维导图等复杂嵌套结构:

render(h) {
  return h('svg', {}, this.nodes.map(node => 
    h('circle', { attrs: { cx: node.x, cy: node.y, r: 10 } })
  ));
}

四、Render 函数 vs 模板

维度模板Render 函数
学习成本低(类 HTML 语法)高(需熟悉 JavaScript 和虚拟 DOM)
灵活性受限(需遵循模板语法规则)极高(完全编程控制)
性能大部分场景足够高效可手动优化复杂场景
适用场景常规业务组件(90% 场景)高阶组件、动态结构
可维护性直观易读逻辑复杂时可能降低可读性

五、总结

何时使用 Render 函数?
  • 需要动态生成复杂组件结构(如递归、动态标签)。
  • 开发高阶组件或通用库(如 UI 组件库)。
  • 需要极致性能优化(如大数据量列表渲染)。
  • TypeScriptJSX 深度集成。
建议
  • 优先使用模板:对大多数业务场景,模板更直观且易于维护
  • 必要时切换 Render 函数:当模板无法简洁实现功能时,再使用 Render 函数。
  • 结合 JSX通过 JSX 提升 Render 函数的可读性

说说你对vue的template编译的理解?🌟

一、模板编译的三个核心阶段

1. 解析阶段(Parse)
  • 输入:模板字符串(如 <div>{{msg}}</div>
  • 输出:抽象语法树(AST)
  • 核心过程
    • 词法分析:通过正则表达式拆分模板字符串为 Token(标签、属性、文本等)
    • 语法分析构建嵌套的 AST 节点树,记录标签层级关系
  • 示例转换
    <!-- 模板 -->
    <div class="container">
      <span v-if="show">{{ message }}</span>
    </div>
    
    → 解析为 AST:
    {
      type: 1, // 元素节点
      tag: 'div',
      attrsList: [{ name: 'class', value: 'container' }],
      children: [
        {
          type: 1,
          tag: 'span',
          if: 'show', // 指令解析
          children: [{
            type: 2, // 表达式节点
            expression: '_s(message)' 
          }]
        }
      ]
    }
    
2. 优化阶段(Optimize)
  • 输入:原始 AST
  • 输出:优化后的 AST(标记静态节点
  • 优化策略
    • 静态节点标记识别无动态绑定的节点(如纯文本 <div>Static Text</div>
    • 静态子树提升:将静态内容提取为常量,避免重复渲染
  • 优化效果
    • 减少 40% 虚拟 DOM 比对运算(Vue 3 中静态提升更激进)
    • 运行时直接复用静态节点对应的 DOM
3. 代码生成(Codegen)
  • 输入:优化后的 AST
  • 输出:渲染函数字符串(可执行的 JavaScript 代码)
  • 生成逻辑
    • 递归遍历 AST,生成 _c(createElement)调用链
    • 处理指令(v-if → 条件表达式、v-for → 循环语句)
  • 生成结果示例
    function render() {
      return _c('div', { class: 'container' }, [
        (show) ? _c('span', [_v(_s(message))]) : _e()
      ])
    }
    

三、编译过程的工程化实现

1. 构建时编译(推荐方案)
  • 工具链vue-loader + @vue/compiler-sfc
  • 编译流程
    .vue 文件 → vue-loader → 提取 template → compiler-core → 生成 render 函数
    
  • 优势:生产环境直接使用预编译代码,减少 30% 运行时开销
2. 运行时编译(开发环境)
  • 使用场景:直接在浏览器中编译模板(需引入完整版 Vue)
  • 代码示例
    <script src="vue.global.js"></script>
    <div id="app">{{ message }}</div>
    <script>
      Vue.createApp({
        data() { return { message: 'Hello' } }
      }).mount('#app')
    </script>
    

五、编译结果的实际应用

1. 渲染函数执行流程
graph LR
  A[模板] --> B[AST]
  B --> C[优化后AST]
  C --> D[渲染函数]
  D --> E[虚拟DOM]
  E --> F[Patch/Diff]
  F --> G[真实DOM更新]
2. 与响应式系统联动
  • 依赖收集:渲染函数执行时触发 getter,建立数据-视图关联
  • 精准更新:数据变更后通过 patchFlag 快速定位变化点

说说你对JSX的理解

在 Vue 中 JSX 是允许在 JavaScript 代码中直接编写类似 HTML 的标记。其核心本质是JavaScript 的语法扩展

一、JSX 的本质

  1. 语法糖
    JSX 会被编译为 h() 函数(即 createElement,生成虚拟 DOM 节点。例如:

    const element = <div>Hello Vue</div>;
    // 编译后:
    const element = h('div', {}, 'Hello Vue');
    
  2. 动态能力
    可直接在标签内嵌入 JavaScript 表达式:

    <button onClick={handleClick}>
      {isLoading ? '加载中...' : '提交'}
    </button>
    

二、Vue 中 JSX 与模板的对比

特性JSX模板语法
灵活性高(可使用完整 JS 逻辑)中(受限于指令系统)
学习成本较高(需熟悉 JSX 语法)低(类 HTML,易上手)
性能优化依赖手动优化编译时自动优化(如静态提升)
代码组织逻辑与模板混合编写逻辑与模板分离
适用场景复杂动态组件、高阶组件常规业务组件、布局

三、Vue JSX 核心用法

export default {
  render() {
    return <div class="container">{this.message}</div>;
  },
  data() {
    return { message: 'Hello Vue JSX' };
  }
};

五、性能优化策略

  1. 避免内联函数

    // ❌ 每次渲染创建新函数
    <button onClick={() => this.handleClick(id)} />
    
    // ✅ 提前绑定
    <button onClick={this.handleClick.bind(this, id)} />
    
  2. 合理使用 Fragment

    render() {
      return (
        <>
          <Header />
          <MainContent />
        </>
      );
    }
    
  3. Memo 优化

    const MemoComponent = Vue.memo(({ data }) => (
      <ExpensiveComponent data={data} />
    ));
    

六、工程化配置

  1. Babel 插件
    安装 @vue/babel-plugin-jsx

    npm install @vue/babel-plugin-jsx -D
    
  2. TypeScript 支持
    配置 tsconfig.json

    {
      "compilerOptions": {
        "jsx": "preserve",
        "jsxFactory": "h"
      }
    }
    

七、适用场景分析

  1. 动态表单生成器
    需要递归渲染复杂表单结构时,JSX 的编程能力更高效。

  2. 可视化拖拽编辑器
    动态节点操作与复杂状态管理更易实现。

  3. 高阶组件(HOC)
    通过函数式组合生成增强组件。

  4. 跨平台渲染
    结合 @vue/runtime-core 实现自定义渲染器(如 Canvas、小程序)。


总结

JSX 在 Vue 中突破了模板语法的限制,为复杂场景提供了更强的编程能力,灵活处理动态渲染逻辑 。但需权衡: 损失模板编译时的自动优化

建议在需要高度动态化复杂逻辑处理时使用 JSX,常规业务场景仍优先考虑模板语法以获得更好的性能与可维护性

Vue中key的原理是什么?请详细的说一说 🌟🌟

总结: key是给每一个vnode的唯—id,也是diff的一种优化策略,可以根据key,更准确,更快的找到对应的vnode节点,识帮助Vue高效更新DOM。

在Vue中,key的作用与虚拟DOM的Diff算法密切相关,它通过唯一标识帮助Vue高效更新DOM,同时确保组件状态的正确性。以下是其核心原理和作用的详细说明:可见设置key能够大大减少对页面的DOM操作,提高了diff效率


1. 虚拟DOM与Diff算法

Vue通过虚拟DOM实现高效的DOM更新。当数据变化时,Vue会生成新的虚拟DOM树,并与旧的树进行对比(Diff算法),找出差异并最小化真实DOM操作。

  • Diff策略:Vue的Diff算法采用同层比较,不会跨层级对比节点。对于同一父节点下的子节点,Vue默认采用就地更新策略:直接复用旧节点,更新属性,而不是移动节点。

2. 没有key时的性能问题

若列表元素没有key,Vue在对比子节点时,会按照顺序比对

<!-- 旧节点 -->
<div>A</div>
<div>B</div>
<div>C</div>

<!-- 新节点 -->
<div>B</div>  <!-- 对比位置0:A → B,更新内容 -->
<div>C</div>  <!-- 对比位置1:B → C,更新内容 -->
<div>D</div>  <!-- 对比位置2:C → D,更新内容 -->
  • 问题:所有节点都会被重新渲染,即使BC只是位置变化。若节点包含状态(如表单输入),会导致状态错乱。

3. key的作用

通过key,Vue能识别节点的唯一性,从而更智能地复用和移动节点:

<!-- 旧节点(key为唯一ID) -->
<div key="1">A</div>
<div key="2">B</div>
<div key="3">C</div>

<!-- 新节点 -->
<div key="2">B</div>  <!-- 找到旧节点key=2,移动至位置0 -->
<div key="3">C</div>  <!-- 找到旧节点key=3,移动至位置1 -->
<div key="4">D</div>  <!-- 新建节点 -->
  • 优化点
    • 复用BC的DOM,仅移动位置。
    • 仅新建D节点,避免不必要的渲染。

4. key与组件状态

当节点为组件时,key决定了组件实例是否复用:

  • 相同key:复用组件实例,触发更新(updated生命周期)。
  • 不同key:销毁旧实例,创建新实例(触发createdmounted)。
<!-- 切换组件时,不同key会强制重新创建实例 -->
<component :is="currentComponent" :key="currentComponent" />

最佳实践

  • 唯一且稳定:使用数据中的唯一标识(如id)作为key

  • 避免随机数:如:key="Math.random()"会导致频繁销毁/重建组件。

  • 静态列表:若无状态变化,可使用索引,但需谨慎。在这种情况下,设置 key 不会带来性能提升,反而可能增加额外的计算开销。

总结

设置 key 值能否提高 diff 效率取决于具体的使用场景。使用唯一且稳定的 key 可以在列表动态变化时提高效率,但如果 key 不稳定或者列表是静态的,设置 key 可能不会带来性能提升,甚至会降低效率

Vue中的$nextTick是什么?有什么作用?底层实现原理?使用场景分别是什么? 🌟🌟

1. 什么是$nextTick

$nextTick是Vue提供的一个实例方法,用于用于在数据更新后,延迟执行回调函数
当数据发生变化时,Vue会异步更新DOM,$nextTick允许你在DOM更新完成后立即执行逻辑

我们可以理解成,Vue在更新DOM时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新


2. 作用

  • 确保操作基于最新的DOM:在数据变化后操作DOM时,避免因DOM未更新导致获取旧状态。
  • 处理异步更新:Vue的响应式更新是异步的,$nextTick提供了一种方式在更新完成后执行代码。

image.png


3. 底层实现原理

3.1 异步更新队列
  • Vue在数据变化时不会立即更新DOM,而是将组件更新函数推入一个队列(异步任务队列)
  • 同一事件循环中多次修改数据只会合并一次DOM更新,减少重复渲染
  • 多个 $nextTick 调用的执行顺序: 如果在同一个事件循环中调用多个 $nextTick,它们会被推到同一个队列中,依次执行。Vue 会在下次 DOM 更新完成后,按顺序依次执行所有的 $nextTick 回调。
3.2 实现机制

Vue通过以下步骤实现$nextTick

  1. 微任务优先:优先使用微任务(Microtask)实现异步延迟
    • 支持Promise时,用Promise.then()
    • 否则降级到MutationObserversetImmediate
  2. 宏任务兜底:在不支持微任务的环境下,使用宏任务(Macrotask)如setTimeout(fn, 0)
3.3 源码简析(Vue 2.x)
// 源码位置:src/core/util/next-tick.js

const callbacks = []; // 回调队列
let pending = false; // 标记是否已向任务队列添加任务

// 执行所有回调
function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

// 选择最优的异步方案
let timerFunc;
if (typeof Promise !== 'undefined') {
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);
  };
} else if (typeof MutationObserver !== 'undefined') {
  // 使用MutationObserver模拟微任务
} else {
  // 降级到setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

// 对外暴露的nextTick方法
export function nextTick(cb?: Function, ctx?: Object) {
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick');
      }
    }
  });
  if (!pending) {
    pending = true;
    timerFunc();
  }
}

4. 使用场景

4.1 操作更新后的DOM
// 修改数据后立即获取DOM状态
this.message = '新消息';
this.$nextTick(() => {
  const element = document.getElementById('message');
  console.log(element.textContent); // 正确获取更新后的内容
});
4.2 与第三方库集成

当使用依赖DOM状态的库(如图表库D3.js)时,需确保DOM更新后再操作:

this.chartData = newData;
this.$nextTick(() => {
  this.renderChart(); // 确保DOM更新后渲染图表
});
4.3 组件更新后操作子组件

父组件修改子组件数据后,等待子组件更新:

// 父组件中
this.$refs.child.someData = '新值';
this.$nextTick(() => {
  this.$refs.child.doSomething(); // 子组件已更新
});
4.4 解决v-if切换后的DOM访问

v-if条件变化后访问新生成的元素:

this.showElement = true;
this.$nextTick(() => {
  this.$refs.newElement.focus(); // 确保元素已渲染
});

5. 注意事项

  • 避免过度使用:频繁调用$nextTick可能影响性能。
  • setTimeout的区别$nextTick使用微任务,比setTimeout更早执行。
  • Vue 3中的变化Vue 3的nextTick直接返回Promise,支持async/await
    await Vue.nextTick();
    // DOM已更新
    

总结

核心点说明
作用确保回调在DOM更新后执行,避免操作旧DOM
实现原理利用微任务(Promise/MutationObserver)或宏任务(setTimeout)实现异步队列
典型场景操作更新后的DOM、集成第三方库、处理组件间异步更新。
性能优化优先使用微任务减少延迟,合并多次数据变更的DOM更新

通过合理使用$nextTick,可以解决Vue中因异步更新导致的DOM操作问题,确保代码逻辑的正确性和可靠性。

为什么vue使用异步更新组件?🌟

Vue 使用异步更新组件主要是为了优化性能确保更新的一致性,其核心原因和机制如下:


1. 性能优化:批量更新

  • 避免重复渲染
    当数据变化时,Vue 会将组件的更新任务推入一个异步队列,而不是立即执行。如果在同一事件循环中有多次数据变更,Vue 会合并这些变更,最终只进行一次批量更新从而减少不必要的 DOM 操作和计算

  • 示例场景
    例如,在一个方法中连续修改多个数据:

    this.a = 1;
    this.b = 2;
    this.c = 3;
    

    如果同步更新,每次赋值都会触发一次渲染;而异步更新会将这些变更合并,仅渲染一次。


2. 保证更新顺序与一致性

  • 依赖收集的稳定性
    组件更新可能依赖其他组件的状态或 DOM 结构。异步更新确保所有数据变更完成后再统一计算依赖和更新视图,避免中间状态导致的渲染错误

  • 示例问题
    若同步更新,当父组件和子组件同时更新时,可能因执行顺序不同导致子组件依赖父组件的数据未更新完成,从而渲染错误。


3. 利用事件循环机制

Vue 的异步更新基于 JavaScript 的事件循环机制:

  1. 将更新任务推入队列
    数据变化时,Vue 不会立即触发更新,而是将需要更新的组件标记为 dirty,并加入异步队列
  2. 在下一个事件循环中执行更新
    通过微任务(如 Promise.then)或宏任务(如 setTimeout)执行队列中的更新任务,确保当前代码执行完毕后再处理渲染。

4. nextTick 的作用

  • 访问更新后的 DOM
    由于更新是异步的,直接通过 this.xxx = value 修改数据后立即访问 DOM,可能得到的是旧值。此时需要使用 Vue.nextTickthis.$nextTick 等待更新完成:
    this.message = '更新后的值';
    this.$nextTick(() => {
      console.log(this.$el.textContent); // 输出更新后的 DOM 内容
    });
    
  • 实现原理
    nextTick 将回调函数推入与组件更新相同的异步队列,确保在 DOM 更新后执行

5. 异步更新的实现细节

  • 优先使用微任务(Microtask)
    现代浏览器中,Vue 默认通过 Promise.then 实现微任务队列,保证更新在同步代码执行后、UI 渲染前完成。
  • 降级策略
    在不支持 Promise 的环境中,Vue 会降级为 MutationObserversetTimeout

6. 与 React 的对比

  • React 的异步更新模式
    React 从 18 版本开始默认启用并发模式(Concurrent Mode),更新任务可中断并分片执行,但核心理念与 Vue 类似:避免同步更新导致的性能问题。
  • Vue 的简单性
    Vue 的异步更新策略更简单直接,开发者无需手动优化,框架自动处理批量更新。

总结

Vue 通过异步更新组件实现了:

  1. 性能优化:合并多次数据变更,减少渲染次数。
  2. 一致性保证:确保依赖关系和 DOM 状态正确。
  3. 开发者友好:通过 nextTick 提供访问更新后 DOM 的能力。

这种设计平衡了性能与开发体验,是 Vue 响应式系统的核心机制之一。

异步组件的加载过程

问题:
  1. Vue 中的异步组件是如何工作的?它如何实现懒加载?
  2. 在使用异步组件时,如果加载失败,Vue 会如何处理?
考察点:
  • 工作原理: 当页面渲染到异步组件时,Vue 会触发该组件的动态导入。这个操作是通过 import() 实现的,它返回一个 Promise,当这个 Promise 被解析时,组件会被加载并渲染。

    const AsyncComponent = () => import('./AsyncComponent.vue');
    

    import() 返回的 Promise 会在加载完成后解析,解析结果就是加载的组件,Vue 会在该组件需要渲染时进行加载

  • 加载失败的处理: 如果异步组件的加载失败,通常可以通过 Promise 的 catch 方法来处理错误。Vue 提供了 error 选项来捕获加载失败的情况,并可以展示一个备用组件或提示信息

    const AsyncComponent = () => import('./AsyncComponent.vue').catch(() => import('./ErrorComponent.vue'));
    

    另外,Vue 提供了 loading 和 error 属性用于处理加载过程中和加载失败时的状态:

    const AsyncComponent = () => ({
      component: import('./AsyncComponent.vue'),
      loading: LoadingComponent,
      error: ErrorComponent,
      delay: 200,  // 延迟200ms后显示 loading 组件
      timeout: 3000  // 超过3秒还没加载完成显示 error 组件
    });
    

异步组件的性能优化

问题:
  1. 如何优化异步组件的加载性能?可以使用哪些策略?
  2. 在多个异步组件的情况下,如何确保它们高效加载?
考察点:
  • 懒加载与拆分: 通过按需加载和拆分大型组件,可以减少初始页面加载的资源量。例如,对于一个大页面,可以把页面拆分成多个异步组件,只有在需要渲染时才加载。

  • 预加载: 如果你知道某个组件即将被使用,可以使用 Vue 的 preload 或 prefetch 来提前加载。prefetch 是一种告诉浏览器在空闲时加载资源的策略,这有助于提升组件加载的速度。

    const AsyncComponent = () => ({
      component: import('./AsyncComponent.vue'),
      loading: LoadingComponent,
      error: ErrorComponent,
      delay: 200,
      timeout: 3000,
      prefetch: true  // 预加载
    });
    
  • 使用 Webpack 的代码分割: 使用 Webpack 时,动态导入会自动分割代码,使得组件在需要时才会被加载到浏览器中。通过设置合理的 chunk 配置,可以更好地管理资源加载。

    const AsyncComponent = () => import(/* webpackChunkName: "async-component" */ './AsyncComponent.vue');
    

    这会将该组件单独分割为一个 async-component.js 文件。

  • 异步组件与路由懒加载结合: Vue Router 支持懒加载路由组件,可以与异步组件结合,在路由切换时按需加载组件。可以在路由懒加载的过程中添加加载提示,常见的做法是在加载时展示一个 loading 组件,加载完成后展示目标组件。

    const routes = [
      {
        path: '/about',
        component: () => import('./About.vue')  // 异步加载 About 组件
      }
    ];
    

什么是虚拟DOM?为什么需要虚拟DOM?如何实现一个虚拟DOM? 🌟🌟🌟

虚拟 DOM(Virtual DOM)是前端框架(如 React、Vue)中用于优化页面渲染性能的核心技术,其本质是 用 JavaScript 对象模拟真实 DOM 的树形结构。通过对比新旧虚拟 DOM 的差异,最小化操作真实 DOM 的次数,从而实现高效更新。


一、为什么需要虚拟 DOM?

1. 真实 DOM 的性能瓶颈
  • 操作成本高:真实 DOM 操作涉及浏览器布局计算、样式重绘等,频繁操作会导致性能下降
  • 低效更新:直接操作 DOM 时,多次修改可能触发多次渲染(如循环中修改 DOM)。
2. 虚拟 DOM 的优势
  • 批量更新:将多次 DOM 操作合并为一次,减少渲染次数。
  • 差异更新(Diff 算法):仅更新变化的部分,避免全量替换。

二、虚拟 DOM 的实现原理

1. 虚拟 DOM 的结构

用 JavaScript 对象描述一个 DOM 节点,例如:

const vnode = {
  tag: 'div',          // 标签名
  props: {             // 属性
    id: 'app',
    className: 'container'
  },
  children: [          // 子节点
    { tag: 'p', children: 'Hello World' },
    { tag: 'button', props: { onClick: handleClick }, children: 'Click Me' }
  ]
};
2. 核心流程
  1. 生成虚拟 DOM:将模板或 JSX 转换为虚拟 DOM 树。
  2. Diff 对比:比较新旧虚拟 DOM 的差异。
  3. Patch 更新:将差异应用到真实 DOM。

三、如何实现一个简易虚拟 DOM?

完整流程图
graph TD
  A[创建 Virtual DOM] --> B[渲染为真实 DOM]
  B --> C[状态变化生成新 Virtual DOM]
  C --> D[Diff 算法比对差异]
  D --> E[生成补丁包]
  E --> F[批量更新真实 DOM]
1. 创建虚拟 DOM 结构

定义一个函数生成虚拟节点:

function createVNode(tag, props, children) {
  return { tag, props: props || {}, children };
}
2. 渲染虚拟 DOM 到真实 DOM

将虚拟节点转换为真实 DOM:

function render(vnode) {
  if (typeof vnode === 'string') {
    return document.createTextNode(vnode); // 文本节点
  }

  const el = document.createElement(vnode.tag);

  // 设置属性
  for (const key in vnode.props) {
    el.setAttribute(key, vnode.props[key]);
  }

  // 递归渲染子节点
  vnode.children.forEach(child => {
    el.appendChild(render(child));
  });

  return el;
}
3. Diff 算法(简化版)

对比新旧节点,找出差异:

function diff(oldVNode, newVNode) {
  // 1. 节点类型不同:直接替换
  if (oldVNode.tag !== newVNode.tag) {
    return { type: 'REPLACE', node: newVNode };
  }

  // 2. 属性变化:更新属性
  const propsPatches = diffProps(oldVNode.props, newVNode.props);

  // 3. 子节点对比
  const childrenPatches = diffChildren(oldVNode.children, newVNode.children);

  return { propsPatches, childrenPatches };
}

function diffProps(oldProps, newProps) {
  const patches = [];
  // 合并新旧属性,找出新增/修改/删除的属性
  const allProps = { ...oldProps, ...newProps };
  for (const key in allProps) {
    if (oldProps[key] !== newProps[key]) {
      patches.push({ type: 'SET_ATTR', key, value: newProps[key] });
    }
  }
  return patches;
}
4. 应用差异到真实 DOM

根据差异更新真实 DOM:

function patch(el, patches) {
  patches.forEach(patch => {
    switch (patch.type) {
      case 'REPLACE':
        const newEl = render(patch.node);
        el.parentNode.replaceChild(newEl, el);
        break;
      case 'SET_ATTR':
        el.setAttribute(patch.key, patch.value);
        break;
      // 处理子节点更新...
    }
  });
}

虚拟 DOM 的局限性

  1. 内存开销需维护虚拟 DOM 树,占用额外内存
  2. 简单场景不适用:静态页面直接操作 DOM 更高效。

Proxy的set函数如何拦截数组的push操作

一、考察点

  • 理解 Proxy 拦截机制,尤其是对数组操作的捕获
  • 掌握数组 push 方法的底层本质(其实是通过 set 操作新增或修改数组元素和 length 属性
  • 能正确通过 Proxy 的 set 捕获数组元素新增和长度变化
  • 了解如何针对数组操作做定制化处理或限制

二、参考答案

2.1 核心原理说明

  • JavaScript 中数组的 push 方法本质是修改数组的“length”属性和新增元素(数组索引属性)

  • 当执行 arr.push(value) 时,触发两次 set

    1. 新索引位置设置新元素值
    2. length 属性增加
  • Proxy 的 set 捕获的是属性赋值操作,能捕获上述两种赋值

  • 通过判断被赋值的属性(prop)是否为数字索引或者 "length",可以针对 push 进行特殊处理

2.2 示例代码

const arr = [];

const proxyArr = new Proxy(arr, {
  set(target, prop, value, receiver) {
    if (prop === 'length') {
      console.log(`数组长度被修改,新长度:${value}`);
    } else if (!isNaN(prop)) {
      console.log(`数组索引 ${prop} 被赋值为 ${value}`);
    }
    // 反射操作,确保正常赋值
    return Reflect.set(target, prop, value, receiver);
  }
});

proxyArr.push(10);
// 控制台输出:
// 数组索引 0 被赋值为 10
// 数组长度被修改,新长度:1

proxyArr.push(20);
// 控制台输出:
// 数组索引 1 被赋值为 20
// 数组长度被修改,新长度:2

2.3 说明

  • Proxy 的 set 拦截函数接收四个参数:目标对象、属性名、赋值、代理对象
  • 数组新增元素本质是给新索引属性赋值,长度变化是对 "length" 属性赋值
  • 可以根据 prop 判断操作类型,从而实现对 push 的拦截、限制或日志打印
  • 如果需要阻止 push,可返回 false 并阻止赋值

三、常见误区或面试陷阱

  • 误以为 Proxy 会单独拦截 push 方法调用(其实是捕获属性赋值)
  • 忽略 "length" 也会被 set 拦截,需要同时处理
  • 没有正确返回 true/false 导致 Proxy 行为异常
  • 只处理数字索引,忽略了 length 属性修改

答题要点

  • 数组 push 触发 set 两次:新元素赋值 + length 赋值
  • 通过判断 prop 是否为数字索引或 "length",即可精准拦截
  • 使用 Reflect.set 保证默认行为正常执行
  • 结合拦截做日志、校验或阻止操作

ajax、fetch、axios这三都有什么区别?🌟 实操

「AJAX、Fetch 和 Axios 是前端开发中用于发送 HTTP 请求的三种主要技术,它们在实现方式、功能特性和使用场景上有显著差异。以下是它们的详细对比:


1. AJAX(Asynchronous JavaScript and XML)

定义与背景
  • 技术本质:AJAX 是一个技术概念(而非具体 API),基于 XMLHttpRequest(XHR)对象实现。
  • 出现时间:2005 年由 Google 推广(Gmail 的异步邮件加载)。
  • 核心特点
    • 异步无刷新更新页面内容。
    • 原生 API 使用复杂,需手动处理JSON 解析
代码示例
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data');
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(JSON.parse(xhr.responseText));
  }
};
xhr.send();
优缺点
优点缺点
兼容所有浏览器代码冗长,需手动处理 JSON 解析

2. Fetch API

定义与背景
  • 技术本质:ES6 新增的原生 API,基于 Promise 实现
  • 出现时间:2015 年随 ES6 标准化。
  • 核心特点
    • 语法简洁,链式调用。
    • 默认不携带 Cookie,需显式配置 credentials: 'include'
代码示例
fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) throw new Error('HTTP error');
    return response.json();
  })
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));
优缺点
优点缺点
现代浏览器原生支持不兼容 IE
Promise 链式调用需手动处理 HTTP 错误,如 404 不会触发 catch),无请求超时机制

3. Axios

定义与背景
  • 技术本质:基于 Promise 的第三方 HTTP 客户端库。
  • 出现时间:2014 年发布,持续维护至今。
代码示例
axios.get('https://api.example.com/data')
  .then(response => console.log(response.data))
  .catch(error => {
    if (error.response) {
      console.log('HTTP Error:', error.response.status);
    } else {
      console.log('Network Error:', error.message);
    }
  });

// 取消请求
const source = axios.CancelToken.source();
axios.get('/data', { cancelToken: source.token });
source.cancel('Request canceled by user');
优缺点
优点缺点
自动处理 JSON 数据,功能丰富(拦截器、取消等),统一处理 HTTP 错误(非 2xx 状态码触发 catch需额外安装(增加包体积),配置相对复杂

三、核心差异对比表

特性AJAX(XHR)Fetch APIAxios
技术类型原生 API原生 API第三方库
数据格式处理手动解析需调用 .json()自动转换
错误处理手动检查状态码需检查 response.ok自动捕获 HTTP 错误
取消请求使用 xhr.abort()AbortController内置 CancelToken
请求超时手动实现需结合 AbortController原生支持 timeout
浏览器兼容性全兼容(包括 IE)现代浏览器现代浏览器 + Polyfill
跨域支持需配置 CORS需配置 CORS自动处理简单请求
上传进度监控支持不支持支持

四、选型建议

  • 老旧项目维护 → 使用 AJAX(兼容性优先)
  • 现代轻量级应用 → 优先选择 Fetch API(原生支持)
  • 企业级复杂应用 → 使用 Axios(功能全面)
  • 需要取消请求/拦截器 → Axios
  • 需要上传进度监控 → AJAX 或 Axios

五、性能对比(示例场景)

// 10 次并发请求测试(Chrome 90)
AJAX(XHR): ~120ms 
Fetch: ~110ms 
Axios: ~115ms

三者性能差异可忽略不计,选择应更多关注功能需求和开发体验。


通过理解这些差异,开发者可以根据项目需求(兼容性、功能复杂度、团队习惯)合理选择 HTTP 请求方案。

为什么要封装axios?主要是封装哪方面的?原理是什么?源码是怎么实现的 🌟

封装 Axios 主要是为了统一管理请求逻辑提高代码复用性、、简化调用方式,并在项目中保持一致的请求行为。以下是分步解答:


一、为什么要封装 Axios?

  1. 全局配置
    统一设置 baseURL、超时时间、请求头(如自动添加 Token)

  2. 拦截器扩展
    在请求前添加 Loading 状态,或在响应后自动解析数据格式。 避免在每个请求中重复编写错误处理代码,通过拦截器集中处理 HTTP 错误、业务逻辑错误等

  3. 增强安全性
    防止 CSRF/XSRF,自动刷新 Token,或实现请求重试机制。

  4. 简化调用
    封装后可通过 api.get('/user') 直接调用,隐藏底层细节(如 URL 拼接、参数处理)。

  5. 解耦依赖
    后续替换为其他 HTTP 库(如 fetch)时,只需修改封装层,无需改动业务代码。


二、主要封装哪些方面?

  1. 默认配置

    const instance = axios.create({
      baseURL: 'https://api.example.com',
      timeout: 5000,
      headers: { 'X-Custom-Header': 'value' }
    });
    
  2. 请求拦截器
    用于添加 Token、修改请求参数:

    instance.interceptors.request.use(config => {
      config.headers.Authorization = `Bearer ${getToken()}`;
      return config;
    });
    
  3. 响应拦截器
    处理响应数据、统一错误码

    instance.interceptors.response.use(
      response => {
        if (response.data.code !== 200) {
          return Promise.reject(response.data.message);
        }
        return response.data; // 直接返回业务数据
      },
      error => {
        if (error.response.status === 401) {
          logoutUser();
        }
        return Promise.reject(error);
      }
    );
    
  4. 封装 API 方法
    提供更简洁的调用方式:

    export const get = (url, params) => instance.get(url, { params });
    export const post = (url, data) => instance.post(url, data);
    
  5. 取消请求
    通过 CancelTokenAbortController 取消重复请求。


三、Axios 封装原理

  1. 基于 Axios 实例
    ** 通过 axios.create() 创建独立实例**,避免污染全局配置。

  2. 拦截器链式调用
    Axios 内部通过 Promise 链式调用拦截器,请求拦截器按添加顺序执行,响应拦截器按相反顺序执行。

  3. 适配器模式
    Axios 在底层使用适配器(Adapter)兼容浏览器(XMLHttpRequest)和 Node.js(HTTP 模块)。


五、完整封装示例

// 1. 创建实例
const instance = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
});

// 2. 请求拦截器
instance.interceptors.request.use(
  config => {
    config.headers.Auth = 'Bearer ' + localStorage.getItem('token');
    return config;
  },
  error => Promise.reject(error)
);

// 3. 响应拦截器
instance.interceptors.response.use(
  response => {
    if (response.data.code === 401) {
      router.push('/login');
    }
    return response.data;
  },
  error => {
    if (error.message.includes('timeout')) {
      alert('请求超时!');
    }
    return Promise.reject(error);
  }
);

// 4. 封装 API
export const get = (url, params) => instance.get(url, { params });
export const post = (url, data) => instance.post(url, data);

总结

封装 Axios 的核心目标是 统一管理请求逻辑,通过拦截器、默认配置和简洁的 API 设计,减少重复代码并提高可维护性。源码通过拦截器链和适配器模式实现灵活性,封装时只需在其基础上扩展业务逻辑。

axios怎么解决跨域的问题?

axios 的是一种异步请求,请求中包括get,post,put, patch ,delete等五种请求方式

解决跨域可以在请求头中添加Access-Control-Allow-Origin,也可以在index.js文件中更改proxyTable配置等解决跨域问题

因为axios在vue中利用中间件http-proxy-middleware做了一个本地的代理服务A,相当于你的浏览器通过本地的代理服务A请求了服务端B,浏览器通过服务A并没有跨域,因此就绕过了浏览器的同源策略,解决了跨域的问题。

如何取消请求

取消请求的核心是通过中断请求连接忽略响应处理实现,不同场景有不同方案,核心方法如下:

1. XMLHttpRequest(XHR)

通过 abort() 方法中断请求:

const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.send();
// 取消请求
xhr.abort();

2. Fetch API

利用 AbortController 控制器:

const controller = new AbortController();
const signal = controller.signal;

fetch('/api/data', { signal })
  .then(res => res.json())
  .catch(err => {
    if (err.name === 'AbortError') console.log('请求已取消');
  });

// 取消请求
controller.abort();

3. Axios

  • 方式 1:用 CancelToken(旧版)

    const CancelToken = axios.CancelToken;
    const source = CancelToken.source();
    
    axios.get('/api/data', { cancelToken: source.token })
      .catch(err => {
        if (axios.isCancel(err)) console.log('请求取消:', err.message);
      });
    
    // 取消请求
    source.cancel('用户取消请求');
    
  • 方式 2:用 AbortController(新版推荐)

    const controller = new AbortController();
    
    axios.get('/api/data', { signal: controller.signal })
      .catch(err => {
        if (err.name === 'AbortError') console.log('请求已取消');
      });
    
    // 取消请求
    controller.abort();
    

4. 场景化处理

  • 重复请求取消:维护请求队列,新请求发起时取消同接口的未完成请求;
  • 页面卸载时取消:在 beforeUnload 或组件销毁钩子中调用取消方法。

核心逻辑

本质是通过控制器(如 AbortController)或专属方法(如 abort())通知浏览器终止请求连接,或标记请求为已取消,后续忽略响应处理。

如何取消重复请求

取消重复请求的核心是 “记录未完成请求,新请求发起时中断旧请求”,通过维护请求标识(如 URL + 参数)与请求控制器的映射关系,实现重复请求的自动取消,具体步骤如下:

1. 维护请求缓存池

用对象 / Map 存储请求标识(如拼接 URL 和参数生成唯一 key)和对应的取消控制器(如AbortController或 Axios 的CancelToken)。

2. 发起请求前检查重复

  • 新请求发起时,先根据标识检查缓存池:若存在未完成的同名请求,调用控制器取消旧请求;
  • 将新请求的控制器存入缓存池,请求完成(成功 / 失败)后从缓存池移除

3. 适配不同请求库(以 Axios 为例)

const pendingRequests = new Map(); // 缓存池:key->控制器

// 请求拦截器
axios.interceptors.request.use(config => {
  const key = `${config.url}-${JSON.stringify(config.params)}`;
  // 取消旧请求
  if (pendingRequests.has(key)) {
    pendingRequests.get(key).abort(); // 或CancelToken的cancel()
    pendingRequests.delete(key);
  }
  // 存入新请求控制器
  const controller = new AbortController();
  config.signal = controller.signal;
  pendingRequests.set(key, controller);
  return config;
});

// 响应拦截器
axios.interceptors.response.use(
  res => {
    const key = `${res.config.url}-${JSON.stringify(res.config.params)}`;
    pendingRequests.delete(key); // 移除缓存
    return res;
  },
  err => {
    const key = `${err.config.url}-${JSON.stringify(err.config.params)}`;
    pendingRequests.delete(key); // 移除缓存
    return Promise.reject(err);
  }
);

核心逻辑

  • 请求标识:需保证唯一性(URL + 参数 / 请求体组合);
  • 自动清理:请求完成后务必从缓存池移除,避免内存泄漏
  • 控制器复用:利用AbortController(Fetch/Axios 新版)或CancelToken(Axios 旧版)实现请求中断。

简言之,通过 “缓存 - 检查 - 取消 - 清理” 的闭环,确保同一接口同时只有一个请求在处理,避免重复请求导致的数据混乱或性能问题。

怎么处理vue项目中的错误?底层逻辑是什么?🌟

在 Vue 项目中处理错误需要结合框架提供的错误捕获机制和 JavaScript 原生错误处理能力,其底层逻辑围绕 错误冒泡、异步队列管理生命周期钩子 展开。以下是详细的解决方案和原理分析:


一、Vue 错误处理的 4 种核心方式

1. 全局错误处理器(Vue.config.errorHandler)

作用:捕获所有未被处理的 Vue 组件内错误(生命周期、模板、计算属性等)。
底层逻辑:Vue 在调用组件方法和渲染时会用 try...catch 包裹代码,将错误传递给全局处理器。

// main.js
Vue.config.errorHandler = (err, vm, info) => {
  console.error('全局捕获:', err, info);
  // 上报错误到监控系统(如 Sentry)
};
2. 组件级错误捕获(errorCaptured 钩子)

作用:捕获子组件的错误,可决定是否阻止错误继续冒泡。
底层逻辑:错误从子组件向根组件冒泡,若某组件返回 false,则停止冒泡。

export default {
  errorCaptured(err, vm, info) {
    console.error('组件捕获:', err, info);
    return false; // 阻止继续冒泡
  }
};
3. 异步错误处理(window.onerror / unhandledrejection)

作用:捕获未被 Vue 处理的全局错误(如 setTimeoutPromise)。
底层逻辑:通过浏览器原生 API 监听未捕获错误。

// 同步错误和资源加载错误
window.onerror = (message, source, lineno, colno, error) => {
  console.error('全局错误:', error);
};

// Promise 未处理的拒绝
window.addEventListener('unhandledrejection', (event) => {
  console.error('Promise 错误:', event.reason);
});
4. 路由错误处理(Vue Router)

作用:捕获导航过程中的错误(如路由守卫中的异常)。
底层逻辑:通过 router.onError 注册全局错误处理器。

// router.js
const router = new VueRouter({ /* ... */ });
router.onError((err) => {
  console.error('路由错误:', err);
});

二、底层逻辑解析

1. 错误冒泡机制
  • 流程:子组件错误 → 父组件 errorCaptured → 根组件 → Vue.config.errorHandler
  • 源码关键点(Vue 2.x)
    // src/core/util/error.js
    function handleError(err, vm, info) {
      if (vm) {
        let cur = vm;
        // 向上遍历父组件调用 errorCaptured
        while ((cur = cur.$parent)) {
          if (cur._errorCaptured) {
            if (cur.errorCaptured(err, vm, info) === false) break;
          }
        }
      }
      // 触发全局处理器
      globalHandleError(err, vm, info);
    }
    
2. 异步更新队列错误处理

Vue 的异步更新队列(如 nextTick)中的错误会被单独捕获,通过 PromisesetTimeout 抛到全局。

// src/core/util/next-tick.js
function flushCallbacks() {
  try {
    callbacks.forEach(cb => cb());
  } catch (e) {
    handleError(e, null, 'nextTick');
  }
}
3. 生命周期钩子错误传播

生命周期钩子(如 createdmounted)中的错误会被 Vue 内部 try...catch 包裹,并传递给错误处理器。

// src/core/instance/lifecycle.js
function callHook(vm, hook) {
  try {
    vm._hooks[hook].forEach(handler => handler.call(vm));
  } catch (e) {
    handleError(e, vm, `${hook} hook`);
  }
}

三、最佳实践:构建健壮的错误处理系统

1. 分层处理策略
层级处理方式示例场景
组件级errorCaptured局部错误隔离(如第三方组件崩溃)
全局级Vue.config.errorHandler全局错误日志上报
网络级window.onerror捕获未处理的脚本错误
异步级unhandledrejectionPromise 未处理拒绝
2. 集成监控工具(如 Sentry)
import * as Sentry from '@sentry/vue';

Sentry.init({
  Vue,
  dsn: 'YOUR_DSN',
  integrations: [new Sentry.BrowserTracing()],
});

Vue.config.errorHandler = (err, vm, info) => {
  Sentry.captureException(err, { extra: { vm, info } });
};
3. 用户友好反馈
<template>
  <div v-if="error">
    <h1>出错了!</h1>
    <button @click="reload">重试</button>
  </div>
  <div v-else>
    <slot></slot>
  </div>
</template>

<script>
export default {
  data: () => ({ error: null }),
  errorCaptured(err) {
    this.error = err;
    return false; // 阻止继续冒泡
  },
  methods: {
    reload() {
      this.error = null;
      this.$forceUpdate();
    }
  }
};
</script>

四、常见问题与解决方案

1. 错误信息不完整
  • 问题:生产环境代码被压缩,难以定位源码位置。
  • 解决
    • 启用 Source Map(通过 Webpack 生成)并上传到监控系统。
    • 使用 vue.config.js 保留组件名称:
      module.exports = {
        configureWebpack: {
          optimization: {
            minimize: false // 或通过 terser 配置保留类名
          }
        }
      };
      
2. 重复上报错误
  • 问题:同一错误被多个层级捕获导致重复日志。
  • 解决:在全局处理器中设置去重逻辑(如错误信息哈希)。
3. 异步代码错误遗漏
  • 问题setTimeoutPromise 中的错误未被捕获。
  • 解决:使用 window.onerrorunhandledrejection 兜底。

五、总结:Vue 错误处理的核心逻辑

机制捕获范围触发条件
errorCaptured子组件树中的错误组件树冒泡
errorHandler全局未处理错误Vue 内部执行上下文(生命周期、渲染)
window.onerror全局脚本错误同步错误、资源加载失败
unhandledrejection未处理的 Promise 拒绝Promise 链未 catch

通过分层处理、监控集成和用户反馈,可以显著提升 Vue 应用的健壮性和可维护性。

使用vue渲染大量数据时应该怎么优化?🌟

在 Vue 中渲染大量数据时,性能优化至关重要。以下是一套系统化的优化方案,涵盖从数据管理到渲染策略的关键技巧:


1. 减少响应式数据量
  • 冻结静态数据:使用 Object.freeze 避免 Vue 对静态大数据进行响应式处理
this.list = Object.freeze(bigStaticDataArray);
  • 扁平化数据结构:将嵌套数据转换为扁平结构,减少响应式递归深度
// 转换前
{ items: [{ id: 1, children: [...] }] }

// 转换后
{ 
  entities: { 1: { ... }, 2: { ... } },
  visibleIds: [1, 2] 
}
2. 数据分片处理
// 分页加载
async loadChunk(page) {
  const chunk = await fetch(`/api/data?page=${page}`);
  this.data = [...this.data, ...chunk];
}

// 时间分片(使用 requestIdleCallback)
processDataChunk() {
  const chunk = data.splice(0, 100);
  this.renderChunk(chunk);
  if (data.length) {
    requestIdleCallback(() => this.processDataChunk());
  }
}

1. 虚拟滚动(核心方案)
<template>
  <VirtualScroller
    :items="bigData"
    :item-height="50"
    :buffer="200"
    class="scroller"
  >
    <template v-slot:item="{ item }">
      <ListItem :item="item" />
    </template>
  </VirtualScroller>
</template>

<!-- 推荐库 -->
<!-- vue-virtual-scroller: https://github.com/Akryum/vue-virtual-scroller -->

四、架构级优化

1. Web Worker 数据处理
// main.js
const worker = new Worker('./data-processor.js');

worker.postMessage(bigData);
worker.onmessage = (e) => {
  this.processedData = e.data;
};

// data-processor.js
self.onmessage = function(e) {
  const result = heavyProcessing(e.data);
  self.postMessage(result);
};
2. 服务端优化组合
  • SSR + Hydration:首屏服务端渲染 + 客户端激活
  • 流式传输:分块发送 HTML 片段
  • Edge Caching:CDN 缓存静态化列表

五、性能分析工具

  1. Vue Devtools 性能面板

    • 组件渲染时间分析
    • 事件追踪
  2. Chrome Performance Monitor

    • FPS 实时监控
    • CPU/Memory 分析
  3. 自定义性能标记

// 关键操作打点
const start = performance.now();
processData();
const end = performance.now();
console.log(`数据处理耗时: ${end - start}ms`);

六、进阶优化技巧

1. 渲染策略控制
// 暂停观察器
this.$pauseTracking();
bulkUpdateData();
this.$resumeTracking();

// 强制数组响应式更新
this.$set(this.items, index, newItem);
2. CSS 渲染优化
.item-container {
  content-visibility: auto;  /* 现代浏览器渲染优化 */
  contain-intrinsic-size: 100px; /* 占位尺寸 */
}

.item {
  will-change: transform; /* 谨慎使用 */
}

性能对比指标

优化方案1万条数据渲染时间内存占用滚动FPS
未优化3200ms450MB8
虚拟滚动80ms120MB60
虚拟滚动+冻结数据65ms90MB60

决策树:如何选择优化方案?

是否需要交互式滚动?
├── 是 → 虚拟滚动 + 冻结数据
└── 否 → 
    数据是否需要动态更新?
    ├── 是 → 分页加载 + 组件复用
    └── 否 → 服务端渲染 + 静态化

通过组合上述优化策略,可以将大数据量场景下的渲染性能提升 10-50 倍。关键要点:

  1. 数据与渲染解耦:虚拟滚动是处理可视区域的银弹方案
  2. 响应式控制:最小化 Vue 的依赖追踪开销
  3. 分层优化:从数据预处理到像素渲染的全链路优化
  4. 工具赋能:善用性能分析工具定位瓶颈

vue-loader是什么?它有什么作用?

vue-loader 是 Vue 生态中核心的构建工具,专门用于解析和转换 Vue 的单文件组件(.vue 文件)。它是 Webpack 的一个加载器(Loader),在 Vue 项目中扮演着关键角色。以下是它的核心作用和工作原理详解:


一、vue-loader 的核心作用

1. 解析 .vue 文件
  • 单文件组件拆分:将 .vue 文件拆解为 templatescriptstyle 三个独立模块。
  • 代码分块处理
    <template> <!-- 转成 render 函数 --> </template>
    <script>   <!-- 转成 JS 模块 -->    </script>
    <style>    <!-- 转成 CSS 代码 -->   </style>
    
2. 支持预处理器
  • 模板:支持 Pug、Haml 等。
  • 脚本:支持 TypeScript、CoffeeScript。
  • 样式:支持 Sass、Less、Stylus。
  • 配置示例
    <template lang="pug">
      div Hello {{ name }}
    </template>
    
    <script lang="ts">
    export default { name: 'App' }
    </script>
    
    <style lang="scss" scoped>
    div { color: $primary-color; }
    </style>
    

二、vue-loader 的工作原理

构建流程示例
  1. Webpack 匹配 .vue 文件
    // webpack.config.js
    module: {
      rules: [
        { test: /\.vue$/, loader: 'vue-loader' }
      ]
    }
    
  2. vue-loader 解析 .vue 文件
    • 提取 <template> → 生成 render 函数。
    • 提取 <script> → 转成 JavaScript 模块。
    • 提取 <style> → 通过 css-loadersass-loader 等处理。
  3. 输出浏览器可执行的代码

三、典型配置示例

1. 使用官方脚手架(Vue CLI)创建的项目

会自动集成 vue-loader。Vue CLI(@vue/cli)是官方推荐的项目构建工具,它默认集成了完整的 Vue 工程化配置,包括:

  • vue-loader(用于解析 .vue 单文件组件)
  • 配套的 vue-template-compiler(编译模板部分)
  • 以及 webpack 相关的 loader(如 css-loaderbabel-loader 等)

创建项目后,无需手动安装 vue-loader,即可直接编写 .vue 文件并运行。

2. 手动搭建的 Vue 项目(基于 webpack/vite)

需要手动配置 vue-loader(或对应工具) 。如果不使用 Vue CLI,而是自己基于 webpack 或 Vite 搭建 Vue 项目,则需要手动安装并配置相关工具:

  • webpack 环境:需安装 vue-loader 和 vue-template-compiler,并在 webpack 配置文件中添加对 .vue 文件的解析规则。
  • Vite 环境:Vite 内置了对 .vue 文件的支持(基于 @vitejs/plugin-vue 插件),无需单独安装 vue-loader(Vite 不依赖 webpack,使用自己的编译逻辑)。
完整 Webpack 配置片段
// webpack.config.js
const { VueLoaderPlugin } = require('vue-loader');

module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.css$/,
        use: ['vue-style-loader', 'css-loader']
      },
      {
        test: /\.scss$/,
        use: ['vue-style-loader', 'css-loader', 'sass-loader']
      }
    ]
  },
  plugins: [new VueLoaderPlugin()]
};

如果你直接使用 Vue CLI 或 Vite,它们已内置对 vue-loader(或 @vitejs/plugin-vue)的封装,开发者无需手动配置即可享受其功能。

Vue2为什么要求组件模板只能有一个根元素?Vue3为什么就可以多个 🌟

Vue2 要求组件模板只能有一个根元素,而 Vue3 支持多根元素(Fragment 片段),核心原因在于两者的模板编译逻辑虚拟 DOM 处理方式的差异:

1. Vue2 为什么要求单根元素?

Vue2 的模板编译和虚拟 DOM 设计中,存在几个关键限制:

  • 虚拟 DOM 节点结构限制:Vue2 的虚拟 DOM(VNode)要求组件必须对应一个单一的根节点。在编译模板时,Vue2 会将模板解析为一个 VNode 树,如果存在多个根元素,会导致 VNode 树的 “根节点” 不唯一,无法形成有效的树形结构(树形结构必须有且仅有一个根)

  • 内部实现简化:组件的 DOM 挂载点($el)需要对应一个明确的根元素,多根元素会导致挂载目标不明确。

image.png

2. Vue3 为什么支持多根元素?

Vue3 对模板编译和虚拟 DOM 进行了重构,引入了 Fragment(片段)  特性,从根本上解决了单根限制:

  • 引入 Fragment 虚拟节点:Vue3 的虚拟 DOM 中新增了 Fragment 类型的 VNode。当模板存在多个根元素时,编译器会自动将它们包裹在一个 Fragment 节点下( Fragment 本身不会渲染为真实 DOM 元素)。这样既保持了 VNode 树的单一根节点结构(Fragment 作为根)又允许模板中存在多个同级元素

  • 更灵活的组件设计:多根元素支持让组件结构更自然,无需额外包裹一个 <div>,避免了多余的 DOM 层级,更符合实际开发需求。

总结

  • Vue2 因虚拟 DOM 设计和编译逻辑的限制,要求组件模板必须有一个唯一根元素,否则无法生成有效的 VNode 树。
  • Vue3 通过引入 Fragment 虚拟节点,在保持 VNode 树结构完整性的同时,允许模板存在多个根元素,既解决了冗余 DOM 问题,又简化了组件设计

什么是高阶组件?🌟 实操

Vue 可以使用高阶组件(Higher-Order Component,简称 HOC),不过它和 React 里的高阶组件在概念上类似,但实现方式存在差异。下面为你详细介绍在 Vue 里使用高阶组件的相关内容。

高阶组件的概念

高阶组件是一个函数,它接收一个组件作为参数,并且返回一个新的组件。其主要用途是复用代码、状态管理、代码增强等。

Vue 中高阶组件的实现方式

函数式高阶组件

在 Vue 里,可以通过函数式组件来实现高阶组件。函数式组件是无状态、无实例的组件,它的渲染速度较快

以下是一个简单的示例:

    function withBorder(WrappedComponent) {
        return {
            functional: true,
            render(h, context) {
                return h('div', {
                    style: {
                        border: '1px solid black',
                        padding: '10px'
                    }
                }, [h(WrappedComponent, context.data, context.children)]);
            }
        };
    }

    // 定义一个普通组件
    const MyComponent = {
        template: '<p>这是一个普通组件</p>'
    };

    // 使用高阶组件包装普通组件
    const BorderedComponent = withBorder(MyComponent);

在这个例子中,withBorder 是一个高阶组件函数,它接收一个组件 WrappedComponent 作为参数,然后返回一个新的函数式组件。这个新组件会在原组件外面包裹一个带有边框的 div 元素。

基于 mixin 的高阶组件

还可以借助 mixin 来实现高阶组件的部分功能。mixin 能够复用组件的选项。

    // 定义一个 mixin
    const withLogging = {
        created() {
            console.log('组件已创建');
        }
    };

    // 定义一个普通组件
    const AnotherComponent = {
        template: '<p>另一个普通组件</p>',
        mixins: [withLogging]
    };

在这个示例中,withLogging 是一个 mixin,它包含一个 created 钩子函数,用于在组件创建时打印日志。AnotherComponent 组件通过 mixins 选项引入了这个 mixin,从而复用了日志记录的功能。

高阶组件的使用场景

  • 代码复用:把一些通用的逻辑封装到高阶组件中,然后在多个组件里复用。

  • 状态管理:在高阶组件中管理一些公共的状态,然后将状态传递给子组件。

  • 性能优化:利用函数式高阶组件来提高渲染性能。

综上所述,Vue 能够使用高阶组件,并且可以通过函数式组件和 mixin 等方式来实现

什么是函数式组件吗?实操

函数式组件(Functional Components)是一种特殊类型的组件,其核心特点在于 无状态(stateless)无实例(no instance)。 在 Vue.js 中,函数式组件通过纯函数的形式定义,专注于根据输入的 props 或上下文(context)进行渲染避免了常规组件的实例化开销,从而在特定场景下显著提升性能。


核心特性

特性函数式组件普通组件
状态管理无状态,无法使用 data有状态,支持响应式数据
生命周期钩子不支持支持完整的生命周期钩子
实例开销无实例化,轻量级需要创建组件实例
性能渲染更快,适合高频渲染场景常规性能,适合复杂交互
上下文访问通过 context 参数获取 props、slots通过 this 访问

使用场景

  1. 纯展示型组件
    仅依赖 props 渲染内容,无需内部状态或交互逻辑。

    <template functional>
      <div class="tag" :style="{ color: props.color }">
        {{ props.text }}
      </div>
    </template>
    
  2. 高性能列表项
    渲染大量列表项时,减少实例化开销。

    <template functional>
      <li class="list-item">
        {{ props.item.name }}
      </li>
    </template>
    
  3. 高阶组件(HOC)
    包装组件并增强功能,不污染原始组件状态。

    const withLoading = (Component) => ({
      functional: true,
      render(h, { props }) {
        return props.isLoading 
          ? h('div', 'Loading...') 
          : h(Component, { props });
      }
    });
    

Vue 2 中的实现方式

1. 选项式声明
Vue.component('functional-button', {
  functional: true,
  props: ['type'],
  render: function (h, context) {
    return h('button', {
      class: `btn-${context.props.type}`,
      on: context.listeners
    }, context.children);
  }
});
2. 单文件组件(SFC)
<template functional>
  <button 
    :class="`btn-${props.type}`" 
    v-on="listeners"
  >
    <slot/>
  </button>
</template>

<script>
export default {
  props: ['type']
}
</script>

Vue 3 中的变化

Vue 3 推荐使用 setup 函数 + Composition API 替代传统函数式组件,但依然支持函数式写法:

import { h } from 'vue';

const FunctionalComponent = (props, { slots }) => {
  return h('div', props.text, slots.default?.());
};

FunctionalComponent.props = ['text'];

性能优化原理

  1. 无实例化
    跳过组件实例创建过程(无 this 上下文),节省内存和初始化时间。

  2. 无响应式追踪
    不依赖 Vue 的响应式系统,避免依赖收集和触发更新的开销。

  3. 更快的渲染
    直接执行渲染函数,无生命周期钩子调用,适合高频渲染场景。


注意事项

  1. 无法使用计算属性和侦听器
    需通过外部传递计算结果。

  2. 限制模板功能
    无法直接使用 v-model,需手动实现事件绑定。

  3. 调试信息较少
    无组件实例,Devtools 中显示为匿名组件。


代码对比

普通组件
<template>
  <div class="user-card">
    <h3>{{ user.name }}</h3>
    <p>Followers: {{ followers }}</p>
    <button @click="follow">Follow</button>
  </div>
</template>

<script>
export default {
  props: ['user'],
  data() {
    return { followers: 0 };
  },
  methods: {
    follow() { this.followers++; }
  }
};
</script>
函数式组件
<template functional>
  <div class="user-card-static">
    <h3>{{ props.user.name }}</h3>
    <p>Followers: {{ props.followers }}</p>
  </div>
</template>

<script>
export default {
  props: ['user', 'followers']
};
</script>

总结

函数式组件是 Vue 中针对高性能场景的优化手段,适用于:

  • 纯静态内容展示
  • 高频渲染的列表项
  • 高阶组件封装

在 Vue 3 中,虽然 Composition API 提供了更灵活的状态管理,但函数式组件仍在对性能敏感的场景中占有一席之地。使用时需权衡功能需求与性能收益,避免在不必要场景中过度优化。

什么是SPA单页面?它跟多页面MPA的区别?如何实现一个SPA?


一、什么是SPA(单页面应用)?

SPA(Single Page Application) 是一种前端架构模式整个应用只有一个HTML页面,通过动态替换内容实现页面切换。其核心特点包括:

  • 客户端渲染页面内容由JavaScript动态生成,减少服务器请求。
  • 无刷新跳转:利用前端路由(如Vue Router、React Router)实现页面切换,用户体验流畅。
  • 前后端分离:前端独立运行,通过API与后端交互数据。

示例:Gmail、Google Maps等现代Web应用。


二、SPA vs MPA(多页面应用)对比

对比维度SPAMPA
页面数量仅一个HTML文件多个HTML文件(每个页面独立)
渲染方式客户端渲染(CSR)服务端渲染(SSR)
页面切换前端路由动态替换内容,无刷新整页刷新,重新加载所有资源
开发复杂度高(需管理状态、路由等)低(每个页面独立开发)
SEO友好性较差(需额外优化)天然友好
首屏加载速度较慢(需加载所有JS较快(仅加载当前页面资源)
适用场景交互密集、用户粘性高 → 选 SPA(如后台系统、社交 App)SEO 优先、功能独立、页面跳转少 → 选 MPA(如官网、电商商品页

三、如何实现一个SPA?

  • 框架:React、Vue、Angular。
  • 构建工具:Webpack、Vite。
  • 路由库:React Router、Vue Router。

vue项目本地开发完成后部署到服务器后报404是什么原因呢?

问题类型解决方案
服务器未重定向配置 Nginx/Apache/Tomcat 重定向到 index.html
路由模式不匹配改用 hash 模式或修复服务器配置
静态资源路径错误检查 publicPath 和部署目录
构建文件缺失确保 dist 目录完整上传至服务器

通过合理配置服务器和检查项目设置,可彻底解决部署后的 404 问题。

provide/inject的原理是什么?

Vue 中 provide/inject 的核心原理是基于组件实例的原型链 / 依赖注入系统,核心逻辑可简单概括为:

  1. 数据存储:父组件通过 provide 定义要暴露的数据 / 方法,会将其挂载到当前组件实例的 provides 对象上;若父组件有上级也用了 provide,则当前 provides 会继承上级的 provides(形成链式结构)。
  2. 数据查找:子组件(无论层级多深)通过 inject 声明需要的依赖时,Vue 会从当前组件实例开始,向上遍历组件树的 provides 链,直到找到匹配的依赖项(找不到则返回默认值或 undefined)。

简单说,就是父组件把数据 “提供” 到自己的实例上,后代组件 “注入” 时向上逐层找这个数据,本质是跨层级的依赖传递绕开了 props 逐级透传的问题,且不受组件层级限制

补充:Vue2 和 Vue3 底层实现略有差异(Vue3 基于 injectKey 精准查找,Vue2 基于对象原型链),但核心逻辑都是 “父存数据、子向上找”。

Vue2 和 Vue3中的问题

  1. createApp API (应用实例) :

    • Vue 2: 通过 new Vue() 创建应用实例,所有配置都是全局的。
    • Vue 3: 引入 createApp 函数来创建应用实例。每个应用实例都有自己的独立作用域,允许在同一个页面上挂载多个独立的 Vue 应用,避免了全局污染。
    • 优势: 更好的模块化和隔离性,方便构建微前端应用或在多框架环境中集成 Vue。
  2. 常见误区或面试陷阱

  • 误区: 认为 Vue 3 废弃了 Options API。纠正: Vue 3 仍然完全支持 Options API,并与 Composition API 可以混合使用。Composition API 是一种补充,旨在解决 Options API 在特定场景下的痛点

Vue2/3 中 provide/inject 的差别

你想了解 provide/inject 在 Vue2 和 Vue3 中的核心差别,核心结论是:二者核心功能一致(跨层级传递数据),但在「语法形式」「响应式支持」「高级特性」上存在显著差异,Vue3 对该 API 进行了优化和增强,使用更灵活、响应式支持更完善

下面从核心维度拆解具体差别,兼顾通俗性和实用性:

一、 语法形式差异(最直观)

1. Vue2:仅支持「选项式 API」

Vue2 中 provide/inject 只能在选项式 API 中使用,无法在组合式 API 中(无 Vue3 组合式语法支持)落地,语法固定为对象或函数形式。

2. Vue3:支持「选项式 API」+「组合式 API」

Vue3 兼容 Vue2 风格的选项式 API,同时新增了组合式 API 下的使用方式(在 setup() 中调用,更灵活),是 Vue3 中的主流用法。

二、 响应式支持差异(核心区别)

1. Vue2:响应式支持「不完善」,需额外处理
  • Vue2 中直接提供静态数据 / 普通对象无响应式:祖先数据更新后,子孙组件注入的数据不会同步更新;
  • 若要实现响应式,需手动做额外处理(2 种方案):① 提供「包含响应式数据的对象」(利用对象引用类型特性,子孙能获取到更新后的值);② 提供「方法」,子孙组件调用方法主动更新 / 获取最新数据;
  • 缺陷:无法实现「自动同步」,使用繁琐,容易踩坑。
// Vue2 选项式 API 示例
// 祖先组件(提供数据)
export default {
  data() {
    return {
      appTheme: "dark" // 响应式数据
    };
  },
  // 形式1:直接对象(无响应式,数据更新后代不同步)
  // provide: {
  //   appTheme: "dark"
  // },
  // 形式2:函数返回对象(可访问 this,支持响应式基础)
  provide() {
    return {
      appTheme: this.appTheme, // 传递响应式数据(需额外处理才能实现同步更新)
      changeTheme: this.changeTheme
    };
  },
  methods: {
    changeTheme() {
      this.appTheme = "light";
    }
  }
};

// 子孙组件(接收数据)
export default {
  // 形式1:数组接收
  // inject: ["appTheme", "changeTheme"],
  // 形式2:对象接收(支持默认值)
  inject: {
    appTheme: {
      default: "light"
    },
    changeTheme: {
      default: () => {}
    }
  },
  mounted() {
    console.log(this.appTheme);
  }
};
2. Vue3:响应式支持「完善且天然」
  • Vue3 中只要通过 ref()/reactive() 包裹响应式数据,再通过 provide 传递,inject 接收后直接保持响应式
  • 原理:provide 传递的是 ref/reactive 实例的引用,子孙组件获取到的是同一个实例,数据更新时会自动触发视图刷新;
  • 优势:无需额外处理,使用简洁,响应式同步更可靠。 支持 inject 配合 Symbol 避免命名冲突、支持按需注入等

总结

  1. 语法上:Vue2 仅支持选项式 API,Vue3 兼容选项式且新增组合式 API 用法,更灵活;
  2. 响应式上:Vue2 需额外处理才能实现响应式同步,Vue3 配合 ref/reactive 天然支持完善响应式;
  3. 核心体验:Vue3 的 provide/inject 功能更强大、使用更简洁,解决了 Vue2 中的诸多痛点,更适合复杂项目的跨层级数据共享。

请详细的讲一讲keep-alive? 🌟

在Vue.js中,<keep-alive>是一个内置的抽象组件,用于缓存不活动的组件实例避免重复渲染,从而提升应用性能。

一、核心作用

  1. 组件缓存
    当包裹动态组件或路由视图时,<keep-alive>会缓存非活跃的组件实例,而不是销毁它们。这意味着:
    • 组件的状态(如数据、DOM状态)会被保留。适用于高频切换的组件(如Tab页、路由视图),减少DOM操作和初始化渲染时间
    • 避免重复执行createdmounted生命周期钩子,减少性能开销。

二、基础用法

1. 包裹动态组件
<keep-alive>
  <component :is="currentComponent"></component>
</keep-alive>
2. 包裹路由视图
<keep-alive>
  <router-view></router-view>
</keep-alive>

三、属性配置

属性说明示例
include匹配的组件名(或正则表达式)会被缓存:include="['ComponentA', /^CompB/]"
exclude匹配的组件名(或正则表达式)不会被缓存:exclude="['ComponentC']"
max最大缓存实例数超出时按LRU算法淘汰:max="10"

四、生命周期钩子

被缓存的组件会触发以下特殊钩子:

钩子触发时机典型用途
activated组件被激活(插入DOM)时重新获取数据启动定时器
deactivated组件被停用(移出DOM)时清理定时器取消事件监听

示例

export default {
  activated() {
    this.fetchData(); // 重新加载数据
    this.timer = setInterval(this.update, 1000);
  },
  deactivated() {
    clearInterval(this.timer); // 清理定时器
  }
}

五、实现原理

1. 缓存机制
  • 缓存对象<keep-alive>内部维护一个cache对象,以组件key为键存储VNode和组件实例
  • LRU淘汰策略:当缓存数量超过max时,移除最久未使用的实例

六、常见场景与最佳实践

1. 动态管理缓存

详情返回数据列表表的时候,结合路由元信息(meta)动态控制缓存:

<keep-alive :include="cachedComponents">
  <router-view></router-view>
</keep-alive>
// 路由配置
{
  path: '/user',
  component: User,
  meta: { keepAlive: true } // 需要缓存
}

// 在全局路由守卫中动态管理缓存列表
router.beforeEach((to, from, next) => {
  if (from.meta.keepAlive) {
    // 将离开的路由组件名加入缓存
    cachedComponents.push(from.matched[0].components.default.name);
  }
  next();
});
2. 强制刷新缓存组件

通过改变组件的key强制重新渲染:

<keep-alive>
  <component :is="currentComponent" :key="componentKey"></component>
</keep-alive>
// 需要刷新时改变key值
this.componentKey += 1;

七、注意事项

  1. 组件命名includeexclude依赖组件的name选项,确保组件已正确命名。
  2. 嵌套路由:缓存整个路由视图时,需注意子路由组件的缓存策略。
  3. 性能权衡:过度缓存可能导致内存占用过高,需合理设置max