初六和家人出去玩,没写完博客。跳票了~
所谓虚拟DOM,是一个用于表示真实 DOM 结构和属性的 JavaScript 对象,这个对象用于对比虚拟 DOM 和当前真实 DOM 的差别化,而后进行局部渲染从而实现性能上的优化。在Vue.js 中虚拟 DOM 的 JavaScript 对象就是 VNode。
接下来咱们一步步分析:html
既然是虚拟 DOM 的做用是转为真实的 DOM,那这就是一个渲染的过程。因此咱们看看 render 方法。在以前的学习中咱们知道了,vue 的渲染函数 _render
方法返回的就是一个 VNode 对象。而在 initRender
初始化渲染的方法中定义的 vm._c
和 vm.$createElement
方法中,createElement
最终也是返回 VNode 对象。因此 VNode 是渲染的关键所在。
话很少说,来看看这个VNode为什么方神圣。前端
// src/core/vdom/vnode.js export default 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 fnScopeId: ?string; // functioanl 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 // 当前节点数据(VNodeData类型) this.children = children // 当前节点子节点 this.text = text // 当前节点文本 this.elm = elm // 当前节点对应的真实DOM节点 this.ns = undefined // 当前节点命名空间 this.context = context // 当前节点上下文 this.fnContext = undefined // 函数化组件上下文 this.fnOptions = undefined // 函数化组件配置项 this.fnScopeId = undefined // 函数化组件ScopeId this.key = data && data.key // 子节点key属性 this.componentOptions = componentOptions // 组件配置项 this.componentInstance = undefined // 组件实例 this.parent = undefined // 当前节点父节点 this.raw = false // 是否为原生HTML或只是普通文本 this.isStatic = false // 静态节点标志 keep-alive this.isRootInsert = true // 是否做为根节点插入 this.isComment = false // 是否为注释节点 this.isCloned = false // 是否为克隆节点 this.isOnce = false // 是否为v-once节点 this.asyncFactory = asyncFactory // 异步工厂方法 this.asyncMeta = undefined // 异步Meta this.isAsyncPlaceholder = false // 是否为异步占位 } // 容器实例向后兼容的别名 get child (): Component | void { return this.componentInstance } }
其实就是一个普通的 JavaScript Class 类,中间有各类数据用于描述虚拟 DOM,下面用一个例子来看看VNode 是如何表现 DOM 的。vue
<div id="app"> <span>{{ message }}</span> <ul> <li v-for="item of list" class="item-cls">{{ item }}</li> </ul> </div> <script> new Vue({ el: '#app', data: { message: 'hello Vue.js', list: ['jack', 'rose', 'james'] } }) </script>
这个例子最终结果如图:
简化后的VNode对象结果如图:node
{ "tag": "div", "data": { "attr": { "id": "app" } }, "children": [ { "tag": "span", "children": [ { "text": "hello Vue.js" } ] }, { "tag": "ul", "children": [ { "tag": "li", "data": { "staticClass": "item-cls" }, "children": [ { "text": "jack" } ] }, { "tag": "li", "data": { "staticClass": "item-cls" }, "children": [ { "text": "rose" } ] }, { "tag": "li", "data": { "staticClass": "item-cls" }, "children": [ { "text": "james" } ] } ] } ], "context": "$Vue$3", "elm": "div#app" }
在看VNode的时候小结如下几点:git
context
选项都指向了 Vue 实例。elm
属性则指向了其相对应的真实 DOM 节点。text
没有 tag
的节点。data
中咱们了解了VNode 是如何描述 DOM 以后,来学习如何将虚拟
DOM 变为真实的 DOM。github
从以前的文章中能够知道,Vue的渲染过程(不管是初始化视图仍是更新视图)最终都将走到 _update
方法中,再来看看这个 _update
方法。web
// src/core/instance/lifecycle.js Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this if (vm._isMounted) { callHook(vm, 'beforeUpdate') } const prevEl = vm.$el const prevVnode = vm._vnode const prevActiveInstance = activeInstance activeInstance = vm vm._vnode = vnode if (!prevVnode) { // 初始化渲染 vm.$el = vm.__patch__( vm.$el, vnode, hydrating, false /* removeOnly */, vm.$options._parentElm, vm.$options._refElm ) // no need for the ref nodes after initial patch // this prevents keeping a detached DOM tree in memory (#5851) vm.$options._parentElm = vm.$options._refElm = null } else { // 更新渲染 vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. }
不难发现更新试图都是使用了 vm.__patch__
方法,咱们继续往下跟。json
// src/platforms/web/runtime/index.js Vue.prototype.__patch__ = inBrowser ? patch : noop
这里啰嗦一句,要找vue的全局方法,如 vm.aaa
,直接查找 Vue.prototype.aaa
便可。
继续找下去:数组
// src/platforms/web/runtime/patch.js export const patch: Function = createPatchFunction({ nodeOps, modules })
找到 createPatchFunction
方法~性能优化
// src/core/vdom/patch.js export function createPatchFunction (backend) { …… return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { // 当前 VNode 未定义、老的 VNode 定义了,调用销毁钩子。 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // 老的 VNode 未定义,初始化。 isInitialPatch = true createElm(vnode, insertedVnodeQueue, parentElm, refElm) } else { // 当前 VNode 和老 VNode 都定义了,执行更新操做 // DOM 的 nodeType http://www.w3school.com.cn/jsref/prop_node_nodetype.asp const isRealElement = isDef(oldVnode.nodeType) // 是否为真实 DOM 元素 if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node // 修改已有根节点 patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } else { // 已有真实 DOM 元素,处理 oldVnode if (isRealElement) { // 挂载一个真实元素,确认是否为服务器渲染环境或者是否能够执行成功的合并到真实 DOM 中 if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { // 调用 insert 钩子 // inserted:被绑定元素插入父节点时调用 invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } } // 不是服务器渲染或者合并到真实 DOM 失败,建立一个空节点替换原有节点 oldVnode = emptyNodeAt(oldVnode) } // 替换已有元素 const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // 建立新节点 createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // 递归更新父级占位节点元素, if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } const insert = ancestor.data.hook.insert if (insert.merged) { for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // 销毁旧节点 if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } // 调用 insert 钩子 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } }
具体解析看代码注释~抛开调用生命周期钩子和销毁就节点不谈,咱们发现代码中的关键在于 createElm
和 patchVnode
方法。
先看 createElm
方法,这个方法建立了真实 DOM 元素。
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { if (isDef(vnode.elm) && isDef(ownerArray)) { vnode = ownerArray[index] = cloneVNode(vnode) } vnode.isRootInsert = !nested // for transition enter check // 建立组件 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { vnode.elm = nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } }
重点关注代码中的方法执行。代码太多,就不贴出来了,简单说说用途。
cloneVNode
用于克隆当前 vnode 对象。createComponent
用于建立组件,在调用了组件初始化钩子以后,初始化组件,而且从新激活组件。在从新激活组件中使用 insert
方法操做 DOM。nodeOps.createElementNS
和 nodeOps.createElement
方法,实际上是真实 DOM 的方法。setScope
用于为 scoped CSS 设置做用域 ID 属性createChildren
用于建立子节点,若是子节点是数组,则遍历执行 createElm
方法,若是子节点的 text 属性有数据,则使用 nodeOps.appendChild(...)
在真实 DOM 中插入文本内容。insert
用于将元素插入真实 DOM 中。因此,这里的 nodeOps
指的确定就是真实的 DOM 节点了。最终,这些全部的方法都调用了 nodeOps
中的方法来操做 DOM 元素。
这里顺便科普下 DOM 的属性和方法。下面把源码中用到的几个方法列出来便于学习:
- appendChild: 向元素添加新的子节点,做为最后一个子节点。
- insertBefore: 在指定的已有的子节点以前插入新节点。
- tagName: 返回元素的标签名。
- removeChild: 从元素中移除子节点。
- createElementNS: 建立带有指定命名空间的元素节点。
- createElement: 建立元素节点。
- createComment: 建立注释节点。
- createTextNode: 建立文本节点。
- setAttribute: 把指定属性设置或更改成指定值。
- nextSibling: 返回位于相同节点树层级的下一个节点。
- parentNode: 返回元素父节点。
- setTextContent: 获取文本内容(这个未在w3school中找到,不过应该就是这个意思了)。
OK,知道以上方法就比较好理解了,createElm
方法的最终目的就是建立真实的 DOM 对象。
看过了建立真实 DOM 后,咱们来学习虚拟 DOM 如何实现 DOM 的更新。这才是虚拟 DOM 的存在乎义 —— 比对并局部更新 DOM 以达到性能优化的目的。
看代码~
// 补丁 vnode function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { // 新旧 vnode 相等 if (oldVnode === vnode) { return } 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 } // 若是新旧 vnode 为静态;新旧 vnode key相同; // 新 vnode 是克隆所得;新 vnode 有 v-once 的属性 // 则新 vnode 的 componentInstance 用老的 vnode 的。 // 即 vnode 的 componentInstance 保持不变。 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 // 执行 data.hook.prepatch 钩子。 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)) { // 遍历 cbs,执行 update 方法 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) // 执行 data.hook.update 钩子 if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } // 旧 vnode 的 text 选项为 undefined if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { // 新旧 vnode 都有 children,且不一样,执行 updateChildren 方法。 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { // 清空文本,添加 vnode if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { // 移除 vnode removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { // 若是新旧 vnode 都是 undefined,清空文本 nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { // 有不一样文本内容,更新文本内容 nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { // 执行 data.hook.postpatch 钩子,代表 patch 完毕 if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
源码中添加了一些注释便于理解,来理一下逻辑。
其中,addVnodes
方法和 removeVnodes
都比较简单,很好理解。这里咱们来看看关键代码 updateChildren
方法。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, vnodeToMove, refElm // removeOnly 是一个只用于 <transition-group> 的特殊标签, // 确保移除元素过程当中保持一个正确的相对位置。 const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { // 开始老 vnode 向右一位 oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { // 结束老 vnode 向左一位 oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { // 新旧开始 vnode 类似,进行pacth。开始 vnode 向右一位 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { // 新旧结束 vnode 类似,进行patch。结束 vnode 向左一位 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // 新结束 vnode 和老开始 vnode 类似,进行patch。 patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) // 老开始 vnode 插入到真实 DOM 中,老开始 vnode 向右一位,新结束 vnode 向左一位 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // 老结束 vnode 和新开始 vnode 类似,进行 patch。 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) // 老结束 vnode 插入到真实 DOM 中,老结束 vnode 向左一位,新开始 vnode 向右一位 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // 获取老 Idx 的 key if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 给老 idx 赋值 idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // 若是老 idx 为 undefined,说明没有这个元素,建立新 DOM 元素。 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { // 获取 vnode vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { // 若是生成的 vnode 和新开始 vnode 类似,执行 patch。 patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue) // 赋值 undefined,插入 vnodeToMove 元素 oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 相同的key不一样的元素,视为新元素 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } // 新开始 vnode 向右一位 newStartVnode = newCh[++newStartIdx] } } // 若是老开始 idx 大于老结束 idx,若是是有效数据则添加 vnode 到新 vnode 中。 if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // 移除 vnode removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
表示已看晕……让咱们慢慢捋一捋……
嗯……就是这样!
毕竟是Vue的核心功能之一,虽然省略了很多代码,但博客篇幅很长。写了两天才写完。不过写完博客后感受对于 Vue 的理解又加深了不少。
在下一篇博客中,咱们一块儿来学习template的解析。
鉴于前端知识碎片化严重,我但愿可以系统化的整理出一套关于Vue的学习系列博客。
本文源码已收入到GitHub中,以供参考,固然能留下一个star更好啦^-^。
https://github.com/violetjack/VueStudyDemos
VioletJack,高效学习前端工程师,喜欢研究提升效率的方法,也专一于Vue前端相关知识的学习、整理。
欢迎关注、点赞、评论留言~我将持续产出Vue相关优质内容。
新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571...
CSDN: http://blog.csdn.net/violetja...
简书: http://www.jianshu.com/users/...
Github: https://github.com/violetjack