最近面试一直被问这个东西都快被问麻了,想着去总结一下 Vue2 和 Vue3 的区别,以便更好地将它们进行横向对比。
# 主要区别
- 性能提升:
- Vue3 在性能方面有了显著的提升,主要得益于新的响应式系统。Vue3 使用了 Proxy 对象来实现响应式,相比于 Vue2 的 Object.defineProperty,在大型应用中能够更高效地追踪状态的变化。
- Composition API:
- Vue3 引入了 Composition API,这是一个新的 API 风格,使得组件的逻辑可以更好地组织和重用。相比于 Vue2 的 Options API,Composition API 更灵活、可读性更好,并且能更好地处理复杂的逻辑。
- Typescript 支持:
- Vue3 在设计之初就考虑了对 Typescript 的更好支持,使得在使用 Typescript 时的开发体验更加流畅。Vue2 对 Typescript 的支持也较为局限,需要额外的配置和声明文件。
- 更小的体积:
- Vue3 在体积方面进行了优化,运行时的体积更小,这也使得 Vue3 更适合在移动端或对体积要求较高的场景中使用。
- 更好的 TypeScript 支持:
- Vue3 对 TypeScript 的支持更加完善。它使用 TypeScript 重写了代码库,并提供了更好的类型推断和支持。这使得在使用 TypeScript 时更容易捕获错误并提高了开发体验。
- Teleport 组件:
- Vue3 引入了 Teleport 组件,可以方便地将子组件移动到父组件以外的 DOM 中,这在处理模态框、弹出式菜单等场景中非常有用。
- 编译器优化:
- Vue3 的编译器经过了优化,生成的代码更加高效,这导致了更快的渲染速度和更小的包大小。
- Tree-shaking 支持:
- Vue3 改进了对 Tree-shaking 的支持,可以更好地消除未使用的代码,进一步减小最终打包的体积。
# 响应式机制的不同
# Vue2
在 Vue2 中,响应式机制是通过 Observer、Watcher 和 Compiler 这三个关键部分实现的。
- Observer:
- 作用:Observer 的职责是将数据对象中的 所有属性 转换为可观测对象。它通过递归遍历数据对象的每一个属性,并使用
Object.defineProperty()
方法将它们转换为 getter/setter。这个过程中, 每个属性 都会被赋予依赖管理的能力。 - 功能:当属性被访问(get)时,Observer 会进行 依赖收集 ,即记录哪些 Watcher 依赖于这个属性。当属性被修改(set)时,它会通知所有依赖的 Watcher 进行更新。
- 作用:Observer 的职责是将数据对象中的 所有属性 转换为可观测对象。它通过递归遍历数据对象的每一个属性,并使用
- Watcher:
- 作用:Watcher 的角色是作为一个 中介 ,它观察数据的变化。在 Vue 中,每个 组件实例 都至少有一个 Watcher 实例,它会在组件渲染过程中把自身注册到所依赖的数据属性的 Observer 上。
- 功能:当数据发生变化时,Watcher 会收到通知,并执行相应的更新操作,如重新渲染视图或执行计算属性等。
- Compile:
- 作用:Compile 负责 解析模板指令 (如
v-model
、v-bind
等),并把模板中的变量与组件实例数据关联起来。 - 功能:在编译阶段,Compile 会遍历模板中的所有节点,解析指令和插值表达式,并 创建相应的 Watcher 。每当模板引用的数据变化时,Watcher 会触发回调,更新 DOM。
- 作用:Compile 负责 解析模板指令 (如
相互配合的工作流程:
- 初始化阶段:在 Vue 组件的挂载过程中,Compile 解析模板,并为模板中每一个需要动态更新的部分创建 Watcher。
- 依赖收集:当 Compile 解析到需要动态数据的部分时,它会读取相应的数据属性,触发 Observer 中定义的 getter。Getter 中会将当前的 Watcher 添加到这个数据属性的依赖列表中。
- 更新触发:当数据变化时,setter 被触发,通知所有依赖于该属性的 Watcher 执行更新函数,这通常涉及对视图的重新渲染或其他逻辑的执行。
# Vue3
Vue3 的响应式系统与 Vue2 在实现机制上有显著的不同。Vue3 使用了更现代的 JavaScript 特性 ——Proxy,来替代 Vue2 中的 Object.defineProperty 方法。
# Vue3 响应式系统的工作原理
-
使用 Proxy:
- Vue3 的响应式系统基于 Proxy 对象构建。Proxy 允许你拦截并自定义对象属性的基本操作,如属性读取、设置、删除等。
- 当创建一个响应式对象时(通过
reactive
或ref
方法),Vue3 会返回这个原始对象的 Proxy 版本。这个 Proxy 控制着对其数据的所有访问和修改,使得 Vue 能够追踪变化并在必要时重新渲染组件。
-
响应式引用(Refs):
- Vue3 引入了
ref
,这是一种包装基本值(如字符串、数字等)以使其成为响应式的方式。ref
返回一个对象,其中包含一个名为value
的属性。当访问或修改value
时,Vue 可以追踪这些操作并触发视图更新。
- Vue3 引入了
-
Reactive 函数:
reactive
函数用于创建复杂类型(如对象或数组)的响应式版本。这与 Vue2 中的Vue.observable()
类似,但由于 Proxy 的使用,reactive
可以更智能地处理嵌套属性和数组。
-
依赖跟踪和触发更新:
- Vue3 使用一个全新的依赖追踪系统。每当响应式对象的属性被读取时,Vue 会记录当前渲染的组件为这个属性的依赖。当属性被修改时,Vue 会通知所有依赖这个属性的组件重新渲染。
# Vue2 与 Vue3 响应式系统的主要区别
-
实现技术:
- Vue2 使用
Object.defineProperty()
来定义响应式属性,只能监听到预先定义的属性的变化。 - Vue3 使用
Proxy
,可以监听到所有类型的改变,包括属性的添加、删除和数组索引的修改,以及更深层次的变化。
- Vue2 使用
-
性能:
- Proxy 的性能通常优于
Object.defineProperty()
,尤其是在处理大型对象和深层嵌套的数据结构时。Proxy 只需要在初始化时处理一次,而Object.defineProperty()
需要递归地处理对象的每个属性。
- Proxy 的性能通常优于
-
数组处理:
- 在 Vue2 中,由于技术限制,特定的数组修改方法(如
push
、pop
)需要特殊处理才能触发更新。 - 在 Vue3 中,由于 Proxy 能够拦截数组的修改操作,这些操作都自然而然地成为了响应式的,不需要额外的处理。
- 在 Vue2 中,由于技术限制,特定的数组修改方法(如
-
支持动态属性:
- Vue2 不能自动将后来添加到对象的新属性变为响应式,除非使用
Vue.set
。 - Vue3 允许动态添加的属性也自动成为响应式,因为 Proxy 会拦截所有属性的操作
- Vue2 不能自动将后来添加到对象的新属性变为响应式,除非使用
# 核心组件
-
响应式对象(Reactive Objects):
- 通过
reactive
和ref
方法,Vue3 把普通的 JavaScript 对象转换为响应式对象。这是通过包装这些对象在一个 Proxy 中实现的。Proxy 允许 Vue 拦截对象属性的访问和修改操作。
- 通过
-
依赖收集(Dependency Tracking):
- 每个响应式属性都关联一个 effect(副作用函数),这些 effect 会在属性被访问时执行。每个 effect 可以订阅多个属性,同时,一个属性也可以被多个 effect 订阅。
- 当一个属性被访问(get 操作)时,当前正在执行的 effect(如果存在的话)会被添加到这个属性的依赖列表中。这是通过一个全局的 stack 来管理当前激活的 effect。
-
触发更新(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 的性能优化
- 响应式系统重写: Vue3 使用 Proxy 对象重写了响应式系统,代替了 Vue2 中的 Object.defineProperty。Proxy 可以拦截对象的任何属性的访问和修改操作,而不是只有预先定义的属性。这使得 Vue3 的响应式系统更加灵活和高效,特别是在处理动态添加或删除属性的场景。
- 编译优化: Vue3 的编译器进行了重大优化,引入了基于树摇(Tree-shaking)的编译过程。这意味着最终的生产包中只会包含应用中实际用到的 Vue 功能代码。此外,Vue3 的模板编译器还引入了更多的编译时优化,如静态提升(hoisting)和事件缓存,这减少了运行时的工作量。
- Fragment、Teleport 和 Suspense:
- Fragment 允许组件返回多个根节点,减少了额外的 DOM 层级和相关的性能开销。
- Teleport 是一个新的内置组件,允许开发者将子组件渲染到 DOM 树的其他位置,有助于解决诸如模态框等场景的性能和样式封装问题。
- Suspense 支持异步组件的等待状态管理,改善了异步组件加载的用户体验和性能。
- 更有效的组件更新: Vue3 引入了基于静态树的分片(static tree hoisting)和事件监听器的静态提升。这些优化减少了虚拟 DOM 的比较范围和更新的复杂度。只有动态内容会参与到虚拟 DOM 的更新中,这大大减少了组件更新的计算量。
- 更小的体积:通过模块化的架构设计,Vue3 允许开发者只引入他们需要的功能,进一步减少了最终应用的体积。
- Composition API: Vue3 的 Composition API 不仅提供了更好的逻辑复用和代码组织方式,而且还减少了因使用 Options API 导致的不必要的重新渲染。开发者可以更精确地控制组件的依赖和副作用。
# Vue3 当中的 keep-alive 组件
在 Vue3 中, keep-alive
是一个内置组件,用于缓存非活动组件实例,而不是销毁它们。这样可以保持组件的状态,并在用户再次访问时,可以迅速恢复显示,而不需要重新创建组件实例。这对于提升性能特别有效,尤其是在需要频繁切换但又不希望状态丢失的组件(如标签页)中非常有用。
# 本质
keep-alive
包裹动态组件时,会对其做出缓存处理,使得组件在被切换出视图时不会被销毁。其核心功能是: 通过创建一个缓存对象来存储被包裹的组件实例 。
# 工作原理
-
缓存和复用:
- 当组件第一次渲染时,
keep-alive
会将其实例缓存起来。如果组件的props
或者slots
没有发生变化,下次再渲染相同的组件时,keep-alive
会复用之前缓存的实例,而不是创建一个新的。 - 这种方式显著减少了重新渲染的成本,因为组件的初始渲染通常比更新更加消耗资源。
- 当组件第一次渲染时,
-
生命周期钩子:
keep-alive
特别管理了组件的生命周期钩子。被keep-alive
包裹的组件不会触发常规的destroyed
(或unmounted
在 Vue3 中)钩子。相反,它会触发特殊的生命周期钩子:activated
和deactivated
。- 当组件被缓存时,会触发
deactivated
钩子;当再次被激活(复用)时,会触发activated
钩子。
-
LRU 缓存策略:
keep-alive
可以配置max
属性,限制缓存的组件实例数量。当存储的实例超过这个值时,keep-alive
会使用最近最少使用(LRU)的策略来移除缓存中最久未被访问的组件实例。
-
使用
include
和exclude
属性管理缓存:keep-alive
提供了include
和exclude
属性,允许你指定哪些组件应该被缓存,哪些不应该。这些属性可以接受一个逗号分隔的字符串、正则表达式或者一个数组,便于灵活配置缓存策略。
# 缓存对象
在 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 更加统一和功能更丰富。
-
基本使用:
- Slot 在组件模板中定义为
<slot>
元素。父组件可以在使用子组件时,将内容放入这些<slot>
中。 - 子组件中的
<slot>
标签可以包含默认内容。如果父组件没有提供相应的插槽内容,就会显示默认内容。
- Slot 在组件模板中定义为
-
具名 Slot:
- Vue 允许定义多个具名 slot,每个 slot 通过
name
属性进行标识。这样,父组件可以针对不同的 slot 插入不同的内容。 - 使用时,只需在父组件中通过
<template v-slot:slotName>
的形式指定内容对应的 slot。
- Vue 允许定义多个具名 slot,每个 slot 通过
-
作用域插槽:
- 作用域插槽允许子组件传递数据回到插槽内容中,这让插槽不仅能够展示内容,还能够基于子组件的内部状态动态地展示内容。
- 它通过
<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 的时候,都是在父组件里面替换掉子组件里面对应位置的内容。