vue核心之虛擬DOM篇

虛擬 DOM 前世今生

虛擬 DOM 也叫 VDOM,虛擬 DOM 其實并不是什么新鮮事物度液,早在多年前網(wǎng)上就已經(jīng)有很多介紹虛擬 DOM 的文章厕宗。
但把虛擬 DOM 發(fā)揚光大的是 React,并且 vue2.0 也引入虛擬 DOM堕担,可以看出虛擬 DOM 在前端舉足輕重的地位已慢。
簡單來說虛擬 DOM 就是用數(shù)據(jù)格式表示 DOM 結(jié)構(gòu),并沒有真實的 append 到 DOM 上霹购,因此稱為虛擬 DOM佑惠。

虛擬 DOM 有何作用

使用虛擬 DOM 帶來的好處是顯而易見:和瀏覽器交互去操作 DOM 的效率遠不及去操作數(shù)據(jù)結(jié)構(gòu)。操作數(shù)據(jù)結(jié)構(gòu)是指改變“虛擬 DOM 對象”厕鹃,這個過程比修改真實 DOM 快很多兢仰。
不過使用虛擬 DOM 并不能使得操作 DOM 的數(shù)量減少,因為虛擬 DOM 也最終是要掛載到瀏覽器上成為真實 DOM 節(jié)點剂碴,但能夠精確的獲取最小的、最必要的操作 DOM 的集合轻专。

這樣一來忆矛,我們抽象表示 DOM,每次通過虛擬 DOM 的 diff 算法計算出視圖前后更新的最小差異,再根據(jù)這個最小差異去渲染/更新真實的 DOM催训,無疑更為可靠洽议,性能更高。

創(chuàng)建虛擬 DOM

說了這么多漫拭,到底該如何創(chuàng)建虛擬 DOM 呢亚兄?
我們仿造一些主流的虛擬 DOM 庫的思想去實現(xiàn)一個簡易版的虛擬 DOM。

如現(xiàn)有如下 DOM 結(jié)構(gòu):

<ul id="ul1">
  <li class="li-stl1">item1</li>
  <li class="li-stl1">item2</li>
  <li class="li-stl1">item3</li>
</ul>

現(xiàn)在如果要用 js 來表示采驻,我們構(gòu)建這樣一個對象結(jié)構(gòu):

const myVirtualDom = {
  tagName: "ul",
  attributes: {
    id: "ul1",
  },
  children: [
    { tagName: "li", attributes: { class: "li-stl1" }, children: ["item1"] },
    { tagName: "li", attributes: { class: "li-stl1" }, children: ["item2"] },
    { tagName: "li", attributes: { class: "li-stl1" }, children: ["item3"] },
  ],
};
  • tagName 表示真實 DOM 標簽類型审胚;
  • attributes 是一個對象,表示真實 DOM 節(jié)點上所有的屬性礼旅;
  • children 對應真實 DOM 的 childNodes膳叨,其中 childNodes 每一項又是類似的結(jié)構(gòu)。

定義好數(shù)據(jù)結(jié)構(gòu)后痘系,現(xiàn)在需要一個可以生成如此結(jié)構(gòu)虛擬 DOM 的方法(類)菲嘴。
用于生產(chǎn)虛擬 DOM:

class VirtualDom {
  constructor(tagName, attributes = {}, children = []) {
    this.tagName = tagName;
    this.attributes = attributes;
    this.children = children;
  }
}

// 封裝 createVirtualDom 方法,內(nèi)部調(diào)用 Element 構(gòu)造函數(shù)
function createVirtualDom(tagName, attributes, children) {
  return new VirtualDom(tagName, attributes, children);
}

上述虛擬 DOM 就可以這樣生成:

const myVirtualDom = createVirtualDom("ul", { id: "ul1" }, [
  createVirtualDom("li", { class: "li-stl1" }, ["item1"]),
  createVirtualDom("li", { class: "li-stl1" }, ["item2"]),
  createVirtualDom("li", { class: "li-stl1" }, ["item3"]),
]);
在這里插入圖片描述

