Переклад статьіdeathmood: How to write your own Virtual DOM
Є дві речі, які вам необхідно знати для написання вашого власного віртуального DOM. Вам навіть не потрібно занурюватися в вихідний код React або в вихідний код будь-який інший імплементації віртуального DOM. Вони такі великі і комплексні, але в дійсності основна частина віртуального DOM може бути написана менше ніж за 50 рядків коду. 50. Строк. Коду.
Тут закладено дві концепції:
- Віртуальний DOM - будь подання цього DOM
- Коли ми щось змінюємо в нашому віртуальному DOM дереві, ми створюємо нове віртуальне дерево. Алгоритм порівнює ці два дерева (старе і нове), знаходить різницю і вносить тільки необхідні мінімальні зміни в справжній DOM, щоб він відповідав віртуальному.
Це все! Давайте заглибимося в кожну з цих концепцій.
Подання нашого DOM дерева
Добре, в першу чергу нам потрібно якось зберігати наше DOM дерево в пам'яті. І ми можемо зробити це за допомогою простих JS об'єктів. Припустимо, у нас є це дерево:
Виглядає просто, чи не так? Як ми можемо це уявити за допомогою простих JS об'єктів:
Тут ви можете помітити дві речі:
- Ми представляємо DOM елементи в об'єктом вигляді
- Ми описуємо текстові ноди простими JS рядками
Але писати великі дерева таким способом досить складно. Так давайте напишемо допоміжну функцію, щоб нам стало простіше розуміти структуру:
Тепер ми можемо описувати наше DOM дерево в такому вигляді:
Виглядає набагато чистіше, чи не так? Але ми навіть можемо піти далі. Ви ж чули про JSX, чи не так? Так, я хотів би застосувати його ідеї тут. Так, як же він працює?
Якщо ви читали офіційну документацію Babel до JSX тут. ви знаєте, що Babel транспілірует цей код:
в щось на зразок цього:
Добре, насправді вона повідомляє Babel: «Ей, скомпілюйте цей jsx, але замість React.createElement. підстав h ». Тут ви можете підставити що завгодно замість h. І це буде скомпілювати.
Отже, ми будемо писати наш DOM таким чином:
І це буде скомпілювати Babel в такий код:
Виконання функції h поверне простий JS об'єкт - наше уявлення віртуального DOM.
Спробуйте самі на JSFiddle (не забудьте вказати Babel в якості мови).
Застосування нашого уявлення DOM
Добре, тепер ми маємо уявлення нашого DOM дерева в JS об'єкті, з нашої власної структурою. Це круто, але нам потрібно якось створити справжній DOM з нього. Звичайно, ми не можемо просто застосувати наше уявлення до DOM.
Перш за все давайте встановимо деякі припущення і визначимося з термінологією:
- Я буду писати все змінні, що зберігають справжні DOM Ноди (елементи, текстові ноди), починаючи з $. таким чином $ parent буде справжнім DOM елементом
- Віртуальне DOM уявлення буде зберігатися у змінній з ім'ям node
- Як і в React, у вас може бути тільки одна коренева нода, всі інші Ноди будуть всередині
Тепер, коли ми з усім цим розібралися, давайте напишемо функцію createElement (...), яка візьме віртуальну DOM ноду і поверне справжню DOM ноду. Забудьте поки про props і children. ми займемося ними пізніше:
Таким чином, ми можемо передавати в неї як віртуальні текстові ноди, так і Ноди віртуальних елементів, і це буде працювати.
Тепер давайте подумаємо про дітей: кожен з них також є текстової нодою або елементом. Тому вони також можуть бути створені за допомогою нашої функції createElement (...). Ви відчуваєте це? Пахне рекурсією :)) Отже, ми можемо викликати createElement (...) для кожного з дочірніх елементів, а потім appendChild () їх в наш елемент наступним чином:
Вау, виглядає відмінно. Давайте поки залишимо осторонь props Ноди. Ми поговоримо про них пізніше. Вони не потрібні нам для базового розуміння концепцій віртуального DOM і тільки додадуть складності.
Давайте підемо далі і спробую це в JSFiddle.
Обробка змін
Добре, тепер ми можемо перетворити наш віртуальний DOM в справжній DOM, пора подумати про порівняння наших віртуальних дерев. Нам потрібно написати алгоритм, який буде порівнювати два віртуальних дерева - старе і нове - і робити тільки необхідні зміни в сьогоденні DOM.
Як порівняти дерева? Нам необхідно обробити такі ситуації:
- Якщо в певному місці немає старої Ноди: нода була додана і нам необхідно використовувати appendChild (...)
- Ні нової Ноди в певному місці: ця нода була видалена і нам потрібно використовувати removeChild (...)
- Інша нода в цьому місці: нода змінилася і нам потрібно використовувати replaceChild (...)
- Ноди ті ж: нам потрібно йти глибше і порівнювати дочірні Ноди
Добре, давайте напишемо функцію updateElement (...), що приймає три параметри: $ parent, newNode і oldNode. $ Parent - батьківський елемент реального DOM нашій віртуальній Ноди. Зараз ми бачимо, як обробити всі ситуації, описані вище.
Якщо немає старої Ноди
Якщо немає нової Ноди
Тут у нас з'являється проблема: якщо немає Ноди в певному місці в новому віртуальному дереві, ми повинні видалити її з реального DOM. Але як це зробити? Ми знаємо батьківський елемент (він передається в функцію) і тому ми повинні викликати $ parent.removeChild (...) і передавати посилання на реальний елемент DOM. Але у нас його немає. Якби ми знали становище нашої Ноди в батьку, ми могли б отримати його посилання за допомогою $ parent.childNodes [index], де index - позиція нашої Ноди в батьківському елементі.
Добре, давайте припустимо, що цей index буде передаватися нашої функції (і це дійсно станеться, як ви побачите пізніше), тоді наш код буде таким:
Нода змінилася
Перш за все нам необхідно написати функцію, що порівнює дві Ноди і що повідомляє нам, якщо вузол дійсно змінився. Ми повинні враховувати, що це можуть бути як елементи, так і текстові ноди:
І тепер, маючи index поточної Ноди батька, ми можемо легко замінити її на новостворену ноду:
Порівняння дочірніх елементів
І останнє, але не менш важливе: ми повинні пройти кожен дочірній елемент на обох нодах, порівнювати їх і викликати updateElement (...) для кожного з них. Так, знову рекурсія.
Але є кілька речей, які потребують розгляду перед написанням коду:
- Ми повинні порівнювати дітей, якщо нода є елементом (текстові ноди не можуть мати дітей)
- Зараз ми передаємо посилання на поточну ноду як на батька
- Ми повинні порівняти всіх дітей один за одним, навіть якщо в якийсь момент ми отримаємо undefined (це нормально, наша функція вміє обробляти це)
- І, нарешті, index - це просто покажчик на дочірню ноду в масиві children
Збираємо всі разом
Так це воно. Ми добралися. Я помістив весь код в JSFiddle і реалізація дійсно становить 50 LOC, як я вам і обіцяв. Можете піти і пограти з ним.
Відкрийте інструменти розробника і подивіться як відбуваються зміни при натисканні кнопки Reload.
висновок
Вітаю! Ми зробили це. Ми написали свою реалізацію віртуального DOM. І він працює. Я сподіваюся, що прочитавши цю статтю, ви зрозуміли основні концепції і те, як віртуальний DOM повинен працювати і як React працює під капотом.
- Установка атрибутів елементів і їх порівняння і оновлення
- Додавання обробників подій для наших елементів
- Можливість нашого віртуального DOM працювати з компонентами, як React
- Отримання посилань на реальні DOM Ноди
- Використання віртуального DOM з бібліотеками, які безпосередньо мутують справжній DOM: jQuery і її плагіни
- І багато іншого…