Створюємо простий Virtual DOM з нуля

28 хв. читання

Повний код можна переглянути за посиланням, а результат його роботи тут.

Примітка

  • Якщо змінна починається з символу $, вона стосується звичайного DOM (наприклад, $div, $el, $app);
  • Якщо змінна починається на v, маємо справу з Virtual DOM (наприклад, vDiv, vEl, vApp);
  • У кожному розділі будемо посилатися на Codesandbox для демонстрації результатів.

Передісторія: Що таке Virtual DOM?

Зазвичай, терміном Virtual DOM позначають прості об'єкти, що представляють звичайний DOM.

Об'єктна Модель Документу або Document Object Model (DOM) — програмний інтерфейс для HTML-документів.

Коли ви пишете:

const $app = document.getElementById('app');

Ви отримуєте на сторінці DOM для <div id="app"></div>. Ви можете керувати цим DOM за допомогою програмного інтерфейсу. Наприклад:

$app.innerHTML = 'Hello world';

Створимо простий об'єкт, який представлятиме $app:

const vApp = {
  tagName: 'div',
  attrs: {
    id: 'app',
  },
};

Примітка

Немає чіткого правила, яке визначало б вигляд Virtual DOM. Ви можете написати tagLabel замість tagName або props замість attrs. Якщо об'єкт представляє DOM, він перетворюється на Virtual DOM.

Virtual DOM не має програмного інтерфейсу, що робить його легким у порівнянні зі звичайним DOM. Проте хибною є думка про повільність DOM, адже браузери потурбувались про його оптимізацію.

Налаштування

Результат на Codesandbox.

Спочатку переходимо до директорії нашого проекту:

$ mkdir /tmp/vdommm
$ cd /tmp/vdommm

Далі створюємо git-репозиторій, файл .gitignore з gitignorer та запускаємо npm:

$ git init
$ gitignore init node
$ npm init -y

Зробимо перший коміт.

$ git add -A
$ git commit -am 'перший коміт'

Далі встановимо Parcel Bundler — пакувальник, який не потребує налаштувань.Він з коробки підтримує усі формати файлів.

$ npm install parcel-bundler

Цікавий факт: більше не треба вказувати --save.

Поки триває встановлення, створимо декілька файлів:

src/index.html

<html>
  <head>
    <title>hello world</title>
  </head>
  <body>
    Hello world
    <script src="./main.js"></script>
  </body>
</html>

src/main.js

const vApp = {
  tagName: 'div',
  attrs: {
    id: 'app',
  },
};

console.log(vApp);

package.json

{
  ...
  "scripts": {
    "dev": "parcel src/index.html", // додайте це
  }
  ...
}

Тепер можемо запустити сервер:

$ npm run dev

> vdommm@0.0.1 dev /private/tmp/vdommm

> parcel src/index.html



Server running at http://localhost:1234

Built in 959ms.

Перейдіть на сторінку http://localhost:1234. Якщо все налаштовано правильно, ви побачите hello world на сторінці та створений Virtual DOM у консолі.

createElement (tagName, options)

Результат на Codesandbox.

Більшість реалізацій Virtual DOM мають функцію createElement (її часто називають h). ЇЇ призначення — просто повертати «віртуальний елемент». Реалізуємо її:

src/vdom/createElement.js

export default (tagName, opts) => {
  return {
    tagName,
    attrs: opts.attrs,
    children: opts.children,
  };
};

За допомогою деструктуризації об'єкта, ми можемо записати так:

src/vdom/createElement.js

export default (tagName, { attrs, children }) => {
  return {
    tagName,
    attrs,
    children,
  };
};

Ми також повинні передбачити створення елементів без будь-яких параметрів, тому вкажемо деякі значення за замовчуванням.

src/vdom/createElement.js

export default (tagName, { attrs = {}, children = [] } = {}) => {
  return {
    tagName,
    attrs,
    children,
  };
};

Згадайте Virtual DOM, який ми створили раніше:

