本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-05-23
目前的主流框架都是声明式操作DOM,不再是jQuery时代的命令式操作DOM,都是通过描述状态和DOM之间的映射关系是怎样的,就可以将状态渲染成DOM。
本质上,我们将状态作为输入,并生成DOM输出到页面上显示出来,这个过程就叫渲染。
在程序运行时,状态会不断发生变化(交互),每当状态发生变化时,都需要重新渲染,由于访问DOM的代价是高昂的并且状态变化通常只有有限的几个节点需要重新渲染,所以我们不仅需要找到哪里需要更新,还需要尽可能少地的访问DOM。
虚拟DOM是解决这个问题的众多解决方案中的一种,通过状态生成一个虚拟节点树,然后使用虚拟节点树进行渲染,在渲染之前,会使用新生成的虚拟节点树和上一次生成的虚拟节点树进行对比,只渲染不同的部分。
事实上,Angular 和 React 的变化侦测有一个共同点,那就是它们都不知道哪些状态变了,因此,就需要进行比较暴力的比对,React 是通过虚拟DOM的对比,Angular 是使用脏检查的流程。
Vue.js 1.0 是使用细粒度的绑定来更新视图,也就是说,当状态发生变化时,它在一定程度上知道哪些节点使用了这个状态,因为其在DOM编译过程中,如果发现这些节点使用了状态,就会生成一个对应的 Watcher 来监听状态的变化,当状态发生变化的时候会接收到通知,从而对这些节点进行操作,但是由于粒度太细,每一个绑定都会有一个对应的 Watcher 来观察状态的变化,这样就会产生一些内存开销及一些依赖追踪的开销,当状态被越多的节点使用,开销就越大,对于一个大型项目,这个开销是非常大。
于是,Vue.js 2.0 选择了一个中粒度的解决方案,引入了虚拟DOM。组件级别是一个 Watcher 实例,就是说即使一个组件内有10个节点使用了某个状态,但其实也只有1个 Watcher 在观察这个状态的变化,所以当这个状态发生变化时,只能通知到组件,然后组件内部通过虚拟DOM去进行比对与渲染。
在Vue.js 中,使用模板来描述状态与DOM之间的映射关系,Vue.js 通过编译将模板转换成渲染函数(render),执行渲染函数就可以得到一个虚拟节点树,使用这个虚拟节点树与上一次渲染视图所使用的旧虚拟节点树,找出真正需要更新的节点来进行真正的DOM操作(同时避免其他无任何改动的操作),从而更新页面。
虚拟DOM在 Vue.js 中主要做两件事:
提供与真实DOM节点所对应的虚拟节点 vnode。
将虚拟节点 vnode 和旧虚拟节点 oldVnode 进行比对,然后更新视图。
vnode 在 JavaScript 中的本质就是一个普通的对象,这个对象的属性上保留了生成DOM节点所需要的一些数据;对两个虚拟节点进行比对是虚拟DOM中最核心的算法(patch),它可以判断出哪些节点发生了变化,从而只对发生了变化的节点进行更新操作。
export class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
}
vnode 是从 VNode 类实例化的对象,是一个节点描述对象,描述了应该怎样去创建真实的DOM节点。例如,tag 表示一个元素节点的名称。渲染视图的过程就是,先创建 vnode,然后再使用 vnode 去生成真实的DOM元素,最后插入到页面渲染视图。
1、注释节点
export const createEmptyNode = text => {
const node = new VNode();
node.text = text;
node.isComment = true;
return node;
}
2、文本节点
export const createTextVNode = val => {
return new VNode(undefined, undefined, undefined, String(val)); // 只有 text 属性
}
3、克隆节点
它的作用是优化静态节点和插槽节点,以静态节点为例,当组件发生更新时需要重新渲染,而静态节点的内容是不变的,所以除了首次渲染需要执行渲染函数获取 vnode 之外,后续更新就不需要执行渲染函数来重新生成 vnode,只需要 clone 即可。
export function cloneVNode (vnode) {
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
)
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
cloned.isComment = vnode.isComment
cloned.fnContext = vnode.fnContext
cloned.fnOptions = vnode.fnOptions
cloned.fnScopeId = vnode.fnScopeId
cloned.asyncMeta = vnode.asyncMeta
cloned.isCloned = true
return cloned
}
4、元素节点
元素节点对应的就是如下一个基本的 vnode:
const vnode = {
children: [VNode, VNode],
context: {...},
data: {...},
tag: "p",
...
}
5、组件节点
组件节点对应如下一个 vnode:
const vnode = {
componentInstance: {...},
componentOptions: {...},
context: {...},
data: {...},
tag: "vue-component-1-componentName",
...
}
其中,componentInstance
是组件的实例,同时也是Vue.js的实例。 componentOptions
是组件的选项参数,其中包含 propsData
、 tag
、 children
等信息。
6、函数式组件
通常,一个函数式组件的 vnode 如下:
const vnode = {
functionContext: {...},
functionOptions: {...},
context: {...},
data: {...},
tag: "div",
}
不同类型的 VNode 表示不同类型的只是 DOM 元素,Vue.js 对组件采用了虚拟DOM来更新视图,当属性发生变化时,整个组件都要进行重新渲染的操作,但组件内并不是所有的 DOM 节点都需要更新,所以将 vnode 缓存并将当前 vnode 和上一次缓存的 oldVnode 进行对比,只对需要更新的部分进行 DOM 操作,这样就可以提升很多性能。