diff算法_详解vue中的diff算法

关键字: diff算法 key更高效 v-for中的index作为key的问题 虚拟dom给真实dom打补丁的过程 v-show v-if 初始数据请求放在created还是mounted里

接上文先补充一下初识数据渲染放在哪个生命周期,一般的话created和mounted都可以,而且如果请求少的话也不会出什么问题,

然后也看了很多分析说放在created的话如果请求过多会较长时间白屏(那这里也有小伙伴说是异步请求不会消耗很长时间,但是我认为的话异步请求只是在http异步请求里面执行代码,完成之后回调会进入事件队列,那假如说请求过多,在js引擎线程的同步事件执行完成之后异步请求还没完成的话,不也会出现长时间白屏影响渲染吗,所以异步请求并不是不要事件呀,只是用其他线程来执行代码)

,那如果又放在mounted里面的话,都知道mounted之前会执行render函数渲染,所以这个异步的时机就很随机了,如果是在渲染完成之后请求了数据,所以就可能会闪屏,因为当watcher被通知数据状态改变的时候,就会重新渲染render,那如果是在渲染之前就拿到了数据,那么也不会再次渲染了,

所以呢我觉得还是都可以吧,如果说数据是依赖dom的话当然在渲染的时候请求会好一些,但是created在某些程度来讲还是会更早拿到数据的,这些也是我在项目中和一些技术文章里面总结出来的,如果有不对的,欢迎大家来讨论。

再提一下v-show和v-if

因为这里的话就是项目里面常见的登录注册页面,这两个页面是同一个组件,但是就有一些注册里面的input框在登录里面是没有的,比如说再次输入密码这样的,那么这里一般会用v-show来控制隐藏,那么这里就会有问题,因为v-show虽然隐藏了,但他的站位还在,相当于visibility:hidden,所以如果方法不做处理的话,再次输入密码的input框会传入一个空值,那在登录里面方法就会报错,这里最简单的方法就是用v-if,改变dom结构,触发重排(v-show是重绘)v-if相当于display:none,就不会再传空值,如果用v-show的话,加一个当v-show为false时的方法直接return也可解决,

所以区别在于初始渲染是vdisplay:none不会渲染,但是后续每一次改变值都会触发重排,而v-show第一次就会渲染,但后续只是重绘而已,比较节省性能吧。

言归正传,从watcher说一下新的vnode生成和oldvnode进行运算并且给真实dom打补丁的过程,这里就涉及diff算法,diff算法基于两点

1相同组件的dom结构是一样的,不同组件的dom结构不同

2同一层级的一组节点,他们可以通过唯一的key值进行区分

基于以上这两点假设,使得虚拟DOM的Diff算法的复杂度从O(n^3)降到了O(n)

上一张图

4062577373bf5db888a1043fcc75d507.png
这里就是一层一层比较的,不会拿下一层的dom和上一层的比较

那用key值的话会减少很多不必要的操作比如这个

8d36af2b1e61152f1ba39d54b20cf801.png

在原来的dom上插入f,如果diff算法没有key值的话,那么在检测到f的时候就会用f来替换c,c替换d,d替换e,在插入e

但如果有key的话,就会直接插入,少了很多额外的操作

144869f097def83a217730b3075d9990.png

4e6c5f2bbb9c7fc02cd666a623002e01.png

那就从上面这张图开始读源码吧,当setter通知所用watcher时,那么就开始打补丁了

那么就会有两种情况出现,当dom的结构和之前一样和不一样的时候,所以先会有一个判断

(出于习惯就在代码里写注释了)

function patch (oldVnode, vnode) {
    // some code
    if (sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode)      //当结构一致时执行patchvnode函数
    } else {//当结构不一样时执行的操作就简单多了,将之前的节点替换掉就行
        const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
        let parentEle = api.parentNode(oEl)  // 父元素
        createEle(vnode)  // 根据Vnode生成新元素
        if (parentEle !== null) {//判断父元素是否为空
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
            api.removeChild(parentEle, oldVnode.el)  // 移除以前的旧元素节点
            oldVnode = null //解除全局变量的引用,回收内存
        }
    }
    // some code 
    return vnode //最后更新虚拟dom
}

那么接下来就分析结构一致的情况,那么什么情况下结构是一致的呢,看一下samevnode源码

function sameVnode (a, b) {
  return (
    a.key === b.key &&  // key值一致
    a.tag === b.tag &&  // 标签名一致
    a.isComment === b.isComment &&  // 是否都为注释节点
    // 是否都定义了data,data包含一些具体信息,例如onclick , style
    isDef(a.data) === isDef(b.data) &&  
    sameInputType(a, b) // 当标签是<input>的时候,type必须相同
  )
}

