DOM-DIFF原理及實(shí)現(xiàn)

面試中如果你簡歷上寫了掌握React属瓣,那面試官肯定會讓你說一下dom-diff算法的過程回挽,
那么今天就實(shí)現(xiàn)一個簡單的dom-diff算法椅文。

虛擬DOM

必須先說一下虛擬DOM的概念饱狂,virtual dom廷没,就是虛擬節(jié)點(diǎn)。是通過js中的Object對象來模擬DOM中的節(jié)點(diǎn)径荔,再通過特定的渲染方法render()將其轉(zhuǎn)化渲染成真實(shí)的DOM節(jié)點(diǎn)督禽。

一個虛擬DOM節(jié)點(diǎn)大概長這樣:
{
type:’節(jié)點(diǎn)類型‘, //ul,div,li,text等等
children:[], //當(dāng)前節(jié)點(diǎn)包含的子節(jié)點(diǎn)集合,內(nèi)部結(jié)構(gòu)也是虛擬DOM節(jié)點(diǎn)組成的數(shù)組
props:{}, //屬性鍵值對

}

//虛擬DOM元素的類:
class Element{
  constructor(type, props, children){
     this.type = type;
     this.props = props;
     this.children = children;
  }
}

那么我們就需要一個方法,創(chuàng)建虛擬DOM節(jié)點(diǎn)总处,并生成這樣結(jié)構(gòu)的對象狈惫;


function createElement(type, props, children){
    return new Element(type, props, children);
}

生成虛擬DOM

我們想在頁面中創(chuàng)建一個ul列表,如下圖所示鹦马,其中包含三個li元素胧谈,每個li中分別包含文本a, b, c,ul,li的屬性分別設(shè)置為class的值 為list以及 item, 用上面的方法如何生成虛擬DOM節(jié)點(diǎn)呢?


virualDom01.png
let virtualDom1 = createElement('ul', { class: 'list'}, [
  createElement('li', { class: 'item', ['a']}),
 createElement('li', { class: 'item', ['b']}),
 createElement('li', { class: 'item', ['c']}),
])

代碼參考:生成虛擬DOM節(jié)點(diǎn)


render

render方法實(shí)現(xiàn)的功能是 將虛擬DOM轉(zhuǎn)化成 真實(shí)DOM荸频,我們可以想到一個方法那就是利用createElement 以及 appendChild菱肖。

/**
* @vnode就是一個Element結(jié)構(gòu)的對象
    {
       type,
       props,
       children,
    }
*/
function render(vnode) {
  //1. 根據(jù)虛擬DOM的節(jié)點(diǎn)類型創(chuàng)建一個該類型的元素
  let element = document.createElement(vnode.type);
  //2. 遍歷其屬性并綁定
  for (let key in vnode.props) {
    //綁定屬性
    setAttr(element, key, vnode.props[key]);
  }
  // .....

  return element;
 }
}

設(shè)置屬性的方法

/*
* node:根據(jù)類型已經(jīng)創(chuàng)建的真實(shí)節(jié)點(diǎn)
* 屬性類型暫時(shí)包含:value值, style樣式,還有其它比如class等旭从。
*/
function setAttr(node, key, value){
   switch(key){
     case 'value':
       let tagName = node.tagName.toUpperCase();
  //如果是文本框或文本區(qū)域則直接將值賦給它的value屬性
       if(/^(INPUT|TEXTAREA)$/.test(tagName)){
         node.value = value;
       }else {
         node.setAttribute(key, value);
       }
       break;
       
     case 'style':
       //如果是內(nèi)聯(lián)樣式屬性則賦值給node.style.cssText屬性
       node.style.cssText = value;
       
       break;
       
     default:
       node.setAttribute(key, value);
       break;
   }
}

接下來就是對vnode的children子節(jié)點(diǎn)的渲染稳强,我們知道一個思路,如果子節(jié)點(diǎn)仍然是Element類型和悦,即虛擬DOM元素退疫,則需要繼續(xù)進(jìn)行遞歸render轉(zhuǎn)化,于是在render方法中還需要:

//3. 遍歷children子元素
(vnode.children || []).forEach(child =>{
  child = (child instanceof Element)? render(child): document.createTextNode(child);
  element.appendChild(child);

})

最后轉(zhuǎn)換成為真實(shí)DOM的節(jié)點(diǎn)還差一步就可以渲染到頁面中:

//渲染節(jié)點(diǎn), 將虛擬dom渲染成真實(shí)DOM
function renderDOM(elements, target){
    target.appendChild(elements);
}

參考代碼:render

DOM-DIFF

DOM-DIFF就是一種比較算法,比較兩個虛擬DOM的區(qū)別,也就是比較兩個對象的區(qū)別鸽素。
也有真實(shí)DOM與虛擬DOM的比較蹄咖,不過我們這里先只討論虛擬DOM之間的比較。

DOM-DIFF的過程

