美文网首页
关于vue的虚拟dom和diff算法

关于vue的虚拟dom和diff算法

作者: 瓢鳍小虾虎 | 来源:发表于2021-10-21 14:40 被阅读0次

    vnode

    首先要说明虚拟节点,本质就是一个对象:

    {
    sel: 'div', // 选择器
    elm: undefined, // 关联的dom对象
    key: undefined,
    text: '', // 标签文本
    data: {},
    children: undefined // 子节点
    }
    

    vnodeh函数产生,模板引擎会最终把标签转换成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的方式。

    snabbdom

    相关文章

      网友评论

          本文标题:关于vue的虚拟dom和diff算法

          本文链接:https://www.haomeiwen.com/subject/tzzooltx.html