生成的虛擬 DOM 對象的數(shù)據(jù)格式更如我們定義的那樣汰翠。
是不是很簡單龄坪?生成了虛擬 DOM 對象后,我們繼續(xù)完成虛擬 DOM 轉(zhuǎn)換為真實 DOM 節(jié)點的過程复唤。

虛擬 DOM 變 真實 DOM

首先創(chuàng)建一個 setAttribute 方法健田,setAttribute 方法的作用是對 DOM 節(jié)點進行屬性設置。
參數(shù)1:DOM 節(jié)點 參數(shù)2:屬性名 參數(shù)3:屬性值

const setAttribute = (node, key, value) => {
  switch (key) {
    case "style":
      node.style.cssText = value;
      break;
    case "value":
      let tagName = node.tagName || "";
      tagName = tagName.toLowerCase();
      if (tagName === "input" || tagName === "textarea") {
        node.value = value;
      } else {
        // 非 input && textarea 則使用 setAttribute 去設置 value 屬性
        node.setAttribute(key, value);
      }
      break;
    default:
      node.setAttribute(key, value);
      break;
  }
};

虛擬 DOM 類中加入 render 實例方法苟穆,該方法的作用是根據(jù)虛擬 DOM 生成真實 DOM 片段:

class VirtualDom {
  constructor(tagName, attributes = {}, children = []) {
    this.tagName = tagName;
    this.attributes = attributes;
    this.children = children;
  }

  render() {
    let element = document.createElement(this.tagName);
    let attributes = this.attributes;

    for (let key in attributes) {
      setAttribute(element, key, attributes[key]);
    }

    let children = this.children;

    // 遍歷子節(jié)點抄课, 若 child 也是虛擬節(jié)點,遞歸調(diào)用 render雳旅,若是字符串跟磨,直接創(chuàng)建文本節(jié)點
    children.forEach((child) => {
      let childElement =
        child instanceof VirtualDom
          ? child.render()
          : document.createTextNode(child);
      element.appendChild(childElement);
    });

    return element;
  }
}

根據(jù) tagName 創(chuàng)建標簽后,借助工具方法 setAttribute 進行屬性的創(chuàng)建攒盈;
對 children 每一項類型進行判斷抵拘,如果是 VirtualDom 實例,進行遞歸調(diào)用 render 方法型豁;
直到遇見文本節(jié)點類型僵蛛,進行內(nèi)容渲染。

真實 DOM 渲染

有了真實的 DOM 節(jié)點片段迎变,我們趁熱打鐵充尉,將真實的 DOM 節(jié)點渲染到瀏覽器上。
實現(xiàn) renderDOM 方法:

const renderDom = (element, target) => {
  target.appendChild(element);
};

截至目前的完整代碼如下:

// 添加屬性方法
const setAttribute = (node, key, value) => {
  switch (key) {
    case "style":
      node.style.cssText = value;
      break;
    case "value":
      let tagName = node.tagName || "";
      tagName = tagName.toLowerCase();
      if (tagName === "input" || tagName === "textarea") {
        node.value = value;
      } else {
        node.setAttribute(key, value);
      }
      break;
    default:
      node.setAttribute(key, value);
      break;
  }
};

// 虛擬 DOM 類
class VirtualDom {
  constructor(tagName, attributes = {}, children = []) {
    this.tagName = tagName;
    this.attributes = attributes;
    this.children = children;
  }

  render() {
    let element = document.createElement(this.tagName);
    let attributes = this.attributes;

    for (let key in attributes) {
      setAttribute(element, key, attributes[key]);
    }

    let children = this.children;

    children.forEach((child) => {
      let childElement =
        child instanceof VirtualDom
          ? child.render()
          : document.createTextNode(child);
      element.appendChild(childElement);
    });

    return element;
  }
}

// 生成虛擬 DOM 方法
function createVirtualDom(tagName, attributes, children) {
  return new VirtualDom(tagName, attributes, children);
}

// 將真實的 DOM 節(jié)點渲染到瀏覽器上
const renderDom = (element, target) => {
  target.appendChild(element);
};

// 執(zhí)行方法 生成虛擬 DOM
const myVirtualDom = createVirtualDom("ul", { id: "ul1" }, [
  createVirtualDom("li", { class: "li-stl1" }, ["item1"]),
  createVirtualDom("li", { class: "li-stl1" }, ["item2"]),
  createVirtualDom("li", { class: "li-stl1" }, ["item3"]),
]);

