Skip to content

Virtual DOM

Virtual DOM (VDOM) 就是用一個 JavaScript 物件來描述 UI 結構的方式。虛擬 DOM 不直接操作真實 DOM,而是先對 UI 的更新在虛擬 DOM 上進行,然後通過比較新舊虛擬 DOM 的差異來決定如何高效地更新真實 DOM。

Virtual DOM Diffing

實作一個簡易的 Virtual DOM

jsx
const App = () => {
  return (
    <div id='1'>
      <span>Hello, Virtual DOM!</span>
    </div>
  );
};

這段程式碼經過 Babel 或是 SWC 轉譯後,會變成:

js
const App = () => {
  return React.createElement(
    'div',
    { id: '1' },
    React.createElement('span', null, 'Hello, Virtual DOM!')
  );
};

那麼接著就來實作這個 React.createElement

React.createElement

它用於創建一個虛擬 DOM 樹,回傳一個包含 type(元素類型)、props(屬性和子元素)的物件。children 可以是文字或其他虛擬 DOM 節點。

js
const React = {
  createElement(type, props, ...children) {
    return {
      type,
      props: {
        ...props,
        children: children.map((child) => {
          if (typeof child === 'object') {
            // 如果子元素是物件(巢狀元素),直接回傳
            return child;
          }
          // 否則將字串轉換為文字元素
          return this.createTextElement(child);
        }),
      },
    };
  },
  // 用於創建文字節點的虛擬 DOM
  createTextElement(text) {
    return {
      type: 'TEXT_ELEMENT',
      props: {
        nodeValue: text,
        children: [],
      },
    };
  },
};

// 測試我們的 createElement 函式
const vdom = React.createElement('div', { id: 1 }, React.createElement('span', null, 'Hello'));
console.log(vdom);

React Fiber

Fiber 是 React 16 引入的一個新的架構,用於解決和最佳化 React 應對複雜 UI 渲染時的性能問題。

Fiber 的作用

為了解決在大元件更新時可能導致的卡頓問題,React 團隊設計出了 Fiber 架構,把同步遞迴無法中斷的更新重構為非同步的可中斷更新。

它達成了 4 個具體目標:

  1. 可中斷的渲染: Fiber 允許將大的渲染任務拆分成多個小的工作單元(units of work),使得 React 可以在空閒時間執行這些小任務。當瀏覽器需要處理高優先順序的任務(如使用者輸入或動畫),可以暫停渲染工作,先處理這些任務,然後再恢復未完成的渲染工作。
  2. 優先順序調度: 在 Fiber 架構中,每個更新都有一個優先順序。React 可以根據任務的優先順序來決定哪些任務應該先執行,而低優先順序的任務可以稍後再處理,這樣可以確保使用者體驗的流暢性。
  3. Double Buffering: Fiber 架構維護了兩個 Fiber 樹:current tree(目前正在渲染的 Fiber 樹)和 work-in-progress tree(正在構建的 Fiber 樹)。React 使用這兩棵樹來保存更新前後的狀態,從而更高效地進行比較和更新。
  4. 任務(時間)切片: 在瀏覽器的空閒時間內(利用 requestIdleCallback 思想),React 可以將渲染任務拆分成多個小片段,逐步完成 Fiber 樹的建構,避免一次性完成所有渲染任務導致的阻塞。

Double Buffering

react 內部會維護兩棵 Fiber 樹,目前畫面上顯示內容對應的 Fiber 樹稱為 fiber tree,正在更新的內容會被構建在 work-in-progress fiber tree 上。

react 應用的 root 會有一個 current 指向目前顯示的 fiber tree,還有一個 alternate 指向正在更新的 work-in-progress fiber tree。當更新完成後,react 會將 current 指向剛剛更新完成的 work-in-progress fiber tree,此時 work-in-progress fiber tree 就變成了新的 fiber tree 完成更新。

Diffing 演算法

當 react 需要更新 UI 時,它會先創建一個新的虛擬 DOM 樹,然後將這個新的虛擬 DOM 樹與舊的虛擬 DOM 樹進行比較,找出差異,最後只更新那些有變化的部分到真實 DOM 上。這個過程稱為 diffing。

