Vue3 框架下,VNode 是如何转化成 DOM,我们在 mount 阶段前为什么无法操作 DOM

457 阅读5分钟

从 VNode 到 DOM

vnode 本质上是用来描述 DOM的 JavaScript 对象,它在 Vue 中可以描述不同类型的节点,比如普通元素节点、组件节点等。

HTML 实现的普通节点:

<button class="btn" style="width:100px;height:50px">click me</button>

vnode 实现的普通节点:

const vnode = {
  type: 'button',
  props: { 
    'class': 'btn',
    style: {
      width: '100px',
      height: '50px'
    }
  },
  children: 'click me'
}

其中,type 属性表示 DOM 的标签类型,props 属性表示 DOM 的一些附加信息,比如 style 、class 等,children 属性表示 DOM 的子节点,它也可以是一个 vnode 数组,只不过 vnode 可以用字符串表示简单的文本 。

组件节点需要先用 vnode 定义:

const CustomComponent = {
  // 在这里定义组件对象
}
const vnode = {
  type: CustomComponent,
  props: { 
    msg: 'test'
  }
}

再在 HTML 中引入:

<custom-component msg="test"></custom-component>

vnode 的优势

  1. 性能优势:通过减少 DOM 操作,从而减少浏览器的重排和重绘次数
  • 减少 DOM 操作:每次直接操作 DOM 都可能会引起浏览器的重排和重绘,而重排重绘是比较消耗性能的。VNode 通过 Diff 算法,可以精确地找出需要更新的部分,然后一次性地对真实 DOM 进行更新,大大减少了不必要的 DOM 操作次数,从而降低交互或者页面更新带来的性能损耗。
  • 批量更新机制:VNode 允许将多个 DOM 更新操作收集起来,在合适的时机进行批量处理。当一个复杂应用在短时间内发生多次状态变化,从而导致多个 DOM 更新操作时,如果没有 VNode 的批量更新机制,这些更新操作可能会逐个触发,导致频繁的 DOM 操作。通过 VNode,可以将这些更新操作缓存在一个队列中,当更新操作积累到一定数量,或者在某个合适的时间点(如在下一个时间循环中),再统一将这些更新应用到真实 DOM 上,减少了浏览器的重排和重绘次数。
  1. 跨平台和组件化开发优势
  • 跨平台复用:因为 VNode 不依赖于 特定浏览器的 DOM API,所以一套代码可以在多端复用,包括原生应用、Web 应用、SSR 应用、小程序应用等
  • 组件化开发优势:每个组件可以看做一棵独立的 VNode 树,组件内部更新时,通过 Diff 算法可以准确更新对应的 DOM,不会影响到其他组件。
  1. 状态管理和响应式更新优势
  • 精确的状态 - 视图映射:状态通常以数据的形式存储在 JavaScript 对象中(如 Vue 中的 data 或者 React 中的 state)。当 data 发生变化时,VNode 会驱动 Diff 算法,准确、及时更新视图
  • 响应式更新的高效性:响应式更新可以提高用户体验,而通过 VNode 的 Diff 算法,可以准确地更新局部 DOM,让响应式更新更加高效。

mount 阶段下创建 VNode 和渲染 VNode

在 beforeCreate 阶段,Vue 会进入模板编译阶段,Vue 文件下的 template 会被构建成 AST 树。随后,在 mount 阶段,Vue 会创建多个 VNode 来绘制各个组件,也是在 mount 阶段,进行渲染 VNode,将 Vue 文件下的 template 部分的代码渲染成真实的 DOM 树。

Vue 源码中的构造了 createVNode 函数,并通过该函数创建了 vnode 对象,该函数具体做的事情就是:对 props 做标准化处理、对 vnode 的类型信息编码、创建 vnode 对象、标准化子节点 Children。

创建完 VNode 对象后,Vue 会判断 VNode 是组件元素还是普通元素,并据此做不同的处理。

  • 组件元素 VNode 处理步骤:
  1. 将该 VNode 通过挂载(mount)操作生成组件根节点元素
  2. 初始化组件:维护组件上下文,包括对 props、插槽、以及其他组件实例的属性(data)的初始化处理;绑定组件事件;浏览器交互激活。
  3. 递归处理组件子树的 VNode
  • 普通元素 VNode 处理步骤:
  1. 将该 VNode 通过挂载(mount)操作转换为虚拟 DOM 元素
  2. 根据虚拟 DOM 创建真实 DOM 元素并插入到文档中
  3. 更新元素的属性
  4. 递归挂载子节点(如有)

将所有类型的节点处理完成后,我们将得到一棵虚拟 DOM 树,这时 Vue 会将虚拟 DOM 树插入 DOM 元素中,至此,大体流程结束。

beforeMount和mounted阶段最适合做什么操作

经过对 mount 阶段的分析,我们可以得知,所有的 DOM 实例对象、子树对象、VNode 对象都需要在 mount 阶段生成,在这个阶段之后才能访问并修改这些对象,同时,浏览器交互与组件事件也在这个阶段激活和绑定,所以浏览器参数初始化以及事件绑定,都得在这个阶段之后进行。

在挂载节点之前,会调用beforeMount生命周期钩子。在这个阶段,组件已经完成了数据初始化、模板编译和 VNode 生成等大部分准备工作,但还没有将 VNode 转换为真实 DOM 并插入到页面中。开发者可以在这个钩子中进行一些最后的准备工作,比如修改数据、添加自定义的逻辑等。

在组件节点挂载完成后,会调用mounted生命周期钩子。此时,组件已经成功地插入到页面中,成为 DOM 树的一部分。开发者可以在这个钩子中进行一些需要在 DOM 挂载后才能进行的操作,如获取 DOM 元素的尺寸、初始化第三方插件(如果插件需要在 DOM 节点存在的基础上进行初始化)等。