最近面试一直被问这个东西都快被问麻了,想着去总结一下 Vue2 和 Vue3 的区别,以便更好地将它们进行横向对比。

# 主要区别

  1. 性能提升:
    • Vue3 在性能方面有了显著的提升,主要得益于新的响应式系统。Vue3 使用了 Proxy 对象来实现响应式,相比于 Vue2 的 Object.defineProperty,在大型应用中能够更高效地追踪状态的变化。
  2. Composition API:
    • Vue3 引入了 Composition API,这是一个新的 API 风格,使得组件的逻辑可以更好地组织和重用。相比于 Vue2 的 Options API,Composition API 更灵活、可读性更好,并且能更好地处理复杂的逻辑。
  3. Typescript 支持:
    • Vue3 在设计之初就考虑了对 Typescript 的更好支持,使得在使用 Typescript 时的开发体验更加流畅。Vue2 对 Typescript 的支持也较为局限,需要额外的配置和声明文件。
  4. 更小的体积:
    • Vue3 在体积方面进行了优化,运行时的体积更小,这也使得 Vue3 更适合在移动端或对体积要求较高的场景中使用。
  5. 更好的 TypeScript 支持:
    • Vue3 对 TypeScript 的支持更加完善。它使用 TypeScript 重写了代码库,并提供了更好的类型推断和支持。这使得在使用 TypeScript 时更容易捕获错误并提高了开发体验。
  6. Teleport 组件:
    • Vue3 引入了 Teleport 组件,可以方便地将子组件移动到父组件以外的 DOM 中,这在处理模态框、弹出式菜单等场景中非常有用。
  7. 编译器优化:
    • Vue3 的编译器经过了优化,生成的代码更加高效,这导致了更快的渲染速度和更小的包大小。
  8. Tree-shaking 支持:
    • Vue3 改进了对 Tree-shaking 的支持,可以更好地消除未使用的代码,进一步减小最终打包的体积。

# 响应式机制的不同

# Vue2

在 Vue2 中,响应式机制是通过 ObserverWatcherCompiler 这三个关键部分实现的。

  1. Observer:
    • 作用:Observer 的职责是将数据对象中的 所有属性 转换为可观测对象。它通过递归遍历数据对象的每一个属性,并使用 Object.defineProperty() 方法将它们转换为 getter/setter。这个过程中, 每个属性 都会被赋予依赖管理的能力。
    • 功能:当属性被访问(get)时,Observer 会进行 依赖收集 ,即记录哪些 Watcher 依赖于这个属性。当属性被修改(set)时,它会通知所有依赖的 Watcher 进行更新。
  2. Watcher:
    • 作用:Watcher 的角色是作为一个 中介 ,它观察数据的变化。在 Vue 中,每个 组件实例 都至少有一个 Watcher 实例,它会在组件渲染过程中把自身注册到所依赖的数据属性的 Observer 上。
    • 功能:当数据发生变化时,Watcher 会收到通知,并执行相应的更新操作,如重新渲染视图或执行计算属性等。
  3. Compile:
    • 作用:Compile 负责 解析模板指令 (如 v-modelv-bind 等),并把模板中的变量与组件实例数据关联起来。
    • 功能:在编译阶段,Compile 会遍历模板中的所有节点,解析指令和插值表达式,并 创建相应的 Watcher 。每当模板引用的数据变化时,Watcher 会触发回调,更新 DOM。

相互配合的工作流程

  • 初始化阶段:在 Vue 组件的挂载过程中,Compile 解析模板,并为模板中每一个需要动态更新的部分创建 Watcher。
  • 依赖收集:当 Compile 解析到需要动态数据的部分时,它会读取相应的数据属性,触发 Observer 中定义的 getter。Getter 中会将当前的 Watcher 添加到这个数据属性的依赖列表中。
  • 更新触发:当数据变化时,setter 被触发,通知所有依赖于该属性的 Watcher 执行更新函数,这通常涉及对视图的重新渲染或其他逻辑的执行。

# Vue3

Vue3 的响应式系统与 Vue2 在实现机制上有显著的不同。Vue3 使用了更现代的 JavaScript 特性 ——Proxy,来替代 Vue2 中的 Object.defineProperty 方法。

