这篇文章主要介绍了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.
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
jquery选中select组件被选中的值方法
vue.js中$set与数组更新方法_vue.js
vue与vue-i18n结合实现后台数据的多语言切换方法
以上就是在vue中实现虚拟dom的patch(详细教程)的详细内容。