src/main.js

const vApp = {
  tagName: 'div',
  attrs: {
    id: 'app',
  },
};

console.log(vApp);

Тепер можемо записати так:

src/main.js

import createElement from './vdom/createElement';

const vApp = createElement('div', {
  attrs: {
    id: 'app',
  },
});

console.log(vApp);

Повернемось до браузера. Ми повинні побачити той самий Virtual DOM, який визначали раніше. Додамо зображення під div.

src/main.js

import createElement from './vdom/createElement';

const vApp = createElement('div', {
  attrs: {
    id: 'app',
  },
  children: [
    createElement('img', {
      attrs: {
        src: 'https://web.archive.org/web/20230605064106/https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
      },
    }),
  ],
});

console.log(vApp);

Повертаємось до браузера та спостерігаємо оновлений Virtual DOM.

Примітка

Літерали об'єктів (наприклад { a: 3 }) автоматично наслідуються від Object. Це означає, що об'єкт, створений літералом, матиме методи, визначені в Object.prototype (наприклад, hasOwnProperty, toString тощо).

Ми можемо зробити наш Virtual DOM трохи «чистішим», додавши Object.create(null). Так ми створимо дійсно простий об'єкт, який не успадковується від Object, тому він null.

src/vdom/createElement.js

export default (tagName, { attrs, children }) => {
  const vElem = Object.create(null);

  Object.assign(vElem, {
    tagName,
    attrs,
    children,
  });

  return vElem;
};

render (vNode)

Результат на Codesandbox.

Рендеринг віртуальних елементів

Тепер у нас є функція, яка генерує Virtual DOM. Нам потрібен спосіб перетворити його у реальний DOM. Визначимо render (vNode), що прийматиме віртуальний вузол та повертатиме відповідний DOM.

src/vdom/render.js

const render = (vNode) => {
  // створюється елемент
  // наприклад <div></div>
  const $el = document.createElement(vNode.tagName);

  // додаються усі атрибути, як зазначено у vNode.attrs
  // наприклад <div id="app"></div>
  for (const [k, v] of Object.entries(vNode.attrs)) {
    $el.setAttribute(k, v);
  }

  // додаються усі дочірні елементи, як зазначено у vNode.children
  // наприклад, <div id="app"><img></div>
  for (const child of vNode.children) {
    $el.appendChild(render(child));
  }

  return $el;
};

export default render;

Цей фрагмент має бути досить зрозумілим.

ElementNode та TextNode

У звичайному DOM існує 8 типів вузлів. У статті оглянемо лише два з них:

  1. ElementNode (наприклад, <div> та <img>);
  2. TextNode — звичайний текст.

Наша структура віртуальних елементів ({tagName, attrs, children}) відображає у DOM лише ElementNode. Тож необхідно якимось чином представити TextNode. Для цього використаємо звичайний String.

Для демонстрації додамо трохи тексту до наявного Virtual DOM.

src/main.js

import createElement from './vdom/createElement';

const vApp = createElement('div', {
  attrs: {
    id: 'app',
  },
  children: [
    'Hello world', // представляє TextNode
    createElement('img', {
      attrs: {
        src: 'https://web.archive.org/web/20230605064106/https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
      },
    }),  // представляє ElementNode
  ],
}); // представляє ElementNode

console.log(vApp);

Організовуємо підтримку TextNode

Як було зазначено, ми розглядаємо два типи вузлів. Наявний render (vNode) відображає лише ElementNode. Розширимо render, щоб TextNode також рендерився.

Спочатку змінимо назву функції на renderElem, щоб краще розуміти її суть. Додамо також деструктуризацію об'єкта, щоб код виглядав краще.

src/vdom/render.js