# Vue3 响应式系统的工作原理

  1. 使用 Proxy:

    • Vue3 的响应式系统基于 Proxy 对象构建。Proxy 允许你拦截并自定义对象属性的基本操作,如属性读取、设置、删除等。
    • 当创建一个响应式对象时(通过 reactiveref 方法),Vue3 会返回这个原始对象的 Proxy 版本。这个 Proxy 控制着对其数据的所有访问和修改,使得 Vue 能够追踪变化并在必要时重新渲染组件。
  2. 响应式引用(Refs):

    • Vue3 引入了 ref ,这是一种包装基本值(如字符串、数字等)以使其成为响应式的方式。 ref 返回一个对象,其中包含一个名为 value 的属性。当访问或修改 value 时,Vue 可以追踪这些操作并触发视图更新。
  3. Reactive 函数:

    • reactive 函数用于创建复杂类型(如对象或数组)的响应式版本。这与 Vue2 中的 Vue.observable() 类似,但由于 Proxy 的使用, reactive 可以更智能地处理嵌套属性和数组。
  4. 依赖跟踪和触发更新:

    • Vue3 使用一个全新的依赖追踪系统。每当响应式对象的属性被读取时,Vue 会记录当前渲染的组件为这个属性的依赖。当属性被修改时,Vue 会通知所有依赖这个属性的组件重新渲染。

# Vue2 与 Vue3 响应式系统的主要区别

  1. 实现技术:

    • Vue2 使用 Object.defineProperty() 来定义响应式属性,只能监听到预先定义的属性的变化。
    • Vue3 使用 Proxy ,可以监听到所有类型的改变,包括属性的添加、删除和数组索引的修改,以及更深层次的变化。
  2. 性能:

    • Proxy 的性能通常优于 Object.defineProperty() ,尤其是在处理大型对象和深层嵌套的数据结构时。Proxy 只需要在初始化时处理一次,而 Object.defineProperty() 需要递归地处理对象的每个属性。
  3. 数组处理:

    • 在 Vue2 中,由于技术限制,特定的数组修改方法(如 pushpop )需要特殊处理才能触发更新。
    • 在 Vue3 中,由于 Proxy 能够拦截数组的修改操作,这些操作都自然而然地成为了响应式的,不需要额外的处理。
  4. 支持动态属性:

    • Vue2 不能自动将后来添加到对象的新属性变为响应式,除非使用 Vue.set
    • Vue3 允许动态添加的属性也自动成为响应式,因为 Proxy 会拦截所有属性的操作

# 核心组件

  1. 响应式对象(Reactive Objects):

    • 通过 reactiveref 方法,Vue3 把普通的 JavaScript 对象转换为响应式对象。这是通过包装这些对象在一个 Proxy 中实现的。Proxy 允许 Vue 拦截对象属性的访问和修改操作。
  2. 依赖收集(Dependency Tracking):

    • 每个响应式属性都关联一个 effect(副作用函数),这些 effect 会在属性被访问时执行。每个 effect 可以订阅多个属性,同时,一个属性也可以被多个 effect 订阅。
    • 当一个属性被访问(get 操作)时,当前正在执行的 effect(如果存在的话)会被添加到这个属性的依赖列表中。这是通过一个全局的 stack 来管理当前激活的 effect。
  3. 触发更新(Triggering Updates):

    • 当一个属性被修改(set 操作)时,所有订阅了该属性的 effect 会被触发。这通常导致组件的重新渲染或计算属性的重新计算。

# 实现细节

  • Effect Stack:

    • Vue 维护一个全局的 effect 栈来跟踪当前正在执行的 effect。这使得 Vue 可以处理嵌套的 effect 和正确地将依赖关系关联到正确的订阅者。
  • Track and Trigger:

    • track 函数用于在属性被访问时收集依赖。它检查当前是否有活跃的 effect,并将其添加到当前属性的依赖列表中。
    • trigger 函数用于在属性被修改时触发更新。它查找所有依赖于修改属性的 effect,并将它们排入调度队列(如果它们尚未被调度)。
  • 调度策略(Scheduling Strategy):

    • Vue 使用异步队列来调度 effect 的执行。这意味着即使一个属性多次修改,关联的 effect 只会被触发一次,从而避免不必要的重复渲染。

