Diff-алгоритм React

      React — це JavaScript бібліотека для створення користувацьких інтерфейсів від Facebook. Вона була розроблена «з нуля», з упором на продуктивність. У цій статті я розповім вам про diff-алгоритмі і механізм рендеринга, який використовує React, що дозволить вам оптимізувати ваші програми.
 
 

Diff Алгоритм

Перед тим як ми заглибимося в деталі реалізації, досить важливо, щоб ви мали уявлення про те, як працює React.
 
 
var MyComponent = React.createClass({
  render: function() {
    if (this.props.first) {
      return <div className="first"><span>A Span</span></div>;
    } else {
      return <div className="second"><p>A Paragraph</p></div>;
    }
  }
});

У будь-який момент часу ви можете описати, як виглядатиме ваш UI. Важливо розуміти, що результат рендеринга не є фактичним DOM деревом. Це всього лише легковагі JS об'єкти, які ми називаємо «віртуальний DOM».
 
React буде використовувати образ вашого віртуального DOMа, щоб знайти мінімальну кількість кроків, які дозволять перейти від попереднього стану відображення до наступного. Наприклад, якщо ми вставляємо
<MyComponent first={true} />
, потім замінюємо його
<MyComponent first={false} />
, а потім видаляємо його, то інструкції для DOM будуть виглядати наступним чином:
 
 
Вихідний стан до першого
 
     
Створити вузол:
<div className="first"><span>A Span</span></div>
 
 Перше до другого
 
     
Замінити атрибут:
className="first"
на
className="second"

 Замінюємо вузол:
<span>A Span</span>
на
<p>A Paragraph</p>

 
 Друге до кінцевого
 
     
Видалити вузол:
<div className="second"><p>A Paragraph</p></div>

 
 
Рівень за рівнем
Знаходження мінімальної кількості модифікацій між двома довільними деревами — завдання O (n ^ 3). Як ви могли здогадатися, це неособо підходить для наших завдань, тому React використовує простий і досить ефективний еврестіческій метод для знаходження апроксимації, яка дозволяє добитися складності алгоритму, наближеною до O (n).
 
React просто порівнює дерева по внутрішніх вузлів. Це радикально змінює складність алгоритму і не є великою втратою, тому що у веб-додатках нам дуже рідко доводиться займатися «вертикальним» (по різних рівнях) переміщенням внутрішнього вузла. Зазвичай ми переміщаємо вузли «горизонтально» (на одному рівні).
 
 image
 
 
Список
Давайте припустимо, що у нас є компонент, який за першу ітерацію отрісоввивает 5 компонентів у вигляді списку, а наступна ітерація додає один компонент в його середину. Погодьтеся, буде вельми складно знайти різницю між двома результуючими деревами.
 
За замовчуванням, React асоціює перший компонент попереднього списку з першим компонентом нового списку і т.д. Ви можете встановити елементу ключ, щоб надати React можливість використовувати маппинг. На практиці, зазвичай не складає труднощів знайти унікальні ключі при «горизонтальному» пошуку.
 
 image
 
 
Компоненти
Додаток, що використовує React, зазвичай складається з великої кількості користувальницьких компонентів, які зрештою перетворюються на дерево, яке складається в основному з
div
ів. Ця додаткова інформація буде включена в розрахунок при роботі diff алгоритму, т.к. React буде порівнювати тільки компоненти одного класу.
 
Наприклад, якщо
<Header>
буде замінений на
<ExampleBlock>
, React просто видалить
<Header>
і створить
<ExampleBlock>
. При такому підході, нам не потрібно витрачати дорогоцінний час, порівнюючи два компоненти, між якими немає нічого спільного.
 
 image
 
 

Делегування подій

Додавання обробників подій до DOM елементів — це завжди надзвичайно повільно і затратно по пам'яті
 
Замість цього, React реалізує популярний підхід, який називається «делегація подій». Більш того, React йде ще далі і створює свою подієву модель, сумісну з реалізацією в W3C. Це означає, що баги обробки подій в IE8 залишаються в минулому, і всі імена подій Консистентне між браузерами.
 
Дозвольте мені пояснити як це реалізовано. Один обробник події прикріплений до кореня документа. Коли виникає подія, браузер надає нам
target
DOM елемент. У порядку розповсюдження події всередині ієрархії DOM, React НЕ перебирає дерево «віртуального DOM».
 