// 執(zhí)行虛擬 DOM 的 render 方法衣形,將虛擬 DOM 轉(zhuǎn)換為真實 DOM
const realDom = myVirtualDom.render();

// 將真實 DOM 渲染倒瀏覽器上
renderDom(realDom, document.body);

到這就實現(xiàn)了從虛擬 DOM 創(chuàng)建到轉(zhuǎn)換為真實 DOM驼侠,并渲染到瀏覽器上的過程姿鸿,實現(xiàn)起來并不困難。

虛擬 DOM diff

有了上面的實現(xiàn)倒源,可以產(chǎn)出一份虛擬 DOM苛预,并轉(zhuǎn)換為真實 DOM 渲染在瀏覽器中。
虛擬 DOM 也不是一塵不變的笋熬,當用戶在特定操作后热某,會產(chǎn)出一份新的虛擬 DOM,開頭我們也說了虛擬 DOM 的優(yōu)勢之一在于“能夠精確的獲取最小的胳螟、最必要的操作 DOM 的集合”昔馋。
那該如何得出前后兩份虛擬 DOM 的差異,并交給瀏覽器需要更新的結(jié)果呢旺隙? 這就涉及到 DOM diff 的過程绒极。
虛擬 DOM 是個樹形結(jié)構(gòu),所以我們需要對兩份虛擬 DOM 進行遞歸比較蔬捷,將變化存儲在一個變量中:

參數(shù)1:舊的虛擬 DOM 對象 參數(shù)2:新的虛擬 DOM 對象

const diff = (oldVirtualDom, newVirtualDom) => {
  let differences = {};

  // 遞歸虛擬 DOM 樹垄提,計算差異后結(jié)果放到 differencess 對象中
  walkToDiff(oldVirtualDom, newVirtualDom, 0, differences);

  // 返回 diff 計算結(jié)果
  return differences;
};

walkToDiff 前兩個參數(shù)是兩個需要比較的舊/新虛擬 DOM 對象;第三個參數(shù)記錄 nodeIndex周拐,在刪除節(jié)點時使用铡俐,初始為 0;第四個參數(shù)是一個閉包變量妥粟,記錄 diff 結(jié)果审丘。
waklToDiff 的具體實現(xiàn)為:

let initialIndex = 0;

const walkToDiff = (oldVirtualDom, newVirtualDom, index, differences) => {
  let diffResult = [];

  // 1.如果 newVirtualDom 不存在,說明該節(jié)點被移除勾给,我們將 type 為 REMOVE 的對象放進 diffResult 數(shù)組滩报,并記錄 index
  if (!newVirtualDom) {
    diffResult.push({
      type: "REMOVE",
      index,
    });
  }
  // 2.如果新舊節(jié)點都是文本節(jié)點,是字符串
  else if (
    typeof oldVirtualDom === "string" &&
    typeof newVirtualDom === "string"
  ) {
    // 比較文本是否相同播急,如果不同則記錄新的結(jié)果
    if (oldVirtualDom !== newVirtualDom) {
      diffResult.push({
        type: "MODIFY_TEXT",
        data: newVirtualDom,
        index,
      });
    }
  }
  // 3.如果新舊節(jié)點類型相同
  else if (oldVirtualDom.tagName === newVirtualDom.tagName) {
    // 比較屬性是否相同
    let diffAttributeResult = {};

    for (let key in oldVirtualDom) {
      if (oldVirtualDom[key] !== newVirtualDom[key]) {
        // 記錄差異脓钾,直接將 attributes 和 children 進行覆蓋
        diffAttributeResult[key] = newVirtualDom[key];
        // 處理屬性被刪除的情況
        if (key === "attributes") {
          // 如果 diffAttributeResult 不含 oldVirtualDom["attributes"] 中的屬性,說明在新的 vdom 中該屬性被刪除
          for (let attr in oldVirtualDom["attributes"]) {
            if (!diffAttributeResult["attributes"].hasOwnProperty(attr)) {
              // 如果屬性被刪除則設置值為空桩警,在渲染時對空值進行判斷即可
              diffAttributeResult["attributes"][attr] = "";
            }
          }
        }
      }
    }

    // 舊節(jié)點不存在的新屬性
    for (let key in newVirtualDom) {
      if (!oldVirtualDom.hasOwnProperty(key)) {
        diffAttributeResult[key] = newVirtualDom[key];
      }
    }

    // 如果該層級有差異可训,將差異結(jié)果記錄到 diffResult 數(shù)組中
    if (Object.keys(diffAttributeResult).length > 0) {
      diffResult.push({
        type: "MODIFY_ATTRIBUTES",
        diffAttributeResult,
      });
    }

    // 如果有子節(jié)點,遍歷子節(jié)點
    oldVirtualDom.children.forEach((child, i) => {
      walkToDiff(child, newVirtualDom.children[i], ++initialIndex, differences);
    });
  }
  // 4.else 說明節(jié)點類型不同捶枢,被直接替換了握截,我們直接將新的結(jié)果 push
  else {
    diffResult.push({
      type: "REPLACE",
      newVirtualDom,
    });
  }

  // 如果不存在舊節(jié)點
  if (!oldVirtualDom) {
    diffResult.push({
      type: "REPLACE",
      newVirtualDom,
    });
  }

  if (diffResult.length) {
    differences[index] = diffResult;
  }
};

