如何快速實現一個虛擬 DOM 系統


虛擬 DOM 是目前主流前端框架的技術核心之一,本文闡述如何實現一個簡單的虛擬 DOM 系統。

為什么需要虛擬 DOM?

虛擬 DOM 就是一棵由虛擬節點組成的樹,這棵樹展現了真實 DOM 的結構。這些虛擬節點是輕量的、無狀態的,一般是字符串或者僅僅包含必要字段的 JavaScript 對象。虛擬節點可以被組裝成節點樹樹,通過特定的 "diff" 算法對兩個節點樹進行對比,找出其中細微的變更點,然后更新到真實 DOM 上去。

之所以會有虛擬 DOM,是因為直接更新真實 DOM 非常昂貴。通過新比對虛擬 DOM,然后只將變化的部分更新到真實 DOM 上去。這么做都是操作純 JavaScript 對象,盡量避免了直接操作 DOM,讀寫成本低很多。

如何實現虛擬 DOM

在開始之前,我們需要明確一個虛擬 DOM 系統應該包含哪些必要的組成部分?

首先,我們要定義清楚什么是虛擬節點。一個虛擬節點可以是一個普通 JavaScript 對象,也可以是一個字符串。

我們定義一個函數 createNode 來創建虛擬節點。一個虛擬節點至少包含三個信息:

  • tag:保存虛擬節點的標簽名,字符串
  • props:保存虛擬節點的 properties/attributes,普通對象
  • children:保存虛擬節點的子節點,數組

下面的代碼是 createNode 實現樣例:

const createNode = (tag, props, children) => ({
  tag,
  props,
  children,
});

我們通過 createNode 可以輕松的創建虛擬節點:

createNode('div', { id: 'app' }, ['Hello World']);

// 返回如下:
{
  tag: 'div',
  props: { id: 'app' },
  children: ['Hello World'],
}

現在,我們需要定義一個 createElement 函數來根據虛擬節點創建真實的 DOM 元素。

createElement 中,我們需要創建一個新的 DOM 元素,然后遍歷虛擬節點的 props 屬性,將其中的屬性添加到 DOM 元素上去,之后再遍歷 children 屬性。如下代碼是一個實現樣例:

const createElement = vnode => {
  if (typof vnode === 'string') {
    return document.createTextNode(vnode); // 如果是字符串就直接返回文本元素
  }
  const el = document.createElement(vnode.tag);
  if (vnode.props) {
    Object.entries(vnode.props).forEach(([name, value]) => {
      el[name] = value;
    });
  }
  if (vnode.children) {
    vnode.children.forEach(child => {
      el.appendChild(createElement(child));
    });
  }
  return el;
}

現在,我們可以通過 createElement 將虛擬節點轉變成真實 DOM 了。

createElement(createNode("div", { id: "app" }, ["Hello World"]));

// 輸出: <div id="app">Hello World</div>

我們再來定義一個 diff 函數來實現 'diff' 算法。這個 diff 函數接收三個參數,一個是已經存在的 DOM 元素,一個是舊的虛擬節點,一個是新的虛擬節點。在這個函數中,我們將對比兩個虛擬節點,在需要的時候,將舊的元素替換掉。

const diff = (el, oldVNode, newVNode) => {
  const replace = () => el.replaceWith(createElement(newVNode));
  if (!newVNode) return el.remove();
  if (!oldVNode) return el.appendChild(createElement(newVNode));
  // 處理純文本的情況
  if (typeof oldVNode === 'string' || typeof newVNode === 'string') {
    if (oldVNode !== newVNode) return replace();
  } else {
    // 對比標簽名
    if (oldVNode.tag !== newVNode.tag) return replace();
    // 對比 props
    if (!oldVNode.props?.some((prop) => oldVNode.props?.[prop] === newVNode.props?.[prop])) return replace();
    // 對比 children
    [...el.childNodes].forEach((child, i) => {
      diff(child, oldVNode.children?.[i], newVNode.children?.[i]);
    });
  }
}

在這個函數中,我們先處理純文本的情況,如果新舊兩個字符串不相同,則直接替換。之后,我們就可以假定兩個虛擬節點都是對象了。我們先對比兩個節點的標簽名是否相同,不同則直接替換。之后對比兩個節點的 props 是否相同,不同也直接替換。最后我們在遞歸的使用 diff 函數對比兩個虛擬節點的 children。

至此,我們就實現了一個簡版虛擬 DOM 系統所必須的所有功能。下面是使用樣例:

const oldVNode = createNode("div", { id: "app" }, ["Hello World"]);
const newVNode = createNode("div", { id: "app" }, ["Goodbye World"]);
const el = createElement(oldVNode);
// <div id="app">Hello World</div>

diff(el, oldVNode, newVNode);
// el will become: <div id="app">Goodbye World</div>

文中的實現側重於展示虛擬 DOM 的實現原理,在實現代碼中並未考慮性能等其他因素。

歡迎關注公眾號“眾里千尋”或者在我的網站瀏覽更多更系統的信息。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM