视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
vue 虚拟dom的patch源码分析
2020-11-27 22:18:14 责编:小采
文档

本文介绍了vue 虚拟dom的patch源码分析,分享给大家,具体如下:

源码目录:src/core/vdom/patch.js

 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

 const canMove = !removeOnly

 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 开始索引大于结束索引,进不了
 if (isUndef(oldStartVnode)) {
 oldStartVnode = oldCh[++oldStartIdx] // Vnode已经被移走了。
 } else if (isUndef(oldEndVnode)) {
 oldEndVnode = oldCh[--oldEndIdx]
 } else if (sameVnode(oldStartVnode, newStartVnode)) {
 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
 oldStartVnode = oldCh[++oldStartIdx] // 索引加1。是去对比下一个节点。比如之前start=a[0],那现在start=a[1],改变start的值后再去对比start这个vnode
 newStartVnode = newCh[++newStartIdx]
 
 } else if (sameVnode(oldEndVnode, newEndVnode)) { 
 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
 oldEndVnode = oldCh[--oldEndIdx]
 newEndVnode = newCh[--newEndIdx]
 } else if (sameVnode(oldStartVnode, newEndVnode)) { 
 patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))// 把节点b移到树的最右边
 oldStartVnode = oldCh[++oldStartIdx]
 newEndVnode = newCh[--newEndIdx]
 
 } else if (sameVnode(oldEndVnode, newStartVnode)) { old.end.d=new.start.d
 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)// Vnode moved left,把d移到c的左边。=old.start->old.end
 oldEndVnode = oldCh[--oldEndIdx] 
 newStartVnode = newCh[++newStartIdx] 

 } else {
 if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
 idxInOld = isDef(newStartVnode.key)
 ? oldKeyToIdx[newStartVnode.key]
 : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
 if (isUndef(idxInOld)) { 
 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) // 创建新节点,后面执行了nodeOps.insertBefore(parent, elm, ref)
 } else {
 vnodeToMove = oldCh[idxInOld]
 /* istanbul ignore if */
 if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
 warn(
 'It seems there are duplicate keys that is causing an update error. ' +
 'Make sure each v-for item has a unique key.'
 )
 }
 if (sameVnode(vnodeToMove, newStartVnode)) {
 patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
 oldCh[idxInOld] = undefined
 canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
 } else {
 // same key but different element. treat as new element
 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
 }
 }
 newStartVnode = newCh[++newStartIdx] 
 
 }
 }
 if (oldStartIdx > oldEndIdx) {
 refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
 addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
 } else if (newStartIdx > newEndIdx) {
 removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) // 删除旧的c,removeNode(ch.elm)

 }
 }
function sameVnode (a, b) {
 return (
 a.key === b.key && (
 (
 a.tag === b.tag &&
 a.isComment === b.isComment &&
 isDef(a.data) === isDef(b.data) &&
 sameInputType(a, b)
 ) || (
 isTrue(a.isAsyncPlaceholder) &&
 a.asyncFactory === b.asyncFactory &&
 isUndef(b.asyncFactory.error)
 )
 )
 )
}

/**
 * 比较新旧vnode节点,根据不同的状态对dom做合理的更新操作(添加,移动,删除)整个过程还会依次调用prepatch,update,postpatch等钩子函数,在编译阶段生成的一些静态子树,在这个过程
 * @param oldVnode 中由于不会改变而直接跳过比对,动态子树在比较过程中比较核心的部分就是当新旧vnode同时存在children,通过updateChildren方法对子节点做更新,
 * @param vnode
 * @param insertedVnodeQueue
 * @param removeOnly
 */
 function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
 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是克隆的,我们只做这个。
 // 如果新节点不是克隆的,则表示呈现函数。
 // 由热重加载api重新设置,我们需要进行适当的重新渲染。
 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 (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
 } else if (isDef(oldCh)) {
 removeVnodes(elm, 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)
 }
 }

function insertBefore (parentNode, newNode, referenceNode) {
 parentNode.insertBefore(newNode, referenceNode);
}

