进阶主题
虚拟 DOM
什么是虚拟 DOM
虚拟 DOM 本质上是 JavaScript 对象,这个对象就是更加轻量级的对 DOM 的描述和抽象
虚拟 DOM 是怎么生成的
虚拟 DOM 的生成过程如下:
- 初始渲染:当 Vue 应用初始化时,Vue 会通过解析组件的模板或渲染函数生成初始的虚拟 DOM 树。这个初始的虚拟 DOM 树与实际的 DOM 结构一一对应,但是它只是存在于内存中,还没有被渲染到浏览器的页面上。
- 数据变更:当 Vue 应用中的数据发生变化时,Vue 会触发重新渲染的过程。在重新渲染之前,Vue 会生成新的虚拟 DOM 树。
- 对比更新:Vue 会将新生成的虚拟 DOM 树与之前的虚拟 DOM 树进行对比,找出两者之间的差异。这个对比的过程称为虚拟 DOM 的 diff 算法。
- 更新实际 DOM:根据对比的结果,Vue 会确定需要进行更新的具体 DOM 节点,并将这些差异应用到实际的 DOM 树上。这个过程称为 DOM 的 patch 操作。
通过使用虚拟 DOM,Vue 可以将对实际 DOM 的操作最小化,从而提高性能。相比直接操作实际的 DOM,虚拟 DOM 的对比和更新过程更高效,因为它是在内存中进行的,不涉及实际的页面渲染。
需要注意的是,虚拟 DOM 并不是所有情况下都会带来性能提升。在某些简单的场景下,直接操作实际的 DOM 可能更加高效。虚拟 DOM 的优势主要体现在复杂的页面结构和频繁的数据变更场景下。
为什么需要虚拟 DOM
- 频繁变动 DOM 会造成浏览器的回流(重排)或者重绘,因此我们需要这一层抽象,在 patch 过程中尽可能地一次性将差异更新到 DOM 中,这样就尽可能的减少了 DOM 的操作,提高了程序性能
- 数据驱动 DOM 的更新,省略手动 DOM 操作可以大大提高开发效率 虚拟 DOM 最初的目的是更好的跨平台,例如 Node.js 就没有 DOM,如果想实现 SSR(服务端渲染),那么一个方式就是借助虚拟 DOM,因为虚拟 DOM 本身是 JavaScript 对象
虚拟 DOM 的解析过程
- 首先对将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性。然后将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。
- 当页面的状态发生改变,需要对页面的 DOM 的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。
- 最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。
为什么虚拟 dom 比真实 dom 快
虚拟 DOM(Virtual DOM)相对于真实 DOM 具有一些优势,使其在某些情况下可以更快地更新和渲染页面。以下是一些原因:
- 批量&异步更新:虚拟 DOM 可以对多个 DOM 更新进行批量处理,异步更新。当应用程序状态发生变化时,虚拟 DOM 可以收集所有的变更,然后一次性更新真实 DOM。相比之下,直接操作真实 DOM 时,每次更新都会立即触发浏览器的重新渲染,这可能导致性能问题。
- 部分更新(节点操作的最小化):虚拟 DOM 可以通过比较前后两个虚拟 DOM 树的差异,找出需要更新的节点,而不是直接操作每个具体的真实 DOM 节点。这样可以避免不必要的节点操作,仅更新需要变化的部分,减少了不必要的计算和操作。而在直接操作真实 DOM 时,通常需要手动处理每个变化,这可能更加繁琐和耗时。
- 虚拟 DOM 的内存操作:虚拟 DOM 是在内存中操作的,而真实 DOM 是浏览器中的实际对象。内存中的操作比浏览器中的操作更快速。虚拟 DOM 可以在内存中进行计算和比较,然后再将最终结果应用到真实 DOM,这样可以减少对浏览器的操作次数。
- 批量样式计算:虚拟 DOM 可以对样式计算进行批量处理。在直接操作真实 DOM 时,每次更改样式都会触发浏览器的重新计算和重新布局,这可能会导致性能下降。虚拟 DOM 可以将多个样式变更收集起来,并一次性应用到真实 DOM,从而减少了不必要的计算和布局操作(减少重绘或回流)。
- 更新预测:虚拟 DOM 可以智能地分层更新,先更新可能依赖的部分,再更新后续部分。优化渲染顺序。
- 跨平台:虚拟 DOM 是 JavaScript 对象,无需考虑浏览器兼容性,运行效率高。真实 DOM 操作需要抹平各种平台差异。
需要注意的是,虚拟 DOM 也有一些开销。虚拟 DOM 需要在内存中维护一个额外的数据结构,并进行比较和计算,这可能会增加一些额外的 CPU 和内存消耗。在一些简单的应用程序中,直接操作真实 DOM 可能更加高效。但对于大型和复杂的应用程序,虚拟 DOM 在维护和管理状态变化方面提供了更好的可扩展性和性能优势。
总结起来,虚拟 DOM 通过批量更新、部分更新、内存操作和批量样式计算等优势,可以提高 DOM 操作的性能。然而,性能的提升效果也受到应用程序的规模和复杂度等因素的影响。
nextTick
JS 运行机制
JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task(宏任务) 和 micro task(微任务),并且每个 macro task 结束后,都要清空所有的 micro task。
for (macroTask of macroTaskQueue) {
// 1. Handle current MACRO-TASK
handleMacroTask()
// 2. Handle all MICRO-TASK
for (microTask of microTaskQueue) {
handleMicroTask(microTask)
}
}
在浏览器环境中 :
- 常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate
- 常见的 micro task 有 MutationObsever 和 Promise.then
回答相关
- nextTick 是 Vue 提供的一个全局 API,由于 vue 的异步更新策略导致我们对数据的修改不会立刻体现在 dom 变化上,此时如果想要立即获取更新后的 dom 状态,就需要使用这个方法
- Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick 方法会在队列中加入一个回调函数,确保该函数在前面的 dom 操作完成后才调用。
- 所以当我们想在修改数据后立即看到 dom 执行结果就需要用到 nextTick 方法。
- 比如,我在干什么的时候就会使用 nextTick,传一个回调函数进去,在里面执行 dom 操作即可。
- 我也有简单了解 nextTick 实现,它会在 callbacks 里面加入我们传入的函数,然后用 timerFunc 异步方式调用它们,首选的异步方式会是 Promise。这让我明白了为什么可以在 nextTick 中看到 dom 操作结果。
页面刷新后 vuex 的 state 数据丢失怎么解决?
store 里的数据是保存在运行内存中的,当页面刷新时,页面会重新加载 vue 实例,store 里面的数据就会被重新赋值初始化。理论上我们是不需要持久存储 vuex 的值的,因为请求我们会去接口拿数据,进行重新渲染,和第一次进入一样
但是如果非要保存上一次的临时状态,其实可以使用 localStorage 进行持久化存储,但是这个时候又得去处理和服务端数据同步的问题
为啥要有 vuex,使用 localStorage 本地存储不行么?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
当多个组件拥有同一个状态的时候,vuex 能够很好的帮我们处理,可以很好的使用 vue 开发者工具调试 vuex 的状态,这些优势是 localStorage 不能够很好的模拟的。
在实际项目中,vue 组件通信有哪些注意点 ?
- 明确通信方式:在组件通信之前,明确使用何种通信方式是很重要的。Vue 中提供了多种通信方式,如 props 和事件总线(Event Bus)、Vuex、provide/inject 等。根据具体情况选择适合的通信方式。
- 组件解耦:组件之间应该尽量保持解耦,即一个组件不应该过于依赖其他组件的内部实现细节。通过明确定义 props 和事件接口,组件可以更加独立地工作,提高复用性和可维护性。
- 单向数据流:在 Vue 中,数据流是单向的,自上而下。父组件通过 props 向子组件传递数据,子组件通过事件向父组件发送消息。遵循这个单向数据流可以使数据流动更加可控和可预测。
- 避免过度通信:过多的组件通信可能导致代码复杂性增加。在设计组件通信时,要避免过度通信,只传递必要的数据和事件,以保持代码的简洁性和可读性。
- 适当使用事件总线:事件总线(Event Bus)是一种简单的组件通信方式,可以在组件之间进行事件的发布和订阅。但是,过度使用事件总线可能导致代码难以维护和调试,因此应该谨慎使用,并在必要时考虑其他更适合的通信方式。
- 谨慎使用 provide/inject:provide/inject 是一种高级的组件通信方式,可以在祖先组件中提供数据,并在后代组件中注入使用。然而,过度使用 provide/inject 可能导致组件之间的依赖关系不明确,难以追踪和理解,因此需要谨慎使用。
- 考虑跨组件通信方案:有时候需要在非父子组件之间进行通信,比如兄弟组件或跨层级组件之间的通信。这时可以考虑使用事件总线、Vuex 或其他第三方库来实现跨组件通信。
- 使用 Vuex 进行状态管理:如果组件之间需要共享状态或进行复杂的通信,考虑使用 Vuex 进行状态管理。Vuex 提供了一个集中式的状态管理方案,可以更好地管理和共享组件之间的状态。
总之,组件通信在实际项目中是一个重要的考虑因素。通过选择合适的通信方式、保持组件解耦、遵循单向数据流原则以及适当使用状态管理等技巧,可以有效地管理组件之间的通信,提高代码的可维护性和可扩展性。