DOM-DIFF作用是通過js層面的計(jì)算付鹿,根據(jù)兩個虛擬對象創(chuàng)建出差異的補(bǔ)丁對象patch澜汤,用來描述改變了哪些內(nèi)容,然后用特定的操作解析patch對象舵匾,更新dom完成頁面的重新渲染俊抵。
那我們按照下圖的變更來實(shí)現(xiàn)一個DOM-diff的過程:


圖2-diff新舊樹對比.png

DOM-DIFF三種優(yōu)化策略

  1. 只比較平級


    規(guī)則一:只比較平級
  2. 不會跨級比較


    規(guī)則二:不跨級
  3. 同一級的變化節(jié)點(diǎn),如果節(jié)點(diǎn)相同只是位置交換坐梯,則會復(fù)用徽诲。


    規(guī)則三:同級易位可復(fù)用

    PS:通過key來實(shí)現(xiàn)。

差異計(jì)算

  1. 先序深度優(yōu)先遍歷


    遍歷次序深度優(yōu)先遍歷.png
  2. JS對象模擬并生成虛擬DOM
  3. 將虛擬DOM轉(zhuǎn)成真實(shí)DOM并插入到頁面中
  4. 若事件觸發(fā)了虛擬DOM的變更吵血,則比較兩棵虛擬DOM樹的差異谎替,得到差異對象patch
  5. 把差異對象patch應(yīng)用到真實(shí)的DOM樹中渲染。
差異對象patch
/*
* 遞歸樹蹋辅,比較后的結(jié)果放到補(bǔ)丁包中
*  * 規(guī)則:
 * 1. 結(jié)點(diǎn)類型type相同時(shí)钱贯,則看props是否相同。產(chǎn)生一個屬性補(bǔ)丁包{ type:'ATTRS',attrs:{ class: 'list-group'}}
 * 2. 新dom結(jié)點(diǎn)不存在的情況侦另,   { type:' REMOVE', index:xxx }
 * 3. 結(jié)點(diǎn)類型不相同秩命,直接替換, { type:'REPLACE', newNode: newNode}
 * 4. 文本的變化,            { type:'TEXT', text: 1}
   
*/

//生成補(bǔ)丁包
 function generate(oldNode, newNode, index, patches){
 //每個元素都有補(bǔ)丁對象
   let currentPatch = [];
   //規(guī)則2:新節(jié)點(diǎn)不存在的情況
   if(!newNode){
     currentPatch.push({type:'REMOVE', index});
   } else if(diffToos.isString(oldNode) && diffTools.isString(newNode)){
     //規(guī)則4:文本節(jié)點(diǎn)變化
    currentPatch.push({ type: 'TEXT', text:newNode});
   } else if(newNode.type === oldNode.type){
     //規(guī)則1:節(jié)點(diǎn)類型相同褒傅,比較屬性
     let attrs = diffTools.attrs(oldNode.props, newNode.props);
     //獲取到的差異屬性放到當(dāng)前節(jié)點(diǎn)的補(bǔ)丁包中
     if(Object.keys(attrs).length >0){
       currentPatch.push({type:'ATTRS', attrs});
     }
     //若有子節(jié)點(diǎn)弃锐,則繼續(xù)比較
     diffTools.child(oldNode.children, newNode.children, index, patches);
     
   }else {//規(guī)則3: 節(jié)點(diǎn)類型不相同,直接替換
     currentPatch.push({type:'REPLACE', newNode});
     
   }
   
   //TODO 如果平級元素互換殿托,會導(dǎo)致重新渲染霹菊,其實(shí)移動一下就可以
   //TODO 新增節(jié)點(diǎn)也不會被更新
   
   //最后如果當(dāng)前節(jié)點(diǎn)的補(bǔ)丁包中有內(nèi)容,則將其放入整棵樹的補(bǔ)丁包中
   if(currentPatch.length){
     patches[index] = currentPatch;
   }
  
  }

打補(bǔ)丁時(shí)需要的工具集

/*
* 一些比較時(shí)用到的工具
*/
let Index = 0;
let diffTools = {
  //判斷節(jié)點(diǎn)是否為文本節(jié)點(diǎn)
  isString(node){
    return Object.prototype.toString.call(node) == '[object String]';
  },
  //比較新舊屬性差異,并返回最終要修改的屬性
  attrs(oldProps, newProps){
    let patch = {};
    //依次遍歷新舊節(jié)點(diǎn)屬性
    for(let key in oldProps){
      //1. 若舊屬性與新屬性不同或者在新屬性中不存在支竹,即為刪除或變更
      patch[key] = newProps[key]; //則以新的為準(zhǔn)旋廷;
      
    }
    
    for(let key in newProps){
      //1. 若新屬性在舊屬性中不存在,即為追加
      if(!oldProps.hasOwnProperty(key)){
        patch[key] = newProps[key]; //也以新的為準(zhǔn)
      }
    }
    return patch;
  },
  //繼續(xù)比較子節(jié)點(diǎn)
  child(oldChild,newChild, index, patches){
    oldChild.forEach((child, idx)=>{
      //每一層比較唾戚,使用同一Index,所以用全局的Index
      generate(child, newChild[idx], ++Index, patches);
    })
  }
}
比較方法

