React 16: огляд нової архітектури fiber

Я Євген Шемет — професійно займаюсь розробкою більше 10-ти років, виступаю на конференціях, організовую IT-мітапи, викладаю у благодійній фундації BrainBasket та з цього навчального року у ВНТУ. Я доповідав на Vinnytsia.js про React 16. І нещодавно мене запросили написати на цю тему статтю для DOU. Статтю пишу вперше, тому буду радий вашим зауваженням та порадам у особистих повідомленнях або коментарях.

Fiber

Fiber — це нова архітектура, що покладена в основу React 16, реліз якого відбувся нещодавно. Велика частина коду була переписана з нуля. Основною метою було створення можливості для пріоритизації оновлень контенту. Також переписана система обробки помилок та усунуті деякі старі незручності, наприклад, необхідність обгортати декілька елементів в один кореневий елемент. Існуюче API, на щастя, майже не зачепили.

Демо

Розпочати знайомство найкраще з проблеми, яку має вирішити нова версія. Її добре видно на демо. Демо синтетичне, симулює щось накшталт екрана диспетчерської таксі. На карті міста жовті машини. На машинах чорні числа, що вказують, скільки машині ще їхати до точки призначення. Числа змінюються кожної секунди, і під час цієї зміни, анімація підлагує. Лаги з’являються тому, що виконуються дві паралельні дії: обробка анімації та оновлення DOM. Анімація працює добре, аж до поки не стається масове оновлення DOM.

Щоб проблему було краще видно, введена штучна затримка, не забувайте що демо синтетичне. Але проблема цілком реальна: поки переобраховується DOM, анімація не програється, через те що всі ресурси покладено на роботу з DOM. І в рамках старої архітектури React цю проблему не можна було вирішити аж ніяк. Треба віддати належне розробникам бібліотеки: вони, зіткнувшись з цією проблемою, переписали значну частину коду. Незважаючи на це, міграція не має викликати великі труднощі.

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

Наразі

React для забезпечення високої швидкості роботи використовує технологію Vitrual DOM. В пам’яті підтримується спрощена копія DOM, де за вузлами закріплені конкретні екземпляри (instance) компонентів, що ними керують. Коли змінюється стан екземпляра, відбувається процес оновлення, що складається з таких етапів:

  • Компоненти опитуються щодо змін.
  • DOM в пам’яті перебудовується.
  • Обраховується різниця з реальним деревом DOM та вносяться безпосередні зміни.

Минулі версії React для оновлення використовували алгоритм, що його заднім числом назвали Stack. І в нього з часом виявився значний недолік: він працює простим пошуком в глибину, і його робота неперервна. А оскільки в браузері все виконується в одному потоці, то під час оновлення інші процеси мають зачекати. У випадку високопріоритетних оновлень, наприклад анімації, це може стати відчутним клопотом.

Новий алгоритм оновлення

Архітектура fiber названа на честь алгоритму, що лежить в її основі. Алгоритм полягає у розбитті процесу оновлення на дві фази:

  1. Фаза узгодження (reconciliation) — коли виконуються переобрахунки компонентів і відбувається оновлення DOM у пам’яті.
  2. Фаза внеску (commit) — коли виконується безпосереднє оновлення DOM.

Варто зазначити, що фазу узгодження (reconciliation) можна переривати. fiber за допомогою requestIdleCallback просить у браузера виділити час, коли той не буде завантажений роботою. При зворотньому виклику браузер вказує, скільки, власне, в нього є вільного часу. Це дає змогу fiber-у планувати частину оновлень на цей проміжок. Якщо браузер не підтримує requestIdleCallback, то React робить поліфіл (polyfill).

Алгоритм fiber у свою чергу названий на честь найменшого об’єкта, що лежить в його основі. За кожним еземпляром (компонента чи елемента) закріплений такий об’єкт, що контролює його стан та зв’язок з іншими компонентами.

{
  stateNode
  return
  child
  sibling
  parent
}

Процес оновлення виглядає таким чином

У нас є поточне (current) дерево компонентів та елементів, сформоване за допомогою об’єктів fiber. Стрілочки вниз це child, вгору parent, вправо sibling.