/**
 *
 * @param vnode根据vnode的数据结构创建真实的dom节点,如果vnode有children则会遍历这些子节点,递归调用createElm方法,
 * @param insertedVnodeQueue记录子节点创建顺序的队列,每创建一个dom元素就会往队列中插入当前的vnode,当整个vnode对象全部转换成为真实的dom 树时,会依次调用这个队列中vnode hook的insert方法
 * @param parentElm
 * @param refElm
 * @param nested
 */

 let inPre = 0
 function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
 vnode.isRootInsert = !nested // 过渡进入检查
 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
 return
 }

 const data = vnode.data
 const children = vnode.children
 const tag = vnode.tag
 if (isDef(tag)) {
 if (process.env.NODE_ENV !== 'production') {
 if (data && data.pre) {
 inPre++
 }
 if (
 !inPre &&
 !vnode.ns &&
 !(
 config.ignoredElements.length &&
 config.ignoredElements.some(ignore => {
 return isRegExp(ignore)
 ? ignore.test(tag)
 : ignore === tag
 })
 ) &&
 config.isUnknownElement(tag)
 ) {
 warn(
 'Unknown custom element: <' + tag + '> - did you ' +
 'register the component correctly? For recursive components, ' +
 'make sure to provide the "name" option.',
 vnode.context
 )
 }
 }
 vnode.elm = vnode.ns
 ? nodeOps.createElementNS(vnode.ns, tag)
 : nodeOps.createElement(tag, vnode)
 setScope(vnode)

 /* istanbul ignore if */
 if (__WEEX__) {
 // in Weex, the default insertion order is parent-first.
 // List items can be optimized to use children-first insertion
 // with append="tree".
 const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
 if (!appendAsTree) {
 if (isDef(data)) {
 invokeCreateHooks(vnode, insertedVnodeQueue)
 }
 insert(parentElm, vnode.elm, refElm)
 }
 createChildren(vnode, children, insertedVnodeQueue)
 if (appendAsTree) {
 if (isDef(data)) {
 invokeCreateHooks(vnode, insertedVnodeQueue)
 }
 insert(parentElm, vnode.elm, refElm)
 }
 } else {
 createChildren(vnode, children, insertedVnodeQueue)
 if (isDef(data)) {
 invokeCreateHooks(vnode, insertedVnodeQueue)
 }
 insert(parentElm, vnode.elm, refElm)
 }

 if (process.env.NODE_ENV !== 'production' && data && data.pre) {
 inPre--
 }
 } 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)
 }
 }
function insert (parent, elm, ref) {
 if (isDef(parent)) {
 if (isDef(ref)) {
 if (ref.parentNode === parent) {
 nodeOps.insertBefore(parent, elm, ref)
 }
 } else {
 nodeOps.appendChild(parent, elm)
 }
 }
 }

function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
 for (; startIdx <= endIdx; ++startIdx) {
 const ch = vnodes[startIdx]
 if (isDef(ch)) {
 if (isDef(ch.tag)) {
 removeAndInvokeRemoveHook(ch)
 invokeDestroyHook(ch)
 } else { // Text node
 removeNode(ch.elm)
 }
 }
 }
 }

updateChildren方法主要通过while循环去对比2棵树的子节点来更新dom,通过对比新的来改变旧的,以达到新旧统一的目的。

通过一个例子来模拟一下:

假设有新旧2棵树,树中的子节点分别为a,b,c,d等表示,不同的代号代表不同的vnode,如:

在设置好状态后,我们开始第一遍比较,此时oldStartVnode=a,newStartVnode=a;命中了sameVnode(oldStartVnode,newStartVnode)逻辑,则直接调用patchVnode(oldStartVnode,newStartVnode,insertedVnodeQueue)方法更新节点a,接着把oldStartIdxnewStartIdx索引分别+1,如图:

更新完节点a后,我们开始第2遍比较,此时oldStartVnode=b,newEndVnode=b;命中了sameVnode(oldStartVnode,newEndVnode)逻辑,则调用patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)方法更新节点b,接着调用canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)),把节点b移到树的最右边,最后把oldStartIdx索引+1,newEndIdx索引-1,如图:

更新完节点b后,我们开始第三遍比较,此时oldEndVnode=d,newStartVnode=d;命中了sameVnode(oldEndVnode, newStartVnode)逻辑,则调用patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)方法更新节点d,接着调用canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm),把d移到c的左边。最后把oldEndIdx索引-1,newStartIdx索引+1,如图:

更新完d后,我们开始第4遍比较,此时newStartVnode=e,节点e在旧树里是没有的,因此应该被作为一个新的元素插入,调用createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm),后面执行了nodeOps.insertBefore(parent, elm, ref)方法把e插入到c之前,接着把newStartIdx索引+1,如图:

插入节点e后,我们可以看到newStartIdx已经大于newEndIdx了,while循环已经完毕。接着调用removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) 删除旧的c,最终如图:

updateChildren通过以上几步操作完成了旧树子节点的更新,实际上只用了比较小的dom操作,在性能上有所提升,并且当子节点越复杂,这种提升效果越明显。vnode通过patch方法生成dom后,会调用mounted hook,至此,整个vue实例就创建完成了,当这个vue实例的watcher观察到数据变化时,会两次调用render方法生成新的vnode,接着调用patch方法对比新旧vnode来更新dom.

下载本文
显示全文
专题