維護(hù)兩樹不同的補(bǔ)丁包

function diff(oldTree, newTree){
  //兩樹的差異補(bǔ)丁包
  let patches = {};
  
  let index = 0; //默認(rèn)從第0個節(jié)點(diǎn)開始比較
  generate(oldTree, newTree, index, patches);
  
  return patches;
  
}

最后模擬兩棵不同的樹結(jié)構(gòu)柳洋,并打印出補(bǔ)丁包

let virtualDom1 = createElement('ul', { class: 'list'}, [
    createElement('li',{class:'item'}, ['a']),
    createElement('li',{class:'item'}, ['b']),
    createElement('li',{class:'item'}, ['c'])
]);
let virtualDom2= createElement('ul', { class: 'list'}, [
    createElement('li',{class:'item'}, ['1']),
    createElement('li',{class:'item-active'}, ['b']),
    createElement('div',{class:'item-div'}, ['3']),
    
]);
let el = render(virtualDom1);
let patches = diff(virtualDom1, virtualDom2);
console.log('打印出補(bǔ)丁包’, patches);
renderDOM(el, window.root);

代碼參考:diff比較生成差異補(bǔ)丁包


打補(bǔ)丁

現(xiàn)在還差一步,就是打補(bǔ)丁

//打補(bǔ)丁規(guī)則
function doPatch(node, patches){
  patches.forEach(patch =>{
    //補(bǔ)丁根據(jù)類型叹坦,ATTRS,TEXT,REPLACE,REMOVE
    switch(patch.type){
      case 'ATTRS':
        for(let key in patch.attrs){
          let value = patch.attrs[key];
          if(value){
            setAttr(node, key,value);
          }else {
            node.removeAttribute(key);
          }
        }
        break;
      case 'TEXT':
        node.textContent = patch.text;
        break;
      case 'REPLACE':
        let newNode = (patch.newNode instanceof Element)?
            render(patch.newNode): document.createTextNode(patch.newNode);
        //用新節(jié)點(diǎn)替換
        node.parentNode.replaceChild(newNode, node)
        break;
      case 'REMOVE':
        node.parentNode.removeChild(newNode, node)
        break;
      default:
        break;
        
    }
  })
  
}

現(xiàn)在就是依次遍歷需要變更的元素并執(zhí)行打補(bǔ)丁的操作

//依次打補(bǔ)丁
let patchIndex = 0;
function patch(node, patches){
  let currentPatch = patches[patchIndex ++]
  let childNodes = node.childNodes;
  
  childNodes.forEach(child=>{
    patch(child, patches);
  });
  
  if(currentPatch){
    doPatch(node, currentPatch);
  }
}

把補(bǔ)丁包給目標(biāo)元素打上熊镣,并更新視圖:

patch(el, patches); 

代碼參考:打補(bǔ)丁

簡易版的DOM-diff就到這里,目前還有一些關(guān)于key的優(yōu)化以及同級元素易位只需替換的邏輯還未實(shí)現(xiàn)募书。
等下一篇再深入去實(shí)現(xiàn)并練習(xí)绪囱。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市莹捡,隨后出現(xiàn)的幾起案子鬼吵,更是在濱河造成了極大的恐慌,老刑警劉巖篮赢,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件齿椅,死亡現(xiàn)場離奇詭異琉挖,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)涣脚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進(jìn)店門示辈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人遣蚀,你說我怎么就攤上這事矾麻。” “怎么了芭梯?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵险耀,是天一觀的道長。 經(jīng)常有香客問我玖喘,道長甩牺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任芒涡,我火速辦了婚禮柴灯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘费尽。我一直安慰自己赠群,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布旱幼。 她就那樣靜靜地躺著查描,像睡著了一般。 火紅的嫁衣襯著肌膚如雪柏卤。 梳的紋絲不亂的頭發(fā)上冬三,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天,我揣著相機(jī)與錄音缘缚,去河邊找鬼勾笆。 笑死,一個胖子當(dāng)著我的面吹牛桥滨,可吹牛的內(nèi)容都是我干的窝爪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼齐媒,長吁一口氣:“原來是場噩夢啊……” “哼蒲每!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起喻括,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤邀杏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后唬血,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體望蜡,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡唤崭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泣特。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浩姥。...
    茶點(diǎn)故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖状您,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情兜挨,我是刑警寧澤膏孟,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站拌汇,受9級特大地震影響柒桑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜噪舀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一魁淳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧与倡,春花似錦界逛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至净响,卻和暖如春少欺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背馋贤。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工赞别, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人配乓。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓仿滔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親扰付。 傳聞我的和親對象是個殘疾皇子堤撵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評論 2 356