比如有 A B C D 四個節點,那麼 react 會先把這些節點變成一個 linked list:

txt
root
 |
 A -> B -> C -> D

然後我們更新成了 A C B E:

  1. {A, B, C, D} 它會去從 map(如果有 key)找出能夠重用的節點,也就是 A C B 進行重用。
  2. 如果 {A B C D} 這個結構沒有出現過 E,說明是新增了新的 fiber 節點。
  3. 如果 {A B C D} 舊節點沒有出現在新節點中 {A C B E},說明 D 節點被刪除了。

diff

程式碼實作

js
const React = {
  createElement(type, props, ...children) {
    return {
      type,
      props: {
        ...props,
        children: children.map((child) => {
          if (typeof child === 'object') {
            return child;
          }
          return this.createTextElement(child);
        }),
      },
    };
  },
  createTextElement(text) {
    return {
      type: 'TEXT_ELEMENT',
      props: {
        nodeValue: text,
        children: [],
      },
    };
  },
};

let nextUnitOfWork = null;
let wipRoot = null; // 正在工作的 fiber 樹
let currentRoot = null; // 舊的 fiber 樹
let deletions = null; // 存放要刪除的 fiber 節點

function render(element, container) {
  // 初始化 fiber 結構
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  };
  deletions = [];
  nextUnitOfWork = wipRoot;
}

function createDom(fiber) {
  const dom =
    fiber.type === 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(fiber.type);

  updateDom(dom, {}, fiber.props);
  return dom;
}

function updateDom(dom, prevProps, nextProps) {
  // 清除舊的屬性
  Object.keys(prevProps)
    .filter((key) => key !== 'children')
    .forEach((name) => {
      dom[name] = '';
    });

  // 設置新的屬性
  Object.keys(nextProps)
    .filter((key) => key !== 'children')
    .forEach((name) => {
      dom[name] = nextProps[name];
    });
}

function workLoop(deadline) {
  let shouldYield = false;

  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }

  // nextUnitOfWork 為空,表示所有工作完成並且可以提交更改
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }

  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);

  if (fiber.child) {
    return fiber.child;
  }

  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }

  return null;
}

function reconcileChildren(wipFiber, elements) {
  // generate new fiber
  // diffing algorithm
  let index = 0;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;

  while (index < elements.length || oldFiber != null) {
    const element = elements[index];

    let newFiber = null;
    const sameType = oldFiber && element && element.type === oldFiber.type;

    // update
    if (sameType) {
      console.log('更新節點:', element);
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        parent: wipFiber,
        dom: oldFiber.dom,
        alternate: oldFiber,
        effectTag: 'UPDATE',
      };
    }

    // add
    if (element && !sameType) {
      console.log('新增節點:', element);
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: 'PLACEMENT',
      };
    }

    // delete
    if (oldFiber && !sameType) {
      console.log('刪除節點:', oldFiber);
      oldFiber.effectTag = 'DELETION';
      deletions.push(oldFiber);
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

    if (index === 0) {
      wipFiber.child = newFiber;
    } else {
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber;
    index++;
  }
}

function commitRoot() {
  deletions.forEach(commitWork);
  commitWork(wipRoot.child);
  currentRoot = wipRoot; // 保存舊的 fiber 樹
  wipRoot = null; // 重置正在工作的 fiber 樹
}

function commitWork(fiber) {
  if (!fiber) return;

  const domParentFiber = fiber.parent;
  if (fiber.effectTag === 'PLACEMENT') {
    domParentFiber.dom.appendChild(fiber.dom);
  } else if (fiber.effectTag === 'UPDATE') {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  } else if (fiber.effectTag === 'DELETION') {
    domParentFiber.dom.removeChild(fiber.dom);
  }

  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

const vdom = React.createElement('div', { id: 1 }, React.createElement('span', null, 'Hello'));
render(vdom, document.getElementById('root'));

setTimeout(() => {
  const vdom2 = React.createElement('div', { id: 1 }, React.createElement('p', null, 'World'));
  render(vdom2, document.getElementById('root'));
}, 2000);