添加 walkToDiff 方法后,整體測試下我們的代碼:

const setAttribute = (node, key, value) => {
  switch (key) {
    case "style":
      node.style.cssText = value;
      break;
    case "value":
      let tagName = node.tagName || "";
      tagName = tagName.toLowerCase();
      if (tagName === "input" || tagName === "textarea") {
        node.value = value;
      } else {
        node.setAttribute(key, value);
      }
      break;
    default:
      node.setAttribute(key, value);
      break;
  }
};

class VirtualDom {
  constructor(tagName, attributes = {}, children = []) {
    this.tagName = tagName;
    this.attributes = attributes;
    this.children = children;
  }

  render() {
    let element = document.createElement(this.tagName);
    let attributes = this.attributes;

    for (let key in attributes) {
      setAttribute(element, key, attributes[key]);
    }

    let children = this.children;

    children.forEach((child) => {
      let childElement =
        child instanceof VirtualDom
          ? child.render()
          : document.createTextNode(child);
      element.appendChild(childElement);
    });

    return element;
  }
}

function createVirtualDom(tagName, attributes, children) {
  return new VirtualDom(tagName, attributes, children);
}

const renderDom = (element, target) => {
  target.appendChild(element);
};

const diff = (oldVirtualDom, newVirtualDom) => {
  let differences = {};

  walkToDiff(oldVirtualDom, newVirtualDom, 0, differences);

  return differences;
};

let initialIndex = 0;

const walkToDiff = (oldVirtualDom, newVirtualDom, index, differences) => {
  let diffResult = [];

  // 如果 newVirtualDom 不存在烂叔,說明該節(jié)點被移除谨胞,我們將 type 為 REMOVE 的對象推進 diffResult 變量,并記錄 index
  if (!newVirtualDom) {
    diffResult.push({
      type: "REMOVE",
      index,
    });
  }
  // 如果新舊節(jié)點都是文本節(jié)點蒜鸡,是字符串
  else if (
    typeof oldVirtualDom === "string" &&
    typeof newVirtualDom === "string"
  ) {
    if (oldVirtualDom !== newVirtualDom) {
      diffResult.push({
        type: "MODIFY_TEXT",
        data: newVirtualDom,
        index,
      });
    }
  }
  // 如果新舊節(jié)點類型相同
  else if (oldVirtualDom.tagName === newVirtualDom.tagName) {
    let diffAttributeResult = {};

    for (let key in oldVirtualDom) {
      if (oldVirtualDom[key] !== newVirtualDom[key]) {
        diffAttributeResult[key] = newVirtualDom[key];
        if (key === "attributes") {
          for (let attr in oldVirtualDom["attributes"]) {
            if (!diffAttributeResult["attributes"].hasOwnProperty(attr)) {
              diffAttributeResult["attributes"][attr] = "";
            }
          }
        }
      }
    }

    for (let key in newVirtualDom) {
      if (!oldVirtualDom.hasOwnProperty(key)) {
        diffAttributeResult[key] = newVirtualDom[key];
      }
    }

    if (Object.keys(diffAttributeResult).length > 0) {
      diffResult.push({
        type: "MODIFY_ATTRIBUTES",
        diffAttributeResult,
      });
    }

    oldVirtualDom.children.forEach((child, index) => {
      walkToDiff(
        child,
        newVirtualDom.children[index],
        ++initialIndex,
        differences
      );
    });
  }
  // else 說明節(jié)點類型不同畜眨,被直接替換了昼牛,我們直接將新的結(jié)果 push
  else {
    diffResult.push({
      type: "REPLACE",
      newVirtualDom,
    });
  }

  if (!oldVirtualDom) {
    diffResult.push({
      type: "REPLACE",
      newVirtualDom,
    });
  }

  if (diffResult.length) {
    console.log(index);
    differences[index] = diffResult;
  }
};

