vue 虚拟dom的patch源码分析
本文介绍了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
,接着把oldstartidx
和newstartidx
索引分别+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
.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。