const renderElem = ({ tagName, attrs, children}) => {
  // створюється елемент
  // наприклад <div></div>
  const $el = document.createElement(tagName);

  // додаються усі атрибути, як зазначено у vNode.attrs
  // наприклад <div id="app"></div>
  for (const [k, v] of Object.entries(attrs)) {
    $el.setAttribute(k, v);
  }

  // додаються усі дочірні елементи, як зазначено у vNode.children
  // наприклад, <div id="app"><img></div>
  for (const child of children) {
    $el.appendChild(render(child));
  }

  return $el;
};

export default render;

Перевизначимо render (vNode). Якщо vNode приймає тип String, тоді ми можемо викликати document.createTextNode(string) для відображення TextNode, в іншому випадку викликаємо renderElem(vNode).

src/vdom/render.js

const renderElem = ({ tagName, attrs, children}) => {
  // створюється елемент
  // наприклад <div></div>
  const $el = document.createElement(tagName);

  // додаються усі атрибути, як зазначено у vNode.attrs
  // наприклад <div id="app"></div>
  for (const [k, v] of Object.entries(attrs)) {
    $el.setAttribute(k, v);
  }

  // додаються усі дочірні елементи, як зазначено у vNode.children
  // наприклад, <div id="app"><img></div>
  for (const child of children) {
    $el.appendChild(render(child));
  }

  return $el;
};

const render = (vNode) => {
  if (typeof vNode === 'string') {
    return document.createTextNode(vNode);
  }

  // припускаємо, що все інше є віртуальним елементом
  return renderElem(vNode);
};

export default render;

Тепер наша функція render (vNode) може відображати два типи віртуальних вузлів:

  1. Віртуальні Елементи, створені функцією createElement ;
  2. Віртуальний Текст, що являє собою string.

Рендеринг vApp

Тепер спробуємо відобразити наш vApp, а також переглянути результат console.log.

import createElement from './vdom/createElement';
import render from './vdom/render';

const vApp = createElement('div', {
  attrs: {
    id: 'app',
  },
  children: [
    'Hello world',
    createElement('img', {
      attrs: {
        src: 'https://web.archive.org/web/20230605064106/https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
      },
    }),
  ],
});

const $app = render(vApp);
console.log($app);

Перейдіть до браузера, і ви побачите DOM для:

<div id="app">
  Hello world
  <img src="https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif">
</div>

mount ($node, $target)

Результат на Codesandbox.

Зараз ми можемо створювати Virtual DOM та відображати його як звичайний DOM. Далі нам необхідно розмістити реальний DOM на сторінці.

Створимо основу нашого застосунку. Замість Hello world на src/index.html розмістимо <div id="app"></div>.

src/index.html

<html>
  <head>
    <title>hello world</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./main.js"></script>
  </body>
</html>

Тепер ми хочемо замінити порожній div на рендеринг нашого $app. Це дуже просто зробити, якщо не зважати на Internet Explorer та Safari. Викликаємо ChildNode.replaceWith().

Визначимо mount ($node, $target). Функція просто замінить $target на $node та поверне $node.

src/vdom/mount.js

export default ($node, $target) => {
  $target.replaceWith($node);
  return $node;
};

Тепер у main.js просто розмістіть наш $app у порожньому div.

src/main.js

import createElement from './vdom/createElement';
import render from './vdom/render';
import mount from './vdom/mount';

const vApp = createElement('div', {
  attrs: {
    id: 'app',
  },
  children: [
    'Hello world',
    createElement('img', {
      attrs: {
        src: 'https://web.archive.org/web/20230605064106/https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
      },
    }),
  ],
});

const $app = render(vApp);
mount($app, document.getElementById('app'));

Наш застосунок з'явиться на сторінці, і ми побачимо зображення.

Зробимо застосунок цікавішим

Результат на Codesandbox.

Огорнемо наш vApp у функцію createVApp. createVApp прийматиме count, а vApp використає його.

src/main.js

import createElement from './vdom/createElement';
import render from './vdom/render';
import mount from './vdom/mount';

const createVApp = count => createElement('div', {
  attrs: {
    id: 'app',
    dataCount: count, // використовуємо count тут
  },
  children: [
    'The current count is: ',
    String(count), // і тут
    createElement('img', {
      attrs: {
        src: 'https://web.archive.org/web/20230605064106/https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
      },
    }),
  ],
});

