-
Vue 2 响应式系统核心流程
Observer(观察者)
- 当 Vue 实例被创建时,
initData阶段会调用observe(data)方法。 observe函数会为data对象创建一个Observer实例。Observer的作用是劫持对象的所有属性,通过Object.defineProperty将它们转换为 getter/setter。- 它还负责递归地观察对象的嵌套属性(深度观测)。
Dep(依赖收集器)
- 每个被观察的属性都会对应一个
Dep实例(依赖收集器)。 Dep内部维护一个subs数组,用来存储依赖于该属性的Watcher。- 在
getter中调用Dep.target(当前活跃的 Watcher)并将其加入subs,完成依赖收集。
Watcher(观察者)
Watcher是连接数据和视图的桥梁。- 分为三种:
- 渲染 Watcher:
vm._watcher,负责更新视图(updateComponent)。 - 计算属性 Watcher:
computed watcher,惰性求值,依赖变化时标记为 dirty。 - 用户 Watcher:
vm.$watch或watch选项创建的,用于执行用户定义的回调。
- 渲染 Watcher:
- 在
getter中触发依赖收集,在setter触发时执行update()。
数据访问与变更
- 读取数据(触发 getter) :
- 如果当前有
Dep.target(比如在渲染函数中读取),则将当前Watcher添加到该属性的Dep中。
- 如果当前有
- 修改数据(触发 setter) :
- 执行
dep.notify(),通知所有subs中的Watcher进行更新。 Watcher调用update(),进入异步更新队列(nextTick)。
- 执行
异步更新队列(nextTick)
- Vue 将所有数据变更后的
Watcher推入一个队列。 - 在下一个事件循环(tick)中批量执行更新,避免重复渲染。
- 这就是为什么
this.$nextTick()能让你在 DOM 更新后执行回调。
- 当 Vue 实例被创建时,
-
Array追踪变化的方式和Object不一样,因为数组中的一些方法会直接修改数组,而不会触发对象的setter,所以Vue2通过创建拦截器去覆盖数组原型的方式来追踪变化,重写其中的 7 个会改变数组的方法:push、pop、shift、unshift、splice、sort、reverse。但有局限性,当通过索引或直接修改数组长度时,仍无法被检测到。 -
Object.defineProperty(obj, prop, descriptor),它接收三个参数。obj:要定义属性的对象。它只接收对象;prop:要定义或修改的属性的名称或Symbol;descriptor: 要定义或修改的属性描述符。descriptor是一个对象,它定义和修改指定的属性,它包含以下的键值,来对原对象进行数据劫持,即对象会执行这里面的逻辑。
每次只能监控一个属性,初始化时需要循环递归遍历obj中的所有属性;无法检测动态属性新增和删除,无法很好的支持数组。
descriptor是一个对象,包含以下可选键(不能同时使用value/writable和get/set。因为value/writable和get/set代表了两种互斥的属性管理模式。一个属性不能同时既有“直接存储的值”,又由“函数来动态计算和控制读写”。):键 类型 说明 valueany属性的值,默认 undefinedwritableboolean是否可写(赋值),默认 falseenumerableboolean是否可枚举( for...in),默认falseconfigurableboolean是否可配置(删除、修改描述符),默认 falsegetfunctiongetter 函数,读取属性时调用 setfunctionsetter 函数,设置属性时调用 -
Proxy(target,handler)接收两个参数。target:要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理);handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理的行为。
常见的handler拦截方法(Traps):方法 触发时机 Vue 3 中的应用 get(target, key, receiver)读取属性时触发 收集依赖(track),实现响应式数据读取 set(target, key, value, receiver)设置属性时触发 触发更新(trigger),派发视图更新 deleteProperty(target, key)删除属性时触发 支持 delete obj.key的响应式has(target, key)使用 in操作符时触发拦截 key in objownKeys(target)使用 Object.keys()、for...in等枚举属性时触发支持对象枚举的响应式 apply(target, thisArg, args)代理目标为函数时,调用函数触发 拦截函数调用,如 fn()construct(target, args, newTarget)使用 new调用代理目标时触发拦截构造函数调用 -
模板编译分为三部分阶段,解析器:将模板字符串解析为
抽象语法树(Abstract Syntax Tree, AST);优化器:遍历AST标记静态节点,以便在渲染时跳过比对;代码生成器:将优化后的AST转换成可执行的渲染函数,返回VNode。当组件需要重新渲染时,就会执行这个render函数,生成新的VNode树,然后与旧的VNode进行diff和patch,更新真实DOM。编译时机:
- 运行时编译:在浏览器中动态编译模板(需要
compiler版本)。 - 预编译:构建时(如 Vue CLI、Vite)提前编译成
render函数,提升运行时性能(推荐)。
SSR:服务端渲染也需要编译模板生成
render函数。
JSX:如果你用 JSX,其实是跳过了模板编译,直接写render函数。 - 运行时编译:在浏览器中动态编译模板(需要
-
Vue 实例初始化
- 初始化 Vue 实例(
vm)的事件与属性; - 触发生命周期钩子
beforeCreate; - 初始化
inject、状态和provide,这里的状态指的是props、methods、data、computed以及watch; - 触发生命周期钩子
created; - 判断用户是否在参数中提供了
el选项,如果提供了el,则调用vm.$mount方法,如果没有提供el,则需要手动调用vm.$mount方法。 $mount阶段:编译与挂载- 模板编译,将模板(
template或el的 innerHTML)编译成render函数。 - 触发
beforeMount钩子。 - 创建渲染
Watcher,执行render()生成VNode。 - 触发
mounted钩子。
- 模板编译,将模板(
- 初始化 Vue 实例(
-
props的验证(包括type、required、default、validator)发生在beforeCreate之前,因此在这些函数中无法访问this上的任何属性。 -
如果
v-on写在组件标签上,那么这个事件会注册到子组件Vue事件系统中;如果是写在HTML标签上,例如div,那么事件会被注册到浏览器事件上。
在实例初始化阶段,初始化事件initEvents指的是父组件在模板中使用v-on监听子组件内触发的事件。
在 Vue 2 中,如果希望监听组件根元素的原生事件(如click),需要使用.native修饰符。父组件传入的所有非.native的v-on监听器会自动收集到子组件的$listeners中,常用于透传事件。
在 Vue 3 中,普通DOM事件(例如click,input,change, 等)将自动将它们作为原生 DOM 事件附加到组件的根元素上,不再需要.native。组件的$attrs和$listeners合并进$attrs,$attrs包含了所有未被声明为props的属性和事件监听器。 -
组件的
Watcher不是直接观察计算属性用到的数据的变化,而是让计算属性的Watcher得到通知后,计算一次计算属性的值,如果发现这一次计算出来的值与上一次计算出来的值不一样,再主动通知组件的Watcher进行重新渲染的操作。只有计算属性的返回值真的变了,才会重新执行渲染函数。 -
<transition-group>组件在实现列表排序或拖拽重排动画时,巧妙地应用了 FLIP 的核心思想,从而实现流畅的视觉效果。- 使用
<transition-group>包裹列表,通过key跟踪元素。 - 当列表顺序变化时:
- 记录每个元素的原始位置(First)
- DOM 更新到新顺序(Last)
- 计算位移差,用
transform: translate()把元素“拉回”原位(Invert) - 移除
transform,触发 CSS 过渡,实现平滑移动(Play)
- 默认应用
v-move类,可通过 CSS 控制动画时长和缓动。
FLIP 是一种高性能动画模式,代表:
- First:记录元素初始位置;
- Last:立即跳到最终布局;
- Invert:用
transform将元素“拉回”原位; - Play:播放动画,让元素平滑移动到目标位置;
核心优势:避免重排(reflow),仅使用
transform,性能极高。 - 使用
-
将路由配置中的
props设置为true时,route.params会自动被映射为组件的props,组件就可以像接收普通 props 一样接收路由参数,而无需通过$route.params手动读取。 -
完整的导航解析流程:
- 导航被触发;
- 在失活的组件里调用
beforeRouteLeave守卫; - 调用全局的
beforeEach守卫; - 在重用的组件里调用
beforeRouteUpdate守卫(2.2+); - 在路由配置里调用
beforeEnter; - 解析异步路由组件;
- 在被激活的组件里调用
beforeRouteEnter; - 调用全局的
beforeResolve守卫(2.5+); - 导航被确认;
- 调用全局的
afterEach钩子; - 触发
DOM更新; - 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入。
-
watch无法提供引用类型变化前的“值快照”,因为oldValue只是一个引用,对象本身已被修改。- 方法一 :创建一个变量,在
watch的handler中使用,每次变化后更新它。 - 方法二 :创建一个计算属性,它返回原对象的深拷贝。然后侦听这个计算属性。
- 方法一 :创建一个变量,在
-
在 Vue 组件的生命周期中(通常在
created、mounted或setup中),当调用this.$watch()来创建一个侦听器时,它会返回一个函数。调用这个返回的函数,就可以立即停止这个侦听器。 -
v-model在文本输入框 (type="text") 和文本域 (textarea) 上会默认得到字符串。为了将用户输入的数字字符串自动转换为 JavaScript 数字类型,可以使用.number修饰符。对于数字输入框 (type="number"),v-model默认就会得到数字。 -
export default返回的对象是对组件的配置描述,在其他组件中使用时,会根据描述创建组件实例,在不同组件中使用都会创建新的实例。 -
当
v-bind绑定的值为null、undefined或false时,对应的 HTML attribute 会被从渲染的 DOM 元素上移除。 -
Vue项目中动态修改
document.title:- 方法一 :在需要修改的路由中通过
meta传递title,在router.beforeEach中修改document.title = to.meta.title; - 方法二 :在
main.js中定义全局自定义指令,自定义指令中修改document.title,在需要修改的页面根结点使用自定义指令,传参title。
- 方法一 :在需要修改的路由中通过
-
在 Vue 2 中,
Vue.filter()是一个用于创建全局过滤器的 API。在 Vue 3 中,过滤器 (filters) 已经被官方正式移除,可以用计算属性 (computed properties) 或方法 (methods) 替代。 -
Vue 2中的三种 Watcher
- Render Watcher:在组件的
mount阶段,当首次执行组件的render函数(或updateComponent)时,Vue 会创建一个 Render Watcher。负责追踪模板中所有依赖的数据(data、props、computed等),并在这些数据变化时,重新执行render函数以生成新的 VNode,从而触发视图更新; - Computed Watcher:当组件实例化时,Vue 会为每个
computed选项中的函数创建一个 Computed Watcher。缓存计算属性的值,并实现惰性求值; - User Watcher:通过
vm.$watch()API 或在组件选项的watch对象中定义时创建。执行用户自定义的副作用 。当监听的数据变化时,执行用户提供的回调函数 (handler)。
- Render Watcher:在组件的
-
Vue 事件修饰符
.prevent- 阻止默认行为;.capture- 使用事件捕获模式;.stop- 阻止事件冒泡;.self- 只当事件在该元素本身触发时触发;.once- 只触发一次;.passive- 以被动模式添加事件侦听器。
-
Vue 的更新流程
- 数据变更
- 当响应式数据(
data,ref,reactive对象等)被修改时,会触发其setter(Vue 2) 或Proxy set拦截器 (Vue 3)。
- 当响应式数据(
- 队列更新
- 通知依赖:响应式系统会通知所有依赖于该数据的 Watcher (Vue 2) 或 Effect (Vue 3)。
- 推入队列:这些 Watcher/Effect 会被推入一个内部的更新队列 (queue) 中。Vue 会确保同一个 Watcher/Effect 在一个事件循环内只被推入一次(去重)。
- 调度
nextTick:Vue 会调用nextTick(flushSchedulerQueue),将一个清空更新队列的函数 (flushSchedulerQueue) 推入微任务队列。此时,真实的 DOM 更新逻辑 (flushSchedulerQueue) 已经被安排在了微任务队列中等待执行。
- 执行微任务
- 当前同步代码执行完毕。
- 浏览器清空微任务队列:这是事件循环的一部分。
- 执行
flushSchedulerQueue:这个函数是 Vue 更新流程的核心,它会在微任务阶段同步执行以下操作:- 排序队列:通常按组件的更新优先级(父组件优先)对队列进行排序。
- 执行 Watcher/Effect:遍历队列,执行每个 Watcher/Effect 的
run方法。 - 更新虚拟 DOM:对于渲染相关的 Watcher/Effect,会重新执行
render函数,生成新的 VNode 树。 - Diff 算法:对比新旧 VNode 树,找出差异。
- Patch (更新真实 DOM) :根据 Diff 结果,直接操作 DOM API 来更新真实 DOM。
- 重绘和回流
- 触发:当真实 DOM 被
Patch操作修改后,如果涉及到:- 回流 (Reflow/Re-layout) :元素的几何属性(宽、高、位置)发生变化,需要重新计算布局。
- 重绘 (Repaint) :元素的外观(颜色、背景)发生变化,但不影响布局。
- 时机:这个过程由浏览器在微任务队列清空之后自动触发。
- 触发:当真实 DOM 被
- 执行UI渲染
- 合成与显示:浏览器的渲染引擎将计算好的布局和绘制结果合成,并在下一个屏幕刷新周期(通常 16.6ms @ 60Hz)将最终画面显示在屏幕上。
requestAnimationFrame:可以使用requestAnimationFrame回调来在浏览器下一次重绘前执行代码。
- 数据变更
-
Diff算法通过只更新变化的部分,避免了全量DOM重建的
O(n)性能灾难,为虚拟DOM的更新操作设定了一个可接受的性能下限。 -
简单 Diff 算法
- 遍历新节点列表,对每个新节点,在旧节点列表中查找
key相同且尚未被复用的节点。- 若找到:
- 比较该节点在旧列表中的索引与当前已处理节点的最大索引(
maxIndex):- 如果旧索引 小于
maxIndex,说明其相对位置已发生变化,需标记为移动操作; - 否则,视为顺序未变,仅复用节点,并更新
maxIndex为该旧索引。
- 如果旧索引 小于
- 比较该节点在旧列表中的索引与当前已处理节点的最大索引(
- 若未找到匹配节点,则标记为新建操作。
- 若找到:
- 完成新节点遍历后,再次扫描旧节点列表,将所有未被复用的节点标记为删除操作。
- 遍历新节点列表,对每个新节点,在旧节点列表中查找
-
双端 Diff 算法(Vue 2)
- 定义四个指针:
oldStartIdx、oldEndIdx、newStartIdx、newEndIdx,分别指向旧节点列表和新节点列表的头部和尾部。 - 循环执行条件为:
oldStartIdx <= oldEndIdx且newStartIdx <= newEndIdx。只要新旧列表都还有未处理的节点,就继续进行比较。
在每一轮循环中,按以下顺序依次尝试匹配:- 新头 vs 旧头:
newStartNode.key === oldStartNode.key
若匹配成功,复用该节点,无需移动 DOM,oldStartIdx和newStartIdx同时向后移动。 - 新尾 vs 旧尾:
newEndNode.key === oldEndNode.key
若匹配成功,复用节点,oldEndIdx和newEndIdx同时向前移动。 - 新尾 vs 旧头:
newEndNode.key === oldStartNode.key
若匹配成功,说明旧列表的头节点移动到了新列表的尾部。将oldStartNode对应的 DOM 移动到oldEndNode之后,oldStartIdx向后移动,newEndIdx向前移动。 - 新头 vs 旧尾:
newStartNode.key === oldEndNode.key
若匹配成功,说明旧列表的尾节点移动到了新列表的头部。将oldEndNode对应的 DOM 移动到oldStartNode之前,oldEndIdx向前移动,newStartIdx向后移动。 - 任意一轮匹配成功后,当前节点视为“已处理”,对应指针按规则移动,进入下一轮循环。
- 如果上述四种情况均未匹配成功,则进入“查找模式”:在旧节点列表的剩余区间
[oldStartIdx, oldEndIdx]中,查找是否存在key与newStartNode.key相同的节点(记为idxInOald)。- 若找到匹配节点:
- 将该节点对应的 DOM 移动到
oldStartNode之前(即插入到当前头部位置); - 标记
oldChildren[idxInOld]为已处理; newStartIdx向后移动一位(oldStartIdx保持不变)。
- 将该节点对应的 DOM 移动到
- 若未找到:
- 说明该节点为全新节点,需创建新的 DOM 元素;
- 将新 DOM 插入到
oldStartNode之前; newStartIdx向后移动一位。
- 若找到匹配节点:
- 新头 vs 旧头:
- 当循环结束后(即
oldStartIdx > oldEndIdx或newStartIdx > newEndIdx),进行收尾清理:- 如果旧节点列表仍有剩余(
oldStartIdx <= oldEndIdx): 说明这些节点在新列表中已不存在,需遍历并删除从oldStartIdx到oldEndIdx的所有旧节点。 - 如果新节点列表仍有剩余(
newStartIdx <= newEndIdx): 说明是新增节点,需将newStartIdx到newEndIdx范围内的所有新节点批量插入到oldEndNode之后。
- 如果旧节点列表仍有剩余(
- 定义四个指针:
-
快速 Diff 算法(Vue 3)
- 从开头进行while循环遍历,对比新旧节点,不同时跳出循环;
- 从尾部进行while循环遍历,对比新旧节点,不同时跳出循环;
- 判断新旧节点列表是否有一方已处理完毕:
- 如果旧节点已处理完(即
i > oldEnd),而新节点还有剩余(newEnd >= i),说明剩余部分均为新增节点,需将[i, newEnd]范围内的新节点全部创建并插入到 DOM 中; - 如果新节点已处理完(即
i > newEnd),而旧节点还有剩余(oldEnd >= i),说明剩余部分均为不再使用的节点,需将[i, oldEnd]范围内的旧节点全部卸载(删除 DOM); - 若新旧节点中间仍有未处理的部分(即
i <= oldEnd且i <= newEnd),则进入中间部分处理阶段:- 遍历旧节点剩余区间
[i, oldEnd],建立key → 索引的Map; - 遍历新节点剩余区间
[i, newEnd],根据其在旧节点列表中的索引值生成source列表; - 对
source数组计算其最长递增子序列的索引列表,记为seq。seq中的索引对应source中那些相对顺序未变、无需移动的节点; - 从后往前处理移动操作:
i指向source的末尾,j指向seq的末尾。从后往前遍历source列表:- 若
source[i] === -1:
表示该新节点在旧列表中无对应节点 → 创建新 DOM 节点并插入; - 若
i === seq[j]:
表示该节点位于最长递增子序列中 → 相对顺序未变,无需移动,j--; - 否则:
表示该节点需要移动,此时i即代表该节点在新列表中的目标位置。
- 若
- 遍历旧节点剩余区间
- 如果旧节点已处理完(即
- Vue 3 的快速 Diff 算法通过“预处理头尾 + LIS 优化中间移动”的策略,最大限度地减少了 DOM 操作次数。相比 Vue 2 的双端 Diff,它在处理中间节点乱序、大规模更新等场景下性能更优,
- 时间复杂度:平均 O(n),和双端 Diff 相同;最坏 O(n log n),优于双端 Diff 的 O(n²)。
-
在虚拟 DOM 的 Diff 过程中,即使节点可以复用(即
sameNode判断为true) ,也必须通过patch函数更新其内容,因为节点的属性、文本、子节点等可能已经发生变化。Diff 决定“是否复用和移动”, Patch 决定“如何更新内容” 。 -
Vue 2:
v-for优先级高,v-if在循环内部判断,性能不佳。Vue 3:不允许v-if和v-for同时出现在同一元素上,强制开发者使用更优方案(如计算属性)。 -
默认情况下,
provide/inject不是响应式的。Vue 2:通过computed包裹provide的值来实现响应式。Vue 3:直接传递ref或reactive对象,天然具备响应性。使用provide提供数据时,可以使用readonly进行包裹,以防后代组件对数据进行修改。只能阻止直接修改,不能阻止替换引用。 -
Vue 3 中
defineComponent不仅提供了更加灵活和细粒度的组件定义方式,还加强了与 TypeScript 的集成,进行类型限制和类型推导。用于在 选项式 API + TS 时使用。在<script setup lang="ts">中,通常不再显式使用defineComponent,而是使用宏defineProps、defineEmits等。- defineExpose:在
<script setup>模式下,所有声明的变量、函数默认都是私有的,不会暴露给父组件。即使父组件使用ref获取到子组件实例,也无法访问其内部的响应式数据或方法。defineExpose用于显式地向父组件暴露当前组件的属性或方法,以便父组件通过模板引用(ref)进行访问。 - defineEmits:在
<script setup>模式下,用于在组件内部定义可以触发的自定义事件,有助于提高代码的可读性和可维护性,还能提供参数的类型安全。但 Vue 并不会阻止通过emit触发未在defineEmits中定义的自定义事件,只作为推荐规范,引导开发者写出更清晰、更安全的组件通信代码。
- defineExpose:在
-
$nextTick利用 JavaScript 的事件循环(Event Loop)机制,确保回调函数在 DOM 更新完成后异步执行。Vue 2:通过Promise、MutationObserver等实现微任务优先,降级使用setTimeout。Vue 3:优先使用标准化的queueMicrotask,语义更清晰,实现更简洁。MutationObserver是一个强大的 Web API,用于监视 DOM 树的变化。它允许你观察一个或多个 DOM 元素的属性、子节点或文本内容的变化,并在变化发生时执行回调函数。queueMicrotask是一个现代的 JavaScript Web API,用于将函数添加到当前任务队列的末尾,作为微任务(microtask)执行。它提供了一种标准化的方式来调度微任务,相比使用Promise.then()更加直接和高效。
-
在 Vue 开发中,将通用型组件(如按钮、弹窗、加载指示器、通知提示等)通过插件化方式全局注册是一种常见且推荐的做法。这种方式能够显著提升组件的复用性和开发效率,使这些组件在项目的任意模板中直接使用,而无需在每个使用位置手动导入和局部注册。
实现这一模式的核心步骤如下:- 创建插件文件:定义一个包含
install方法的对象(或函数),该方法会在插件被安装时自动调用。 - 在
install方法中注册组件:通过传入的 Vue 应用实例(app),使用app.component()方法将通用组件逐一注册为全局组件。 - 在项目入口文件中安装插件:通过调用
app.use(Plugin)将插件注册到应用实例上,完成全局组件的初始化。
- 创建插件文件:定义一个包含
-
在 Vue 3 中,
app.use()是注册全局插件的主要途径(在 Vue 2 中对应的是Vue.use())。当执行app.use()安装插件时,Vue 会进行重复安装检测:它会检查该插件是否已被标记为已安装。只有在未安装的情况下,才会调用插件的install方法,从而避免重复注册带来的性能损耗或状态冲突。
install方法接收两个参数:- 第一个参数:当前 Vue 应用实例(
app),用于调用app.component、app.directive、app.provide等 API; - 第二个参数(可选) :用户传入的配置选项,可用于定制插件行为(如主题、默认配置等)。
一旦
install方法执行完毕,Vue 会将该插件实例记录到内部的已安装插件集合中,完成“已安装”状态的标记。 - 第一个参数:当前 Vue 应用实例(
-
使用 Pinia 进行状态管理时,在组件中直接解构 store 的状态(state)和 getter 会导致其失去响应式,可通过 Pinia 提供的
storeToRefs方法进行解构,以确保解构后的属性仍是ref对象。 -
scrollBehavior是 Vue Router 的一个配置选项,允许在路由变更时自定义页面的滚动行为。接受三个参数:目标路由 (to)、源路由 (from) 和保存的滚动位置 (savedPosition)。可以基于这些参数决定页面应该滚动到什么位置。