Vue学习笔记

126 阅读10分钟
  1. 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 是连接数据和视图的桥梁。
    • 分为三种:
      • 渲染 Watchervm._watcher,负责更新视图(updateComponent)。
      • 计算属性 Watchercomputed watcher,惰性求值,依赖变化时标记为 dirty。
      • 用户 Watchervm.$watch 或 watch 选项创建的,用于执行用户定义的回调。
    • 在 getter 中触发依赖收集,在 setter 触发时执行 update()

    数据访问与变更

    • 读取数据(触发 getter)
      • 如果当前有 Dep.target(比如在渲染函数中读取),则将当前 Watcher 添加到该属性的 Dep 中。
    • 修改数据(触发 setter)
      • 执行 dep.notify(),通知所有 subs 中的 Watcher 进行更新。
      • Watcher 调用 update(),进入异步更新队列(nextTick)。

    异步更新队列(nextTick)

    • Vue 将所有数据变更后的 Watcher 推入一个队列。
    • 在下一个事件循环(tick)中批量执行更新,避免重复渲染。
    • 这就是为什么 this.$nextTick() 能让你在 DOM 更新后执行回调。

    简化版的 Vue 2 响应式系统实现

  2. Array 追踪变化的方式和 Object 不一样,因为数组中的一些方法会直接修改数组,而不会触发对象的 setter,所以 Vue2 通过创建拦截器去覆盖数组原型的方式来追踪变化,重写其中的 7 个会改变数组的方法:pushpopshiftunshiftsplicesortreverse。但有局限性,当通过索引或直接修改数组长度时,仍无法被检测到。

  3. Object.defineProperty(obj, prop, descriptor),它接收三个参数。obj:要定义属性的对象。它只接收对象;prop:要定义或修改的属性的名称或 Symboldescriptor: 要定义或修改的属性描述符。descriptor 是一个对象,它定义和修改指定的属性,它包含以下的键值,来对原对象进行数据劫持,即对象会执行这里面的逻辑。
    每次只能监控一个属性,初始化时需要循环递归遍历 obj 中的所有属性;无法检测动态属性新增和删除,无法很好的支持数组。
    descriptor 是一个对象,包含以下可选键(不能同时使用 value/writableget/set。因为 value/writableget/set 代表了两种互斥的属性管理模式。一个属性不能同时既有“直接存储的值”,又由“函数来动态计算和控制读写”。):

    类型说明
    valueany属性的值,默认 undefined
    writableboolean是否可写(赋值),默认 false
    enumerableboolean是否可枚举(for...in),默认 false
    configurableboolean是否可配置(删除、修改描述符),默认 false
    getfunctiongetter 函数,读取属性时调用
    setfunctionsetter 函数,设置属性时调用
  4. 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 obj
    ownKeys(target)使用 Object.keys()for...in 等枚举属性时触发支持对象枚举的响应式
    apply(target, thisArg, args)代理目标为函数时,调用函数触发拦截函数调用,如 fn()
    construct(target, args, newTarget)使用 new 调用代理目标时触发拦截构造函数调用
  5. 模板编译分为三部分阶段,解析器:将模板字符串解析为 抽象语法树(Abstract Syntax Tree, AST)优化器:遍历 AST 标记静态节点,以便在渲染时跳过比对;代码生成器:将优化后的 AST 转换成可执行的渲染函数,返回 VNode。当组件需要重新渲染时,就会执行这个 render 函数,生成新的 VNode 树,然后与旧的 VNode 进行 diffpatch,更新真实 DOM

    编译时机

    • 运行时编译:在浏览器中动态编译模板(需要 compiler 版本)。
    • 预编译:构建时(如 Vue CLI、Vite)提前编译成 render 函数,提升运行时性能(推荐)。

    SSR:服务端渲染也需要编译模板生成 render 函数。
    JSX:如果你用 JSX,其实是跳过了模板编译,直接写 render 函数。

  6. Vue 实例初始化

    • 初始化 Vue 实例(vm)的事件与属性;
    • 触发生命周期钩子 beforeCreate
    • 初始化 inject状态provide,这里的状态指的是 propsmethodsdatacomputed 以及 watch
    • 触发生命周期钩子 created
    • 判断用户是否在参数中提供了 el 选项,如果提供了 el,则调用 vm.$mount 方法,如果没有提供 el,则需要手动调用 vm.$mount 方法。
    • $mount 阶段:编译与挂载
      • 模板编译,将模板(template 或 el 的 innerHTML)编译成 render 函数。
      • 触发 beforeMount 钩子
      • 创建渲染 Watcher,执行 render() 生成 VNode
      • 触发 mounted 钩子
  7. props 的验证(包括 typerequireddefaultvalidator)发生在beforeCreate 之前,因此在这些函数中无法访问 this 上的任何属性。

  8. 如果 v-on 写在组件标签上,那么这个事件会注册到子组件 Vue 事件系统中;如果是写在 HTML 标签上,例如div,那么事件会被注册到浏览器事件上。
    在实例初始化阶段,初始化事件 initEvents 指的是父组件在模板中使用 v-on 监听子组件内触发的事件。
    在 Vue 2 中,如果希望监听组件根元素的原生事件(如 click),需要使用 .native 修饰符。父组件传入的所有.nativev-on 监听器会自动收集到子组件的 $listeners 中,常用于透传事件
    在 Vue 3 中,普通DOM事件(例如 clickinputchange, 等)将自动将它们作为原生 DOM 事件附加到组件的根元素上,不再需要 .native。组件的 $attrs$listeners 合并进 $attrs$attrs 包含了所有未被声明为 props 的属性和事件监听器。

  9. 组件的 Watcher 不是直接观察计算属性用到的数据的变化,而是让计算属性的 Watcher 得到通知后,计算一次计算属性的值,如果发现这一次计算出来的值与上一次计算出来的值不一样,再主动通知组件的 Watcher 进行重新渲染的操作。只有计算属性的返回值真的变了,才会重新执行渲染函数。

  10. <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,性能极高

  11. 将路由配置中的 props 设置为 true 时,route.params 会自动被映射为组件的 props,组件就可以像接收普通 props 一样接收路由参数,而无需通过 $route.params 手动读取。

  12. 完整的导航解析流程:

    • 导航被触发;
    • 在失活的组件里调用 beforeRouteLeave 守卫;
    • 调用全局的 beforeEach 守卫;
    • 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+);
    • 在路由配置里调用 beforeEnter
    • 解析异步路由组件
    • 在被激活的组件里调用 beforeRouteEnter
    • 调用全局的 beforeResolve 守卫(2.5+);
    • 导航被确认;
    • 调用全局的 afterEach 钩子;
    • 触发 DOM 更新;
    • 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
  13. watch 无法提供引用类型变化前的“值快照”,因为 oldValue 只是一个引用,对象本身已被修改。

    • 方法一 :创建一个变量,在 watch 的 handler 中使用,每次变化后更新它。
    • 方法二 :创建一个计算属性,它返回原对象的深拷贝。然后侦听这个计算属性。
  14. 在 Vue 组件的生命周期中(通常在 createdmountedsetup 中),当调用 this.$watch() 来创建一个侦听器时,它会返回一个函数。调用这个返回的函数,就可以立即停止这个侦听器。

  15. v-model 在文本输入框 (type="text") 和文本域 (textarea) 上会默认得到字符串。为了将用户输入的数字字符串自动转换为 JavaScript 数字类型,可以使用 .number 修饰符。对于数字输入框 (type="number")v-model 默认就会得到数字。

  16. export default 返回的对象是对组件的配置描述,在其他组件中使用时,会根据描述创建组件实例,在不同组件中使用都会创建新的实例。

  17. v-bind 绑定的值为 nullundefinedfalse 时,对应的 HTML attribute 会被从渲染的 DOM 元素上移除。

  18. Vue项目中动态修改 document.title

    • 方法一 :在需要修改的路由中通过 meta 传递 title,在 router.beforeEach 中修改 document.title = to.meta.title
    • 方法二 :在 main.js 中定义全局自定义指令,自定义指令中修改 document.title,在需要修改的页面根结点使用自定义指令,传参 title
  19. Vue 2 中,Vue.filter() 是一个用于创建全局过滤器的 API。在 Vue 3 中,过滤器 ( filters ) 已经被官方正式移除,可以用计算属性 (computed properties)  或方法 (methods)  替代。

  20. Vue 2中的三种 Watcher

    • Render Watcher:在组件的 mount 阶段,当首次执行组件的 render 函数(或 updateComponent)时,Vue 会创建一个 Render Watcher。负责追踪模板中所有依赖的数据(datapropscomputed 等),并在这些数据变化时,重新执行 render 函数以生成新的 VNode,从而触发视图更新;
    • Computed Watcher:当组件实例化时,Vue 会为每个 computed 选项中的函数创建一个 Computed Watcher。缓存计算属性的值,并实现惰性求值
    • User Watcher:通过 vm.$watch() API 或在组件选项的 watch 对象中定义时创建。执行用户自定义的副作用 。当监听的数据变化时,执行用户提供的回调函数 (handler)。
  21. Vue 事件修饰符

    • .prevent - 阻止默认行为
    • .capture - 使用事件捕获模式
    • .stop - 阻止事件冒泡
    • .self - 只当事件在该元素本身触发时触发
    • .once - 只触发一次
    • .passive - 以被动模式添加事件侦听器
  22. Vue 的更新流程

    • 数据变更
      • 当响应式数据(datarefreactive 对象等)被修改时,会触发其 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) :元素的外观(颜色、背景)发生变化,但不影响布局。
      • 时机:这个过程由浏览器在微任务队列清空之后自动触发。
    • 执行UI渲染
      • 合成与显示:浏览器的渲染引擎将计算好的布局和绘制结果合成,并在下一个屏幕刷新周期(通常 16.6ms @ 60Hz)将最终画面显示在屏幕上。
      • requestAnimationFrame:可以使用 requestAnimationFrame 回调来在浏览器下一次重绘前执行代码。
  23. Diff算法通过只更新变化的部分,避免了全量DOM重建的 O(n) 性能灾难,为虚拟DOM的更新操作设定了一个可接受的性能下限

  24. 简单 Diff 算法

    • 遍历新节点列表,对每个新节点,在旧节点列表中查找 key 相同且尚未被复用的节点。
      • 若找到:
        • 比较该节点在旧列表中的索引与当前已处理节点的最大索引(maxIndex):
          • 如果旧索引 小于 maxIndex,说明其相对位置已发生变化,需标记为移动操作;
          • 否则,视为顺序未变,仅复用节点,并更新 maxIndex 为该旧索引。
      • 若未找到匹配节点,则标记为新建操作。
    • 完成新节点遍历后,再次扫描旧节点列表,将所有未被复用的节点标记为删除操作。
  25. 双端 Diff 算法(Vue 2)

    • 定义四个指针:oldStartIdxoldEndIdxnewStartIdxnewEndIdx,分别指向旧节点列表和新节点列表的头部和尾部。
    • 循环执行条件为:oldStartIdx <= oldEndIdxnewStartIdx <= newEndIdx。只要新旧列表都还有未处理的节点,就继续进行比较。
      在每一轮循环中,按以下顺序依次尝试匹配:
      • 新头 vs 旧头newStartNode.key === oldStartNode.key
        若匹配成功,复用该节点,无需移动 DOM,oldStartIdxnewStartIdx 同时向后移动。
      • 新尾 vs 旧尾newEndNode.key === oldEndNode.key
        若匹配成功,复用节点,oldEndIdxnewEndIdx 同时向前移动。
      • 新尾 vs 旧头newEndNode.key === oldStartNode.key
        若匹配成功,说明旧列表的头节点移动到了新列表的尾部。将 oldStartNode 对应的 DOM 移动到 oldEndNode 之后,oldStartIdx 向后移动,newEndIdx 向前移动。
      • 新头 vs 旧尾newStartNode.key === oldEndNode.key
        若匹配成功,说明旧列表的尾节点移动到了新列表的头部。将 oldEndNode 对应的 DOM 移动到 oldStartNode 之前,oldEndIdx 向前移动,newStartIdx 向后移动。
      • 任意一轮匹配成功后,当前节点视为“已处理”,对应指针按规则移动,进入下一轮循环。
      • 如果上述四种情况均未匹配成功,则进入“查找模式”:在旧节点列表的剩余区间 [oldStartIdx, oldEndIdx] 中,查找是否存在 keynewStartNode.key 相同的节点(记为 idxInOald)。
        • 若找到匹配节点:
          • 将该节点对应的 DOM 移动到 oldStartNode 之前(即插入到当前头部位置);
          • 标记 oldChildren[idxInOld] 为已处理;
          • newStartIdx 向后移动一位(oldStartIdx 保持不变)。
        • 若未找到:
          • 说明该节点为全新节点,需创建新的 DOM 元素;
          • 将新 DOM 插入到 oldStartNode 之前;
          • newStartIdx 向后移动一位。
    • 当循环结束后(即 oldStartIdx > oldEndIdxnewStartIdx > newEndIdx),进行收尾清理:
      • 如果旧节点列表仍有剩余(oldStartIdx <= oldEndIdx): 说明这些节点在新列表中已不存在,需遍历并删除从 oldStartIdxoldEndIdx 的所有旧节点。
      • 如果新节点列表仍有剩余(newStartIdx <= newEndIdx): 说明是新增节点,需将 newStartIdxnewEndIdx 范围内的所有新节点批量插入到 oldEndNode 之后。
  26. 快速 Diff 算法(Vue 3)

    • 从开头进行while循环遍历,对比新旧节点,不同时跳出循环;
    • 从尾部进行while循环遍历,对比新旧节点,不同时跳出循环;
    • 判断新旧节点列表是否有一方已处理完毕:
      • 如果旧节点已处理完(即 i > oldEnd),而新节点还有剩余(newEnd >= i),说明剩余部分均为新增节点,需将 [i, newEnd] 范围内的新节点全部创建并插入到 DOM 中;
      • 如果新节点已处理完(即 i > newEnd),而旧节点还有剩余(oldEnd >= i),说明剩余部分均为不再使用的节点,需将 [i, oldEnd] 范围内的旧节点全部卸载(删除 DOM);
      • 若新旧节点中间仍有未处理的部分(即 i <= oldEndi <= newEnd),则进入中间部分处理阶段
        • 遍历旧节点剩余区间 [i, oldEnd],建立 key → 索引Map
        • 遍历新节点剩余区间 [i, newEnd],根据其在旧节点列表中的索引值生成 source 列表;
        • source 数组计算其最长递增子序列的索引列表,记为 seqseq 中的索引对应 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²)。
  27. 在虚拟 DOM 的 Diff 过程中,即使节点可以复用(即 sameNode 判断为 true ,也必须通过 patch 函数更新其内容,因为节点的属性、文本、子节点等可能已经发生变化。Diff 决定“是否复用和移动”, Patch 决定“如何更新内容”

  28. Vue 2v-for 优先级高,v-if 在循环内部判断,性能不佳Vue 3不允许 v-if 和 v-for 同时出现在同一元素上,强制开发者使用更优方案(如计算属性)。

  29. 默认情况下,provide/inject 不是响应式的。Vue 2:通过 computed 包裹 provide 的值来实现响应式。Vue 3:直接传递 ref 或 reactive 对象,天然具备响应性。使用 provide 提供数据时,可以使用 readonly 进行包裹,以防后代组件对数据进行修改。只能阻止直接修改,不能阻止替换引用

  30. Vue 3 中 defineComponent 不仅提供了更加灵活和细粒度的组件定义方式,还加强了与 TypeScript 的集成,进行类型限制和类型推导。用于在 选项式 API + TS 时使用。在 <script setup lang="ts"> 中,通常不再显式使用 defineComponent,而是使用宏definePropsdefineEmits等。

    • defineExpose:在 <script setup> 模式下,所有声明的变量、函数默认都是私有的,不会暴露给父组件。即使父组件使用 ref 获取到子组件实例,也无法访问其内部的响应式数据或方法。defineExpose 用于显式地向父组件暴露当前组件的属性或方法,以便父组件通过模板引用(ref)进行访问。
    • defineEmits:在 <script setup> 模式下,用于在组件内部定义可以触发的自定义事件,有助于提高代码的可读性和可维护性,还能提供参数的类型安全。但 Vue 并不会阻止通过 emit 触发未在 defineEmits 中定义的自定义事件,只作为推荐规范,引导开发者写出更清晰、更安全的组件通信代码。
  31. $nextTick 利用 JavaScript 的事件循环(Event Loop)机制,确保回调函数在 DOM 更新完成后异步执行。Vue 2:通过 PromiseMutationObserver 等实现微任务优先,降级使用 setTimeoutVue 3:优先使用标准化的 queueMicrotask,语义更清晰,实现更简洁。

    • MutationObserver 是一个强大的 Web API,用于监视 DOM 树的变化。它允许你观察一个或多个 DOM 元素的属性、子节点或文本内容的变化,并在变化发生时执行回调函数。
    • queueMicrotask 是一个现代的 JavaScript Web API,用于将函数添加到当前任务队列的末尾,作为微任务(microtask)执行。它提供了一种标准化的方式来调度微任务,相比使用 Promise.then() 更加直接和高效。
  32. 在 Vue 开发中,将通用型组件(如按钮、弹窗、加载指示器、通知提示等)通过插件化方式全局注册是一种常见且推荐的做法。这种方式能够显著提升组件的复用性和开发效率,使这些组件在项目的任意模板中直接使用,而无需在每个使用位置手动导入和局部注册。
    实现这一模式的核心步骤如下:

    • 创建插件文件:定义一个包含 install 方法的对象(或函数),该方法会在插件被安装时自动调用。
    • 在 install 方法中注册组件:通过传入的 Vue 应用实例(app),使用 app.component() 方法将通用组件逐一注册为全局组件。
    • 在项目入口文件中安装插件:通过调用 app.use(Plugin) 将插件注册到应用实例上,完成全局组件的初始化。
  33. 在 Vue 3 中,app.use() 是注册全局插件的主要途径(在 Vue 2 中对应的是 Vue.use())。当执行 app.use() 安装插件时,Vue 会进行重复安装检测:它会检查该插件是否已被标记为已安装。只有在未安装的情况下,才会调用插件的 install 方法,从而避免重复注册带来的性能损耗或状态冲突。
    install 方法接收两个参数:

    • 第一个参数:当前 Vue 应用实例(app),用于调用 app.componentapp.directiveapp.provide 等 API;
    • 第二个参数(可选) :用户传入的配置选项,可用于定制插件行为(如主题、默认配置等)。

    一旦 install 方法执行完毕,Vue 会将该插件实例记录到内部的已安装插件集合中,完成“已安装”状态的标记。

  34. 使用 Pinia 进行状态管理时,在组件中直接解构 store 的状态(state)和 getter 会导致其失去响应式,可通过 Pinia 提供的 storeToRefs 方法进行解构,以确保解构后的属性仍是 ref 对象。

  35. scrollBehavior 是 Vue Router 的一个配置选项,允许在路由变更时自定义页面的滚动行为。接受三个参数:目标路由 (to)、源路由 (from) 和保存的滚动位置 (savedPosition)。可以基于这些参数决定页面应该滚动到什么位置。