let count = 0;
const vApp = createVApp(count);
const $app = render(vApp);
mount($app, document.getElementById('app'));

setInterval буде збільшувати значення нашого лічильника count кожну секунду, а потім створювати, рендерити та розміщувати наш застосунок на сторінці знову й знову.

Зверніть увагу, тут використано $rootEl для відстеження кореневого елемента. Так mount знатиме де розміщувати кожен новий застосунок.

Якщо повернутися до браузера, ми побачимо наш лічильник у дії.

Рендеринг створеного застосунку передбачуваний і зрозумілий. Якщо ви знаєте як усе відбувається при використанні jQuery, ви цінуватимете чистоту підходу, який ми розглянули.

Однак, є деякі проблеми з повторним відображенням усього застосунку кожної секунди:

  1. Реальний DOM набагато важчий за віртуальний. Тому рендеринг всього застосунку коштує дорого;
  2. Елементи втратять свій стан. Наприклад, <input> втратить свій фокус при повторному розміщенні застосунку на сторінці. Переконайтеся у цьому тут.

Ми розв'яжемо ці проблеми далі.

diff (oldVTree, newVTree)

Результат на Codesandbox.

Уявіть, що у нас є функція diff (oldVTree, newVTree), що знаходить відмінності між двома віртуальними деревами. Вона повертає функцію patch, що приймає DOM як значення oldVTree і виконує необхідні операції, щоб він виглядав як newVTree.

Якщо б у нас була така функція diff, ми могли б переписати код так:

import createElement from './vdom/createElement';
import render from './vdom/render';
import mount from './vdom/mount';
import diff from './vdom/diff';

const createVApp = count => createElement('div', {
  attrs: {
    id: 'app',
    dataCount: count, // використовуємо count тут
  },
  children: [
    'The current count is: ',
    String(count), // і тут
    createElement('img', {
      attrs: {
        src: 'https://web.archive.org/web/20230605064106/https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
      },
    }),
  ],
});

let count = 0;
let vApp = createVApp(count);
const $app = render(vApp);
let $rootEl = mount($app, document.getElementById('app'));

setInterval(() => {
  count++;
  const vNewApp = createVApp(count)
  const patch = diff(vApp, vNewApp);

  // ми можемо замінити увесь $rootEl,
  // тому ми хочемо, щоб patch повертав нове значення $rootEl
  $rootEl = patch($rootEl);

  vApp = vNewApp;
}, 1000);

Спробуємо реалізувати diff (oldVTree, newVTree). Почнемо з простих випадків:

  • newVTree є undefined: ми можемо просто видалити $node, передавши його у patch.

  • Обидва параметри — TextNode (рядки):

    • Якщо ці рядки однакові, нічого не робіть;
    • В іншому випадку, замініть $node на render(newVTree).
  • Одне з дерев — TextNode, інше — ElementNode: у цьому випадку вони, очевидно, не однакові, тому замініть $node на render(newVTree).

  • oldVTree.tagName !== newVTree.tagName:

    • Припускаємо, що в цьому випадку, старі й нові дерева абсолютно різні;
    • Замість пошуку відмінностей між деревами, ми лише замінюємо $node на render(newVTree);
    • Це припущення також є в React

З двох елементів різних типів отримуємо різні дерева.

src/vdom/diff.js

import render from './render';