Створюється паралельне робоче (workInProgress) дерево, що частково складається зі старого дерева.

Визначаються компоненти, що мають зміни (позначені зірочками).

Дерево поступово розгортається, і на його основі відбудовується нове дерево. Там де є оновлення — клонуються елементи і вносяться зміни. Там де оновлень немає — використовуються наявні елементи.

В результаті формується внесок (pending commit). Що для застосування очікує вже більшого проміжку часу. Тому що фазу внеску переривати не можна.

Після того як відбувається внесок (commit), поточне (current) дерево не знищується. Для економії часу дерева просто міняються місцями. Це називається подвійна буферизація (double buffering).

Застосування

Для того щоб відчути нові можливості, необхідно застосувати режим відкладених оновлень ReactDOM.unstable_deferredUpdates. (Всі експериментальні можливості спочатку поставляються з префіксом unstable_).

tick() {
  ReactDOM.unstable_deferredUpdates(() => (
    this.setState((prevState) => ({
      tick: prevState.tick + 1
    }))
  ))
}

Оновлення, що відбуваються в рамках deferredUpdates, проходять паралельно.
Зверніть увагу:

  • Необхідно застосовувати setState зі зворотним викликом (callback), setState з об’єктом стає застарілим (deprecated).
  • Якщо новий стан буде залежати від поточного стану, то необхідно використовувати параметр зворотнього виклику prevState замість this.state. Тому що він може бути викликаний декілька разів.

Порівняння

Жовтим позначені — оновлення, фіолетовим — анімації, червоним — лаги.

Stack:

Fiber:

Як бачите, обробка CSS анімацій не зупиняється навіть при високій завантаженості оновленнями DOM.

Також

Разом з новою архітектурою при переписуванні React були виправлені деякі невеликі архітектурні помилки.

Фрагменти (Fragments)

Відтепер, якщо компонент повертає набір елементів, його не обов’язково обгортати в один корінний елемент. Ви можете повертати масив елементів, що дуже зручно в місцях, де неможливо просто обгорнути елементи в <div>. Наприклад, в роботі з таблицями і списками, якщо компонент має повернути декілька рядків або елементів списку. Також тепер можна повертати стрічки.

const TableHeader = () => {
  return [
    <tr><th colspan="2">Автомобіль</th><th colspan="2">Водій</th></tr>,
    <tr><th>Номер</th><th>Марка</th><th>Позивний</th><th>Телефон</th></tr>,
  ]
}

Кордони помилок (Error boundaries)

Запроваджена нова система обробки помилок. Тепер, якщо в компоненті виникає помилка, можна застосувати метод життєвого циклу componentDidCatch.

class Map extends React.Component {
  constructor(props) {
    super(props)

    this.state = { hasError: false }
  }

  componentDidCatch(error, info) {
    this.setState(() => { hasError: true })
  }

  render() {
    if (this.state.hasError) {
      return <h1>На жаль, сталась прикра помилка.</h1>
    }
    return <MapContent />;
  }
}

Портали (Portals)

Іноді виникає необхідність створити елемент не в рамках поточної ієрархії, а приєднати, наприклад, як у випадку з модальними вікнами, до <body>. На допомогу приходять портали.

render() {
  return ReactDOM.createPortal(<Modal />, domElement)
}

Атрибути (Attributes)

React 16 дозволяє вам використовувати власні атрибути.

<div hello="world" />

Будьте обережні. Це означає, що фільтрація атрибутів більше не виконується.

<div myData="[Object object]" />

Тим не менш, атрибути, що мають канонічне ім’я, все одно валідуються. І ви отримаєте попередження, якщо використовуєте неправильне ім’я атрибута.

// Warning: Invalid DOM property `tabindex`. Did you mean `tabIndex`?
<div tabindex="-1" />

Майбутнє

Потенціал нової архітектури реалізовано не повністю. І у розробників є багато планів на майбутнє, що стали реальними завдяки fiber.

