Virtual Dom & Diff

Virtual Dom & Diff

liohi

响应式原理

Vue 的响应式系统是其核心特点之一,它使得数据的变化能够自动地更新视图。响应式系统的原理主要涉及以下几个方面:

  1. 侦听属性(Getter 和 Setter): Vue 在初始化数据对象时,会使用 JavaScript 的 Object.definePropertyProxy 来劫持对象属性的访问(Getter)和修改(Setter)。这意味着当你访问一个属性时,Vue 能够捕捉到这个操作,并执行相应的逻辑。

  2. 依赖追踪: 当组件渲染时,它会读取数据属性,这时 Vue 会将这些属性与当前组件实例建立关联。在关联建立后,属性变化时就会触发相应的更新操作。这个关联关系被称为“依赖”,Vue 使用一种叫做“依赖追踪”的机制来收集依赖关系。

  3. Watcher(观察者): 在模板中,如果一个属性被用于渲染,那么 Vue 会创建一个 Watcher 对象来追踪这个属性的依赖关系。每个 Watcher 对象都和一个表达式相关联,这个表达式是属性在模板中的引用。当属性变化时,依赖于这个属性的所有 Watcher 都会被通知,从而触发更新。

  4. 响应式数据的生成: 在 Vue 实例化过程中,会将数据对象中的每个属性转化为“响应式”的。当你修改这些属性时,Vue 会通知相关的 Watcher,从而触发视图的更新。

  5. 批量异步更新: 为了提高性能,Vue 在一次数据变更中会进行异步的批量更新。它将数据变更的操作放入一个异步队列中,在下一个事件循环周期中,才会批量处理这些更新操作。这样可以减少不必要的 DOM 操作,提高性能。

综上所述,Vue 的响应式系统的核心思想是通过拦截属性的访问和修改操作,来建立属性与依赖之间的关联。当属性发生变化时,所有依赖于这个属性的 Watcher 都会被通知,从而自动触发视图的更新,实现了数据与视图的自动同步。这种机制使得开发者能够更便捷地管理和维护数据状态,并且提供了更好的用户体验。

Virtual DOM 和 Diff 算法

前言

对于JS 代码或者CSS样式代码,每次对 DOM 直接进行操作,会触发回流(reflow)和重绘(repaint),造成巨大开销,所以减少 DOM 操作是网页优化的策略之一,由此出现了虚拟 DOM 和 Diff 算法;顾名思义,虚拟 DOM 并非是一个真实存在的 DOM,也就是还并未上树,Diff 算法也就是比较新旧虚拟 DOM 两者(oldVnodenewVnode)的不同之处,在逻辑里对比得出最小量更新再进行上树,也就减少了 DOM 操作。

reflowrepaint:浏览器使用的是流式布局(Flow Based Layout),会把 HTML 解析成 DOM 树,把 CSS 解析成 CSSOM 树,DOMCSSOM 合并成为 Render Tree,然后就可以进行后续渲染… 由于浏览器的流式布局。