const diff = (oldVTree, newVTree) => {
  // припустимо, що oldVTree не undefined
  if (newVTree === undefined) {
    return $node => {
      $node.remove();
      // patch повинен повернути новий кореневий вузол
      // оскільки в даному випадку їх немає
      // ми просто повертаємо undefined.
      return undefined;
    }
  }

  if (typeof oldVTree === 'string' ||
    typeof newVTree === 'string') {
    if (oldVTree !== newVTree) {
      // можуть бути 2 випадки:
      // 1.обидва дерева типу string і приймають різні значення
      // 2. одне з дерев — text node 
      // а інше — elem node
      // у будь-якому випадку ми лише викличемо render(newVTree)!
      return $node => {
         const $newNode = render(newVTree);
         $node.replaceWith($newNode);
         return $newNode;
       };
    } else {
      // означає, що обидва дерева типу string
      // і приймають однакові значення
      return $node => $node;
    }
  }

  if (oldVTree.tagName !== newVTree.tagName) {
    // припустимо, що вони повністю різні
    // та не намагатимемось знайти відмінності
    // викличемо render для newVTree та встановимо його.
    return $node => {
      const $newNode = render(newVTree);
      $node.replaceWith($newNode);
      return $newNode;
    };
  }

  // (A)
};

export default diff;

Якщо код досягає відмітки (A), це означає, що:

  1. oldVTree та newVTree — віртуальні елементи;
  2. У них однаковий tagName;
  3. У них можуть бути різні attrs та children.

Ми реалізуємо дві функції diffAttrs (oldAttrs, newAttrs) і diffChildren (oldVChildren, newVChildren), щоб окремо працювати з attrs та children. Кожна з них повертатиме patch. Як вже відомо, тут ми не будемо заміняти $node. Ми можемо безпечно повернути $node після застосування обох patch.

src/vdom/diff.js

import render from './render';

const diffAttrs = (oldAttrs, newAttrs) => {
  return $node => {
    return $node;
  };
};

const diffChildren = (oldVChildren, newVChildren) => {
  return $node => {
    return $node;
  };
};

const diff = (oldVTree, newVTree) => {
  // припустимо, що oldVTree не undefined!
  if (newVTree === undefined) {
    return $node => {
      $node.remove();
    // patch повинен повернути новий кореневий вузол
      // оскільки в даному випадку їх немає
      // ми просто повертаємо undefined.
      return undefined;
    }
  }

  if (typeof oldVTree === 'string' ||
    typeof newVTree === 'string') {
    if (oldVTree !== newVTree) {
     // можуть бути 2 випадки:
      // 1.обидва дерева типу string і приймають різні значення
      // 2.одне з дерев — text node 
      // а інше — elem node
      // у будь-якому випадку ми лише викличемо render(newVTree)!
      return $node => {
         const $newNode = render(newVTree);
         $node.replaceWith($newNode);
         return $newNode;
       };
    } else {
      // це означає, що обидва дерева типу string
      // і приймають однакове значення
      return $node => $node;
    }
  }

  if (oldVTree.tagName !== newVTree.tagName) {
   // припустимо, що вони повністю різні
    // та не намагатимемось знайти відмінності
    // викличемо render для newVTree та встановимо його.
    return $node => {
      const $newNode = render(newVTree);
      $node.replaceWith($newNode);
      return $newNode;
    };
  }

  const patchAttrs = diffAttrs(oldVTree.attrs, newVTree.attrs);
  const patchChildren = diffChildren(oldVTree.children, newVTree.children);

  return $node => {
    patchAttrs($node);
    patchChildren($node);
    return $node;
  };
};

export default diff;

diffAttrs (oldAttrs, newAttrs)

Спочатку зосередимось на diffAttrs. Тут усе досить просто. Атрибути задаємо як newAttrs. Після цього пробігаємось усіма ключами у oldAttrs, щоб впевнитись, що вони наявні у newAttrs. В іншому випадку, видаляємо їх.

const diffAttrs = (oldAttrs, newAttrs) => {
  const patches = [];

  // задаємо newAttrs
  for (const [k, v] of Object.entries(newAttrs)) {
    patches.push($node => {
      $node.setAttribute(k, v);
      return $node;
    });
  }

  // видалення attrs
  for (const k in oldAttrs) {
    if (!(k in newAttrs)) {
      patches.push($node => {
        $node.removeAttribute(k);
        return $node;
      });
    }
  }

  return $node => {
    for (const patch of patches) {
      patch($node);
    }
    return $node;
  };
};