Пріоритизація

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

  • Synchronous — синхронний, виконується зараз;
  • Task — задача до наступного тіку (tick);
  • Animation — анімація до наступного кадру (frame);
  • High — високий;
  • Low — низький;
  • Offscreen/Hidden — схований або поза межами екрана.

Превізуалізація (pre-rendering)

Оскільки візуалізація розбита на дві фази, то можна чітко визначити, коли все необхідне завантажене, обраховане і готове до відображення. В майбутньому це відкриває шлях до потокового завантаження превізуалізованих на сервері компонентів. Також це має спростити етап завантаження великих аплікацій.

Абстракція

Мабуть, ви знаєте, що React наразі працює на великій кількості платформ. Наприклад:

  • Браузер: react-dom
  • Мобільні: react-native
  • Термінал: react-blessed
  • Віртуальна реальність: aframe-react
  • Arduino: react-hardware

Команда React активно працює над тим, щоб зробити React незалежним від оточення. З версії v0.14 ReactDOM був виділений в окремий пакет. З версії v0.16 розробники рапортують, що React став (майже :)) повністю незалежний від браузера.

Проблеми

Коли запускається наступне оновлення, а минуле ще не обраховане до кінця, то старе оновлення припиняється і його проміжний результат відкидається. Якщо таке стається регулярно, виникає ситуація, коли дані на сторінці оновлюються нерегулярно або зовсім не оновлюються. Така ситуація називається голодування starvation. Симулювати це можна збільшивши затримку при оновленні в демо.

Міграція

Коли

Вже.

Життєвий цикл компонента

Будьте уважні та обережні, якщо ви використовуєте відкладені оновлення. Деякі методи життєвого циклу під час одного оновлення можуть викликатись двічі або більше разів. Пов’язано це з тим, що оновлення може бути відкладене через більш нагальні оновлення, а потім переобраховане. Це методи фази узгодження (reconciliation):

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

Методи фази внеску (commit), викликаються тільки один раз:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

Помилки

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

Атрибути

React більше не видаляє незнайомі атрибути, і вам бажано (але не обов’зково) це робити самим.

setState

Виклик виду setState({ key: value }) вважається застарілим (deprecated). Використовуйте setState зі зворотним викликом (callback).

Матеріали та ресурси

Посилання

Ресурси

LinkedIn

32 комментария

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Пропозиції роботи цікавлять? :)
Шукаємо крутого React.JS дева: jobs.dou.ua/...​/elopage/vacancies/50939

Дякую за притомний переклад термінології! До купи, мабуть, було варто згадати й про рисування pdf (react-pdf)

Запроваджена нова система обробки помилок. Тепер, якщо в компоненті виникає помилка, можна застосувати метод життєвого циклу componentDidCatch.

Я думаю, тут стоит упомянуть что componentDidCatch ловит ошибки только при рендере вложенных элементах (children). Если ошибка произойдет на уровне самого компонента, componentDidCatch не сработает.

Дякую за зауваження.

Необхідно застосовувати setState зі зворотним викликом (callback), setState з об’єктом стає застарілим (deprecated).

reactjs.org/...​6.0.html#new-deprecations
в react 16 new deprecations про такое ни слова

Дякую за зауваження. Насправді декілька останніх версій йде мова про те щоб його зробити застарілим але мабуть не на часі. Ймовірно це станеться, коли вони випустять deferredUpdates без префіксу. Бо deferredUpdates і setState з об’єктом це небезпечна сполука, через можливість багаторазового виклику методів життєвого циклу.

Он не просто не задепрекейчен. setState(obj, callback) еще и не является рекомендованным:

The second parameter to setState() is an optional callback function that will be executed once setState is completed and the component is re-rendered. Generally we recommend using componentDidUpdate() for such logic instead.
Он не просто не задепрекейчен. setState(obj, callback) еще и не является рекомендованным:

Речь идет не о функции, которая вызывается после, а про первый аргумент в setState(), когда при создании нового state используются значения с текущих state или props:
reactjs.org/...​dates-may-be-asynchronous
И как раз является очень рекомендованным.

Я отвечал на фрагмент статьи