# Vue3 相较于 Vue2 的性能优化

  1. 响应式系统重写: Vue3 使用 Proxy 对象重写了响应式系统,代替了 Vue2 中的 Object.defineProperty。Proxy 可以拦截对象的任何属性的访问和修改操作,而不是只有预先定义的属性。这使得 Vue3 的响应式系统更加灵活和高效,特别是在处理动态添加或删除属性的场景。
  2. 编译优化: Vue3 的编译器进行了重大优化,引入了基于树摇(Tree-shaking)的编译过程。这意味着最终的生产包中只会包含应用中实际用到的 Vue 功能代码。此外,Vue3 的模板编译器还引入了更多的编译时优化,如静态提升(hoisting)和事件缓存,这减少了运行时的工作量。
  3. Fragment、Teleport 和 Suspense:
    • Fragment 允许组件返回多个根节点,减少了额外的 DOM 层级和相关的性能开销。
    • Teleport 是一个新的内置组件,允许开发者将子组件渲染到 DOM 树的其他位置,有助于解决诸如模态框等场景的性能和样式封装问题。
    • Suspense 支持异步组件的等待状态管理,改善了异步组件加载的用户体验和性能。
  4. 更有效的组件更新: Vue3 引入了基于静态树的分片(static tree hoisting)和事件监听器的静态提升。这些优化减少了虚拟 DOM 的比较范围和更新的复杂度。只有动态内容会参与到虚拟 DOM 的更新中,这大大减少了组件更新的计算量。
  5. 更小的体积:通过模块化的架构设计,Vue3 允许开发者只引入他们需要的功能,进一步减少了最终应用的体积。
  6. Composition API: Vue3 的 Composition API 不仅提供了更好的逻辑复用和代码组织方式,而且还减少了因使用 Options API 导致的不必要的重新渲染。开发者可以更精确地控制组件的依赖和副作用。

# Vue3 当中的 keep-alive 组件

在 Vue3 中, keep-alive 是一个内置组件,用于缓存非活动组件实例,而不是销毁它们。这样可以保持组件的状态,并在用户再次访问时,可以迅速恢复显示,而不需要重新创建组件实例。这对于提升性能特别有效,尤其是在需要频繁切换但又不希望状态丢失的组件(如标签页)中非常有用。

# 本质

keep-alive 包裹动态组件时,会对其做出缓存处理,使得组件在被切换出视图时不会被销毁。其核心功能是: 通过创建一个缓存对象来存储被包裹的组件实例

# 工作原理

  1. 缓存和复用

    • 当组件第一次渲染时, keep-alive 会将其实例缓存起来。如果组件的 props 或者 slots 没有发生变化,下次再渲染相同的组件时, keep-alive 会复用之前缓存的实例,而不是创建一个新的。
    • 这种方式显著减少了重新渲染的成本,因为组件的初始渲染通常比更新更加消耗资源。
  2. 生命周期钩子

    • keep-alive 特别管理了组件的生命周期钩子。被 keep-alive 包裹的组件不会触发常规的 destroyed (或 unmounted 在 Vue3 中)钩子。相反,它会触发特殊的生命周期钩子: activateddeactivated
    • 当组件被缓存时,会触发 deactivated 钩子;当再次被激活(复用)时,会触发 activated 钩子。
  3. LRU 缓存策略

    • keep-alive 可以配置 max 属性,限制缓存的组件实例数量。当存储的实例超过这个值时, keep-alive 会使用最近最少使用(LRU)的策略来移除缓存中最久未被访问的组件实例。
  4. 使用 includeexclude 属性管理缓存

    • keep-alive 提供了 includeexclude 属性,允许你指定哪些组件应该被缓存,哪些不应该。这些属性可以接受一个逗号分隔的字符串、正则表达式或者一个数组,便于灵活配置缓存策略。

# 缓存对象

在 Vue 的 keep-alive 组件中,"缓存对象" 是一个 JavaScript 对象,用来存储被缓存的组件实例。这个对象以组件的名字或者是其他唯一标识符作为键,而存储的值则是组件实例本身。这使得 keep-alive 能够在需要时快速检索和复用这些实例。

# 存储位置

这个缓存对象存在于 keep-alive 组件的内部状态中。当 keep-alive 组件被创建时,它会初始化这个缓存对象,并在其生命周期中维护这个缓存。具体来说,这个对象不是全局的,而是与每一个 keep-alive 实例关联,每个实例管理自己的缓存。

