虛擬DOM解析及其在框架裏的應用

虛擬DOM解析及其在框架裏的應用


瀏覽器是怎樣解析HTML而且繪出整個頁面的

webkit處理流程
上圖爲webkit引擎瀏覽器的處理流程,如上圖大體分爲4大步:

第一步,HTML解析器分析html,構建一顆DOM樹;

第二步,CSS解析器會分析外聯的css文件和內聯的一些樣式,建立一個頁面的樣式表;

第三步,將DOM樹和樣式表關聯起來,建立一顆Render樹。這一過程又被稱爲Attachment,每一個DOM節點上都有一個attach方法,會接收對應的樣式表,返回一個render對象。這些render對象最終會結合成一個render tree;

第四步,有了render tree後,瀏覽器就能夠爲render tree上的每一個節點在屏幕上分配一個精確的位置座標,而後各個節點會調用自身的paint方法結合座標信息,在瀏覽器中繪製中整個頁面了。
css

迴流(reflow)和重繪

迴流和重繪都是瀏覽器自身的行爲

迴流:當render tree中的一部分(或所有)由於元素的規模尺寸,佈局,隱藏等改變而須要從新構建。這就稱爲迴流(reflow)。每一個頁面至少須要一次迴流,就是在頁面第一次加載的時候。在迴流的時候,瀏覽器會使渲染樹中受到影響的部分失效,並從新構造這部分渲染樹,完成迴流後,瀏覽器會從新繪製受影響的部分到屏幕中,該過程成爲重繪。

重繪:當render tree中的一些元素須要更新屬性,而這些屬性只是影響元素的外觀,風格,而不會影響佈局的,好比background-color。則就叫稱爲重繪。

減小回流的辦法(不是此次討論重點):好比cssText、避免使用會強制reflush隊列的屬性等等html

手動操做DOM帶來的性能憂患

在使用原生的js api或者jquery等一些方法直接去操做dom的時候,可能會引發頁面的reflow,而頁面的迴流所帶來的代價是很是昂貴的。頻繁的去操做dom,會引發頁面的卡頓,影響用戶的體驗。
這裏打印一個最簡單的真實DOM節點裏面的屬性
webkit處理流程前端

虛擬DOM就是爲了解決頻繁操做DOM的性能問題創造出來的。
例如:
若是使用原生api去操做一個會致使迴流的DOM操做10次,那麼瀏覽器會每次都會從新走一次上面的全流程,包括一些沒變化的位置計算。
而虛擬DOM不會當即去操做DOM,而是將這10次更新的diff內容保存到本地的一個js對象中,最終將這個js對象一次性attach到DOM樹上,通知瀏覽器去執行繪製工做,這樣能夠避免大量的無謂的計算量。vue

virtual dom 是什麼

這是一段真實的DOM tree結構react

<div id="container">
    <p>Real DOM</p>
    <ul>
        <li class="item">Item 1</li>
        <li class="item">Item 2</li>
        <li class="item">Item 3</li>
    </ul>
</div>

若是用js對象來模擬上述的DOM Treejquery

let virtualDomTree = CreateElement('div', { id: 'container' }, [
    CreateElement('p', {}, ['Virtual DOM']),
    CreateElement('ul', {}, [
        CreateElement('li', { class: 'item' }, ['Item 1']),
        CreateElement('li', { class: 'item' }, ['Item 2']),
        CreateElement('li', { class: 'item' }, ['Item 3']),
    ]),
]);

let root = virtualDomTree.render();   //轉換爲一個真正的dom結構或者dom fragment
document.getElementById('virtualDom').appendChild(root);

這樣的好處,避免了因直接去修改真實的dom而帶來的性能隱患。能夠先把頁面的一些改動反應到這個虛擬dom對象上,等更新完後再一次統一去把變化同步到真實的dom中。

下面是CreateElement的實現方法web

function CreateElement(tagName, props, children) {
    if (!(this instanceof CreateElement)) {
        return new CreateElement(tagName, props, children);
    }

    this.tagName = tagName;
    this.props = props || {};
    this.children = children || [];
    this.key = props ? props.key : undefined;
}

render方法算法