Помітьте як ми зробили обгортку patch та циклічно пробіглися масивом patches.

diffChildren (oldVChildren, newVChildren)

Тут буде трохи складніше. Можемо розглянути три випадки:

  • oldVChildren.length === newVChildren.length
    • Викличемо diff(oldVChildren[i], newVChildren[i]), де i пробігає у циклі значення від 0 до oldVChildren.length;
  • oldVChildren.length > newVChildren.length
    • Тут можна застосувати той самий підхід: diff(oldVChildren[i], newVChildren[i]), де i пробігає у циклі значення від 0 до oldVChildren.length;
    • newVChildren[j] буде undefined для j >= newVChildren.length;
    • Але усе чудово, тому що наш diff обробляє такий випадок: diff(vNode, undefined).
  • oldVChildren.length < newVChildren.length
    • Тут можемо також зробити diff(oldVChildren[i], newVChildren[i]), де i пробігає у циклі значення від 0 до oldVChildren.length;
    • У циклі будуть створюватись patches для вже наявних children;
    • Нам лише необхідно створити остаточний додатковий children, наприклад newVChildren.slice(oldVChildren.length).

На завершення, ми циклічно обходимо oldVChildren та викликаємо diff(oldVChildren[i], newVChildren[i]). Рендеримо додатковий children (якщо є) та приєднуємо його до $node.

const diffChildren = (oldVChildren, newVChildren) => {
  const childPatches = [];
  oldVChildren.forEach((oldVChild, i) => {
    childPatches.push(diff(oldVChild, newVChildren[i]));
  });

  const additionalPatches = [];
  for (const additionalVChild of newVChildren.slice(oldVChildren.length)) {
    additionalPatches.push($node => {
      $node.appendChild(render(newVChildren));
      return $node;
    });
  }

  return $parent => {
    // оскільки childPatches очікують $child, а не $parent,
    // ми не можемо просто циклічно обійти масив і викликати patch($parent)
    $parent.childNodes.forEach(($child, i) => {
      childPatches[i]($child);
    });

    for (const patch of additionalPatches) {
      patch($parent);
    }
    return $parent;
  };
};

Код виглядатиме більш елегантно, якщо ми використаємо функцію zip.

import render from './render';

const zip = (xs, ys) => {
  const zipped = [];
  for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
    zipped.push([xs[i], ys[i]]);
  }
  return zipped;
};

const diffChildren = (oldVChildren, newVChildren) => {
  const childPatches = [];
  oldVChildren.forEach((oldVChild, i) => {
    childPatches.push(diff(oldVChild, newVChildren[i]));
  });

  const additionalPatches = [];
  for (const additionalVChild of newVChildren.slice(oldVChildren.length)) {
    additionalPatches.push($node => {
      $node.appendChild(render(additionalVChild));
      return $node;
    });
  }

  return $parent => {
    // оскільки childPatches очікують $child, а не $parent,
    // ми не можемо просто циклічно обійти масив і викликати patch($parent)
    for (const [patch, $child] of zip(childPatches, $parent.childNodes)) {
      patch($child);
    }

    for (const patch of additionalPatches) {
      patch($parent);
    }
    return $parent;
  };
};

Завершений diff.js

src/vdom/diff.js

import render from './render';

const zip = (xs, ys) => {
  const zipped = [];
  for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
    zipped.push([xs[i], ys[i]]);
  }
  return zipped;
};

const diffAttrs = (oldAttrs, newAttrs) => {
  const patches = [];

  // встановлення newAttrs
  for (const [k, v] of Object.entries(newAttrs)) {
    patches.push($node => {
      $node.setAttribute(k, v);
      return $node;
    });
  }

  // видалення attrs
  for (const k in oldAttrs) {
    if (!(k in newAttrs)) {
      patches.push($node => {
        $node.removeAttribute(k);
        return $node;
      });
    }
  }

  return $node => {
    for (const patch of patches) {
      patch($node);
    }
    return $node;
  };
};

