vnode
首先要说明虚拟节点,本质就是一个对象:
{
sel: 'div', // 选择器
elm: undefined, // 关联的dom对象
key: undefined,
text: '', // 标签文本
data: {},
children: undefined // 子节点
}
vnode由h函数产生,模板引擎会最终把标签转换成h函数表达式,即h(sel,data,c)的形式,最终得到的就是vnode对象。在渲染的过程中,vnode会作为patch函数的参数,patch函数负责真正的渲染工作,即进行diff算法并且操作dom。patch函数是vue引擎调用的,具体时机是在数据劫持之后。
diff算法
diff算法是发生在vnode上的,即是在生成dom节点插入dom树之前的行为,比较的是vnode。
diff算法确认相同vnode的规则:选择器(sel)相同且key相同。
关于“列表”中增删改元素:如果我们没有给元素设置key属性,则判断vnode只能依据sel(可以说就是标签名),相当于不管你怎么主观移动列表元素,diff算法只会把新旧vnode按列表顺序逐一比较,不一样就改,最终多了就删,少了就在后面append,所以现象就是总会在后面追加或删除dom,然后从头把旧列表对照新列表重新改一遍,当然对比vnode一样时候的不会动旧dom。
patch函数流程
patch执行函数需要2个参数,旧vnode和新vnode。
这里有个特殊情况,首先通过是否有sel属性判断传入的旧vnode是否是原生dom对象,如果是原生dom需要先把他包装成一个vnode,然后才是diff算法处理dom的过程。
diff算法完整过程
判定新旧vnode不同:
patch会直接根据新vnode创建dom并insertBefore到旧dom上,然后删除旧vnode对应的dom,子dom会被连带脱离dom树。
判定新旧vnode相同:
[1] 首先判断新旧dom是不是同一个对象,是就什么都不用做(省去多余dom操作)
(以新vnode为基准分条件判断:)
[2.1] 如果新vnode内部有文本,并且跟旧vnode相同,也什么都不做。(省去多余dom操作)
[2.2] 如果新vnode内部有文本,并且跟旧vnode不同,或者旧vnode就没有(内部是子vnode),直接innerText插值
[3.1] 如果新vnode内部没有文本,即有子vnode,需要先看旧vnode有没有文本,有就先删除旧dom中文本然后再appendChild子节点(因为appendChild并不会替换掉文本)
[3.2] 如果新vnode内部没有文本,即有子vnode,并且旧vnode没有文本,
如果旧vnode子节点是空数组或者undefined,则把根据新vnode的children新建dom并appendChild进去;
如果旧vnode有子节点,开始同层逐一比较
diff算法最复杂的部分在这里,即children都有内容的时候判断更新,这里使用的diff策略其实用的是经典的内容对比算法(跟git中的新旧对比一样),具体要分4种情况:新增节点、删除节点、上移节点、下移节点
这个算法巧妙的地方是利用了4个指针:新前、新后、旧前、旧后
新前、新后指的是新vnode的children列表开头和结尾的指针,前指针只会往后移动,后指针只会往前移动
旧前、旧后指的是旧vnode的children列表开头和结尾的指针,前指针只会往后移动,后指针只会往前移动整个循环以新指针为基准开始,根据规则循环移动指针,循环终止条件是新前不能大于新后 && 旧前不能大于旧后
每次循环会根据优先级规则判断是否命中,命中则移动指针并进入下次循环,未命中会根据下一优先级规则判断命中:新前与旧前>新后与旧后>新后与旧前>新前与旧后,规则1-新前与旧前: 判断新前vnode是否与旧前vnode相同,相同则新前和旧前指针后移,当次循环结束;否则进入规则2。
最终循环结束的时候:如果如果新前与新后率先汇合,则旧前与旧后之间的vnode需要删除,对应dom应该被删除;相反新前与新后之间的vnode需要被插入。规则2-新后与旧后:判断新后vnode是否与旧后vnode相同,相同则新前和旧前指针前移,当次循环结束;否则进入规则3。
最终循环结束的时候:如果新后与新前率先汇合,则旧后与旧前之间的vnode需要“删除”,对应dom应该被删除;相反新后与新前之间的vnode应该被插入。规则3-新后与旧前:判断新后vnode是否与旧前vnode相同,相同则把旧前标记为undefined避免错误dom操作,并把旧前对应dom移动到旧后之后,当次循环结束;否则新后前移,旧前后移,进入规则4。
规则4-新前与旧后:判断新前vnode是否与旧后vnode相同,相同则把旧后标记为undefined避免错误dom操作,并把旧后对应dom移动到旧前之前,当次循环结束;否则进入规则1。
这个过程可以理解为一个收拢的过程,收拢的过程中,过滤掉相同的,同时整理内部的顺序方便下一次收拢。
这么一轮循环之后,该调整的调整,该删除的删除,该新增的新增,安排的明明白白。
由此可见,vue的patch只能“同层”比较,比较的是同层的单个新旧vnode,新旧“不同”就暴力insertBefore新dom然后删除旧dom;新旧“相同”并且都有children的时候会使用经典diff算法优化dom操作。比的是“同层”vnode,最多会连带下一级children。
同时还要注意,vue的dom操作是在diff算法过程中的,并且创建dom是一个根据vnode递归的创建过程,子dom节点插入是appendChild的方式。
网友评论