vue知识点-3--diff算法 vnode

虚拟dom是通过状态生成一个虚拟节点树,然后使用虚拟节点数进行渲染,在渲染之前,会使用新生成的虚拟节点数和上一个上传的虚拟节点数进行对比,只渲染不同的部分。
之前说过vue的变化侦测,当属性变化时vue会通知到对应的组件,然后组件内部通过虚拟dom进行对比渲染。

编写vue文件模板-->render渲染函数-->生成vnode-->渲染视图

VNode是一个类,可以生成不同的vnode实例,vonde只是一个对象,不要想象的太神秘。
新旧两个vnode对比的过程就是diff算法,内部函数是patch(补丁)

源码来源 https://www.cnblogs.com/wind-lanyan/p/9061684.html 写的很不错这篇文章

function patch (oldVnode, vnode) {
    // some code
    if (sameVnode(oldVnode, vnode)) {
        //如果一样进入下一层对比
        patchVnode(oldVnode, vnode)
    } 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
}

sameVnode就是对比新旧两个vnode是否相同 如果相同进行下一次对比,否则直接替换oldVnode,
diff是逐层对比,也是同层对比,判断两个vnode是否相同主要是看他们的key,tag,isComment,vode上的data。
如果父级不相同也就不会再进入子集对比了哦!
insertBefore 传入三个值,父节点,移动的节点(真正的内容),要移动到的地方(会被替换掉)

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
判断Vnode和oldVnode是否指向同一个对象,如果是,那么直接return;
如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点。
如果oldVnode有子节点而Vnode没有,则删除el的子节点
如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
如果两者都有子节点,则执行updateChildren函数比较子节点
上面这几条看代码基本能读懂意思

updateChildren 就是具体的规则来对比新老节点的复用问题,复用就是指原本有的子节点被他找到了,他发现新老对比只是移动了位置就只直接移动内容位置,或是位置都没有变(位置没变还是会调用回patchVnode)。
updateChildren内部的具体代码还是要调用回来 patchVnode
patchVnode 中的createEle、removeChildren、setTextContent 就已经是具体操作dom的地方了,仔细思考层层递归,到了最底层多半就是这些情况了,要不新增、要不删除、要不替换文字。

patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el
    let i, oldCh = oldVnode.children, ch = vnode.children
    if (oldVnode === vnode) 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)
        }
    }
}

下方updateChildren是复制过来的,又臭又长的源码。
updateChildren 就是对比子级用的。
oldCh和vCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和vCh至少有一个已经遍历完了,就会结束比较。 原博客的介绍,感觉有些太概括了。

再仔细的说一下

updateChildren 对比子层级的时候是从两边向中间循环。
所以开头的这些变量oldStartIdx、newStartIdx、oldEndIdx、oldStartVnode、newEndIdx、newStartVnode....是用来记录位置的,
新vnode左侧第一个id 右侧第一个id 老vnode右侧第一个...
四种对比方式是 新前-旧前 新后-旧后 新后-旧前 新前-旧后
是这么对比的,如果判断为相等就去更改内容或是移动位置。
如果四种方式都没比较成功就用key循环对比。
最后根据前面的id 判断哪个先循环完毕了,
如果旧vnode循环完毕了新的vnode还要节点就说明剩下的都是新增的
如果新vnode循环完毕就vnode还要节点就说明旧的上剩余的节点都是需要删除的。

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) {
        if (oldStartVnode == null) {   // 对于vnode.key的比较,会把oldVnode = 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]
        }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]
        }else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode)
            api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        }else {
           // 使用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]
            }
            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]
            }
        }
    }
    if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
    }else if (newStartIdx > newEndIdx) {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
}

核心源码解读就是上面这些。

再说下自己的理解。其实对比新旧vnode是否相同就是直接通过sameVnode传递进新老节点对比即可,但是为什么updateChildren内要写这么多代码,四种对比方式
新前-旧前 新后-旧后 新后-旧前 新前-旧后,其实用最笨的方法直接循环对比就行,但是这样对比要双重循环,新老双向循环这样就很耗费性能,这四种方式就可以看成在碰运气,往四个方向比较一下,比较上了就找到了,比较不上再去循环用key比较。但是你不要感觉这样没有道理,真实场景思考一下,实际的界面上位置基本不会大变这四种情况碰上的概率还是很高的。

在思考一个问题。为什么vue推荐我们循环时要加上key,可以优化性能复用节点。

当 Vue 更新已使用 v-for 渲染的元素列表时,默认会采用“就地填充”策略。如果数据项的顺序发生了变化,不是移动 DOM 元素来匹配列表项的顺序,Vue 会将每个元素填充到恰当的位置,并且确保最终反映为,在该特定索引处放置应该呈现的内容

官网上是这么写的
再来解读下 updateChildren 关于key的部分


058F80DB095860A947105DE79C9605C5.jpg

diff算法基本就是这样子。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345