測試

const myVirtualDom1 = createVirtualDom("ul", { id: "ul1" }, [
  createVirtualDom("li", { class: "li-stl1" }, ["item1"]),
  createVirtualDom("li", { class: "li-stl1" }, ["item2"]),
  createVirtualDom("li", { class: "li-stl1" }, ["item3"]),
]);

const myVirtualDom2 = createVirtualDom("ul", { id: "ul2" }, [
  createVirtualDom("li", { class: "li-stl2" }, ["item4"]),
  createVirtualDom("li", { class: "li-stl2" }, ["item5"]),
  createVirtualDom("li", { class: "li-stl2" }, ["item6"]),
]);

diff(myVirtualDom1, myVirtualDom2);

得到比較后的結(jié)果數(shù)組

var result = {
  "0": [
    {
      type: "MODIFY_ATTRIBUTES",
      diffAttributeResult: {
        attributes: { id: "ul2" },
        children: [
          {
            tagName: "li",
            attributes: { class: "li-stl2" },
            children: ["item4"],
          },
          {
            tagName: "li",
            attributes: { class: "li-stl2" },
            children: ["item5"],
          },
          {
            tagName: "li",
            attributes: { class: "li-stl2" },
            children: ["item6"],
          },
        ],
      },
    },
  ],
  "1": [
    {
      type: "MODIFY_ATTRIBUTES",
      diffAttributeResult: {
        attributes: { class: "li-stl2" },
        children: ["item4"],
      },
    },
  ],
  "2": [{ type: "MODIFY_TEXT", data: "item4", index: 2 }],
  "3": [
    {
      type: "MODIFY_ATTRIBUTES",
      diffAttributeResult: {
        attributes: { class: "li-stl2" },
        children: ["item5"],
      },
    },
  ],
  "4": [{ type: "MODIFY_TEXT", data: "item5", index: 4 }],
  "5": [
    {
      type: "MODIFY_ATTRIBUTES",
      diffAttributeResult: {
        attributes: { class: "li-stl2" },
        children: ["item6"],
      },
    },
  ],
  "6": [{ type: "MODIFY_TEXT", data: "item6", index: 6 }],
};

測試結(jié)果符合我們的預期术瓮。
此刻我們已經(jīng)通過 diff 方法對兩個虛擬 DOM 進行比對康聂,在 diff 方法中得到差異 differences。得到差異后如何更新視圖呢胞四?
拿到差異 differences恬汁,調(diào)用 patchDiff 方法

const patchDiff = (node, differences) => {
  // 用來取差異數(shù)組中每項的索引
  // 之所以用對象的形式,是因為在 renderDiff 中被遞歸傳遞辜伟,使用對象可以保證 index 的值不會重復氓侧,如果使用普通的值類型遞歸調(diào)用會出問題
  let differ = { index: 0 };
  renderDiff(node, differ, differences);
};

patchDiff 方法接收一個真實的 DOM 節(jié)點,它是需要進行更新的 DOM 節(jié)點导狡,同時接收一個差異集合约巷,該集合對接 diff 方法返回的結(jié)果。
在 patchDiff 方法內(nèi)部旱捧,我們調(diào)用了 renderDiff 函數(shù):

