总共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)与实例选项
- 生命周期初始化:初始化
beforeCreate和created阶段的内部状态
2. 初始化响应式系统
-
初始化顺序:
props、methods、data,并挂载到组件实例this上,但此时 DOM 还未生成,数据仅存在于内存中,修改不会触发 DOM 渲染。 -
示例:
created()中this.count = 1能修改数据,但无 DOM 可更新。
// 内部执行
initProps(vm) // 处理props
initMethods(vm) // 绑定方法
initData(vm) // 数据劫持
initComputed(vm) // 计算属性初始化
initWatch(vm) // 监听器设置
- 使用
Object.defineProperty(Vue2)或Proxy(Vue3)实现数据劫持 - 建立
Dep和Watcher的依赖收集关系
3. 编译模板(仅运行时+编译器版本需要)
// 编译过程伪代码
const ast = parse(template) // 生成抽象语法树
optimize(ast) // 静态节点标记
const code = generate(ast) // 生成渲染函数
-
输入:
template字符串(如<div>{{message}}</div>) -
输出:可执行的
render函数(如_c('div', [_v(_s(message))]))问题:
- Vue 挂载时,
template、render和el的关系是什么? - 如果同时提供了
template和render,优先级如何?
关键点:
- 优先级:
render>template>el内部的 HTML。
- Vue 挂载时,
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性能提升主要是通过哪几方面体现的?⭐️⭐️
-
响应式系统优化 用
Proxy替代 Vue2 的Object.defineProperty- 原生支持监听对象新增 / 删除属性、数组索引变化,无需手动调用
Vue.set,减少额外代码开销。 - 只在访问属性时才递归触发响应式(懒代理),避免 Vue2 中初始化时对所有属性递归劫持的性能损耗,尤其适合大对象。
- 原生支持监听对象新增 / 删除属性、数组索引变化,无需手动调用
-
编译阶段优化 编译器对模板进行静态分析,生成更高效的渲染代码:Vue3 的渲染速度比 Vue2 提升约 55%,
- 静态节点标记:识别并标记纯静态内容(如固定文本),运行时跳过比对,直接复用。
- 动态节点区块化:将模板拆分为 “区块”,每个区块只包含动态节点,更新时仅遍历动态部分,减少虚拟 DOM 比对范围。
- 事件缓存:对无内联表达式的事件(如
@click="handleClick")缓存函数引用,避免每次渲染创建新函数导致的虚假更新。
-
体积优化(Tree-shaking 支持) 采用 ES 模块语法,支持 Tree-shaking:
- 只打包项目中实际使用的 API(如未用
Teleport则不打包相关代码),核心库体积比 Vue2 减少约 40%。
- 只打包项目中实际使用的 API(如未用
Vue3相比较于vue2,在编译阶段有哪些改进?⭐️⭐️
Vue3 在编译阶段的改进主要围绕提升渲染性能和优化代码体积展开,通过对模板的静态分析和编译优化,减少运行时的计算开销。以下是核心改进:
1. 静态节点标记与复用
- Vue2:无论节点是否静态(内容不变),每次更新都会参与虚拟 DOM 的比对,产生不必要的性能消耗。
- Vue3:编译时会标记静态节点(如纯文本、无动态绑定的元素),生成
_hoisted_前缀的常量,运行时直接复用这些节点,跳过比对过程。例:<div>静态文本</div>会被标记为静态节点,更新时不重新渲染。
2. 动态节点区块化(Block Tree)
- Vue2:虚拟 DOM 比对时需遍历整个节点树,效率低。
- Vue3:编译时将模板拆分为 “区块(Block)”,每个区块内只包含动态节点(有
v-bind、v-if等动态绑定的节点)。运行时更新时,只需遍历区块内的动态节点,无需处理整个树,大幅减少比对次数。例:一个包含多个静态元素和少量动态数据的列表,只会对动态数据所在节点进行比对。
3. 按需生成代码(Tree-shaking 友好)
- Vue2:编译器会生成固定结构的渲染函数,包含所有可能用到的运行时 API,即使某些功能未使用也会打包。
- Vue3:编译时根据模板中使用的功能(如是否有
v-for、v-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 算法仅在同层级节点之间比较,深度优先,不会跨层级递归。如果发现节点层级变化(如父节点不同),则直接销毁旧节点并创建新节点。
2. 双端比较策略
在对比子节点时,Vue 采用 双端指针(头尾指针) 策略,通过四次快速对比减少遍历次数:
- 头头对比:新旧头节点对比。
- 尾尾对比:新旧尾节点对比。
- 旧头与新尾对比:若匹配,将旧头节点移动到尾部。
- 旧尾与新头对比:若匹配,将旧尾节点移动到头部。
比较的过程中,循环从两边向中间收拢 具体详情见pdf文档
3. Key标识节点
通过 key 复用相同节点, 避免不必要的销毁和重建。
二、Diff 算法的具体步骤
1. 节点类型不同
若新旧节点标签名不同(如 div vs span),直接替换整个节点及其子树。
// 伪代码
if (oldVnode.tag !== newVnode.tag) {
replaceNode(oldVnode, newVnode);
}
2. 节点类型相同
若标签名相同,比较属性和子节点差异。
2.1 属性更新
对比新旧节点的属性差异(如 class、style),仅更新变化的属性。
// 伪代码
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 个静态节点直接跳过,对比次数骤减。
总结
- 核心优化点:Vue3 新增 PatchFlags 静态标记,只对比动态节点,Vue2 无标记、全量对比;
- 列表对比优化:Vue3 用最长递增子序列减少 DOM 移动,Vue2 列表对比效率较低;
- 性能结果:Vue3 diff 算法在大型列表 / 复杂页面中,对比次数减少约 90%,渲染性能提升显著。
三、使用场景与表现差异
-
Vue2 在组件层级不深、节点结构稳定时表现良好;
-
Vue3 在结构复杂、更新频繁的页面中性能更优,尤其适合:
- 动态长列表;
- 表格拖拽重排;
- Fragment、Teleport 场景。
四、常见误区
- ❌ 认为 Vue2 与 Vue3 的 Diff 算法本质一样(Vue3 是全新设计);
- ❌ 误以为 Vue3 不再需要 key(实际上依然推荐使用 key 明确标识);
- ❌ 忽视了编译优化与运行时性能的协同配合;
- ❌ 把 PatchFlag 误认为是手动添加的,实际是 Vue 编译器自动生成;
答题要点
- 说明 Diff 的本质作用及常见策略;
- 明确 Vue2 和 Vue3 在 Diff 实现上的关键区别;
- 能结合
PatchFlag、LIS等术语深入讲解 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 的响应式系统(
effect,track,trigger),但入口形式和封装语义不同。
选择原则:基础类型用 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 是如何递归地处理嵌套对象的?
-
核心机制:Proxy 本身无递归能力,Vue 3 采用**惰性递归(懒代理)**实现深度响应式 —— 仅在读取(
get拦截)嵌套对象属性时,才对该嵌套对象动态创建 Proxy 代理,而非初始化时一次性递归所有层级。 -
执行流程:
- 初始化仅代理根对象;
- 读取嵌套对象(如
obj.b)时,触发根 Proxy 的get,检测到值为对象则递归调用reactive()创建嵌套对象的 Proxy; - 深层嵌套(如
obj.b.d)会重复上述逻辑,最终所有嵌套对象都被 Proxy 拦截。
-
性能优化:
- 惰性代理避免初始化时递归大对象的性能损耗;
- 通过
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 步
-
数据劫持:beforeCreate 后初始化响应式系统,通过
Object.defineProperty对data对象的属性设置getter/setter,监听属性读写。这个过程发生Observe中。 -
依赖收集:编译模板时,为每个指令 / 插值表达式创建
Watcher,触发getter将Watcher存入Dep(依赖管理器)。这个过程发生在Compile中。 -
触发更新:数据变更时触发
setter,Dep通知所有Watcher,Watcher更新对应 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 触发更新]
双向绑定的核心原理
双向绑定基于以下两种技术实现:
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:监听指定数据,触发自定义回调。
关键逻辑解释:
- 初始化组件时,会创建一个组件渲染 Watcher,执行
get()方法。 get()方法将当前 Watcher 赋值给Dep.target(全局唯一),然后执行组件的渲染函数。- 渲染函数中会读取响应式数据,触发数据的
get方法,调用dep.depend(),将Dep.target(当前 Watcher)加入 Dep 的订阅列表。 - 当数据被修改时,触发
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 收集依赖的简化实现
核心逻辑解释:
- 调用
effect(fn)时,创建ReactiveEffect实例,执行run()方法。 run()方法将activeEffect设为当前 Effect,然后执行传入的fn(如组件渲染函数)。fn中访问响应式数据时,触发 Proxy 的get拦截器,调用track(),将activeEffect加入该数据属性对应的 Effect 集合。- 当数据被修改时,触发 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中移除自己,避免内存泄漏和无效更新。
总结
- 核心角色:Vue 2 中
Dep管理依赖列表、Watcher代表依赖的逻辑;Vue 3 中用track/trigger替代Dep,Effect替代Watcher,核心都是 “收集依赖 - 触发更新”。 - 依赖收集触发时机:组件初始化渲染时,执行渲染函数访问响应式数据,触发数据的读取拦截,将当前组件的 Watcher/Effect 加入依赖列表。
- 视图更新联动:数据修改触发写入拦截,通知所有依赖的 Watcher/Effect 重新执行渲染函数,通过 VNode 对比(patch)更新真实 DOM,且 Vue 会通过异步批处理优化更新性能。
- 跟踪机制:通过 “数据属性 -> 依赖容器 -> 组件 Watcher/Effect” 的精准映射,实现组件级别的依赖跟踪和更新,避免全局无效渲染。
Vue3 响应式系统的实现原理是什么?与 Vue2 有何不同?⭐️⭐️
Vue 3 的响应式系统主要包括以下几个核心概念:
- Proxy:使用 Proxy 对象来拦截对象的读取和修改操作,通过定义 get 和 set 方法来实现对数据变化的自动追踪。
- Reflect:通过 Reflect API 来实现对对象属性的读取和修改操作,提供了与
Object.defineProperty类似的功能,但更加强大和灵活。 - activeEffect:一个全局变量,用于保存当前正在执行的 effect 函数,以便在追踪依赖时使用。
- 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 没有对数组索引进行拦截,而是通过重写数组原型上的方法(如
push、pop、splice等)来监听数组的变更。但这种方式只能覆盖通过这些方法修改数组的场景,直接修改索引(例如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 中用于在对象上设置属性并确保新属性是响应式的方法。其实现原理可以简化为以下几个步骤:
- 处理数组情况: 如果目标是数组,并且键是有效的数组索引,使用
splice方法添加新元素以保持响应性。 - 处理已有属性: 如果属性已经存在于对象中,直接赋值。
- 处理新属性: 如果目标对象不是响应式对象,直接赋值新属性。
- 添加响应式新属性: 如果目标对象是响应式的,通过
defineReactive方法将新属性定义为响应式。这包括定义 getter 和 setter。 - 通知依赖更新: 调用
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
三、核心总结
- Vue2:初始化时全量深度递归,不管属性是否被访问,都提前绑定
get/set,大对象 / 深层级时性能差; - 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.$set、Vue.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,否则在严格模式下会报错,这是 Proxyset拦截器的规范要求。
总结
- get 拦截器:读取数据时,先收集 “谁在用这个数据”(依赖收集),再返回数据,同时处理嵌套对象的懒代理;
- set 拦截器:修改 / 新增数据时,先判断值是否真变化,再更新数据,最后通知 “用到这个数据的地方” 更新(触发更新);
- 两者配合是 Vue 3 响应式的核心,相比 Vue 2 的
Object.defineProperty,能天然支持新增属性、嵌套对象等场景。
Vue 有了数据响应式,为何还要 diff ?
Vue 的数据响应式系统和虚拟 DOM 的 diff 算法是两个不同的概念,解决的是不同的问题:
- 数据响应式:Vue 的响应式系统通过
Object.defineProperty(Vue 2.x)或Proxy(Vue 3.x)实现,它使得数据的变化能够自动通知视图更新。这种响应式机制确保了数据和视图的一致性,但并不直接处理 DOM 的更新。 - 虚拟 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 */) ]) ) }
源码学习建议
-
调试技巧
# 克隆源码 git clone https://github.com/vuejs/core.git # 安装依赖 pnpm install # 构建开发版本 pnpm run dev通过
examples目录创建测试用例逐步调试 -
关键断点位置
- 响应式追踪:
reactivity/src/effect.ts第 170 行 - 虚拟 DOM 创建:
runtime-core/src/vnode.ts第 156 行 - 组件挂载:
runtime-core/src/renderer.ts第 1350 行
- 响应式追踪:
总结
Vue 源码通过以下设计实现高效开发:
- 响应式系统:自动追踪依赖关系
- 虚拟 DOM:最小化 DOM 操作成本
- 组件化架构:高内聚低耦合的代码组织
- 编译优化:在构建时提前优化运行时性能
深入源码可帮助开发者:
- 精准定位复杂问题
- 编写高性能 Vue 代码
- 定制特殊功能(如自定义指令)
- 理解框架设计哲学(如渐进式理念)
建议结合官方源码解析文档(如 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 组件库)。
- 需要极致性能优化(如大数据量列表渲染)。
- 与 TypeScript 或 JSX 深度集成。
建议
- 优先使用模板:对大多数业务场景,模板更直观且易于维护。
- 必要时切换 Render 函数:当模板无法简洁实现功能时,再使用 Render 函数。
- 结合 JSX:通过 JSX 提升 Render 函数的可读性。
说说你对vue的template编译的理解?🌟
一、模板编译的三个核心阶段
1. 解析阶段(Parse)
- 输入:模板字符串(如
<div>{{msg}}</div>) - 输出:抽象语法树(AST)
- 核心过程:
- 词法分析:通过正则表达式拆分模板字符串为 Token(标签、属性、文本等)
- 语法分析:构建嵌套的 AST 节点树,记录标签层级关系
- 示例转换:
→ 解析为 AST:<!-- 模板 --> <div class="container"> <span v-if="show">{{ message }}</span> </div>{ 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→ 循环语句)
- 递归遍历 AST,生成
- 生成结果示例:
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 的本质
-
语法糖
JSX 会被编译为h()函数(即createElement),生成虚拟 DOM 节点。例如:const element = <div>Hello Vue</div>; // 编译后: const element = h('div', {}, 'Hello Vue'); -
动态能力
可直接在标签内嵌入 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' };
}
};
五、性能优化策略
-
避免内联函数
// ❌ 每次渲染创建新函数 <button onClick={() => this.handleClick(id)} /> // ✅ 提前绑定 <button onClick={this.handleClick.bind(this, id)} /> -
合理使用 Fragment
render() { return ( <> <Header /> <MainContent /> </> ); } -
Memo 优化
const MemoComponent = Vue.memo(({ data }) => ( <ExpensiveComponent data={data} /> ));
六、工程化配置
-
Babel 插件
安装@vue/babel-plugin-jsx:npm install @vue/babel-plugin-jsx -D -
TypeScript 支持
配置tsconfig.json:{ "compilerOptions": { "jsx": "preserve", "jsxFactory": "h" } }
七、适用场景分析
-
动态表单生成器
需要递归渲染复杂表单结构时,JSX 的编程能力更高效。 -
可视化拖拽编辑器
动态节点操作与复杂状态管理更易实现。 -
高阶组件(HOC)
通过函数式组合生成增强组件。 -
跨平台渲染
结合@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,更新内容 -->
- 问题:所有节点都会被重新渲染,即使
B和C只是位置变化。若节点包含状态(如表单输入),会导致状态错乱。
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> <!-- 新建节点 -->
- 优化点:
- 复用
B和C的DOM,仅移动位置。 - 仅新建
D节点,避免不必要的渲染。
- 复用
4. key与组件状态
当节点为组件时,key决定了组件实例是否复用:
- 相同
key:复用组件实例,触发更新(updated生命周期)。 - 不同
key:销毁旧实例,创建新实例(触发created和mounted)。
<!-- 切换组件时,不同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提供了一种方式在更新完成后执行代码。
3. 底层实现原理
3.1 异步更新队列
- Vue在数据变化时不会立即更新DOM,而是将组件更新函数推入一个队列(异步任务队列)
- 在同一事件循环中多次修改数据,只会合并一次DOM更新,减少重复渲染。
- 多个
$nextTick调用的执行顺序: 如果在同一个事件循环中调用多个$nextTick,它们会被推到同一个队列中,依次执行。Vue 会在下次 DOM 更新完成后,按顺序依次执行所有的$nextTick回调。
3.2 实现机制
Vue通过以下步骤实现$nextTick:
- 微任务优先:优先使用微任务(Microtask)实现异步延迟:
- 支持
Promise时,用Promise.then()。 - 否则降级到
MutationObserver或setImmediate。
- 支持
- 宏任务兜底:在不支持微任务的环境下,使用宏任务(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 的事件循环机制:
- 将更新任务推入队列
数据变化时,Vue 不会立即触发更新,而是将需要更新的组件标记为dirty,并加入异步队列。 - 在下一个事件循环中执行更新
通过微任务(如Promise.then)或宏任务(如setTimeout)执行队列中的更新任务,确保当前代码执行完毕后再处理渲染。
4. nextTick 的作用
- 访问更新后的 DOM
由于更新是异步的,直接通过this.xxx = value修改数据后立即访问 DOM,可能得到的是旧值。此时需要使用Vue.nextTick或this.$nextTick等待更新完成:this.message = '更新后的值'; this.$nextTick(() => { console.log(this.$el.textContent); // 输出更新后的 DOM 内容 }); - 实现原理
nextTick将回调函数推入与组件更新相同的异步队列,确保在 DOM 更新后执行。
5. 异步更新的实现细节
- 优先使用微任务(Microtask)
现代浏览器中,Vue 默认通过Promise.then实现微任务队列,保证更新在同步代码执行后、UI 渲染前完成。 - 降级策略
在不支持Promise的环境中,Vue 会降级为MutationObserver或setTimeout。
6. 与 React 的对比
- React 的异步更新模式
React 从 18 版本开始默认启用并发模式(Concurrent Mode),更新任务可中断并分片执行,但核心理念与 Vue 类似:避免同步更新导致的性能问题。 - Vue 的简单性
Vue 的异步更新策略更简单直接,开发者无需手动优化,框架自动处理批量更新。
总结
Vue 通过异步更新组件实现了:
- 性能优化:合并多次数据变更,减少渲染次数。
- 一致性保证:确保依赖关系和 DOM 状态正确。
- 开发者友好:通过
nextTick提供访问更新后 DOM 的能力。
这种设计平衡了性能与开发体验,是 Vue 响应式系统的核心机制之一。
异步组件的加载过程
问题:
- Vue 中的异步组件是如何工作的?它如何实现懒加载?
- 在使用异步组件时,如果加载失败,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 组件 });
异步组件的性能优化
问题:
- 如何优化异步组件的加载性能?可以使用哪些策略?
- 在多个异步组件的情况下,如何确保它们高效加载?
考察点:
-
懒加载与拆分: 通过按需加载和拆分大型组件,可以减少初始页面加载的资源量。例如,对于一个大页面,可以把页面拆分成多个异步组件,只有在需要渲染时才加载。
-
预加载: 如果你知道某个组件即将被使用,可以使用 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. 核心流程
- 生成虚拟 DOM:将模板或 JSX 转换为虚拟 DOM 树。
- Diff 对比:比较新旧虚拟 DOM 的差异。
- 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 的局限性
- 内存开销:需维护虚拟 DOM 树,占用额外内存。
- 简单场景不适用:静态页面直接操作 DOM 更高效。
Proxy的set函数如何拦截数组的push操作
一、考察点
- 理解 Proxy 拦截机制,尤其是对数组操作的捕获
- 掌握数组
push方法的底层本质(其实是通过set操作新增或修改数组元素和length属性) - 能正确通过 Proxy 的
set捕获数组元素新增和长度变化 - 了解如何针对数组操作做定制化处理或限制
二、参考答案
2.1 核心原理说明
-
JavaScript 中数组的
push方法本质是修改数组的“length”属性和新增元素(数组索引属性) -
当执行
arr.push(value)时,触发两次set:- 新索引位置设置新元素值
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 API | Axios |
|---|---|---|---|
| 技术类型 | 原生 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?
-
全局配置
统一设置 baseURL、超时时间、请求头(如自动添加 Token)。 -
拦截器扩展
在请求前添加 Loading 状态,或在响应后自动解析数据格式。 避免在每个请求中重复编写错误处理代码,通过拦截器集中处理 HTTP 错误、业务逻辑错误等 -
增强安全性
防止 CSRF/XSRF,自动刷新 Token,或实现请求重试机制。 -
简化调用
封装后可通过api.get('/user')直接调用,隐藏底层细节(如 URL 拼接、参数处理)。 -
解耦依赖
后续替换为其他 HTTP 库(如fetch)时,只需修改封装层,无需改动业务代码。
二、主要封装哪些方面?
-
默认配置
const instance = axios.create({ baseURL: 'https://api.example.com', timeout: 5000, headers: { 'X-Custom-Header': 'value' } }); -
请求拦截器
用于添加 Token、修改请求参数:instance.interceptors.request.use(config => { config.headers.Authorization = `Bearer ${getToken()}`; return config; }); -
响应拦截器
处理响应数据、统一错误码: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); } ); -
封装 API 方法
提供更简洁的调用方式:export const get = (url, params) => instance.get(url, { params }); export const post = (url, data) => instance.post(url, data); -
取消请求
通过CancelToken或AbortController取消重复请求。
三、Axios 封装原理
-
基于 Axios 实例
** 通过axios.create()创建独立实例**,避免污染全局配置。 -
拦截器链式调用
Axios 内部通过Promise链式调用拦截器,请求拦截器按添加顺序执行,响应拦截器按相反顺序执行。 -
适配器模式
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 处理的全局错误(如 setTimeout、Promise)。
底层逻辑:通过浏览器原生 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)中的错误会被单独捕获,通过 Promise 或 setTimeout 抛到全局。
// src/core/util/next-tick.js
function flushCallbacks() {
try {
callbacks.forEach(cb => cb());
} catch (e) {
handleError(e, null, 'nextTick');
}
}
3. 生命周期钩子错误传播
生命周期钩子(如 created、mounted)中的错误会被 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 | 捕获未处理的脚本错误 |
| 异步级 | unhandledrejection | Promise 未处理拒绝 |
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. 异步代码错误遗漏
- 问题:
setTimeout或Promise中的错误未被捕获。 - 解决:使用
window.onerror和unhandledrejection兜底。
五、总结: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 缓存静态化列表
五、性能分析工具
-
Vue Devtools 性能面板
- 组件渲染时间分析
- 事件追踪
-
Chrome Performance Monitor
- FPS 实时监控
- CPU/Memory 分析
-
自定义性能标记
// 关键操作打点
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 |
|---|---|---|---|
| 未优化 | 3200ms | 450MB | 8 |
| 虚拟滚动 | 80ms | 120MB | 60 |
| 虚拟滚动+冻结数据 | 65ms | 90MB | 60 |
决策树:如何选择优化方案?
是否需要交互式滚动?
├── 是 → 虚拟滚动 + 冻结数据
└── 否 →
数据是否需要动态更新?
├── 是 → 分页加载 + 组件复用
└── 否 → 服务端渲染 + 静态化
通过组合上述优化策略,可以将大数据量场景下的渲染性能提升 10-50 倍。关键要点:
- 数据与渲染解耦:虚拟滚动是处理可视区域的银弹方案
- 响应式控制:最小化 Vue 的依赖追踪开销
- 分层优化:从数据预处理到像素渲染的全链路优化
- 工具赋能:善用性能分析工具定位瓶颈
vue-loader是什么?它有什么作用?
vue-loader 是 Vue 生态中核心的构建工具,专门用于解析和转换 Vue 的单文件组件(.vue 文件)。它是 Webpack 的一个加载器(Loader),在 Vue 项目中扮演着关键角色。以下是它的核心作用和工作原理详解:
一、vue-loader 的核心作用
1. 解析 .vue 文件
- 单文件组件拆分:将
.vue文件拆解为template、script、style三个独立模块。 - 代码分块处理:
<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 的工作原理
构建流程示例:
- Webpack 匹配
.vue文件:// webpack.config.js module: { rules: [ { test: /\.vue$/, loader: 'vue-loader' } ] } - vue-loader 解析
.vue文件:- 提取
<template>→ 生成render函数。 - 提取
<script>→ 转成 JavaScript 模块。 - 提取
<style>→ 通过css-loader、sass-loader等处理。
- 提取
- 输出浏览器可执行的代码。
三、典型配置示例
1. 使用官方脚手架(Vue CLI)创建的项目
会自动集成 vue-loader。Vue CLI(@vue/cli)是官方推荐的项目构建工具,它默认集成了完整的 Vue 工程化配置,包括:
vue-loader(用于解析.vue单文件组件)- 配套的
vue-template-compiler(编译模板部分) - 以及 webpack 相关的 loader(如
css-loader、babel-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)需要对应一个明确的根元素,多根元素会导致挂载目标不明确。
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 访问 |
使用场景
-
纯展示型组件
仅依赖 props 渲染内容,无需内部状态或交互逻辑。<template functional> <div class="tag" :style="{ color: props.color }"> {{ props.text }} </div> </template> -
高性能列表项
渲染大量列表项时,减少实例化开销。<template functional> <li class="list-item"> {{ props.item.name }} </li> </template> -
高阶组件(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'];
性能优化原理
-
无实例化
跳过组件实例创建过程(无this上下文),节省内存和初始化时间。 -
无响应式追踪
不依赖 Vue 的响应式系统,避免依赖收集和触发更新的开销。 -
更快的渲染
直接执行渲染函数,无生命周期钩子调用,适合高频渲染场景。
注意事项
-
无法使用计算属性和侦听器
需通过外部传递计算结果。 -
限制模板功能
无法直接使用v-model,需手动实现事件绑定。 -
调试信息较少
无组件实例,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(多页面应用)对比
| 对比维度 | SPA | MPA |
|---|---|---|
| 页面数量 | 仅一个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 的核心原理是基于组件实例的原型链 / 依赖注入系统,核心逻辑可简单概括为:
- 数据存储:父组件通过
provide定义要暴露的数据 / 方法,会将其挂载到当前组件实例的provides对象上;若父组件有上级也用了provide,则当前provides会继承上级的provides(形成链式结构)。 - 数据查找:子组件(无论层级多深)通过
inject声明需要的依赖时,Vue 会从当前组件实例开始,向上遍历组件树的provides链,直到找到匹配的依赖项(找不到则返回默认值或undefined)。
简单说,就是父组件把数据 “提供” 到自己的实例上,后代组件 “注入” 时向上逐层找这个数据,本质是跨层级的依赖传递,绕开了 props 逐级透传的问题,且不受组件层级限制。
补充:Vue2 和 Vue3 底层实现略有差异(Vue3 基于 injectKey 精准查找,Vue2 基于对象原型链),但核心逻辑都是 “父存数据、子向上找”。
Vue2 和 Vue3中的问题
-
createAppAPI (应用实例) :- Vue 2: 通过
new Vue()创建应用实例,所有配置都是全局的。 - Vue 3: 引入
createApp函数来创建应用实例。每个应用实例都有自己的独立作用域,允许在同一个页面上挂载多个独立的 Vue 应用,避免了全局污染。 - 优势: 更好的模块化和隔离性,方便构建微前端应用或在多框架环境中集成 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避免命名冲突、支持按需注入等
总结
- 语法上:Vue2 仅支持选项式 API,Vue3 兼容选项式且新增组合式 API 用法,更灵活;
- 响应式上:Vue2 需额外处理才能实现响应式同步,Vue3 配合
ref/reactive天然支持完善响应式; - 核心体验:Vue3 的
provide/inject功能更强大、使用更简洁,解决了 Vue2 中的诸多痛点,更适合复杂项目的跨层级数据共享。
请详细的讲一讲keep-alive? 🌟
在Vue.js中,<keep-alive>是一个内置的抽象组件,用于缓存不活动的组件实例,避免重复渲染,从而提升应用性能。
一、核心作用
- 组件缓存
当包裹动态组件或路由视图时,<keep-alive>会缓存非活跃的组件实例,而不是销毁它们。这意味着:- 组件的状态(如数据、DOM状态)会被保留。适用于高频切换的组件(如Tab页、路由视图),减少DOM操作和初始化渲染时间
- 避免重复执行
created和mounted生命周期钩子,减少性能开销。
二、基础用法
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;
七、注意事项
- 组件命名:
include和exclude依赖组件的name选项,确保组件已正确命名。 - 嵌套路由:缓存整个路由视图时,需注意子路由组件的缓存策略。
- 性能权衡:过度缓存可能导致内存占用过高,需合理设置
max。