1. 程式人生 > 其它 >如何快速實現一個虛擬 DOM 系統

如何快速實現一個虛擬 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 的實現原理,在實現程式碼中並未考慮效能等其他因素。