DOM
是文档对象模型(Document Object Model
)的简写,在浏览器中咱们能够经过js来操做DOM
,可是这样的操做性能不好,因而Virtual Dom
应运而生。个人理解,Virtual Dom
就是在js中模拟DOM
对象树来优化DOM
操做的一种技术或思路。html
本文将对于Vue框架2.1.8版本中使用的Virtual Dom
进行分析。vue
一个VNode的实例对象包含了如下属性node
tag
: 当前节点的标签名web
data
: 当前节点的数据对象,具体包含哪些字段能够参考vue源码types/vnode.d.ts
中对VNodeData
的定义算法
children
: 数组类型,包含了当前节点的子节点数组
text
: 当前节点的文本,通常文本节点或注释节点会有该属性浏览器
elm
: 当前虚拟节点对应的真实的dom节点app
ns
: 节点的namespace框架
context
: 编译做用域dom
functionalContext
: 函数化组件的做用域
key
: 节点的key属性,用于做为节点的标识,有利于patch的优化
componentOptions
: 建立组件实例时会用到的选项信息
child
: 当前节点对应的组件实例
parent
: 组件的占位节点
raw
: raw html
isStatic
: 静态节点的标识
isRootInsert
: 是否做为根节点插入,被<transition>
包裹的节点,该属性的值为false
isComment
: 当前节点是不是注释节点
isCloned
: 当前节点是否为克隆节点
isOnce
: 当前节点是否有v-once
指令
VNode
能够理解为vue框架的虚拟dom的基类,经过new
实例化的VNode
大体能够分为几类
EmptyVNode
: 没有内容的注释节点
TextVNode
: 文本节点
ElementVNode
: 普通元素节点
ComponentVNode
: 组件节点
CloneVNode
: 克隆节点,能够是以上任意类型的节点,惟一的区别在于isCloned
属性为true
...
const SIMPLE_NORMALIZE = 1 const ALWAYS_NORMALIZE = 2 function createElement (context, tag, data, children, normalizationType, alwaysNormalize) { // 兼容不传data的状况 if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } // 若是alwaysNormalize是true // 那么normalizationType应该设置为常量ALWAYS_NORMALIZE的值 if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE // 调用_createElement建立虚拟节点 return _createElement(context, tag, data, children, normalizationType) } function _createElement (context, tag, data, children, normalizationType) { /** * 若是存在data.__ob__,说明data是被Observer观察的数据 * 不能用做虚拟节点的data * 须要抛出警告,并返回一个空节点 * * 被监控的data不能被用做vnode渲染的数据的缘由是: * data在vnode渲染过程当中可能会被改变,这样会触发监控,致使不符合预期的操做 */ if (data && data.__ob__) { process.env.NODE_ENV !== 'production' && warn( `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` + 'Always create fresh vnode data objects in each render!', context ) return createEmptyVNode() } // 当组件的is属性被设置为一个falsy的值 // Vue将不会知道要把这个组件渲染成什么 // 因此渲染一个空节点 if (!tag) { return createEmptyVNode() } // 做用域插槽 if (Array.isArray(children) && typeof children[0] === 'function') { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } // 根据normalizationType的值,选择不一样的处理方法 if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns // 若是标签名是字符串类型 if (typeof tag === 'string') { let Ctor // 获取标签名的命名空间 ns = config.getTagNamespace(tag) // 判断是否为保留标签 if (config.isReservedTag(tag)) { // 若是是保留标签,就建立一个这样的vnode vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) // 若是不是保留标签,那么咱们将尝试从vm的components上查找是否有这个标签的定义 } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) { // 若是找到了这个标签的定义,就以此建立虚拟组件节点 vnode = createComponent(Ctor, data, context, children, tag) } else { // 兜底方案,正常建立一个vnode vnode = new VNode( tag, data, children, undefined, undefined, context ) } // 当tag不是字符串的时候,咱们认为tag是组件的构造类 // 因此直接建立 } else { vnode = createComponent(tag, data, context, children) } // 若是有vnode if (vnode) { // 若是有namespace,就应用下namespace,而后返回vnode if (ns) applyNS(vnode, ns) return vnode // 不然,返回一个空节点 } else { return createEmptyVNode() } }
简单的梳理了一个流程图,能够参考下
patch
函数的定义在src/core/vdom/patch.js
中,咱们先来看下这个函数的逻辑
patch
函数接收6个参数:
oldVnode
: 旧的虚拟节点或旧的真实dom节点
vnode
: 新的虚拟节点
hydrating
: 是否要跟真是dom混合
removeOnly
: 特殊flag,用于<transition-group>
组件
parentElm
: 父节点
refElm
: 新节点将插入到refElm
以前
patch
的策略是:
若是vnode
不存在可是oldVnode
存在,说明意图是要销毁老节点,那么就调用invokeDestroyHook(oldVnode)
来进行销毁
若是oldVnode
不存在可是vnode
存在,说明意图是要建立新节点,那么就调用createElm
来建立新节点
当vnode
和oldVnode
都存在时
若是oldVnode
和vnode
是同一个节点,就调用patchVnode
来进行patch
当vnode
和oldVnode
不是同一个节点时,若是oldVnode
是真实dom节点或hydrating
设置为true
,须要用hydrate
函数将虚拟dom和真是dom进行映射,而后将oldVnode
设置为对应的虚拟dom,找到oldVnode.elm
的父节点,根据vnode建立一个真实dom节点并插入到该父节点中oldVnode.elm
的位置
这里面值得一提的是patchVnode
函数,由于真正的patch算法是由它来实现的(patchVnode中更新子节点的算法实际上是在updateChildren
函数中实现的,为了便于理解,我统一放到patchVnode
中来解释)。
patchVnode
算法是:
若是oldVnode
跟vnode
彻底一致,那么不须要作任何事情
若是oldVnode
跟vnode
都是静态节点,且具备相同的key
,当vnode
是克隆节点或是v-once
指令控制的节点时,只须要把oldVnode.elm
和oldVnode.child
都复制到vnode
上,也不用再有其余操做
不然,若是vnode
不是文本节点或注释节点
若是oldVnode
和vnode
都有子节点,且2方的子节点不彻底一致,就执行更新子节点的操做(这一部分实际上是在updateChildren
函数中实现),算法以下
分别获取oldVnode
和vnode
的firstChild
、lastChild
,赋值给oldStartVnode
、oldEndVnode
、newStartVnode
、newEndVnode
若是oldStartVnode
和newStartVnode
是同一节点,调用patchVnode
进行patch
,而后将oldStartVnode
和newStartVnode
都设置为下一个子节点,重复上述流程
若是oldEndVnode
和newEndVnode
是同一节点,调用patchVnode
进行patch
,而后将oldEndVnode
和newEndVnode
都设置为上一个子节点,重复上述流程
若是oldStartVnode
和newEndVnode
是同一节点,调用patchVnode
进行patch
,若是removeOnly
是false
,那么能够把oldStartVnode.elm
移动到oldEndVnode.elm
以后,而后把oldStartVnode
设置为下一个节点,newEndVnode
设置为上一个节点,重复上述流程
若是newStartVnode
和oldEndVnode
是同一节点,调用patchVnode
进行patch
,若是removeOnly
是false
,那么能够把oldEndVnode.elm
移动到oldStartVnode.elm
以前,而后把newStartVnode
设置为下一个节点,oldEndVnode
设置为上一个节点,重复上述流程
若是以上都不匹配,就尝试在oldChildren
中寻找跟newStartVnode
具备相同key
的节点,若是找不到相同key
的节点,说明newStartVnode
是一个新节点,就建立一个,而后把newStartVnode
设置为下一个节点
若是上一步找到了跟newStartVnode
相同key
的节点,那么经过其余属性的比较来判断这2个节点是不是同一个节点,若是是,就调用patchVnode
进行patch
,若是removeOnly
是false
,就把newStartVnode.elm
插入到oldStartVnode.elm
以前,把newStartVnode
设置为下一个节点,重复上述流程
若是在oldChildren
中没有寻找到newStartVnode
的同一节点,那就建立一个新节点,把newStartVnode
设置为下一个节点,重复上述流程
若是oldStartVnode
跟oldEndVnode
重合了,而且newStartVnode
跟newEndVnode
也重合了,这个循环就结束了
若是只有oldVnode
有子节点,那就把这些节点都删除
若是只有vnode
有子节点,那就建立这些子节点
若是oldVnode
和vnode
都没有子节点,可是oldVnode
是文本节点或注释节点,就把vnode.elm
的文本设置为空字符串
若是vnode
是文本节点或注释节点,可是vnode.text != oldVnode.text
时,只须要更新vnode.elm
的文本内容就能够
patch
提供了5个生命周期钩子,分别是
create
: 建立patch时
activate
: 激活组件时
update
: 更新节点时
remove
: 移除节点时
destroy
: 销毁节点时
这些钩子是提供给Vue内部的directives
/ref
/attrs
/style
等模块使用的,方便这些模块在patch的不一样阶段进行相应的操做,这里模块定义在src/core/vdom/modules
和src/platforms/web/runtime/modules
2个目录中
vnode
也提供了生命周期钩子,分别是
init
: vdom初始化时
create
: vdom建立时
prepatch
: patch以前
insert
: vdom插入后
update
: vdom更新前
postpatch
: patch以后
remove
: vdom移除时
destroy
: vdom销毁时
vue组件的生命周期底层其实就依赖于vnode的生命周期,在src/core/vdom/create-component.js
中咱们能够看到,vue为本身的组件vnode已经写好了默认的init
/prepatch
/insert
/destroy
,而vue组件的mounted
/activated
就是在insert
中触发的,deactivated
就是在destroy
中触发的
在Vue里面,Vue.prototype.$createElement
对应vdom的createElement
方法,Vue.prototype.__patch__
对应patch
方法,我写了个简单的demo来验证下功能
<p data-height="265" data-theme-id="0" data-slug-hash="rjZKZz" data-default-tab="html,result" data-user="JoeRay" data-embed-version="2" data-pen-title="Vue Virtual Dom" class="codepen">See the Pen Vue Virtual Dom by zhulei (@JoeRay) on CodePen.</p>
<script async src="https://production-assets.cod...