# 结构和管理

这个缓存对象的结构通常是这样的:

{
  "componentKey1": {
    instance: componentInstance1,
    vnode: componentVNode1
  },
  "componentKey2": {
    instance: componentInstance2,
    vnode: componentVNode2
  },
  // 更多被缓存的组件
}
  • Key(键):通常是组件的名称或者是一个由 keep-alive 自动生成的唯一标识符。
  • Value(值):包含组件的实例( instance )和该实例的虚拟节点( vnode )。虚拟节点是 Vue 的内部表示,包含了渲染输出和组件详细信息的对象。

# 功能实现

当一个组件由于 keep-alive 而被缓存时,它的实例和相关的虚拟节点会被存入这个缓存对象。当组件需要重新激活时, keep-alive 会检查这个缓存,如果找到相应的条目,就会复用存储的实例和虚拟节点,而不是重新创建新的。这样做可以显著减少重渲染的开销,特别是对于那些创建成本较高的组件。

此外, keep-alive 还管理这个缓存的大小和生命周期,确保不会因为长时间缓存过多的组件而消耗过多的内存。这是通过上述提到的 LRU(最近最少使用)策略或者通过 include/exclude 属性来实现的,这些属性允许开发者指定哪些组件应该被缓存或排除在缓存之外。

希望这个解释能帮助你更好地理解 keep-alive 组件中的缓存对象的工作原理和存储位置。如果你还有其他问题或需要更多细节,请随时提问!

# Vue3 当中的 slot 组件

在 Vue 中,slot 是一种非常重要的内容分发机制,允许我们将模板的一部分内容封装到可复用的组件中。这种模式广泛用于组件库和应用程序中,以实现布局的抽象化和内容的灵活插入。在 Vue3 中,slot 的概念得到了保留和增强,让我们来详细探讨一下。

# Slot 的本质

Slot 本质上是一个占位符,用于在组件内部预留位置,这样使用该组件的开发者可以插入自定义的内容或标记。这使得组件更加灵活,能够处理不同的内容结构和展示需求,而不必在组件内部硬编码所有可能的变体。

# Vue3 中的 Slot

在 Vue3 中,slot 的使用和 Vue2 类似,但内部实现更为高效,且 API 更加统一和功能更丰富。

  1. 基本使用

    • Slot 在组件模板中定义为 <slot> 元素。父组件可以在使用子组件时,将内容放入这些 <slot> 中。
    • 子组件中的 <slot> 标签可以包含默认内容。如果父组件没有提供相应的插槽内容,就会显示默认内容。
  2. 具名 Slot

    • Vue 允许定义多个具名 slot,每个 slot 通过 name 属性进行标识。这样,父组件可以针对不同的 slot 插入不同的内容。
    • 使用时,只需在父组件中通过 <template v-slot:slotName> 的形式指定内容对应的 slot。
  3. 作用域插槽

    • 作用域插槽允许子组件传递数据回到插槽内容中,这让插槽不仅能够展示内容,还能够基于子组件的内部状态动态地展示内容。
    • 它通过 <slot> 标签的一个特殊属性向父组件传递数据,父组件可以在 <template v-slot> 标签中使用这些数据。

# 示例

这里是一个 Vue3 的 slot 使用示例:

<!-- 子组件 -->
<template>
  <div>
    <h2>我是子组件的标题</h2>
    <slot name="main">默认内容</slot>
  </div>
</template>

<script>
export default {
  name: "ChildComponent",
};
</script>
<!-- 父组件 -->
<template>
  <div>
    <ChildComponent>
      <template v-slot:main>
        <p>这是通过具名 slot 插入的内容</p>
      </template>
    </ChildComponent>
  </div>
</template>

<script>
import ChildComponent from "./ChildComponent.vue";

export default {
  name: "ParentComponent",
  components: {
    ChildComponent,
  },
};
</script>

在这个例子中,子组件定义了一个名为 main 的具名 slot,并提供了默认内容。父组件通过 <template v-slot:main> 指定了要插入的内容,替换了默认内容。 一般使用 slot 的时候,都是在父组件里面替换掉子组件里面对应位置的内容。