全面理解虛擬Dom,如何來實現(xiàn)虛擬Dom

1.為什么需要虛擬DOM

DOM是很慢的卡骂,其元素非常龐大粘姜,頁面的性能問題由JS引起的,大部分都是由DOM操作引起的乞榨。如果對前端工作進行抽象的話秽之,主要就是維護狀態(tài)和更新視圖;而更新視圖和維護狀態(tài)都需要DOM操作吃既。其實近年來考榨,前端的框架主要發(fā)展方向就是解放DOM操作的復雜性。

在jQuery出現(xiàn)以前鹦倚,我們直接操作DOM結(jié)構(gòu)河质,這種方法復雜度高,兼容性也較差;有了jQuery強大的選擇器以及高度封裝的API掀鹅,我們可以更方便的操作DOM散休,jQuery幫我們處理兼容性問題,同時也使DOM操作變得簡單乐尊;但是聰明的程序員不可能滿足于此戚丸,各種MVVM框架應(yīng)運而生,有angularJS扔嵌、avalon限府、vue.js等,MVVM使用數(shù)據(jù)雙向綁定痢缎,使得我們完全不需要操作DOM了胁勺,更新了狀態(tài)視圖會自動更新,更新了視圖數(shù)據(jù)狀態(tài)也會自動更新独旷,可以說MMVM使得前端的開發(fā)效率大幅提升姻几,但是其大量的事件綁定使得其在復雜場景下的執(zhí)行性能堪憂;有沒有一種兼顧開發(fā)效率和執(zhí)行效率的方案呢势告?ReactJS就是一種不錯的方案蛇捌,雖然其將JS代碼和HTML代碼混合在一起的設(shè)計有不少爭議,但是其引入的Virtual DOM(虛擬DOM)卻是得到大家的一致認同的咱台。

2.理解虛擬DOM

虛擬的DOM的核心思想是:對復雜的文檔DOM結(jié)構(gòu)络拌,提供一種方便的工具,進行最小化地DOM操作回溺。這句話春贸,也許過于抽象,卻基本概況了虛擬DOM的設(shè)計思想

(1) 提供一種方便的工具遗遵,使得開發(fā)效率得到保證萍恕。

(2) 保證最小化的DOM操作,使得執(zhí)行效率得到保證车要。

(1).用JS表示DOM結(jié)構(gòu)

DOM很慢允粤,而javascript很快,用javascript對象可以很容易地表示DOM節(jié)點翼岁。DOM節(jié)點包括標簽类垫、屬性和子節(jié)點,通過VElement表示如下琅坡。

//虛擬dom悉患,參數(shù)分別為標簽名、屬性對象榆俺、子DOM列表

var VElement = function(tagName, props, children) {

//保證只能通過如下方式調(diào)用:new VElement

if (!(this instanceof VElement)) {

return new VElement(tagName, props, children);

}

//可以通過只傳遞tagName和children參數(shù)

if (util.isArray(props)) {

children = props;

props = {};

}

//設(shè)置虛擬dom的相關(guān)屬性

this.tagName = tagName;

this.props = props || {};

this.children = children || [];

this.key = props ? props.key : void 666;

var count = 0;

util.each(this.children, function(child, i) {

if (child instanceof VElement) {

count += child.count;

} else {

children[i] = '' + child;

}

count++;

});

this.count = count;

}

通過VElement售躁,我們可以很簡單地用javascript表示DOM結(jié)構(gòu)坞淮。比如

var vdom = velement('div', { 'id': 'container' }, [

velement('h1', { style: 'color:red' }, ['simple virtual dom']),

velement('p', ['hello world']),

velement('ul', [velement('li', ['item #1']), velement('li', ['item #2'])]),

]);

上面的javascript代碼可以表示如下DOM結(jié)構(gòu):

simple virtual dom

