从 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 的优势
- 性能优势:通过减少 DOM 操作,从而减少浏览器的重排和重绘次数
- 减少 DOM 操作:每次直接操作 DOM 都可能会引起浏览器的重排和重绘,而重排重绘是比较消耗性能的。
VNode 通过 Diff 算法,可以精确地找出需要更新的部分,然后一次性地对真实 DOM 进行更新,大大减少了不必要的 DOM 操作次数,从而降低交互或者页面更新带来的性能损耗。 - 批量更新机制:VNode 允许将多个 DOM 更新操作收集起来,在合适的时机进行批量处理。当一个复杂应用在短时间内发生多次状态变化,从而导致多个 DOM 更新操作时,如果没有 VNode 的批量更新机制,这些更新操作可能会逐个触发,导致频繁的 DOM 操作。通过 VNode,可以将这些更新操作缓存在一个队列中,当更新操作积累到一定数量,或者在某个合适的时间点(如在下一个时间循环中),再统一将这些更新应用到真实 DOM 上,减少了浏览器的重排和重绘次数。
- 跨平台和组件化开发优势
- 跨平台复用:因为 VNode 不依赖于 特定浏览器的 DOM API,所以一套代码可以在多端复用,包括原生应用、Web 应用、SSR 应用、小程序应用等
- 组件化开发优势:每个组件可以看做一棵独立的 VNode 树,组件内部更新时,通过 Diff 算法可以准确更新对应的 DOM,不会影响到其他组件。
- 状态管理和响应式更新优势
- 精确的状态 - 视图映射:状态通常以数据的形式存储在 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 处理步骤:
- 将该 VNode 通过挂载(mount)操作生成组件根节点元素
- 初始化组件:维护组件上下文,包括对 props、插槽、以及其他组件实例的属性(data)的初始化处理;绑定组件事件;浏览器交互激活。
- 递归处理组件子树的 VNode
- 普通元素 VNode 处理步骤:
- 将该 VNode 通过挂载(mount)操作转换为虚拟 DOM 元素
- 根据虚拟 DOM 创建真实 DOM 元素并插入到文档中
- 更新元素的属性
- 递归挂载子节点(如有)
将所有类型的节点处理完成后,我们将得到一棵虚拟 DOM 树,这时 Vue 会将虚拟 DOM 树插入 DOM 元素中,至此,大体流程结束。
beforeMount和mounted阶段最适合做什么操作
经过对 mount 阶段的分析,我们可以得知,所有的 DOM 实例对象、子树对象、VNode 对象都需要在 mount 阶段生成,在这个阶段之后才能访问并修改这些对象,同时,浏览器交互与组件事件也在这个阶段激活和绑定,所以浏览器参数初始化以及事件绑定,都得在这个阶段之后进行。
在挂载节点之前,会调用beforeMount生命周期钩子。在这个阶段,组件已经完成了数据初始化、模板编译和 VNode 生成等大部分准备工作,但还没有将 VNode 转换为真实 DOM 并插入到页面中。开发者可以在这个钩子中进行一些最后的准备工作,比如修改数据、添加自定义的逻辑等。
在组件节点挂载完成后,会调用mounted生命周期钩子。此时,组件已经成功地插入到页面中,成为 DOM 树的一部分。开发者可以在这个钩子中进行一些需要在 DOM 挂载后才能进行的操作,如获取 DOM 元素的尺寸、初始化第三方插件(如果插件需要在 DOM 节点存在的基础上进行初始化)等。