diff算法原理

首先進(jìn)行總結(jié)

  • diff算法的本質(zhì)是找出兩個(gè)對(duì)象之間的差異
  • diff算法的核心是子節(jié)點(diǎn)數(shù)組對(duì)比,思路是通過(guò)首尾兩端對(duì)比
  • key的作用主要是
    • 決定節(jié)點(diǎn)是否可以復(fù)用
    • 建立key-index的索引,主要是替代遍歷,提升性能泡一。

diff算法的作用

渲染真實(shí)DOM的開銷很大,有時(shí)候我們修改了某個(gè)數(shù)據(jù),直接渲染到真實(shí)dom上會(huì)引起整個(gè)dom樹的重繪和重排捕虽。我們希望只更新我們修改的那一小塊dom,而不是整個(gè)dom坡脐,diff算法就幫我們實(shí)現(xiàn)了這點(diǎn)泄私。
diff算法的本質(zhì)就是:找出兩個(gè)對(duì)象之間的差異,目的是盡可能做到節(jié)點(diǎn)復(fù)用备闲。
此處說(shuō)到的對(duì)象晌端,指的其實(shí)就是vue中的virtual dom(虛擬dom樹),即使用js對(duì)象來(lái)表示頁(yè)面中的dom結(jié)構(gòu)恬砂。

真實(shí)dom與虛擬dom

如下一個(gè)真實(shí)dom節(jié)點(diǎn):

<div id='app'>
   <span id='child'>1</span>
 </div>

可見咧纠,一個(gè)dom節(jié)點(diǎn)主要包含以下三個(gè)部分:

  1. 自身的標(biāo)簽名(div)
  2. 自身的屬性(id='app')
  3. 子節(jié)點(diǎn)(span)
    那我們?cè)趺从胘s對(duì)象結(jié)構(gòu)來(lái)表示這樣一個(gè)dom節(jié)點(diǎn)呢?如下即可:
const vnode = {
    tag: 'div', // 自身的標(biāo)簽名
    attts: {id:'app'}, // 自身的屬性
    children: [{
        tag:'span', attrs: {id:'child'}, chidlren: ['1']
    }] // 自身的子節(jié)點(diǎn)
}

那么當(dāng)用戶對(duì)界面進(jìn)行操作泻骤,比如把div的id改為app2漆羔,將子節(jié)點(diǎn)span的文本子節(jié)點(diǎn)1改為2梧奢,那么我們就能得到如下vnode:

const vnode = {
    tag: 'div', // 自身的標(biāo)簽名
    attts: {id:'app2'}, // 自身的屬性
    children: [{
        tag:'span', attrs: {id:'child'}, chidlren: ['2']
    }] // 自身的子節(jié)點(diǎn)
}

我們已經(jīng)提到,diff算法的本質(zhì)是找出兩個(gè)對(duì)象之間的差異演痒,目的是盡可能做到節(jié)點(diǎn)復(fù)用亲轨。那么我們運(yùn)行diff(vnode, vnode2)就能知道vnode與vnode2之間的差異為:

  • div的id由app改為了app2
  • span的文本子節(jié)點(diǎn)由1改為了2

知道了以上的差異部分,就能根據(jù)這些差異來(lái)進(jìn)行視圖的局部更新:

document.getElementById('app').setAttribute('id, 'app2') // id改為app2
document.getElementById('child').firstChild.textContent = '2' // 1改為2

在我們改變一個(gè)節(jié)點(diǎn)的時(shí)候鸟顺,我們其實(shí)主要改動(dòng)了以下兩部分:

  • 自身的屬性(style惦蚊、class等)
  • 子節(jié)點(diǎn)

那么diff算法就可以抽象為兩部分:

function diff(vnoe, newVnode) {
    diffAttr(vnode.attr, newVnode.attr)
    diffChildren(vnode.children, newVnode.children)
}

在vue之前的源碼中,是先用diff得到差異讯嫂,再根據(jù)差異去patch真實(shí)dom蹦锋,也就是分兩步:

  1. diff
  2. patch

但這樣性能上會(huì)有損失,因?yàn)閐iff過(guò)程中會(huì)遍歷一整個(gè)dom樹欧芽,patch的時(shí)候又會(huì)再遍歷一遍莉掂,其實(shí)這兩次遍歷可以合并成一次,也就是在diff的同時(shí)進(jìn)行patch千扔。
所以我們可以把流程改進(jìn)為:

    function patchVnode(oldVnode, vnode, parentElm) {
        patchAttr(oldVnode.attr, vnode.attr, parentElm)
        patchChildren(parentElm, oldVnode.children, vnode.children)
    }