Необхідно застосовувати setState зі зворотним викликом (callback), setState з об’єктом стає застарілим (deprecated).

Тут явно идет речь о том, что якобы setState(updater) явлется депрекейтнутым, а setState(updater, callback) нет.

А апдейтить напрямую через this.state ну для этого нужно вообще доков не читать.

Ні, тут іде мова про виклик setState зі зворотним викликом, тобто setState(callback), а не setState(updater, callback). Пересвідчити в цьому можна переглянувши приклад коду, пов’язаний з цією цитатою.

Наверно имелось ввиду:

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied.
...
React may batch multiple setState() calls into a single update for performance.

Например, вместо:

...
addRow(title, name) {
  const rows = [
      ...this.state.rows,
      {
          id: v4(),
          title,
          name
      }
  ]
  this.setState({rows})
}
...

для React v16 будет:

...
addRow(title, name) {
  this.setState(prevState => ({
      rows: [
          ...prevState.rows,
          {
              id: v4(),
              title,
              name
          }
      ]
  }))
}
...

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

Он ненадежен только в случае, если новое состояние зависит от предыдущего. Если такой зависимости нет, вполне можно использовать объект.

Також він може залежати від props, що могли змінитись.
callback — викликається безпосередньо перез оновленням, а об’єкт може бути застосовано із затримкою.

Класна стяття. Та ще й технічна. Та ще й українською. Дякую.

Здравствуй $digest? Или куда это все катится? Что-то мне стремно, когда думаю о дебаге этих отложенных событий. А еще о том, что $user будет писать далеко не оптимальный код, и множественные вызовы методов фазы согласования в итоге не дадут общего сильного прироста. У меня, например, есть контейнер, реализующий вызовы валидации для внутреннего компонента с определенным интерфейсом. Он цепляется на willReceiveProps того, что у него в чилдах, и творит безобразия, включая вызов валидации на сервере. И что теперь? Как мне отказаться от плюшек и объямнить реакту, что не нужно прерывать receiveProps, что пользователь подождет, никуда не денется?

Ви можете не використовувати режим відкладених оновлень. Але згоден з вами великі можливості — велика відповідальність.

У Васи просто архитектура отклеилась

Стоит также упомянуть переход от патентной лицензии к MIT и уменьшние размера библиотеки в полтора раза.

PS Спасибо за статью

Зміна ліцензії не пов’язана напряму з цим випуском, а про зменшення об’єму я чесно кажучи не знав. Дякую за доповнення.

Оперативно =)
Автору спасибо!

Тех. статья на Доу, ахах, доу прекрати...
Где старый добрый доу с сырами и тракторами.
Автор спасибо за статью.

Спасибо за познавательную статью!
Отдельная благодарность за отсутствие англо-украинского суржика, действительно было приятно читать текст без всяких «файберов», «старвейшинов» и т.д..
Автор, пишите ещё :)

Евгений, спасибо за статью. Прояснил для себя некоторые вещи.

Что то я не очень догнал демо. Я как включаю Fiber , так они и перестают каждую секунду обновляться

Мають змінюватись числа. Спробуйте ще раз. Я зменшив навантаження. Можливо для вашого пристрою затримка завелика і виникає ситуація що описана в кінці статті — starvation. Нажаль технологія ще не стабільна, тому поки що поставляється з префіксом _unstable.

Також можливі негаразди з Firefox. Демо точно має добре працювати на Google Chrome.

Якщо ситуція не виправилась, ви можете спробувати «офіційне» демо .

Видимо да. Потому что даже официальное демо обновляться не хочет так же.

В фоксе аналогично, полный starvation)) в хроме иногда числа таки обновляются, но редко-редко. Интересно было бы посмотреть разную статистику на разном железе и браузерах))

Официальное демо кстати в хроме терпимо работает) а в фоксе числа так же не обновляются) надеюсь, это дофиксят в будущем)

Собственно, по этому поводу и вопрос — а как оно вообще работает в браузерах, где requestIdleCallback либо вообще не поддерживается, либо по умолчанию отключён?

Подписаться на комментарии