那这些都一致了的话我们就要看他们的内容(文本)和子节点了

patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el
    let i, oldCh = oldVnode.children, ch = vnode.children
    if (oldVnode === vnode) return  //如果两个节点完全一样就直接return
    if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
        api.setTextContent(el, vnode.text) //如果是文本节点当两个节点的文本不为空并且不相等是,直接替换
    }else {
        updateEle(el, vnode, oldVnode)
        if (oldCh && ch && oldCh !== ch) {
            updateChildren(el, oldCh, ch)
        }else if (ch){
            createEle(vnode) //create el's children dom
        }else if (oldCh){
            api.removeChildren(el)
        }
    }
}

当文本内容不同是直接替换,之后做了如下事情

  • 找到对应的真实dom,称为el
  • 判断VnodeoldVnode是否指向同一个对象,如果是,那么直接return
  • 如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点。
  • 如果oldVnode有子节点而Vnode没有,则删除el的子节点
  • 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
  • 如果两者都有子节点,则执行updateChildren函数比较子节点,这一步很重要

现在看一看重要的updateChildren,代码量较大,就写在注释里了

updateChildren (parentElm, oldCh, newCh) {
    let oldStartIdx = 0, 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
    let idxInOld
    let elmToMove
    let before //四个指针
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { //四个指针往中间走,
//当开始和结束指针位置相反时,表示有一个vnode已经走完了跳出循环
        if (oldStartVnode == null) {   // 后面用key值时会将已经移动的vnode用null替换,所以这里
//如果遇到了只要将指针往后面移动就行
            oldStartVnode = oldCh[++oldStartIdx] 
        }else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx]
        }else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx]
        }else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldStartVnode, newStartVnode)) { 
            patchVnode(oldStartVnode, newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
 //头指针和头指针比较如果相同调用patchVnode()这里会有递归知道把后面的更新完才会return,
//最简单的情况是两个内容相同直接return,之后两个头指针往中间走
        }else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
//同理之后两个尾指针往中间走
        }else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode)
            api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
//这里的比较会很大的提升性能,将oldvnode的头指针和newnode的尾指针调用patchvonde(),这里和上面
//也是一样的,不同的是这里会在真实dom上将返回的vnode从之前的位置到oldEndVnode指针对应的节点之前,
//之后相同的key值的指针往前面走
        }else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode)
            api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
//这里会在真实dom上将返回的vnode插入到oldStartVnode指针对应的节点之前
        }else {
           // 当前面所用情况都没有时就使用key比较,那newvnode的当前头索引在oldvnode的key值里面找
            if (oldKeyToIdx === undefined) {
                oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
 // 有key生成index表
            }
            idxInOld = oldKeyToIdx[newStartVnode.key]
            if (!idxInOld) {
                api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                newStartVnode = newCh[++newStartIdx]
//如果新的vnode中的key值在oldvode中不存在,则真实dom的在oldStartVnode前插入现在的newvnode节点,
//指针向后
            }
            else {
                elmToMove = oldCh[idxInOld]
                if (elmToMove.sel !== newStartVnode.sel) {
                    api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                }else {
                    patchVnode(elmToMove, newStartVnode)
                    oldCh[idxInOld] = null
                    api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
                }
                newStartVnode = newCh[++newStartIdx]
            }
//如果存在则在真实dom的oldStartVnode前插入该节点,并将oldvnode的值设置为null,索引后移
        }
    }
    if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
//如果oldvnode里面头索引大于尾索引,说明newVnode里面还有节点,这时需要将newvnode里头索引到尾索引
//的node添加到真实dom中
    }else if (newStartIdx > newEndIdx) {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
//反之说明需要将oldvnode中多余的值删掉
}

下面是从别处看到的例子

cdc91c5ea2da4e984cf48fb9531ab2b7.png

5c1f731ddcb88fddbe503fc0c8a89cbf.png

9ad3cf0cbe3e30f01bfa11f8c62818ab.png

22db4791e66424646e5617d6d97bc967.png

最后再用一张图来进行总结吧,组件的diff较为简单,而元素的话就有递归了

798fa82f6eaa269fe490f46ef2ef1d92.png

这里在补充一个小的问题就是为什么不推荐v-for的时候用index作为key,是因为如果对list进行操作的话会导致元素显示的混乱,比如将list[2]这个元素删掉,那么之前的list[3]的key就会是现在的list[2]的索引,那之后的显示也会乱掉

ada898522d3684dbd71b3e96ba04186a.png
人类身份验证 - SegmentFault​segmentfault.com 图文详解 vue diff 核心方法 updateChildren​blog.csdn.net
c24ec9ee4dc07306f254de1a4ad31980.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值