Vue源码分析之-虚拟DOM

什么是虚拟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)
            }
          }

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操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值