const renderDiff = (node, differ, differences) => {
  // 獲取差異數(shù)組中的每一項
  let currentDiff = differences[differ.index];

  // 真實 DOM 節(jié)點
  let childNodes = node.childNodes;

  // 遞歸調(diào)用自身
  childNodes.forEach((child) => {
    differ.index++;
    renderDiff(child, differ, differences);
  });

  // 對于當前節(jié)點的差異調(diào)用 updateRealDom 方法進行更新
  if (currentDiff) {
    updateRealDom(node, currentDiff);
  }
};

renderDiff 方法進行自身遞歸独郎,對于當前節(jié)點的差異調(diào)用 updateRealDom 方法進行更新。
updateRealDom 對四種類型的 diff 進行處理:

const updateRealDom = (node, currentDiff) => {
  currentDiff.forEach((dif) => {
    switch (dif.type) {
      case "MODIFY_ATTRIBUTES":
        const attributes = dif.diffAttributeResult.attributes;
        for (let key in attributes) {
          // 不是元素節(jié)點就返回
          if (node.nodeType !== 1) return;

          const value = attributes[key];
          if (value) {
            setAttribute(node, key, value);
          } else {
            // 當 value 為空時枚赡,也就是屬性值在新的虛擬 DOM 中被移除了
            node.removeAttribute(key);
          }
        }
        break;
      case "MODIFY_TEXT":
        node.textContent = dif.data;
        break;
      case "REPLACE":
        let newNode =
          dif.newNode instanceof VirtualDom
            ? render(dif.newNode)
            : document.createTextNode(dif.newNode);
        node.parentNode.replaceChild(newNode, node);
        break;
      case "REMOVE":
        node.parentNode.removeChild(node);
        break;
      default:
        break;
    }
  });
};

到這里簡易版的虛擬 DOM 庫就實現(xiàn)了氓癌,下面對代碼進行測試。

完整代碼:

const setAttribute = (node, key, value) => {
  switch (key) {
    case "style":
      node.style.cssText = value;
      break;
    case "value":
      let tagName = node.tagName || "";
      tagName = tagName.toLowerCase();
      if (tagName === "input" || tagName === "textarea") {
        node.value = value;
      } else {
        node.setAttribute(key, value);
      }
      break;
    default:
      node.setAttribute(key, value);
      break;
  }
};

class VirtualDom {
  constructor(tagName, attributes = {}, children = []) {
    this.tagName = tagName;
    this.attributes = attributes;
    this.children = children;
  }

  render() {
    let element = document.createElement(this.tagName);
    let attributes = this.attributes;

    for (let key in attributes) {
      setAttribute(element, key, attributes[key]);
    }

    let children = this.children;

    children.forEach((child) => {
      let childElement =
        child instanceof VirtualDom
          ? child.render() // 若 child 也是虛擬節(jié)點贫橙,遞歸進行
          : document.createTextNode(child); // 若是字符串贪婉,直接創(chuàng)建文本節(jié)點
      element.appendChild(childElement);
    });

    return element;
  }
}

function createVirtualDom(tagName, attributes, children) {
  return new VirtualDom(tagName, attributes, children);
}

const renderDom = (element, target) => {
  target.appendChild(element);
};

const diff = (oldVirtualDom, newVirtualDom) => {
  let differences = {};

  // 遞歸樹 比較后的結(jié)果放到 differences
  walkToDiff(oldVirtualDom, newVirtualDom, 0, differences);

  return differences;
};

let initialIndex = 0;