patchAttr

    function patchAttr(oldVnode={}, vnode={}, parentElm) {
        each(oldVnode, (key, val) => { // 遍歷oldVnode巫湘,看newTreeAttr是否還有對(duì)應(yīng)的屬性
            if (vnode[key]) {
                val !== vnode[key] && setAttr(parentElm, key, vnode[key])
            }
            else {
                parentElm.removeAttribute(key)
            }
        })

        each(vnode, (key, val) => { // 看oldVnode是否還有對(duì)應(yīng)的屬性,沒(méi)有就新增昏鹃。
            !oldVnode[key] && setAttr(parentElm, key, val)
        })
    }

    function each(obj, fn) { // 遍歷對(duì)象
        if (Object.prototype.toString.call(obj) !== '[object object]') {
            console.error('只能遍歷對(duì)象尚氛!')
            return
        }

        for (var key in obj) {
            if (obj.hasOwnProperty(key)) {
                var val = obj[key]
                fn(key, val)
            }
        }
    }

    function setAttr(node, key, value) {
        switch (key) {
            case 'style':
                each(value, (key, val) => {
                    node.style[key] = val
                })
                break
            case 'value':
                var tag = node.tag || ''
                tag = tag.toLowerCase()
                if (
                        tag === 'input' || tag === 'textarea'
                ) {
                    node.value = value
                } else {
                    // if it is not a input or textarea, use 'setAttribute' to set
                    node.setAttribute(key, value)
                }
                break
            default:
                node.setAttribute(key, value)
                break
        }
    }

該函數(shù)主要做了如下幾件事:

  1. 遍歷oldVnode,看newTreeAttr是否還有對(duì)應(yīng)的屬性洞渤;
  2. 如果有并且不相等阅嘶,則修改對(duì)應(yīng)的屬性;
  3. 如果沒(méi)有载迄,則直接刪除對(duì)應(yīng)的屬性讯柔;
  4. 遍歷oldVnode,是否還有對(duì)應(yīng)的屬性护昧,沒(méi)有就新增魂迄。

patchChildren

    function patchChildren(parentElm, oldCh, newCh) {
        let oldStartIdx = 0;
        let oldEndIdx = oldCh.length - 1;
        let oldStartVnode = oldCh[0];
        let oldEndVnode = oldCh[oldEndIdx];

        let newStartIdx = 0;
        let newEndIdx = newCh.length - 1;
        let newStartVnode = newCh[0];
        let newEndVnode = newCh[newEndIdx];
        let oldKeyToIdx, idxInOld, elmToMove, refElm;

        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
            if (!oldStartVnode) {
                oldStartVnode = oldCh[++oldStartIdx];
            } else if (!oldEndVnode) {
                oldEndVnode = oldCh[--oldEndIdx];
            }

            else if (sameVnode(oldStartVnode, newStartVnode)) { //舊首 和 新首相同
                patchVnode(oldStartVnode.elm, oldStartVnode, newStartVnode);
                oldStartVnode = oldCh[++oldStartIdx];
                newStartVnode = newCh[++newStartIdx];
            }

            else if (sameVnode(oldEndVnode, newEndVnode)) { //舊尾 和 新尾相同
                patchVnode(oldEndVnode.elm, oldEndVnode, newEndVnode);
                oldEndVnode = oldCh[--oldEndIdx];
                newEndVnode = newCh[--newEndIdx];
            }

            else if (sameVnode(oldStartVnode, newEndVnode)) { //舊首 和 新尾相同,將舊首移動(dòng)到 最后面
                patchVnode(oldStartVnode.elm, oldStartVnode, newEndVnode);
                nodeOps.insertBefore(parentElm, oldStartVnode.elm, oldEndVnode.elm.nextSibling)//將 舊首 移動(dòng)到最后一個(gè)節(jié)點(diǎn)后面
                oldStartVnode = oldCh[++oldStartIdx];
                newEndVnode = newCh[--newEndIdx];
            }

            else if (sameVnode(oldEndVnode, newStartVnode)) {//舊尾 和 新首相同 ,將 舊尾 移動(dòng)到 最前面
                patchVnode(oldEndVnode.elm, oldEndVnode, newStartVnode);
                nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
                oldEndVnode = oldCh[--oldEndIdx];
                newStartVnode = newCh[++newStartIdx];
            }

            else {//首尾對(duì)比 都不 符合 sameVnode 的話
                //1. 嘗試 用 newCh 的第一項(xiàng)在 oldCh 內(nèi)尋找 sameVnode
                let elmToMove = oldCh[idxInOld];
                if (!oldKeyToIdx) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
                idxInOld = newStartVnode.key ? oldKeyToIdx[newStartVnode.key] : null;
                if (!idxInOld) {//如果 oldCh 不存在 sameVnode 則直接創(chuàng)建一個(gè)
                    nodeOps.createElm(newStartVnode, parentElm);
                    newStartVnode = newCh[++newStartIdx];
                } else {
                    elmToMove = oldCh[idxInOld];
                    if (sameVnode(elmToMove, newStartVnode)) {
                        patchVnode(elmToMove, newStartVnode);
                        oldCh[idxInOld] = undefined;
                        nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm);
                        newStartVnode = newCh[++newStartIdx];
                    } else {
                        nodeOps.createElm(newStartVnode, parentElm);
                        newStartVnode = newCh[++newStartIdx];
                    }
                }
            }
        }

        if (oldStartIdx > oldEndIdx) {
            refElm = (newCh[newEndIdx + 1]) ? newCh[newEndIdx + 1].elm : null;
            nodeOps.addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx);
        } else if (newStartIdx > newEndIdx) {
            nodeOps.removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
        }


    }