Замість цього ми використовуємо той факт, що будь-який компонент React має свій унікальний id, який описує ієрархічне положення об'єкта в DOM. Ми можемо використовувати найпростіші маніпуляції з рядками, щоб отримати доступ до всіх id батьків. Шляхом збереження обробників подій в хеш-таблиці , ми виявили, що добиваємося більшої продуктивності, ніж прикріплюючи їх до віртуального DOM. Нижче наведено приклад того, що відбувається, коли виникає подія:
 
 
// dispatchEvent('click', 'a.b.c', event)
clickCaptureListeners['a'](event);
clickCaptureListeners['a.b'](event);
clickCaptureListeners['a.b.c'](event);
clickBubbleListeners['a.b.c'](event);
clickBubbleListeners['a.b'](event);
clickBubbleListeners['a'](event);

Браузер створює новий екземпляр об'єкта події для кожного обробника, що дозволяє отримати доступ до посилання на об'єкт події або навіть змінювати його. Як би там не було, це означає велику кількість виділеної пам'яті. React на початку своєї роботи створює пул таких об'єктів, і коли відбувається звернення до одного з них, він береться саме з цього пулу. Це суттєво зменшує кількість сміття.
 
 

Рендеринг

 
Угруповання
Кожен раз, коли ви викликаєте
setState
у компоненту, React відзначає його, як змінений. Наприкінці виконання ланцюжка подій, React знаходить все змінені компоненти і перерісоввивает їх.
 
Подібне угрупування означає, що за весь час виконання ланцюжка подій, DOM зміниться всього один раз. Такий підхід є ключем до створення продуктивних додатків, але на жаль, цього все ще надзвичайно важко домогтися, використовуючи нативний JavaScript. У додатках, створених за допомогою React, ви отримуєте це «за замовчуванням».
 
 image
 
 
Перемальовування піддерев
Коли відбувається виклик
setState
, компонент перебудовує віртуальний DOM для своїх дітей. Якщо ви викликаєте
setState
з кореневого елемента, то відбудеться повний ререндер всього React програми: метод
render
буде викликаний у всіх компонентів, навіть якщо вони не були змінені. Це звучить трохи лякає і неефективно, але на практиці це чудово працює, адже ми не чіпаємо справжній DOM.
 
Насамперед, ми говоримо про відображення для користувача інтерфейсу. Через те, що простір екрану обмежена, ви зазвичай відображаєте
від сотень до тисяч елементів одночасно. Тому, JavaScript досить швидко отримав можливість створювати повністю керований інтерфейс.
 
Інший важливий аспект — це те, коли ви пишете React код, ви зазвичай не викликаєте
setState
у кореневого елемента програми, коли-небудь змінюється. Ви викликаєте його у компоненту, у якого виникає подія зміни стану, або на пару вузлів вище. Таким чином, ви майже завжди уникаєте виклику змін у кореневого вузла. Це означає, що зміни локалізовані в тих компонентах, де вони відбуваються.
 
 image
 
 
Вибіркова перерисовка піддерев
Іноді у вас є можливість запобігти перерисовку деяких піддерев — якщо ви реалізуєте наступний метод у своєму компоненті:
 
 
boolean shouldComponentUpdate(object nextProps, object nextState)

Грунтуючись на різниці попереднього і наступного стану компонента, ви можете сказати React, що компонент не потребує перемальовуванні. Використовуючи дану властивість, ви можете добитися величезних поліпшень в продуктивності.
 
Перед тим як використовувати його, вам необхідно порівняти два JavaScript об'єкта. Виникає багато питань, наприклад чи повинно порівняння бути поверхневим або глибоким? І, якщо воно глибоке, чи повинні ми використовувати неизменяемую структуру даних або варто використовувати глибокі копії?
 
І ви б так само хотіли знати, що функція, яка буде викликатися весь час, буде виконуватися швидше, ніж за час, який би треба було для повторної отрисовки компонента, навіть якщо воно і не було б потрібно.
 
 
 
 

Висновок

Технології, які роблять React швидким, не нові. Ми всі чудово знаємо, що зміна DOM є дорогою операцією, що варто групувати операції запису і читання з нього, що делегування подій швидше…
 
Люди продовжують говорити про це, тому що на практиці їх досить важко використовувати в звичайному JavaScript коді. Саме це і виділяє React із всіх інших — все оптимізації роботи з DOM зроблені «за замовчуванням». Такий підхід не дає вистрілити собі в ногу і зробити додаток повільним.
 
Модель зміни вартості продуктивності React дуже проста: кожен виклик
setState
перерісоввивает все поддерево. Якщо ви хочете вичавити максимальну продуктивність, викликайте його якомога рідше і використовуйте
shouldComponentUpdate
, щоб уникнути зайвих перерісовок великих піддерев.

  
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.