vue的diff算法知识点总结
源码:
虚拟dom
diff算法首先要明确一个概念就是diff的对象是虚拟dom,更新真实dom则是diff算法的结果
vnode基类
constructor ( 。。。 ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.fncontext = undefined this.fnoptions = undefined this.fnscopeid = undefined this.key = data && data.key this.componentoptions = componentoptions this.componentinstance = undefined this.parent = undefined this.raw = false this.isstatic = false this.isrootinsert = true this.iscomment = false this.iscloned = false this.isonce = false this.asyncfactory = asyncfactory this.asyncmeta = undefined this.isasyncplaceholder = false }
这个部分的代码 主要是为了更好地知道在diff算法中具体diff的属性的含义,当然也可以更好地了解vnode实例
整体过程
核心函数是patch函数
- isundef判断(是不是undefined或者null)
- // empty mount (likely as component), create new root elementcreateelm(vnode, insertedvnodequeue) 这里可以发现创建节点不是一个一个插入,而是放入一个队列中统一批处理
- 核心函数samevnode
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) ) ) ) }
这里是一个外层的比较函数,直接去比较了两个节点的key,tag(标签),data的比较(注意这里的data指的是vnodedata),input的话直接比较type。
export interface vnodedata { key?: string | number; slot?: string; scopedslots?: { [key: string]: scopedslot }; ref?: string; tag?: string; staticclass?: string; class?: any; staticstyle?: { [key: string]: any }; style?: object[] | object; props?: { [key: string]: any }; attrs?: { [key: string]: any }; domprops?: { [key: string]: any }; hook?: { [key: string]: function }; on?: { [key: string]: function | function[] }; nativeon?: { [key: string]: function | function[] }; transition?: object; show?: boolean; inlinetemplate?: { render: function; staticrenderfns: function[]; }; directives?: vnodedirective[]; keepalive?: boolean; }
这会确认两个节点是否有进一步比较的价值,不然直接替换
替换的过程主要是一个createelm函数 另外则是销毁oldvnode
// destroy old node if (isdef(parentelm)) { removevnodes(parentelm, [oldvnode], 0, 0) } else if (isdef(oldvnode.tag)) { invokedestroyhook(oldvnode) }
插入过程简化来说就是判断node的type分别调用
createcomponent(会判断是否有children然后递归调用)
createcomment
createtextnode
创建后使用insert函数
之后需要用hydrate函数将虚拟dom和真是dom进行映射
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 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 } 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) } }
const el = vnode.el = oldvnode.el 这是很重要的一步,让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化。
- 比较二者引用是否一致
- 之后asyncfactory不知道是做什么的,所以这个比较看不懂
- 静态节点比较key,相同后也不做重新渲染,直接拷贝componentinstance(once命令在此生效)
- 如果vnode是文本节点或注释节点,但是vnode.text != oldvnode.text时,只需要更新vnode.elm的文本内容就可以
- children的比较
- 如果只有oldvnode有子节点,那就把这些节点都删除
- 如果只有vnode有子节点,那就创建这些子节点,这里如果oldvnode是个文本节点就把vnode.elm的文本设置为空字符串
- 都有则updatechildren,这个之后详述
- 如果oldvnode和vnode都没有子节点,但是oldvnode是文本节点或注释节点,就把vnode.elm的文本设置为空字符串
updatechildren
这部分重点还是关注整个算法
首先四个指针,oldstart,oldend,newstart,newend,两个数组,oldvnode,vnode。
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 while (oldstartidx <= oldendidx && newstartidx <= newendidx) { if (isundef(oldstartvnode)) { oldstartvnode = oldch[++oldstartidx] // vnode has been moved left } else if (isundef(oldendvnode)) { oldendvnode = oldch[--oldendidx] } else if (samevnode(oldstartvnode, newstartvnode)) { patchvnode(oldstartvnode, newstartvnode, insertedvnodequeue) oldstartvnode = oldch[++oldstartidx] newstartvnode = newch[++newstartidx] } else if (samevnode(oldendvnode, newendvnode)) { patchvnode(oldendvnode, newendvnode, insertedvnodequeue) oldendvnode = oldch[--oldendidx] newendvnode = newch[--newendidx] } else if (samevnode(oldstartvnode, newendvnode)) { // vnode moved right patchvnode(oldstartvnode, newendvnode, insertedvnodequeue) canmove && nodeops.insertbefore(parentelm, oldstartvnode.elm, nodeops.nextsibling(oldendvnode.elm)) oldstartvnode = oldch[++oldstartidx] newendvnode = newch[--newendidx] } else if (samevnode(oldendvnode, newstartvnode)) { // vnode moved left patchvnode(oldendvnode, newstartvnode, insertedvnodequeue) canmove && nodeops.insertbefore(parentelm, oldendvnode.elm, oldstartvnode.elm) 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)) { // new element createelm(newstartvnode, insertedvnodequeue, parentelm, oldstartvnode.elm, false, newch, newstartidx) } else { vnodetomove = oldch[idxinold] 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, false, newch, newstartidx) } } 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) } }
一个循环比较的几种情况和处理(以下的++ --均指index的++ --)比较则是比较的node节点,简略写法 不严谨 比较用的是samevnode函数也不是真的全等
整体循环不结束的条件oldstartidx <= oldendidx && newstartidx <= newendidx
- oldstart === newstart,oldstart++ newstart++
- oldend === newend,oldend-- newend--
- oldstart === newend, oldstart插到队伍末尾 oldstart++ newend--
- oldend === newstart, oldend插到队伍开头 oldend-- newstart++
- 剩下的所有情况都走这个处理简单的说也就两种处理,处理后newstart++
- newstart在old中发现一样的那么将这个移动到oldstart前
- 没有发现一样的那么创建一个放到oldstart之前
循环结束后并没有完成
还有一段判断才算完
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) }
简单的说就是循环结束后,看四个指针中间的内容,old数组中和new数组中,多退少补而已
总结
整体认识还很粗糙,不过以目前的水平和对vue的了解也就只能到这了