Повний код можна переглянути за посиланням, а результат його роботи тут.
Примітка
- Якщо змінна починається з символу
$
, вона стосується звичайного 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, адже браузери потурбувались про його оптимізацію.
Налаштування
Спочатку переходимо до директорії нашого проекту:
$ 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)
Більшість реалізацій 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)
Рендеринг віртуальних елементів
Тепер у нас є функція, яка генерує 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 типів вузлів. У статті оглянемо лише два з них:
-
ElementNode
(наприклад,<div>
та<img>
); -
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)
може відображати два типи віртуальних вузлів:
- Віртуальні Елементи, створені функцією
createElement
; - Віртуальний Текст, що являє собою
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
)
Зараз ми можемо створювати 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'));
Наш застосунок з'явиться на сторінці, і ми побачимо зображення.
Зробимо застосунок цікавішим
Огорнемо наш 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, ви цінуватимете чистоту підходу, який ми розглянули.
Однак, є деякі проблеми з повторним відображенням усього застосунку кожної секунди:
- Реальний DOM набагато важчий за віртуальний. Тому рендеринг всього застосунку коштує дорого;
- Елементи втратять свій стан. Наприклад,
<input>
втратить свій фокус при повторному розміщенні застосунку на сторінці. Переконайтеся у цьому тут.
Ми розв'яжемо ці проблеми далі.
diff
(oldVTree
, newVTree
)
Уявіть, що у нас є функція 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)
, це означає, що:
-
oldVTree
таnewVTree
— віртуальні елементи; - У них однаковий
tagName
; - У них можуть бути різні
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;
Зробимо наш застосунок більш складним
Наш застосунок не використовує усі можливості створеного 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
.
Ще немає коментарів