CreateElement.prototype.render = function() {
    let el = document.createElement(this.tagName);
    let props = this.props;

    for (let propName in props) {
        setAttr(el, propName, props[propName]);
    }

    this.children.forEach((child) => {
        let childEl = (child instanceof Element) ? child.render() : document.createTextNode(child);
        el.appendChild(childEl);
    });

    return el;
};

直到如今,已經能夠在頁面中建立一個真實的DOM結構了。api

diff算法

上面已經完成了虛擬DOM -> 真實DOM的一個轉換工做,如今只須要把頁面上全部的改動都更新到虛擬DOM上。這就是一個diff過程。瀏覽器

兩棵樹若是徹底比較時間複雜度是O(n^3),但參照《深刻淺出React和Redux》一書中的介紹,React的Diff算法的時間複雜度是O(n)。要實現這麼低的時間複雜度,意味着只能平層地比較兩棵樹的節點,放棄了深度遍歷。這樣作,彷佛犧牲了必定的精確性來換取速度,但考慮到現實中前端頁面一般也不會跨層級移動DOM元素,因此這樣作是最優的。

只考慮相同等級diff,能夠分爲下面4中狀況:

第一種。若是節點類型變了,好比下面的p標籤變成了h3標籤,則直接卸載舊節點裝載新節點,這個過程稱爲REPLACE。
第一種狀況

第二種狀況。節點類型同樣,僅僅是屬性變化了,這一過程叫PROPS。好比

renderA: <ul>
renderB: <ul class: 'marginLeft10'>
=> [addAttribute class "marginLeft10"]

這一過程只會執行節點的更新操做,不會觸發節點的卸載和裝載操做。

第三種。只是文本變化了,TEXT過程。該過程只會替換文本。

第四種。節點發生了移動,增長,或者刪除操做。該過程稱爲REOREDR。虛擬DOM Diff算法解析
插入DOM-1
若是在一些節點中間插入一個F節點,簡單粗暴的作法是:卸載C,裝載F,卸載D,裝載C,卸載E,裝載D,裝載E。以下圖:

插入DOM-2
這種方法顯然是不高效的。

而若是給每一個節點惟一的標識(key),那麼就能找到正確的位置去插入新的節點。
插入DOM-3
這也就是爲何vue/react等框架,會要求咱們在寫循環遍歷結構的時候要寫key值

在vue2.0中是如何使用虛擬dom來綁定和渲染模板的

流程圖1

  1. 監聽數據的變化
    vue內部是經過數據劫持的方式來作到數據綁定的,其中最核心的方法就是經過Object.defineProperty()的getter和setter來實現對數據劫持,達到監聽數據變更的目的。

  2. vue中虛擬DOM生成DOM的過程
    流程圖2

  3. DocumentFragment和vue異步更新隊列
    DocumentFragment是允許把一些DOM操做先應用到一個dom片斷裏,而後再將這個片斷append到DOM樹裏,從而來減小頁面的reflow次數。
var fragment = document.createDocumentFragment();

//add DOM to fragment

for(var i = 0; i < 10; i++) {
    var spanNode = document.createElement("span");
    spanNode.innerHTML = "number:" + i;
    fragment.appendChild(spanNode);
}

//add this DOM to body
document.body.appendChild(spanNode);

vue裏面的一些dom的一些變化,都是在DocumentFragment容器中去操做的,最後將這個更新片斷append到el的根dom中。

走到這裏,你會發現還有一個問題。就算vue使用了虛擬dom,將一些改動先同步到虛擬對象上,而後去改動真實DOM。這其中,去改動真實DOM仍是使用了原生的api去操做DOM,仍是會不可避免的去reflow整個頁面,若是不能把這些更新操做打包起來集中去更新真實DOM,那其實徹底散失了虛擬DOM的做用性,反而變得更加冗餘。

這時候框架的價值就體現出來了,vue中,若是在同一次事件循環中若是觀察到有多個數據變化,vue會開啓一個異步更新隊列,並緩衝在同一事件循環中發生的全部數據改變。而後在下一個的事件循環‘tick’中,vue刷新隊列並執行實際工做。這樣就能夠批量的去更新屢次數據變化到虛擬dom對象中,diff差別,同步到頁面中的真實dom裏。

在react中是如何使用虛擬dom來綁定和渲染模板的

相關文章
相關標籤/搜索