上述代碼的本質(zhì)是找出兩個(gè)數(shù)組的差異
比如說(shuō):

  • 舊數(shù)組[a, b, c, d]
  • 新數(shù)組[e, f, g, h]

怎么找出新舊數(shù)組之間的差異呢?可以定義以下名詞:

  • 舊首(舊數(shù)組的第一個(gè)元素)
  • 舊尾(舊數(shù)組的最后一個(gè)元素)
  • 新首(新數(shù)組的第一個(gè)元素)
  • 新尾(新數(shù)組的最后一個(gè)元素)

代碼中的一些函數(shù)工具:

  • sameVnode:用于判斷字節(jié)是否應(yīng)該復(fù)用惋耙,此處做了一些簡(jiǎn)化捣炬,實(shí)際的diff算法要更復(fù)雜一些。這里只用tag和key相同我們就復(fù)用绽榛,執(zhí)行patchVnode湿酸,即對(duì)節(jié)點(diǎn)進(jìn)行修改。
function sameVnode(a, b) {
  return a.key === b.key && a.tag === b.tag
}
  • createKeyToOldIdx:建立key-index的索引灭美,主要是替代遍歷推溃,提升性能。
function createKeyToOldIdx(children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}
  • 舊首 和 新首 對(duì)比
if (sameVnode(oldStartVnode, newStartVnode)) { 
      patchVnode(oldStartVnode.elm, oldStartVnode, newStartVnode);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    }
  • 舊尾 和 新尾 對(duì)比
if (sameVnode(oldEndVnode, newEndVnode)) { //舊尾 和 新尾相同
      patchVnode(oldEndVnode.elm, oldEndVnode, newEndVnode);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    }
  • 舊首 和 新尾 對(duì)比
if (sameVnode(oldStartVnode, newEndVnode)) { //舊首 和 新尾相同,將舊首移動(dòng)到 最后面
      patchVnode(oldStartVnode.elm, oldStartVnode, newEndVnode);
      nodeOps.insertBefore(parentElm, oldStartVnode.elm, oldEndVnode.elm.nextSibling)
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    }
  • 舊尾 和 新首 對(duì)比届腐;將 舊尾 移動(dòng)到最前面
if (sameVnode(oldEndVnode, newStartVnode)) {//舊尾 和 新首相同 ,將 舊尾 移動(dòng)到 最前面
      patchVnode(oldEndVnode.elm, oldEndVnode, newStartVnode);
      nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    }
  • 首位對(duì)比 都不符合 sameVnode的話
  1. 嘗試用newCh的第一項(xiàng)在oldCh內(nèi)尋找sameVnode铁坎,如果在oldCh不存在對(duì)應(yīng)的sameVnode蜂奸,則直接創(chuàng)建一個(gè),存在的話則判斷硬萍;
  2. 符合sameVnode窝撵,則移動(dòng)oldCh對(duì)應(yīng)的節(jié)點(diǎn);
  3. 不符合sameVnode襟铭,則創(chuàng)建新節(jié)點(diǎn)。
  • 最后通過(guò)oldStartIdx > oldEndIex短曾,來(lái)判斷oldCh和newCh哪一個(gè)先遍歷完成
  1. oldCh先遍歷完成寒砖,則證明newCh還有多余的節(jié)點(diǎn),需要新增這些節(jié)點(diǎn)嫉拐;
  2. newCh先遍歷完成哩都,則證明oldCh還有多余節(jié)點(diǎn),需要刪除這些節(jié)點(diǎn)婉徘。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末漠嵌,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子盖呼,更是在濱河造成了極大的恐慌儒鹿,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件几晤,死亡現(xiàn)場(chǎng)離奇詭異约炎,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)蟹瘾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門圾浅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人憾朴,你說(shuō)我怎么就攤上這事狸捕。” “怎么了众雷?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵灸拍,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我砾省,道長(zhǎng)株搔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任纯蛾,我火速辦了婚禮纤房,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘翻诉。我一直安慰自己炮姨,他們只是感情好捌刮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著舒岸,像睡著了一般绅作。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蛾派,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天俄认,我揣著相機(jī)與錄音,去河邊找鬼洪乍。 笑死眯杏,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的壳澳。 我是一名探鬼主播岂贩,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼巷波!你這毒婦竟也來(lái)了萎津?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤抹镊,失蹤者是張志新(化名)和其女友劉穎锉屈,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體垮耳,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡部念,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了氨菇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片儡炼。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖查蓉,靈堂內(nèi)的尸體忽然破棺而出乌询,到底是詐尸還是另有隱情,我是刑警寧澤豌研,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布妹田,位于F島的核電站,受9級(jí)特大地震影響鹃共,放射性物質(zhì)發(fā)生泄漏鬼佣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一霜浴、第九天 我趴在偏房一處隱蔽的房頂上張望晶衷。 院中可真熱鬧,春花似錦、人聲如沸晌纫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)锹漱。三九已至箭养,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間哥牍,已是汗流浹背毕泌。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嗅辣,地道東北人撼泛。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像辩诞,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子纺涤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354