hello world

  • item #1
  • item #2
  • 同樣我們可以很方便地根據(jù)虛擬DOM樹構(gòu)建出真實的DOM樹。具體思路:根據(jù)虛擬DOM節(jié)點的屬性和子節(jié)點遞歸地構(gòu)建出真實的DOM樹陪捷。見如下代碼:

    VElement.prototype.render = function() {

    //創(chuàng)建標簽

    var el = document.createElement(this.tagName);

    //設(shè)置標簽的屬性

    var props = this.props;

    for (var propName in props) {

    var propValue = props[propName]

    util.setAttr(el, propName, propValue);

    }

    //依次創(chuàng)建子節(jié)點的標簽

    util.each(this.children, function(child) {

    //如果子節(jié)點仍然為velement碾盐,則遞歸的創(chuàng)建子節(jié)點,否則直接創(chuàng)建文本類型節(jié)點

    var childEl = (child instanceof VElement) ? child.render() : document.createTextNode(child);

    el.appendChild(childEl);

    });

    return el;

    }

    對一個虛擬的DOM對象VElement揩局,調(diào)用其原型的render方法毫玖,就可以產(chǎn)生一顆真實的DOM樹。

    vdom.render();

    既然我們可以用JS對象表示DOM結(jié)構(gòu)凌盯,那么當數(shù)據(jù)狀態(tài)發(fā)生變化而需要改變DOM結(jié)構(gòu)時付枫,我們先通過JS對象表示的虛擬DOM計算出實際DOM需要做的最小變動,然后再操作實際DOM驰怎,從而避免了粗放式的DOM操作帶來的性能問題阐滩。

    (2).比較兩棵虛擬DOM樹的差異

    在用JS對象表示DOM結(jié)構(gòu)后,當頁面狀態(tài)發(fā)生變化而需要操作DOM時县忌,我們可以先通過虛擬DOM計算出對真實DOM的最小修改量掂榔,然后再修改真實DOM結(jié)構(gòu)(因為真實DOM的操作代價太大)。

    如下圖所示症杏,兩個虛擬DOM之間的差異已經(jīng)標紅:

    為了便于說明問題装获,我當然選取了最簡單的DOM結(jié)構(gòu),兩個簡單DOM之間的差異似乎是顯而易見的厉颤,但是真實場景下的DOM結(jié)構(gòu)很復雜穴豫,我們必須借助于一個有效的DOM樹比較算法。

    設(shè)計一個diff算法有兩個要點:

    如何比較兩個兩棵DOM樹

    如何記錄節(jié)點之間的差異

    <1> 如何比較兩個兩棵DOM樹

    計算兩棵樹之間差異的常規(guī)算法復雜度為O(n3)逼友,一個文檔的DOM結(jié)構(gòu)有上百個節(jié)點是很正常的情況精肃,這種復雜度無法應(yīng)用于實際項目。針對前端的具體情況:我們很少跨級別的修改DOM節(jié)點帜乞,通常是修改節(jié)點的屬性司抱、調(diào)整子節(jié)點的順序、添加子節(jié)點等黎烈。因此习柠,我們只需要對同級別節(jié)點進行比較,避免了diff算法的復雜性怨喘。對同級別節(jié)點進行比較的常用方法是深度優(yōu)先遍歷:

    function diff(oldTree, newTree) {

    //節(jié)點的遍歷順序

    var index = 0;

    //在遍歷過程中記錄節(jié)點的差異

    var patches = {};

    //深度優(yōu)先遍歷兩棵樹

    dfsWalk(oldTree, newTree, index, patches);

    return patches;

    }

    <2>如何記錄節(jié)點之間的差異

    由于我們對DOM樹采取的是同級比較津畸,因此節(jié)點之間的差異可以歸結(jié)為4種類型:

    修改節(jié)點屬性, 用PROPS表示

    修改節(jié)點文本內(nèi)容, 用TEXT表示

    替換原有節(jié)點, 用REPLACE表示

    調(diào)整子節(jié)點,包括移動必怜、刪除等,用REORDER表示

    對于節(jié)點之間的差異后频,我們可以很方便地使用上述四種方式進行記錄梳庆,比如當舊節(jié)點被替換時:

    {type:REPLACE,node:newNode}

    而當舊節(jié)點的屬性被修改時:

    {type:PROPS,props: newProps}

    在深度優(yōu)先遍歷的過程中暖途,每個節(jié)點都有一個編號,如果對應(yīng)的節(jié)點有變化膏执,只需要把相應(yīng)變化的類別記錄下來即可驻售。下面是具體實現(xiàn):

    function dfsWalk(oldNode, newNode, index, patches) {

    var currentPatch = [];

    if (newNode === null) {

    //依賴listdiff算法進行標記為刪除

    } else if (util.isString(oldNode) && util.isString(newNode)) {

    if (oldNode !== newNode) {

    //如果是文本節(jié)點則直接替換文本

    currentPatch.push({

    type: patch.TEXT,

    content: newNode

    });

    }

    } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {

    //節(jié)點類型相同

    //比較節(jié)點的屬性是否相同

    var propsPatches = diffProps(oldNode, newNode);

    if (propsPatches) {

    currentPatch.push({

    type: patch.PROPS,

    props: propsPatches

    });

    }

    //比較子節(jié)點是否相同

    diffChildren(oldNode.children, newNode.children, index, patches, currentPatch);

    } else {

    //節(jié)點的類型不同,直接替換

    currentPatch.push({ type: patch.REPLACE, node: newNode });

    }

    if (currentPatch.length) {

    patches[index] = currentPatch;

    }

    }

    比如對上文圖中的兩顆虛擬DOM樹更米,可以用如下數(shù)據(jù)結(jié)構(gòu)記錄它們之間的變化:

    var patches = {

    1:{type:REPLACE,node:newNode}, //h1節(jié)點變成h5

    5:{type:REORDER,moves:changObj} //ul新增了子節(jié)點li

    }

    (3).對真實DOM進行最小化修改

    通過虛擬DOM計算出兩顆真實DOM樹之間的差異后欺栗,我們就可以修改真實的DOM結(jié)構(gòu)了。上文深度優(yōu)先遍歷過程產(chǎn)生了用于記錄兩棵樹之間差異的數(shù)據(jù)結(jié)構(gòu)patches, 通過使用patches我們可以方便對真實DOM做最小化的修改征峦。

    //將差異應(yīng)用到真實DOM

    function applyPatches(node, currentPatches) {

    util.each(currentPatches, function(currentPatch) {

    switch (currentPatch.type) {

    //當修改類型為REPLACE時

    case REPLACE:

    var newNode = (typeof currentPatch.node === 'String')

    ? document.createTextNode(currentPatch.node)

    : currentPatch.node.render();

    node.parentNode.replaceChild(newNode, node);

    break;

    //當修改類型為REORDER時

    case REORDER:

    reoderChildren(node, currentPatch.moves);

    break;

    //當修改類型為PROPS時

    case PROPS:

    setProps(node, currentPatch.props);

    break;

    //當修改類型為TEXT時

    case TEXT:

    if (node.textContent) {

    node.textContent = currentPatch.content;

    } else {

    node.nodeValue = currentPatch.content;

    }

    break;

    default:

    throw new Error('Unknow patch type ' + currentPatch.type);

    }

    });

    }

    ?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
    • 序言:七十年代末迟几,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子栏笆,更是在濱河造成了極大的恐慌类腮,老刑警劉巖,帶你破解...
      沈念sama閱讀 206,214評論 6 481
    • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛉加,死亡現(xiàn)場離奇詭異蚜枢,居然都是意外死亡,警方通過查閱死者的電腦和手機针饥,發(fā)現(xiàn)死者居然都...
      沈念sama閱讀 88,307評論 2 382
    • 文/潘曉璐 我一進店門厂抽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人丁眼,你說我怎么就攤上這事修肠。” “怎么了户盯?”我有些...
      開封第一講書人閱讀 152,543評論 0 341
    • 文/不壞的土叔 我叫張陵嵌施,是天一觀的道長。 經(jīng)常有香客問我莽鸭,道長吗伤,這世上最難降的妖魔是什么? 我笑而不...
      開封第一講書人閱讀 55,221評論 1 279
    • 正文 為了忘掉前任硫眨,我火速辦了婚禮足淆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘礁阁。我一直安慰自己巧号,他們只是感情好,可當我...
      茶點故事閱讀 64,224評論 5 371
    • 文/花漫 我一把揭開白布姥闭。 她就那樣靜靜地躺著丹鸿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棚品。 梳的紋絲不亂的頭發(fā)上靠欢,一...
      開封第一講書人閱讀 49,007評論 1 284
    • 那天廊敌,我揣著相機與錄音,去河邊找鬼门怪。 笑死骡澈,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的掷空。 我是一名探鬼主播肋殴,決...
      沈念sama閱讀 38,313評論 3 399
    • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼坦弟!你這毒婦竟也來了护锤?” 一聲冷哼從身側(cè)響起,我...
      開封第一講書人閱讀 36,956評論 0 259
    • 序言:老撾萬榮一對情侶失蹤减拭,失蹤者是張志新(化名)和其女友劉穎蔽豺,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拧粪,經(jīng)...
      沈念sama閱讀 43,441評論 1 300
    • 正文 獨居荒郊野嶺守林人離奇死亡修陡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
      茶點故事閱讀 35,925評論 2 323
    • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了可霎。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片魄鸦。...
      茶點故事閱讀 38,018評論 1 333
    • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖癣朗,靈堂內(nèi)的尸體忽然破棺而出拾因,到底是詐尸還是另有隱情,我是刑警寧澤旷余,帶...
      沈念sama閱讀 33,685評論 4 322
    • 正文 年R本政府宣布绢记,位于F島的核電站,受9級特大地震影響正卧,放射性物質(zhì)發(fā)生泄漏蠢熄。R本人自食惡果不足惜,卻給世界環(huán)境...
      茶點故事閱讀 39,234評論 3 307
    • 文/蒙蒙 一炉旷、第九天 我趴在偏房一處隱蔽的房頂上張望签孔。 院中可真熱鬧,春花似錦窘行、人聲如沸饥追。這莊子的主人今日做“春日...
      開封第一講書人閱讀 30,240評論 0 19
    • 文/蒼蘭香墨 我抬頭看了看天上的太陽但绕。三九已至,卻和暖如春翘骂,著一層夾襖步出監(jiān)牢的瞬間壁熄,已是汗流浹背帚豪。 一陣腳步聲響...
      開封第一講書人閱讀 31,464評論 1 261
    • 我被黑心中介騙來泰國打工碳竟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留草丧,地道東北人。 一個月前我還...
      沈念sama閱讀 45,467評論 2 352
    • 正文 我出身青樓莹桅,卻偏偏與公主長得像昌执,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子诈泼,可洞房花燭夜當晚...
      茶點故事閱讀 42,762評論 2 345

    推薦閱讀更多精彩內(nèi)容