const walkToDiff = (oldVirtualDom, newVirtualDom, index, differences) => {
  let diffResult = [];

  // 如果 newVirtualDom 不存在,說明該節(jié)點被移除卢肃,我們將 type 為 REMOVE 的對象推進 diffResult 變量疲迂,并記錄 index
  if (!newVirtualDom) {
    diffResult.push({
      type: "REMOVE",
      index,
    });
  }
  // 如果新舊節(jié)點都是文本節(jié)點,是字符串
  else if (
    typeof oldVirtualDom === "string" &&
    typeof newVirtualDom === "string"
  ) {
    // 比較文本是否相同莫湘,如果不同則記錄新的結(jié)果
    if (oldVirtualDom !== newVirtualDom) {
      diffResult.push({
        type: "MODIFY_TEXT",
        data: newVirtualDom,
        index,
      });
    }
  }
  // 如果新舊節(jié)點類型相同
  else if (oldVirtualDom.tagName === newVirtualDom.tagName) {
    // 比較屬性是否相同
    let diffAttributeResult = {};

    for (let key in oldVirtualDom) {
      if (oldVirtualDom[key] !== newVirtualDom[key]) {
        diffAttributeResult[key] = newVirtualDom[key];
        if (key === "attributes") {
          // 如果 diffAttributeResult 不含 oldVirtualDom["attributes"] 中的屬性尤蒿,說明在新的 vdom 中該屬性被刪除
          for (let attr in oldVirtualDom["attributes"]) {
            if (!diffAttributeResult["attributes"].hasOwnProperty(attr)) {
              diffAttributeResult["attributes"][attr] = "";
            }
          }
        }
      }
    }

    for (let key in newVirtualDom) {
      // 舊節(jié)點不存在的新屬性
      if (!oldVirtualDom.hasOwnProperty(key)) {
        diffAttributeResult[key] = newVirtualDom[key];
      }
    }

    if (Object.keys(diffAttributeResult).length > 0) {
      diffResult.push({
        type: "MODIFY_ATTRIBUTES",
        diffAttributeResult,
      });
    }

    // 如果有子節(jié)點,遍歷子節(jié)點
    oldVirtualDom.children.forEach((child, index) => {
      walkToDiff(
        child,
        newVirtualDom.children[index],
        ++initialIndex,
        differences
      );
    });
  }
  // else 說明節(jié)點類型不同逊脯,被直接替換了优质,我們直接將新的結(jié)果 push
  else {
    diffResult.push({
      type: "REPLACE",
      newVirtualDom,
    });
  }

  if (!oldVirtualDom) {
    diffResult.push({
      type: "REPLACE",
      newVirtualDom,
    });
  }

  if (diffResult.length) {
    differences[index] = diffResult;
  }
};

const patchDiff = (node, differences) => {
  let differ = { index: 0 };
  renderDiff(node, differ, differences);
};

const renderDiff = (node, differ, differences) => {
  let currentDiff = differences[differ.index];

  let childNodes = node.childNodes;

  childNodes.forEach((child) => {
    differ.index++;
    renderDiff(child, differ, differences);
  });

  if (currentDiff) {
    updateRealDom(node, currentDiff);
  }
};

const updateRealDom = (node, currentDiff) => {
  currentDiff.forEach((dif) => {
    switch (dif.type) {
      case "MODIFY_ATTRIBUTES":
        const attributes = dif.diffAttributeResult.attributes;
        for (let key in attributes) {
          if (node.nodeType !== 1) return;
          const value = attributes[key];
          if (value) {
            setAttribute(node, key, value);
          } else {
            node.removeAttribute(key);
          }
        }
        break;
      case "MODIFY_TEXT":
        node.textContent = dif.data;
        break;
      case "REPLACE":
        let newNode =
          dif.newNode instanceof VirtualDom
            ? render(dif.newNode)
            : document.createTextNode(dif.newNode);
        node.parentNode.replaceChild(newNode, node);
        break;
      case "REMOVE":
        node.parentNode.removeChild(node);
        break;
      default:
        break;
    }
  });
};

// 虛擬 DOM1
const myVirtualDom1 = createVirtualDom("ul", { id: "ul1", class: "class1" }, [
  createVirtualDom("li", { class: "li-stl1" }, ["item1"]),
  createVirtualDom("li", { class: "li-stl1" }, ["item2"]),
  createVirtualDom("li", { class: "li-stl1" }, ["item3"]),
]);

// 虛擬 DOM2
const myVirtualDom2 = createVirtualDom(
  "ul",
  { id: "ul2", style: "color:pink;" },
  [
    createVirtualDom("li", { class: "li-stl2" }, ["item4"]),
    createVirtualDom("li", { class: "li-stl2" }, ["item5"]),
    createVirtualDom("li", { class: "li-stl2" }, ["item6"]),
  ]
);