当浏览器渲染完成后,Render Tree中部分或者全部尺寸、结构、或某些属性发生变化的时候,浏览器重新渲染的过程成为回流(reflow),以下操作都会导致回流:

  • 页面首次渲染

  • 浏览器窗口大小发生改变

  • 元素尺寸或位置发生改变

  • 元素内容变化(文字数量或图片大小等等)

  • 元素字体大小变化

  • 添加或者删除可见DOM元素

  • 激活CSS伪类(例如::hover

  • 查询某些属性或调用某些方法

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:colorbackground-colorvisibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘

​ 显然,回流的代价比重绘要高,现代浏览器会对频繁的回流或重绘操作进行优化:

​ 浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达 到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。

回流必将引起重绘,重绘不一定会引起回流。

Virtual DOM

虚拟 DOM(Virtual DOM)是 Vue、React 等前端框架中的核心概念,它是一种用 JavaScript 对象表示真实 DOM 结构的方式。虚拟 DOM 的原理是通过对比前后两个虚拟 DOM 树的差异,最小化对实际 DOM 的操作,从而提高性能。

虚拟 DOM 的原理主要包括以下步骤:

  1. 初始渲染: 当组件首次渲染时,会生成一个虚拟 DOM 树,这个树与实际 DOM 结构类似,但只是在内存中的一个 JavaScript 对象表示。

  2. 更新数据: 当数据发生变化,需要更新组件时,会生成一个新的虚拟 DOM 树。

  3. Diff 算法: 新旧虚拟 DOM 树会进行 Diff(差异)计算。Diff 算法的目标是找出新旧虚拟 DOM 之间的差异,即找出哪些节点需要更新、添加或删除。

  4. 批量更新: 根据 Diff 计算的结果,将需要更新的部分抽象成一系列操作,然后一次性更新到实际 DOM 中。这样可以减少实际 DOM 操作的次数,提高性能。

  5. 应用更新: 将更新后的虚拟 DOM 树渲染到实际 DOM 上,用户最终看到的就是更新后的界面。

虚拟 DOM 的核心优势在于它将实际 DOM 操作的成本降低到最低。在每次数据变化时,框架会生成一个新的虚拟 DOM 树,然后与之前的虚拟 DOM 树进行比较,找出需要更新的部分。这个比较过程能够在 JavaScript 层面快速完成,避免了直接操作实际 DOM 所带来的性能损耗。最终,只有真正需要更新的部分会触发实际 DOM 操作,从而提高了性能效率。

总之,虚拟 DOM 的原理是通过在 JavaScript 中维护一个轻量级的 DOM 抽象,通过比较差异来最小化实际 DOM 操作,从而在性能和用户体验方面带来优势。

Diff 算法

Diff 算法是虚拟 DOM 技术的关键部分,用于比较新旧虚拟 DOM 树的差异并最小化对实际 DOM 的操作。下面是 Diff 算法的一般流程:

  1. 比较根节点: 首先,比较新旧虚拟 DOM 树的根节点。如果它们的类型不同,直接将旧节点替换为新节点,从而触发整个子树的重新渲染。

  2. 深度优先遍历: 如果根节点类型相同,Diff 算法会进行深度优先遍历。它会逐层比较新旧节点的子节点,找出它们之间的差异。

  3. 同级节点比较: 在遍历过程中,Diff 算法会按照同级节点的顺序进行比较。对于同级节点,通常情况下会比较它们的标签名(节点类型)、Key(如果存在)、属性和事件等。

  4. 更新操作: 如果同级节点存在差异,Diff 算法会执行相应的更新操作。这些操作可以是插入新节点、移除旧节点、替换节点、更新属性、更新文本内容等。

  5. Key 的作用: 如果节点存在 Key 属性,Diff 算法会通过 Key 来判断节点是否可复用。如果新旧节点的 Key 相同,表示它们是同一节点,可以复用,否则需要进行替换操作。

  6. 递归处理子节点: 对于有子节点的节点,Diff 算法会递归地对子节点进行比较和更新。这样能够保证整个 DOM 树的比较过程。

  7. 批量更新: 在比较完所有节点后,Diff 算法会将需要更新的操作收集起来,并一次性应用到实际 DOM 上,从而减少实际 DOM 操作的次数。

需要注意的是,Diff 算法在进行节点比较时,会尽可能地在同级节点中找到相同的节点,从而复用之前的实际 DOM 结构。这样能够避免频繁地添加、移除节点,从而提高性能。

虽然这是一个简化的 Diff 算法流程,实际框架会根据不同的情况进行优化,以提高算法效率。但总体来说,Diff 算法的目标是找出新旧虚拟 DOM 树之间的差异,将这些差异抽象为一系列操作,并最小化对实际 DOM 的操作,从而提高性能。

模板编译

模板编译是将 Vue 中的模板语法(如指令、插值等)转换为渲染函数的过程。渲染函数可以生成虚拟 DOM,从而实现视图的渲染。以下是模板编译的一般原理:

  1. 解析: 模板编译开始时,会先对模板字符串进行解析。解析器会将模板字符串分析成一个个 Token,每个 Token 包含了类型、标签名、属性等信息。这个解析过程将模板字符串转化为一个 Token 流。

  2. 优化: 在得到 Token 流后,编译器会对 Token 进行优化。其中一个重要的优化是静态节点的检测。静态节点是在编译时就能确定不会变化的节点,编译器会将它们标记出来,这样在后续的渲染过程中可以跳过对这些节点的比较和更新,提高性能。

  3. 生成渲染函数: 优化后的 Token 流会被传递给代码生成器,它会根据 Token 生成对应的渲染函数。渲染函数是一个 JavaScript 函数,它接受数据作为参数,生成虚拟 DOM。生成的渲染函数会嵌套在一个闭包内部,保持对编译器生成的一些辅助函数的引用。

  4. 渲染函数执行: 当数据发生变化时,调用渲染函数,传入新的数据。渲染函数会根据数据生成新的虚拟 DOM,然后通过 Diff 算法比较新旧虚拟 DOM,找出需要更新的部分。

  5. 批量更新: 根据 Diff 算法的结果,将需要更新的操作批量执行。这样能够最小化对实际 DOM 的操作,提高性能。

需要注意的是,模板编译是在构建阶段进行的,它将模板转化为渲染函数,因此只需要执行一次。这个过程能够将模板中的语法转化为 JavaScript 代码,使得在运行时能够快速地生成虚拟 DOM 并进行视图的更新。

总结起来,模板编译的原理是通过解析模板字符串、优化生成渲染函数,将模板中的语法转化为 JavaScript 代码,以实现快速生成虚拟 DOM 并进行视图渲染。这个过程在 Vue 框架中发挥着关键作用。

组件化架构

Vue 的组件化架构是其核心特点之一,它基于组件的概念构建用户界面。以下是 Vue 组件化架构的一般原理:

  1. 组件定义: 在 Vue 中,一个组件可以通过创建一个 Vue 实例来定义。你可以使用 Vue.extend() 或直接使用 Vue 构造函数来定义一个组件。组件可以包含数据、模板、样式、方法等。

  2. 模板: 组件通常包含一个模板部分,用于描述组件的外观。模板可以使用 Vue 的模板语法,包括插值、指令、条件渲染、列表渲染等。模板中的数据绑定会自动响应数据的变化。

  3. 数据: 每个组件都可以拥有自己的数据,这些数据可以在组件内部使用。你可以使用 Vue 的响应式数据绑定,通过定义数据属性来实现自动更新视图。

  4. 属性和事件: 组件可以接收父组件传递的属性(props),这些属性可以用于定制组件的外观和行为。组件还可以通过 $emit 方法触发自定义事件,与父组件或其他组件进行通信。

  5. 组件嵌套: 在 Vue 中,你可以将一个组件嵌套在另一个组件内部。这种嵌套可以形成组件树的结构,从而构建复杂的界面。父组件可以传递数据和属性给子组件,实现数据的传递和共享。

  6. 生命周期: Vue 组件有一系列生命周期钩子函数,用于在组件不同阶段执行逻辑。例如,created 钩子在组件被创建后调用,mounted 钩子在组件被添加到 DOM 后调用。

  7. 全局和局部组件: 你可以在全局范围内注册一个组件,使其在任何地方都可用。也可以在局部范围内注册一个组件,使其只在特定的组件中可用。这样能够更好地管理组件的作用域和可见性。

  8. 动态组件: Vue 允许你使用 <component> 元素动态地渲染不同的组件。通过绑定一个动态的组件名,你可以根据数据的变化来切换显示不同的组件。

  9. 异步组件: Vue 支持异步加载组件,通过使用 import() 或返回 Promise 的工厂函数来定义异步组件。这可以优化应用的性能,只在需要时才加载组件。

  10. 插槽: 插槽(Slot)是 Vue 中用于分发内容的一种机制。通过插槽,你可以将内容插入到组件的特定位置,使得组件更具灵活性。

综上所述,Vue 的组件化架构通过定义组件、模板、数据、属性、事件等实现了界面的模块化和可组合性。通过组件的嵌套和复用,能够构建出复杂的用户界面。Vue 的生命周期和响应式数据绑定等机制使得组件可以具有自身的行为和状态,从而实现了更高的可维护性和可重用性。

生命周期钩子

Vue 的生命周期钩子是一组在组件生命周期不同阶段被调用的函数。这些钩子函数允许你在组件的不同生命周期阶段执行自定义的逻辑。Vue 的生命周期钩子原理涉及以下几个方面:

  1. 初始化阶段:

    • beforeCreate: 在组件实例被创建之前被调用。此时,组件的数据和方法还未初始化。
    • created: 在组件实例创建完成后被调用。此时,组件的数据已初始化,但 DOM 还未生成。
  2. 模板编译和挂载阶段:

    • beforeMount: 在组件模板被编译并挂载到实际 DOM 之前被调用。此时,组件模板已编译完成,但还未渲染到页面上。
    • mounted: 在组件模板被编译并挂载到实际 DOM 之后被调用。此时,组件已经渲染到页面上,并且可以访问到 DOM 元素。
  3. 更新阶段:

    • beforeUpdate: 在数据更新导致重新渲染之前被调用。此时,组件数据已更新,但 DOM 还未重新渲染。
    • updated: 在数据更新导致重新渲染之后被调用。此时,组件数据已更新,DOM 也已重新渲染。
  4. 销毁阶段:

    • beforeDestroy: 在组件实例销毁之前被调用。此时,组件还处于活动状态,可以执行清理工作。
    • destroyed: 在组件实例销毁之后被调用。此时,组件已经被完全销毁,不再能访问到组件的数据和方法。

生命周期钩子的原理是在 Vue 实例化时,将钩子函数注册到组件的生命周期中。当组件在不同阶段进行相应的操作时,Vue 会自动调用相应的钩子函数。例如,在创建组件实例时,会调用 beforeCreatecreated 钩子;在更新组件数据时,会调用 beforeUpdateupdated 钩子,依此类推。

需要注意的是,Vue 3 在生命周期钩子方面有一些变化,引入了 beforeMountmounted 钩子的代替品:beforeMount 变为 onBeforeMountmounted 变为 onMounted,以此类推。这些变化是为了更好地与 Vue 3 的 Composition API 结合使用。

总之,Vue 的生命周期钩子原理是在组件生命周期的不同阶段,自动调用注册的钩子函数,使得开发者能够在不同的时刻执行自定义逻辑,实现更灵活的组件行为。

指令和事件

Vue 提供了一系列指令(如 v-modelv-bindv-if 等)和事件(如 @click@input 等)来操作 DOM 和实现交互。这些指令和事件能够方便地绑定数据、响应用户输入以及更新视图。

指令原理:

  1. 解析和编译阶段: 在 Vue 的编译阶段,会解析模板中的指令,例如 v-bindv-model 等。解析后的指令会被转化成对应的虚拟 DOM 渲染函数。

  2. 运行时渲染阶段: 在组件渲染时,虚拟 DOM 渲染函数会执行指令的逻辑。例如,v-bind 指令会根据数据动态绑定元素属性,v-model 指令会双向绑定输入框的值。

  3. 响应式绑定: 指令通常涉及到数据的绑定和更新。Vue 会根据数据的变化,自动更新指令绑定的内容。例如,v-bind 指令会监听数据变化,实时更新绑定的属性值。

事件原理:

  1. 事件绑定: 在模板中,可以使用 @v-on 指令来绑定事件监听器。例如,@clickv-on:click 绑定点击事件。

  2. 事件代理: Vue 通过事件代理的方式,将事件监听器绑定到组件的根元素上。这样就可以在组件内部的子元素上触发事件,事件会冒泡到根元素上。

  3. 事件处理: 当触发事件时,Vue 会调用事件监听器。事件监听器是 Vue 组件中的方法,它可以访问组件的数据和方法。你可以在事件监听器中执行逻辑,更新数据或与服务器进行交互。

  4. 事件参数: 事件监听器可以接收事件参数,例如 $event。你可以将事件参数传递给监听器方法,以便在方法内部使用。

  5. 事件修饰符: Vue 提供事件修饰符,用于控制事件的行为。例如,.prevent 修饰符可以阻止默认行为,.stop 修饰符可以停止事件冒泡。

需要注意的是,Vue 的指令和事件是通过 JavaScript 代码实现的,它们会在编译和运行时被转化成相应的逻辑。指令通过虚拟 DOM 渲染函数实现数据绑定和更新,事件通过事件代理和监听器实现用户交互和逻辑处理。

总之,Vue 的指令和事件机制使得开发者可以方便地操作 DOM 元素和处理用户交互。这些机制基于 Vue 的编译和运行时系统,以及响应式数据绑定,使得前端开发更加便捷和灵活。

异步更新队列

Vue 在进行数据更新时,通常将更新操作放入异步队列中,然后在一个事件循环中进行更新。这样可以保证更新的顺序性,同时也能够合并多次数据变更,提升性能。

Vue 异步更新队列是 Vue 在更新视图时的一种优化机制,它将多个数据变更操作合并到一次更新中,从而提高性能。这个机制涉及到 Vue 的异步更新策略、事件循环和 nextTick 原理。以下是 Vue 异步更新队列的原理:

  1. 响应式数据变更: 当 Vue 组件的响应式数据发生变化时,Vue 会标记这些变化,并将这些变化放入一个队列中。

  2. 异步更新策略: Vue 使用异步更新策略,将数据变更操作延迟到下一个事件循环中执行。这样做的目的是为了将多次数据变更操作合并为一次,从而减少不必要的 DOM 操作和提高性能。

  3. 事件循环: JavaScript 运行在事件循环中,每一轮事件循环被称为一个 tick。在每个 tick 内,JavaScript 会执行一些任务,然后检查是否需要更新视图。Vue 利用事件循环来实现异步更新队列。

  4. nextTick 原理: nextTick 是 Vue 提供的一个 API,它能够在 DOM 更新之后执行回调函数。在 Vue 内部,nextTick 利用了 JavaScript 的事件循环机制,将回调函数推入微任务队列(microtask queue)中,在 DOM 更新之后立即执行。

  5. 更新队列处理: 在每个事件循环中,Vue 会检查更新队列,将队列中的变更操作合并并执行。这样,多个数据变更会在同一个事件循环内更新视图,从而提高性能。

  6. 同步更新情况: 在某些情况下,如果你希望立即执行数据变更操作并更新视图,可以使用 $nextTick 或者在异步操作内部调用 this.$forceUpdate()

通过异步更新队列,Vue 能够将多个数据变更操作合并为一次,减少不必要的性能开销。这种机制保证了响应式数据的更新和视图的同步性,同时也使得开发者能够更好地控制数据的更新时机。

响应式 API

Vue 的响应式 API 是其核心功能之一,它通过数据劫持和依赖追踪实现了数据的自动更新。以下是 Vue 响应式 API 的原理:

  1. 数据劫持: 在 Vue 中,每当创建一个数据对象时,Vue 会将数据对象的属性转换成 getter 和 setter。这样,当访问和修改属性时,Vue 可以捕获这些操作,并进行相应的处理。

  2. 依赖追踪: 在数据劫持过程中,Vue 会为每个属性创建一个依赖追踪系统。每个属性都会有一个依赖集合,用于存储依赖于该属性的观察者(Watcher)。

  3. 观察者模式: 观察者模式是 Vue 响应式系统的关键。当属性被访问时,Vue 会收集订阅了该属性的观察者,这些观察者会被添加到属性的依赖集合中。

  4. 编译阶段: 在模板编译阶段,Vue 会解析模板中的表达式,识别出其中的数据引用。这些数据引用将被转化为 getter 调用,从而将观察者与属性建立联系。

  5. 依赖收集: 在组件实例化时,Vue 会创建一个 Watcher 实例。这个 Watcher 实例会在模板中的每个数据引用处执行 getter,从而触发属性的依赖收集过程。

  6. 派发更新: 当属性的值发生变化时,其 setter 会被调用。在 setter 中,Vue 会通知属性的所有观察者进行更新,从而保证视图与数据的同步性。

  7. 异步更新: 为了提高性能,Vue 使用异步更新队列将多个数据变更操作合并到一次更新中。这样可以减少不必要的 DOM 操作,提高渲染性能。

  8. 虚拟 DOM: 当数据更新时,Vue 会生成一个虚拟 DOM 树。虚拟 DOM 与之前的虚拟 DOM 进行比较,找出需要更新的部分,然后进行最小化的 DOM 操作。

通过数据劫持、依赖追踪和观察者模式,Vue 实现了数据的自动更新。这种机制使得开发者无需手动操作 DOM,而是通过操作数据,自动触发视图的更新。这种响应式的特性使得 Vue 构建动态、高效的用户界面成为可能。

  • 标题: Virtual Dom & Diff
  • 作者: liohi
  • 创建于 : 2023-08-29 21:04:58
  • 更新于 : 2023-08-30 08:30:01
  • 链接: https://liohi.github.io/2023/08/29/Virtual DOM 和 Diff 算法/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
 评论