Vue源码之虚拟DOM
什么是虚拟DOM
虚拟DOM是随着时代发展而诞生的产物。
在web早期,都是命令式的操作DOM,虽然简单好用,但是不会维护。
现在,三大主流框架都是声明式的操作DOM,通过描述状态和DOM之间的映射关系,来渲染成视图。状态怎么生成视图,不需要你来关心,框架会帮你搞定。
当某个状态发生改变时,如何只更新与这个状态相关联的DOM节点。
虚拟DOM的解决方式是:根据状态生成一个虚拟节点树,然后使用虚拟节点树进行渲染。在渲染前会将新的虚拟节点树和旧的虚拟节点树进行对比,只渲染不同的地方。
虚拟节点树是由组件树建立起来的整个虚拟节点(vnode)树。
vnode是JavaScript中一个很普通的对象,这个对象上保存了生成DOM节点需要的一些数据。
为什么要引入虚拟DOM
在Angular和 React中,它们只知道有状态(state)变化了,但是不是到具体是哪个或者哪些状态(state)变化了,所以就需要进行比较暴力的对比,React是通过虚拟Dom进行对比,Angular是使用脏检查流程。
Vue的变化侦测和这两个都不一样,Vue在一定程度是知道哪些状态发生了变化。但是如果细粒度太细,每一个绑定都会有一个watcher实例来观察状态的变化,这样就会有一些内存开销以及一些依赖追踪的开销。当状态越多的节点被使用时,开销就越大。
因此在Vue2把细粒度调整到中,组件级别是一个watcher实例,不管组件内部使用了多少次,因此当状态变化时,只通知到组件,在组件内部使用虚拟DOM去进行对比更新。
Vue.js 中的虚拟DOM
在vue中使用模板来描述状态和DOM之间的映射关系,根据模板生成render渲染函数,执行render渲染函数生成虚拟节点树,然后再与旧的虚拟节点树对比,最后渲染DOM。
虚拟DOM在vue中主要做了两件事:
- 提供与真实DOM相对应的vnode
- 把新的虚拟节点树和旧的虚拟节点树进行对比,更新不同的地方
对两个虚拟节点进行对比是整个虚拟DOM中最核心的算法,它可以判断出那个节点发生了变更,从而只更新变更的节点。
vnode
Vue 中有一个 VNode 类,可以实例化不同类型的 vnode,不同类型的 vnode 表示不同类型的 DOM 元素:
class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching devtoolsMeta: ?Object; // used to store functional render context for devtools fnScopeId: ?string; // functional scope id support 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 可以理解为节点描述对象,描述了怎么去创建一个真实的DOM,DOM元素上的所有属性堵在 vnode 上有对应的属性。
vnode 表示一个真正的 DOM 元素所有真实的 DOM 元素都使用 vnode 创建,并插入到视图中去。
vnode的作用
每次渲染视图时,都会创建新的 vnode ,与旧的 vnode 进行对比,找出不一样的地方并更新。
Vue 目前采用的是中等密度的细粒度,当状态变化时,只会通知到组件,在组件内部使用虚拟DOM来渲染视图。
也就是说只要组件内部有一个状态发生了改变了,整个组件都会重新渲染。如果组件只变化了一个节点,却要重新渲染所有的节点,这会造成性能浪费。
因此对比新旧 vnode ,只更新不同的部分,就显得尤为重要。
VNode的类型
注释节点
export const createEmptyVNode = (text: string = '') => { const node = new VNode() node.text = text node.isComment = true return node }
文本节点
export function createTextVNode (val: string | number) { return new VNode(undefined, undefined, undefined, String(val)) }
元素节点
元素节点通常包括一下4种属性:
- tag 节点名称,例如:div
- data 节点数据,例如:class style
- children 当前节点的子节点列表
- context 当前组件的Vue实例
组件节点
和元素节点差不多,有两个特有属性:
- componentOptions 组件节点的选项参数,包括:propsData、tag、children
- componentInstance 组件实例。在 Vue 中每个组件都是一个Vue实例。
函数式节点
和组件节点类似,有两个特有属性:
- functionalContext
- functionalOptions
克隆节点
克隆节点是将现有节点的所有属性都复制到新节点下面,直接复用现有的 vnode ,出了首次渲染外,后续更新都不需要执行渲染函数生成相同的 vnode ,从而提升一定程度的性能。
export function cloneVNode (vnode: VNode): VNode { const cloned = new VNode( vnode.tag, vnode.data, // #7975 // clone children array to avoid mutating original in case of cloning // a child. 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 }
总结:
在 Vue 中,VNode 是一个类,不同类型的 VNode 实例代表不同类型的元素节点。
Vue2 对状态的侦测策略采用了中等粒度,当状态发生变化时,只会通知到组件,组件内部再使用虚拟DOM进行更新。组件内不是所有的节点都需要更新,所以将渲染函数新生成的 vnode 和旧的 vnode 进行对比,只更新不同的部分。
下一篇会介绍虚拟DOM最核心的算法:patch。