將虛擬 DOM 轉(zhuǎn)換為真實 DOM

var element = myVirtualDom1.render();

看到此時的 element 為真實的 DOM 節(jié)點,如下圖所示


在這里插入圖片描述

將真實 DOM 節(jié)點渲染到瀏覽器上

renderDom(element, document.body);

此時军洼,真實的 DOM 節(jié)點已經(jīng)被渲染到瀏覽器上巩螃,如下圖所示


在這里插入圖片描述

比較兩個虛擬 DOM 差異

const differences = diff(myVirtualDom1, myVirtualDom2);

得到的差異對象 differences 如下圖所示


在這里插入圖片描述

分析差異,更新視圖

patchDiff(element, differences);

執(zhí)行后匕争,id 更新為 ul2避乏,并且移除了 class 屬性,添加了 style 屬性甘桑,其余值也修改正確


在這里插入圖片描述

完結(jié)

盡管缺少了很多細節(jié)優(yōu)化和邊界問題的處理拍皮,但是我們的虛擬 DOM 實現(xiàn)的還是非常強大的歹叮,基本思想和 snabbdom 等一些虛擬 DOM 庫高度一致。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末铆帽,一起剝皮案震驚了整個濱河市咆耿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌爹橱,老刑警劉巖萨螺,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異愧驱,居然都是意外死亡慰技,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門组砚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吻商,“玉大人,你說我怎么就攤上這事糟红“剩” “怎么了?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵改化,是天一觀的道長掩蛤。 經(jīng)常有香客問我,道長陈肛,這世上最難降的妖魔是什么揍鸟? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮句旱,結(jié)果婚禮上阳藻,老公的妹妹穿的比我還像新娘。我一直安慰自己谈撒,他們只是感情好腥泥,可當我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著啃匿,像睡著了一般蛔外。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上溯乒,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天夹厌,我揣著相機與錄音,去河邊找鬼裆悄。 笑死矛纹,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的光稼。 我是一名探鬼主播或南,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼孩等,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了采够?” 一聲冷哼從身側(cè)響起肄方,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎吁恍,沒想到半個月后扒秸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡冀瓦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了写烤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翼闽。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖洲炊,靈堂內(nèi)的尸體忽然破棺而出感局,到底是詐尸還是另有隱情,我是刑警寧澤暂衡,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布询微,位于F島的核電站,受9級特大地震影響狂巢,放射性物質(zhì)發(fā)生泄漏撑毛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一唧领、第九天 我趴在偏房一處隱蔽的房頂上張望藻雌。 院中可真熱鬧,春花似錦斩个、人聲如沸胯杭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽做个。三九已至,卻和暖如春滚局,著一層夾襖步出監(jiān)牢的瞬間居暖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工核畴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留膝但,地道東北人。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓谤草,卻偏偏與公主長得像跟束,于是被迫代替她去往敵國和親莺奸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,697評論 2 351

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

  • 一冀宴、真實DOM和其解析流程灭贷? 瀏覽器渲染引擎工作流程都差不多,大致分為5步略贮,創(chuàng)建DOM樹——創(chuàng)建StyleRu...
    LoveBugs_King閱讀 190,415評論 23 336
  • 真是DOM 的缺陷: js 操縱Dom 會 影響到整個渲染流水線 我們可以調(diào)用document.body.appe...
    Lyan_2ab3閱讀 692評論 0 1
  • 導讀 React的虛擬DOM和Diff算法是React的非常重要的核心特性甚疟,這部分源碼也非常復雜,理解這部分知識的...
    you的日常閱讀 522評論 1 4
  • 前言 ??Vue2.0引入了虛擬DOM逃延,比Vue1.0的初始渲染速度提升了2~4倍览妖,并大大降低了內(nèi)存消耗。目前主流...
    A鄭家慶閱讀 13,436評論 0 10
  • 文章結(jié)構(gòu): React中的虛擬DOM是什么? 虛擬DOM的簡單實現(xiàn)(diff算法) 虛擬DOM的內(nèi)部工作原理 Re...
    李輕舟閱讀 3,007評論 2 14