什么是虚拟DOM
- 虚拟DOM(Virtual DOM)是使用JavaScript对象描述真实DOM!
- Vue.js中的虚拟DOM借鉴Snabbdom,并添加了Vue.js的特性。
- 例如: 指令和组件机制
为什么要使用虚拟DOM
- 避免直接操作DOM,提高开发效率
- 作为一个中间层可以跨平台
- 虚拟DOM不一定能够提高性能
- 首次渲染的时候会增加开销
- 复杂视图的情况下(如中大型的单页应用SPA)提升渲染性能
- 如果有频繁DOM操作的话,虚拟DOM在更新真实DOM之前,首先会通过diff算法,对比新旧两个虚拟DOM树对象的差异,最终把差异更新到真实DOM;而不会每次都操作真实DOM。
- 另外通过给节点设置key属性,可以让节点最大程度得到重用,避免大量的重绘。
虚拟DOM 代码演示
new Vue()实例时,render的定义中有个习惯命名为h的函数类型参数,参数h()函数实际上对应我们源码中的$createElement()。
h() 函数的作用是用来创建一个虚拟节点VNode,调用 h(tag, data, children) 函数的时候需要传入三(4)个参数:
- tag:标签的名称:string类型 或者 组件的选项对象;
- data:是用来描述tag的,如果tag是标签,data中可以设置标签的属性或者它定义的DOM的元素属性等等,还可以注册事件。
- children:可以是字符串或者是数组,
- 如果是字符串的话,是设置的tag标签里面的内容textContent;
- 如果是数组的话,是设置tag标签中的子节点,子节点也是vnode类型的。
这里的 h函数 跟 snabbdom中的有些类似,核心作用都是一样的,都是为了创建vnode。 只不过Vue中的 h函数 它里面支持组件 component,并且还支持 slots 插槽。
使用示例:
data: { msg: 'Hello World' },
render(h) {
// h(tag, data, children)
// return h('h1', this.msg)
// return h('h1', { domProps: { innerHTML: this.msg }})
// return h('h1', { attrs: { id: 'title' }}, this.msg)
const vnode = h('h1', { attrs: { id: 'title' }}, this.msg)
console.log(vnode)
return vnode
}
打印的VNode实例
VNode {
asyncFactory: undefined
asyncMeta: undefined
children: [VNode]
componentInstance: undefined
componentOptions: undefined
context: Vue {_uid: 1, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
data: {attrs: {…}}
elm: h1#title
fnContext: undefined
fnOptions: undefined
fnScopeId: undefined
isAsyncPlaceholder: false
isCloned: false
isComment: false
isOnce: false
isRootInsert: true
isStatic: false
key: undefined
ns: undefined
parent: undefined
raw: false
tag: "h1"
text: undefined
child: undefined
[[Prototype]]: Object
}
VNode相较于真实DOM还是少相当多内容的。
h1#title
__vue__: Vue {_uid: 1, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
accessKey: ""
align: ""
ariaAtomic: null
ariaAutoComplete: null
ariaBusy: null
ariaChecked: null
ariaColCount: null
ariaColIndex: null
ariaColSpan: null
ariaCurrent: null
ariaDescription: null
ariaDisabled: null
ariaExpanded: null
ariaHasPopup: null
ariaHidden: null
ariaInvalid: null
ariaKeyShortcuts: null
ariaLabel: null
ariaLevel: null
ariaLive: null
ariaModal: null
ariaMultiLine: null
ariaMultiSelectable: null
ariaOrientation: null
ariaPlaceholder: null
ariaPosInSet: null
ariaPressed: null
ariaReadOnly: null
ariaRelevant: null
ariaRequired: null
ariaRoleDescription: null
ariaRowCount: null
ariaRowIndex: null
ariaRowSpan: null
ariaSelected: null
ariaSetSize: null
ariaSort: null
ariaValueMax: null
ariaValueMin: null
ariaValueNow: null
ariaValueText: null
assignedSlot: null
attributeStyleMap: StylePropertyMap {size: 0}
attributes: NamedNodeMap {0: id, id: id, length: 1}
autocapitalize: ""
autofocus: false
baseURI: "http://127.0.0.1:5500/examples/01-runtime+compiler/index.html"
childElementCount: 0
childNodes: NodeList [text]
children: HTMLCollection []
classList: DOMTokenList [value: '']
className: ""
clientHeight: 45
clientLeft: 0
clientTop: 0
clientWidth: 273
contentEditable: "inherit"
dataset: DOMStringMap {}
dir: ""
draggable: false
elementTiming: ""
enterKeyHint: ""
firstChild: text
firstElementChild: null
hidden: false
id: "title"
inert: false
innerHTML: "Hello World"
innerText: "Hello World"
inputMode: ""
isConnected: true
isContentEditable: false
lang: ""
lastChild: text
lastElementChild: null
localName: "h1"
namespaceURI: "http://www.w3.org/1999/xhtml"
nextElementSibling: script
nextSibling: text
nodeName: "H1"
nodeType: 1
nodeValue: null
nonce: ""
offsetHeight: 45
offsetLeft: 8
offsetParent: body
offsetTop: 21
offsetWidth: 273
onabort: null
onanimationend: null
onanimationiteration: null
onanimationstart: null
onauxclick: null
onbeforecopy: null
onbeforecut: null
onbeforematch: null
onbeforepaste: null
onbeforexrselect: null
onblur: null
oncancel: null
oncanplay: null
oncanplaythrough: null
onchange: null
onclick: null
onclose: null
oncontextlost: null
oncontextmenu: null
oncontextrestored: null
oncopy: null
oncuechange: null
oncut: null
ondblclick: null
ondrag: null
ondragend: null
ondragenter: null
ondragleave: null
ondragover: null
ondragstart: null
ondrop: null
ondurationchange: null
onemptied: null
onended: null
onerror: null
onfocus: null
onformdata: null
onfullscreenchange: null
onfullscreenerror: null
ongotpointercapture: null
oninput: null
oninvalid: null
onkeydown: null
onkeypress: null
onkeyup: null
onload: null
onloadeddata: null
onloadedmetadata: null
onloadstart: null
onlostpointercapture: null
onmousedown: null
onmouseenter: null
onmouseleave: null
onmousemove: null
onmouseout: null
onmouseover: null
onmouseup: null
onmousewheel: null
onpaste: null
onpause: null
onplay: null
onplaying: null
onpointercancel: null
onpointerdown: null
onpointerenter: null
onpointerleave: null
onpointermove: null
onpointerout: null
onpointerover: null
onpointerrawupdate: null
onpointerup: null
onprogress: null
onratechange: null
onreset: null
onresize: null
onscroll: null
onsearch: null
onsecuritypolicyviolation: null
onseeked: null
onseeking: null
onselect: null
onselectionchange: null
onselectstart: null
onslotchange: null
onstalled: null
onsubmit: null
onsuspend: null
ontimeupdate: null
ontoggle: null
ontransitioncancel: null
ontransitionend: null
ontransitionrun: null
ontransitionstart: null
onvolumechange: null
onwaiting: null
onwebkitanimationend: null
onwebkitanimationiteration: null
onwebkitanimationstart: null
onwebkitfullscreenchange: null
onwebkitfullscreenerror: null
onwebkittransitionend: null
onwheel: null
outerHTML: "<h1 id=\"title\">Hello World</h1>"
outerText: "Hello World"
ownerDocument: document
parentElement: body
parentNode: body
part: DOMTokenList [value: '']
prefix: null
previousElementSibling: null
previousSibling: text
role: null
scrollHeight: 45
scrollLeft: 0
scrollTop: 0
scrollWidth: 273
shadowRoot: null
slot: ""
spellcheck: true
style: CSSStyleDeclaration {accentColor: '', additiveSymbols: '', alignContent: '', alignItems: '', alignSelf: '', …}
tabIndex: -1
tagName: "H1"
textContent: "Hello World"
title: ""
translate: true
virtualKeyboardPolicy: ""
[[Prototype]]: HTMLHeadingElement
align: (...)
constructor: ƒ HTMLHeadingElement()
Symbol(Symbol.toStringTag): "HTMLHeadingElement"
accessKey: (...)
ariaAtomic: (...)
ariaAutoComplete: (...)
ariaBusy: (...)
ariaChecked: (...)
ariaColCount: (...)
ariaColIndex: (...)
ariaColSpan: (...)
ariaCurrent: (...)
ariaDescription: (...)
ariaDisabled: (...)
ariaExpanded: (...)
ariaHasPopup: (...)
ariaHidden: (...)
ariaInvalid: (...)
ariaKeyShortcuts: (...)
ariaLabel: (...)
ariaLevel: (...)
ariaLive: (...)
ariaModal: (...)
ariaMultiLine: (...)
ariaMultiSelectable: (...)
ariaOrientation: (...)
ariaPlaceholder: (...)
ariaPosInSet: (...)
ariaPressed: (...)
ariaReadOnly: (...)
ariaRelevant: (...)
ariaRequired: (...)
ariaRoleDescription: (...)
ariaRowCount: (...)
ariaRowIndex: (...)
ariaRowSpan: (...)
ariaSelected: (...)
ariaSetSize: (...)
ariaSort: (...)
ariaValueMax: (...)
ariaValueMin: (...)
ariaValueNow: (...)
ariaValueText: (...)
assignedSlot: (...)
attributeStyleMap: (...)
attributes: (...)
autocapitalize: (...)
autofocus: (...)
baseURI: (...)
childElementCount: (...)
childNodes: (...)
children: (...)
classList: (...)
className: (...)
clientHeight: (...)
clientLeft: (...)
clientTop: (...)
clientWidth: (...)
contentEditable: (...)
dataset: (...)
dir: (...)
draggable: (...)
elementTiming: (...)
enterKeyHint: (...)
firstChild: (...)
firstElementChild: (...)
hidden: (...)
id: (...)
inert: (...)
innerHTML: (...)
innerText: (...)
inputMode: (...)
isConnected: (...)
isContentEditable: (...)
lang: (...)
lastChild: (...)
lastElementChild: (...)
localName: (...)
namespaceURI: (...)
nextElementSibling: (...)
nextSibling: (...)
nodeName: (...)
nodeType: (...)
nodeValue: (...)
nonce: (...)
offsetHeight: (...)
offsetLeft: (...)
offsetParent: (...)
offsetTop: (...)
offsetWidth: (...)
onabort: (...)
onanimationend: (...)
onanimationiteration: (...)
onanimationstart: (...)
onauxclick: (...)
onbeforecopy: (...)
onbeforecut: (...)
onbeforematch: (...)
onbeforepaste: (...)
onbeforexrselect: (...)
onblur: (...)
oncancel: (...)
oncanplay: (...)
oncanplaythrough: (...)
onchange: (...)
onclick: (...)
onclose: (...)
oncontextlost: (...)
oncontextmenu: (...)
oncontextrestored: (...)
oncopy: (...)
oncuechange: (...)
oncut: (...)
ondblclick: (...)
ondrag: (...)
ondragend: (...)
ondragenter: (...)
ondragleave: (...)
ondragover: (...)
ondragstart: (...)
ondrop: (...)
ondurationchange: (...)
onemptied: (...)
onended: (...)
onerror: (...)
onfocus: (...)
onformdata: (...)
onfullscreenchange: (...)
onfullscreenerror: (...)
ongotpointercapture: (...)
oninput: (...)
oninvalid: (...)
onkeydown: (...)
onkeypress: (...)
onkeyup: (...)
onload: (...)
onloadeddata: (...)
onloadedmetadata: (...)
onloadstart: (...)
onlostpointercapture: (...)
onmousedown: (...)
onmouseenter: undefined
onmouseleave: undefined
onmousemove: (...)
onmouseout: (...)
onmouseover: (...)
onmouseup: (...)
onmousewheel: (...)
onpaste: (...)
onpause: (...)
onplay: (...)
onplaying: (...)
onpointercancel: (...)
onpointerdown: (...)
onpointerenter: (...)
onpointerleave: (...)
onpointermove: (...)
onpointerout: (...)
onpointerover: (...)
onpointerrawupdate: (...)
onpointerup: (...)
onprogress: (...)
onratechange: (...)
onreset: (...)
onresize: (...)
onscroll: (...)
onsearch: (...)
onsecuritypolicyviolation: (...)
onseeked: (...)
onseeking: (...)
onselect: (...)
onselectionchange: (...)
onselectstart: (...)
onslotchange: (...)
onstalled: (...)
onsubmit: (...)
onsuspend: (...)
ontimeupdate: (...)
ontoggle: (...)
ontransitioncancel: (...)
ontransitionend: (...)
ontransitionrun: (...)
ontransitionstart: (...)
onvolumechange: (...)
onwaiting: (...)
onwebkitanimationend: (...)
onwebkitanimationiteration: (...)
onwebkitanimationstart: (...)
onwebkitfullscreenchange: (...)
onwebkitfullscreenerror: (...)
onwebkittransitionend: (...)
onwheel: (...)
outerHTML: (...)
outerText: (...)
ownerDocument: (...)
parentElement: (...)
parentNode: (...)
part: (...)
prefix: (...)
previousElementSibling: (...)
previousSibling: (...)
role: (...)
scrollHeight: (...)
scrollLeft: (...)
scrollTop: (...)
scrollWidth: (...)
shadowRoot: (...)
slot: (...)
spellcheck: (...)
style: (...)
tabIndex: (...)
tagName: (...)
textContent: (...)
title: (...)
translate: (...)
virtualKeyboardPolicy: (...)
get align: ƒ align()
set align: ƒ align()
[[Prototype]]: HTMLElement
h函数 总结:
- vm.$createElement(tag, data, children, normalizeChildren)
- tag: 标签名称或者组件对象。
- data:描述tag,可以设置DOM的属性或者标签的属性。
- children:tag中的文本内容字符串或者子节点数组。
- 返回结果
- VNode对象实例vnode
VNode
-
VNode的核心属性
- tag
- data
- children
- text
- elm
- key
虚拟DOM创建的整体过程分析
Vue创建虚拟DOM的整体过程
vm._init()
初始化vue中的实例
vm.$mount()
mountComponent()
挂载组件
创建Watcher对象
核心
updateComponent()
vm._update(vm._render(), hydrating)
vm.render()
vnode = render.call(vm._renderProxy, vm.$createElement)
h函数就是vm.$createElement()方法
vm.$createElement()
h函数,render()中调用,通过代码演示
createElement(vm, a, b, c, d, true)
_createElement(context, tag, data, children, normalizationType)
vm._createElement()
vnode = new VNode(config.parsePlatformTagName(tag), data, children, undefined, undefined, context)
vm._render()结束,返回vnode
vm.update()
负责把 虚拟DOM 渲染成 真实DOM
首次执行
vm.__patch__(vm.$el, vnode, hydrating, false)
数据更新
vm.__patch__(prevVnode, vnode)
vm.__patch__()
patchVnode()
updateChildren()
VNode的创建过程
createElement上
createElement下
patch函数的初始化
- src/core/vdom/patch.js
patch函数的执行过程
- 核心
- createElm():
- 作用:把vnode 转化为真实DOM,然后挂载到DOM 树上
- // create new node
createElm(
vnode, // 要转换挂载的vnode
insertedVnodeQueue, // patch过程中对比差异时要插入的vnode
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm, // 将来要append的DOM的parent元素
nodeOps.nextSibling(oldElm) // 插入位置是oldElm的下一个兄弟节点
)
- patchVnode()
- 作用:对比新旧vnode,找到他们的差异,更新到真实DOM,也就是执行diff算法。
-
function patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { if (oldVnode === vnode) { return } if (isDef(vnode.elm) && isDef(ownerArray)) { // clone reused vnode vnode = ownerArray[index] = cloneVNode(vnode) } const elm = vnode.elm = oldVnode.elm if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // reuse element for static trees. // note we only do this if the vnode is cloned - // if the new node is not cloned it means the render functions have been // reset by the hot-reload-api and we need to do a proper re-render. if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(ch) } if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { removeVnodes(oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
- createElm():
updateChildren()
- 触发时机:在patchVnode执行过程中,当新老节点都有子节点,并且子节点是 sameVnode(oldVnode, vnode)为true 的时候,会调用updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly)
- 作用:对比新老子节点,找到这些子节点的差异,更新到DOM树;如果节点没有发生变化的话,会重用该节点。
- oldCh, newCh都是数组,对比的过程中,会根据DOM操作的特点进行优化:
- 先对比两数组的开始和结束的四个顶点。
- 老头
- 老尾
- 老头 - 新头
- 老尾 - 新尾
- 老头 - 新尾
- 老尾 - 新头
- 新的开始节点的key 去 老的节点数组中 依次找 相同key 的老节点
- 没找到 - createElm()
- 找到 - sameVnode ? patchVnode() : createElm()
- 结束处理
- 旧节点数组先遍历完,新节点数组还没有遍历完 - 把新节点数组多出来的 - addVnodes()
- 新节点数组先遍历完,老节点数组还没有遍历完 - 把老节点数组多出来的 - removeVnodes()
- 先对比两数组的开始和结束的四个顶点。
它跟snabbdom中的算法是一样的。
设置 key 的作用
开头结尾都有相同元素节点的情况下,最大的限度的重用vnode节点的,减少DOM操作的次数。
总结:
设置key 可以优化我们的DOM操作