React/Vue都用到了虚拟DOM,围绕虚拟DOM,本篇主要解决下面3个问题。vue
为何要使用虚拟DOM? 如何定义(建立)虚拟dom呢? 虚拟DOM如何映射为真实DOM?node
咱们的编码目标是下面的demo可以成功渲染。web
let vm = new Vue({
el: '#app',
render (h) {
return h('h1', 'Hello Vue!')
}
})
复制代码
将下列代码拷贝至浏览器中运行:算法
let d = document.createElement('div')
for(let key in d) console.log(key)
复制代码
咱们会发现,真实dom上有很是多的属性,经过自定义虚拟dom可以有效节省空间。小程序
另外,真实dom的重排重绘是很是消耗性能的,应该尽可能少修改,借助虚拟DOM的diff算法,可以有效提高性能。浏览器
最重要的是,当前有很是多的跨端开发需求,如原生、web、小程序等等,借助虚拟DOM有助于跨端开发,一段代码到处运行。bash
VNode必备属性只有tag/data/children/text/elm,其余属性为vue功能须要,如componetOptions/componentInstance只在组件节点中才被使用。app
export class VNode {
tag?: string
data?: VNodeData
children?: Array<VNode>
text?: string
elm?: Node
context?: Vue
componentOptions?: VueOptions
componentInstance?: Vue
parent?: VNode
key?: string | number
constructor(
tag?: string,
data?: VNodeData,
children?: Array<VNode>,
text?: string,
elm?: Node,
context?: Vue,
componentOptions?: VueOptions
) {
this.tag = tag
this.data = data || ({} as VNodeData)
this.children = children
this.text = text
this.elm = elm
this.context = context || bindContenxt
this.componentOptions = componentOptions
}
}
复制代码
在vue-render方法中,此处h
即为建立虚拟节点的函数。dom
new Vue({
render (h) {
return h('h1', 'hello world')
}
})
复制代码
咱们知道真实DOM的节点类型很是多,如Element、Attr、Comment、Document、DocumentFragment、Text等,而VNode,只作4种形式:组件节点、子节点(children属性不为空)、文本节点、注释节点。异步
h为重载函数,根据参数不一样生成不一样类型的vnode:
子节点类型,其tag和children属性不为空,其text属性为空。
v1 = h('h1', [h('', 'hello world')])
{
children: [
{
children: undefined,
data: {},
elm: undefined,
tag: undefined,
text: 'hello world'
}
],
data: {},
elm: undefined,
tag: "h1",
text: undefined,
}
复制代码
文本节点类型,其tag和children属性为空,其text属性不为空。
v2 = h('', 'hello world')
{
children: undefined,
data: {},
elm: undefined,
tag: undefined,
text: 'hello world'
}
复制代码
文本节点类型,其tag属性为!
,children属性为空,其text属性不为空。
v3 = h('!', 'hello comment')
{
children: undefined,
data: {},
elm: undefined,
tag: '!',
text: 'hello world'
}
复制代码
组件节点类型,其componentOptions属性不为空。
v4 = h('button-count', [])
{
children: undefined
componentInstance: Proxy {$refs: {…}, $options: {…}}
componentOptions: {Ctor: ƒ, propsData: undefined, children: Array(1), tag: "button-counter"}
data: {on: undefined, hook: {…}}
elm: button
tag: "vue-component-1-button-counter"
text: undefined
}
复制代码
经过属性状态划分为4种类型,在进行diff算法时,针对不一样的类型将进行不一样的处理,如组件节点会调用createComponentInstanceForVnode
进行初始化。
咱们建立了本身的虚拟DOM,接下来,将虚拟DOM映射为真实DOM,将Hello Vue
渲染至浏览器。
映射过程有一个很是重要的方法patch
,patch接收新旧节点,执行diff算法。
sameVnode
关系,则调用patchVnode
webMethods.append
的本质是执行parentElm.appendChild(createElm(vnode))
function patch(oldVnode: VNode, vnode: VNode) {
let parentElm = webMethods.parentNode(oldVnode.elm)
if (isSameVnode(oldVnode, vnode)) {
patchNode(oldVnode, vnode)
} else {
webMethods.remove(parentElm, oldVnode.elm)
webMethods.append(parentElm, createElm(vnode))
}
return parentElm
}
复制代码
createElm
须要根据虚拟节点的类型进行不一样的处理,同时它会将生成好的真实DOM挂载在vnode.elm
属性之上,方便对真实dom进行操做。
function createElm(vnode: VNode): Node {
// 组件节点
if (createComponent(vnode)) {
return vnode.elm
}
if (vnode.tag === '!') {
// 注释节点
vnode.elm = webMethods.createComment(vnode.text!)
} else if (!vnode.tag) {
// 文本节点
vnode.elm = webMethods.createText(vnode.text!)
} else {
// 子节点
vnode.elm = webMethods.createElement(vnode.tag!)
}
return vnode.elm
}
复制代码
接着对相同虚拟节点(sameVNode
)进行比较,根据children属性分状况处理,如updateChilden(比较子节点),removeChildren(删除子节点),insertChildren(添加子节点),setTextContent(修改文本的内容)。
function patchNode(oldVnode: VNode, vnode: VNode) {
let i: any
const data = vnode.data,
oldCh = oldVnode.children,
ch = vnode.children,
elm = (vnode.elm = oldVnode.elm!)
if (oldVnode === vnode) return
if (oldCh) {
// 子节点
if (ch) {
if (ch === oldCh) return
updateChildren(elm!, oldCh, ch)
} else {
removeChildren(elm!, oldCh, 0, oldCh.length - 1)
webMethods.setTextContent(elm!, vnode.text!)
}
} else {
// 文本节点
if (ch) {
webMethods.setTextContent(elm, '')
insertChildren(elm!, null, ch, 0, ch.length - 1)
} else {
webMethods.setTextContent(elm!, vnode.text!)
}
}
}
复制代码
最终经过不断递归,比较完全部虚拟DOM。
回顾咱们的DEMO,咱们须要页面可以渲染出<h1>Hello Vue!</h1>
let vm = new Vue({
el: '#app',
render (h) {
return h('h1', 'Hello Vue!')
}
})
复制代码
初始化vue实例后,调用render函数会返回vnode,而el指向的根节点会被初始化为oldVnode,即:
oldVnode = {
tag: 'DIV'
elm: //指向真实dom
}
vnode = {
tag: 'h1',
ele: undefined,
children: [
{
tag: '',
text: 'hello world'
}
]
}
复制代码
接着执行patch(oldVnode, vnode)
,对节点进行比较,完成渲染。
咱们根据上面的流程实现下功能吧。
补充说明下方法:h
为生成VNode的函数,createNodeAt
将真实DOM转为虚拟DOM,patch
是进行映射的核心函数。
class Vue {
constructor (options) {
this.$options = options
this._vnode = null
if(options.el) {
this.$mount(options.el)
}
},
_render () {
return this.$options.render.call(this, h)
},
_update (vnode) {
let oldVnode = this._vnode
this._vnode = vnode
patch(oldVnode, vnode)
}
$mount (el) {
this._vnode = createNodeAt(documeng.querySelector(options.el))
this._update(this._render())
}
}
复制代码
ps: 还没有验证(运行)上述代码,后期将进行验证。
虚拟DOM的diff算法可能没有表述清楚,推荐直接看snabbdom。基于虚拟DOM技术进行跨平台开发的方案有:ReactNative、Weex、taro等,还没有学习故不作叙述。
虚拟DOM究竟提高了多少性能?(www.zhihu.com/question/31…
虚拟DOM的起源?(juejin.im/post/5d085c…
虚拟DOM的diff算法?