const diffChildren = (oldVChildren, newVChildren) => {
  const childPatches = [];
  oldVChildren.forEach((oldVChild, i) => {
    childPatches.push(diff(oldVChild, newVChildren[i]));
  });

  const additionalPatches = [];
  for (const additionalVChild of newVChildren.slice(oldVChildren.length)) {
    additionalPatches.push($node => {
      $node.appendChild(render(additionalVChild));
      return $node;
    });
  }

  return $parent => {
    // оскільки childPatches очікують $child, а не $parent,
    // ми не можемо просто циклічно обійти масив і викликати patch($parent)
    for (const [patch, $child] of zip(childPatches, $parent.childNodes)) {
      patch($child);
    }

    for (const patch of additionalPatches) {
      patch($parent);
    }
    return $parent;
  };
};

const diff = (oldVTree, newVTree) => {
// припустимо, що oldVTree не undefined!
  if (newVTree === undefined) {
    return $node => {
      $node.remove();
			// patch повинен повернути новий кореневий вузол
      // оскільки в даному випадку їх немає
      // ми просто повертаємо undefined.
      return undefined;
    }
  }

  if (typeof oldVTree === 'string' ||
    typeof newVTree === 'string') {
    if (oldVTree !== newVTree) {
    // можуть бути 2 випадки:
      // 1.обидва дерева типу string і приймають різні значення
      // 2. одне з дерев — text node 
      // а інше — elem node
      // у будь-якому випадку ми лише викличемо render(newVTree)!
      return $node => {
         const $newNode = render(newVTree);
         $node.replaceWith($newNode);
         return $newNode;
       };
    } else {
       // означає, що обидва дерева типу string
      // і приймають однакові значення
      return $node => $node;
    }
  }

  if (oldVTree.tagName !== newVTree.tagName) {
    // припустимо, що вони повністю різні
    // та не намагатимемось знайти відмінності
    // просто викличемо render для newVTree та встановимо його.
    return $node => {
      const $newNode = render(newVTree);
      $node.replaceWith($newNode);
      return $newNode;
    };
  }

  const patchAttrs = diffAttrs(oldVTree.attrs, newVTree.attrs);
  const patchChildren = diffChildren(oldVTree.children, newVTree.children);

  return $node => {
    patchAttrs($node);
    patchChildren($node);
    return $node;
  };
};

export default diff;

Зробимо наш застосунок більш складним

Результат на Codesandbox.

Наш застосунок не використовує усі можливості створеного Virtual DOM. Аби продемонструвати усю міць нашого віртуального DOM, ускладнимо застосунок:

src/main.js

import createElement from './vdom/createElement';
import render from './vdom/render';
import mount from './vdom/mount';
import diff from './vdom/diff';

const createVApp = count => createElement('div', {
  attrs: {
    id: 'app',
    dataCount: count, // використовуємо count тут
  },
  children: [
    'The current count is: ',
    String(count), // і тут
    ...Array.from({ length: count }, () => createElement('img', {
      attrs: {
        src: 'https://web.archive.org/web/20230605064106/https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
      },
    })),
  ],
});

let vApp = createVApp(0);
const $app = render(vApp);
let $rootEl = mount($app, document.getElementById('app'));

setInterval(() => {
  const n = Math.floor(Math.random() * 10);
  const vNewApp = createVApp(n);
  const patch = diff(vApp, vNewApp);

  // ми можемо замінити увесь $rootEl,
  // тому ми хочемо, щоб patch повертав нове значення $rootEl
  $rootEl = patch($rootEl);

  vApp = vNewApp;
}, 1000);

У застосунку буде генеруватись випадкове число n від 0 до 9. Саме стільки зображень буде розміщено на сторінці. Якщо ви відкриєте dev tools, ви побачите як «розумно» вставляються і видаляються <img>, залежно від значення n.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація