Предлагаю читателям «Хабрахабра» перевод статьи «How I learned to stop worrying and love React».

Если вы спросите меня, что я думал о React два месяца назад, я бы сказал…
Где мои шаблоны? Что этот сумасшедший HTML делает в моем JavaScript? JSX выглядит странно! Скорее! Сжечь это!



Это потому, что я его не понял.

Но я уверяю, React — это определенно правильный путь. Пожалуйста, выслушайте меня.

Старый добрый MVC


Корень зла в интерактивном приложении — это управление состоянием. «Традиционный» подход — MVC архитектура или некоторые ее вариации.

MVC предполагает, что ваша модель — это единственный источник истины, все состояние живет там. Представления — это производные модели и должны быть синхронизированы. Когда модель изменяется — изменяется и представление.

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


Render представления при изменении модели

Это выглядит довольно просто. Во-первых, мы должны описать наше представление — как оно преобразовывает состояние модели в DOM. Затем, всякий раз, когда пользователь что-то делает, мы обновляем модель и перерендериваем все. Верно? Не так быстро. К сожалению, тут не все гладко. По 2 причинам:

  1. Вообще-то DOM имеет некоторое состояние, такое как содержимое текстовых полей. Если вы перерендериваете ваш DOM полностью, то это содержимое будет потеряно;
  2. DOM операции (такие как удаление и вставка узлов) действительно медленные. Постоянное перерендеривание всего ведет к ужасной производительности.

Так как же нам держать модель и представления синхронизированными и избежать этих проблем?

Data binding


За последние 3 года самая распространенная фича фреймворков, введенная для решения этих проблем, была data binding.

Data binding — это возможность держать ваши модель и представления синхронизированными автоматически. Обычно в JavaScript это ваши объекты и ваш DOM.

Это достигается через возможность объявить зависимости между кусками данных в вашем приложении. Изменения в состоянии будут распространяться по всему приложению и все зависимости обновятся автоматически.

Давайте посмотрим, как это работает на практике в некоторых известных фреймворках.

Knockout


Knockaut выступает за MVVM (Model-View-ViewModel) подход и помогает реализовать часть View:

 // View (a template)
<p>First name: <input data-bind="value: firstName" /></p>  
<p>Last name: <input data-bind="value: lastName" /></p>  
<h2>Hello, <span data-bind="text: fullName"> </span>!</h2>

// ViewModel (diplay data... and logic?)
var ViewModel = function(first, last) {  
  this.firstName = ko.observable(first);
  this.lastName = ko.observable(last);

  this.fullName = ko.pureComputed(function() {
      // Knockout tracks dependencies automatically. It knows that fullName depends on firstName and lastName, because these get called when evaluating fullName.
      return this.firstName() + " " + this.lastName();
  }, this);
}; 

И, вуаля. Изменение значения любого из input будет провоцировать изменение в span. Вы никогда не писали код для его подключения. Классно, да?

Но подождите, что насчет того, что модель — это единственный источник истины? Откуда ViewModel должна получить свое состояние? Откуда она знает что модель изменилась? Интересные вопросы.

Angular


Angular описывает data binding с точки зрения хранения модели и представления синхронизированными. Из документации:



Но… представление должно общаться с моделью напрямую? Они тесно связаны?

В любом случае, давайте посмотрим на hello world:

// View (a template) 
<div ng-controller="HelloController as hello">  
  <label>Name:</label>
  <input type="text" ng-model="hello.firstName">
  <input type="text" ng-model="hello.lastName">
  <h1>Hello {{hello.fullName()}}!</h1>
</div>

// Controller 
angular.module('helloApp', [])  
.controller('HelloController', function() {
  var hello = this;
  hello.fullName = function() {
    return hello.firstName + hello.lastName;
  };
});

Из этого примера похоже, что контроллер имеет состояние и ведет себя как модель, или, возможно, как ViewModel? Предполагая, что модель находится в другом месте, как она синхронизируется с контроллером?

Моя голова начинает немного болеть.

Проблемы с data binding


Data binding работает замечательно на маленьких примерах. Однако вместе с тем, как ваше приложение растет, вы, вероятно, столкнетесь с некоторыми из следующих проблем:

Декларирование зависимостей может быстро привести к зацикливанию


Наиболее распространенная задача — это управиться с сайд-эффектами от изменения вашего состояния. Эта картинка из introduction of Flux довольно четко объясняет, как ад зависимостей начинает подкрадываться:



В этом случае можете ли предсказать, какие изменения произойдут, если произойдет одно изменение в одной модели? Очень сложно рассуждать о коде, который может быть выполнен в совершенно произвольном порядке, когда любая зависимость изменена.

Шаблон и логика отображения искусственно разделены


Какова роль представления? Представление данных пользователю. Какова роль ViewModel? Представление данных пользователю. Какая разница? Никакой.

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

Довольно просто, {{# each}}, ng-repeat и databind=«foreach» — это все плохая замена для нативного и тривиального цикла for в JavaScript. И они не могут пойти дальше. Нет filter или map.

Data binding — хак вокруг ререндеринга


Святой Грааль простоты не обсуждается. Все всегда хотели перерендеривать все приложение, когда меняется состояние. Таким образом, мы могли бы перестать иметь дело с корнем зла: состояния меняется с течением времени — мы могли бы просто описать, что наше приложение представляет какое-любое конкретное состояние.

Ввод React от Facebook


Оказывается, они сделали это. React реализует виртуальный DOM, который вроде как подает нам Святой Грааль.

В любом случае что такое виртуальный DOM?


Я рад, что вы спросили! Давайте посмотрим на простой пример React.

var Hello = React.createClass({  
    render: function() {
        return <div>Hello {this.props.name}</div>;
    }
});

React.render(<Hello name="World" />, document.getElementById('container')); 

Это все необходимое для React компонента. Вы должны иметь метод render. Сложно, да?

Хорошо, но что за <div>? Это не JavaScript! Точно не он.

Ваш новый друг, JSX


Вообще-то этот код написан на JSX. Супер набор JavaScript, который включает в себя синтаксис скобок для определения компонентов. Код выше, когда скомпилируется в JavaScript, на самом деле станет таким:

var Hello = React.createClass({displayName: "Hello",  
    render: function() {
        return React.createElement("div", null, "Hello ", this.props.name);
    }
});

React.render(React.createElement(Hello, {name: "World"}), document.getElementById('container'));  

Вы заметили вызовы createElement? Эти объекты составляют реализацию виртуального DOM.

Довольно просто: React сначала собирает всю структуру вашего приложения в памяти, используя эти объекты. Затем преобразует эту структуру в актуальные узлы DOM и вставляет их в DOM вашего браузера.

Хорошо, но какой смысл писать наш HTML с этими странными функциями createElement?

Виртуальный DOM — быстрый


Как мы уже обсуждали — операции по изменению DOM до смеха дорогостоящие, поэтому DOM должен меняться наименьшее количество раз, насколько это возможно.

Виртуальный DOM в React, однако, делает это действительно быстро, сравнивая два дерева и находя именно то, что изменилось между ними. Таким образом React способен вычислить минимальное количество изменений, необходимых для обновления DOM.

Практически говоря, React может сравнить два DOM дерева и вычислить минимальный набор операций, которых нужно выполнить. Это означает две вещи:

  1. Если текстовые поля с текстом перерендериваются то React ожидает, что есть контент и не будет касаться его. Нет больше потери состояния;
  2. Сравнение виртуальных DOM совсем не дорого, так что мы можем сравнивать их столько сколько нам нравится. Когда diff готов, чтобы на самом деле изменить DOM он сделает это минимальным количеством операций. Нет больше медленного макета.

Воспоминание о 2 проблемах с перерендериванием целого приложения при смене состояния?

Прошло.

React мапит состояние в DOM


Virtual DOM rendering и diffing — это единственная магическая часть React. Его отличная производительность позволяет нам иметь гораздо более простую архитектуру. Насколько простую?
React компоненты — это идемпотентные функции. Они описывают ваш UI в любой момент времени. Просто как отрендереное на сервере приложение.

Pete Hunt, React: Rethinking best practices

Это все, каким React компонент должен быть на самом деле. Он отображает текущее состояние приложения в DOM. У вас есть вся мощь JavaScript для описания пользовательского интерфейса: циклы, функции, скоупы, композиции, полноценный язык шаблонов.

var CommentList = React.createClass({  
  render: function() {
    var commentNodes = this.props.data.map(function (comment) {
      return (
        <Comment author={comment.author}>
          {comment.text}
        </Comment>
      );
    });
    return (
      <div className="commentList">
        {commentNodes}
      </div>
    );
  }
});

var CommentBox = React.createClass({  
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.props.data} />
      </div>
    );
  }
});

React.render(  
  <CommentBox data={data} />,
  document.getElementById('content')
);

Начните использовать React сегодня


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

Документация у React превосходная. Вы должны попробовать его и следовать туториалу. Я уверен, что вы полюбите React, если дадите ему шанс.

Хорошего кодинга.

Оригинал статьи: How I learned to stop worrying and love React
Автор статьи: Guilherme Rodrigues

Комментарии (35)


  1. Fesor
    02.06.2015 13:54
    +5

    Супер набор… а, это перевод…


  1. xGromMx
    02.06.2015 13:59
    +2

    По поводу быстроты React. Это не та фича на которой стоит акцентировать свое внимание blog.500tech.com/is-reactjs-fast


    1. serf
      02.06.2015 17:47
      +1

      Был доклад какой-то по angular 2, там в демках реакт сильно проиграл angular 2, в котором принципиально переработаны многие вещи.

      Но вообще если реакт не быстр, то зачем он вообще такой громоздкий и не особо удобный нужен.


      1. xGromMx
        02.06.2015 17:49
        -1

        Посмотрите на jsblocks.com


        1. serf
          02.06.2015 17:56
          +1

          Полагаю они тестировали (если тестировали) с ангуляром 1, во втором там обсерверы используются и изменения проверяются иерархично (по дереву). Может быть jsblocks и быстр, но дело в том что для больших и длительных проектов важны еще другие моменты, а для себя можно использовать что угодно.

          ps 2к звезд на гитхабе, серьзено, а я не слышал даже о jsblocks раньше.


      1. Fesor
        02.06.2015 19:27

        Все дело в концепции. Скажем как бы я не любил Angular, он потворствует плохим решениям (во всяком случае первая версия). Кто не видел deep-watch-ей в контроллерах? Кто не видел в принципе ватчей в контроллерах? Или еще веселее — в сервисах. Или кучи условий в шаблонах и т.д. Некоторые в шаблоны логику выборок пихают и т.д.

        С реактом же сделать плохо и медленно намного сложнее. Я частенько слышу фразы типа «чертов ангуляр, а на реакте вышло б нежно», при том что можно нежно сделать и на ангуляре. Просто он из коробки идет с вещами которые предлагают решить проблему в лоб и не заморачиваться.

        Что до «доклад какой-то», были доклады где реакт рвал ангуляр первый на рендринге таблиц за счет виртуального DOM, но добавление track by в ng-repeat меняло ситуацию в другую сторону (виртуальный DOM добавляет свои накладные расходы, но позволяет не париться, а ангуляр надо оптимизировать зная как меняются данные, что не круто но эффективнее).


    1. alemiks
      03.06.2015 09:33
      +2

      >Это не та фича на которой стоит акцентировать свое внимание
      а на какой тогда фиче акцентировать внимание? Вот RiotJS в десятки раз меньше по размеру, умеет всё, что и реакт (в т.ч. поддерживает архитектуру flux), зачем тогда реакт? Потомушта фейсбук или больше звёзд на гитхабе?


      1. Imbolc
        03.06.2015 12:02
        +1

        Если реакт и не быстрый, то райот совсем мееедленный. Затем у него нет (небыло когда смотрел) версии с хоть какими-то отладочными сообщениями, просто не работает и всё. И у него нет серверного пререндеренга.


        1. alemiks
          03.06.2015 15:23
          -1

          а вы какую версию смотрели? Серверный рендеринг, например, есть muut.com/riotjs/guide/#server-side


          1. Imbolc
            03.06.2015 16:00
            +1

            Не, это о другом. Да, в райот можно на сервере отрендерить хтмл. Но этот хтмл никак ни синхронизируется с вашим клиентским js. А реакт рендерит на сервере, а потом на клиенте подхватывает состояние. Гляньте хтмл отрендеренный реактом, там айдишники в каждом теге как раз для этого.


            1. nuit
              03.06.2015 17:19
              -1

              айдишники из-за корявой реализации евентов в реакте, чтобы маунтить поверх реального дома можно и не проставлять никакие айдишники.


  1. k12th
    02.06.2015 14:24
    -1

    У вас есть вся мощь JavaScript для описания пользовательского интерфейса: циклы, функции, скоупы, композиции, полноценный язык шаблонов.

    Ага, а почему бы просто не взять полноценный язык шаблонов? Собственно, в Ractive так и сделали, в результате код отдельно, разметка отдельно, виртуальный DOM есть.


    1. xGromMx
      02.06.2015 15:25
      -1

      Не знаю как Ractive, но сейчас быстые обороты популярности набирает этот фреймфорк(который еще и позволяет использовать его на server side) jsblocks.com


      1. Arilas
        02.06.2015 18:45

        Это чем он оборот набирает? Тем что он представляет собой класический RFP без какой либо очереди изменений? Он вообще ничего не привнес нового, все уже было. Берем задачу, изменить 1000 элементов на странице, во время правок, нам нужно (допустим) делать синхронный запрос на сервер, что мы получаем? Правильно, юзер видит незаконченные данные на странице. Виртуальный DOM лучше обычной идеи «ячеек» тем, что он позволяет применять правки практически мгновенно, после всей логики, которая выполняется.


        1. Arilas
          02.06.2015 18:50

          FRP, опечатался)


          1. xGromMx
            02.06.2015 19:09

            Уж если FRP c VDOM то есть вот такой проект github.com/staltz/cycle и его альтернатива с React github.com/pH200/cycle-react


      1. Imbolc
        03.06.2015 12:04

        А что, jsblocks может полноценный пререндеринг на сервере? В их seeds ничего такого не видно.


      1. lega
        03.06.2015 21:59

        Чем он хорош? Используется observable подход — а это шаг назад (когда надо двигаться в сторону Object.observe), для получения значений используется оператор «with», что очень не рекомендуется и снижает производительность.
        Добавил его в простой бенчмарк.


    1. nwalker
      02.06.2015 16:55

      почему бы просто не взять полноценный язык шаблонов?

      Потому что он не нужен.
      Он все равно скомпилируется все в тот же JS, но при этом у него будет довольно ущербная семантика с костылями и медсестрами.
      Это дополнительные усилия, дополнительные абстракции и дополнительное время на обучение. Зачем?


      1. k12th
        02.06.2015 17:02

        А, то есть JSX не компилируется в JS с костылями и медсестрами.


        1. nwalker
          02.06.2015 17:08
          +1

          JSX это очень чистый синтаксический сахар без своей семантики. Он транслируется в JS один-в-один, предельно прямолинейно, костыли и медсестры предоставляются JS.


  1. Quilin
    02.06.2015 14:29
    +3

    Про Knockout.js:

    Откуда ViewModel должна получить свое состояние? Откуда она знает что модель изменилась? Интересные вопросы.

    Ну, а ответы на них почему не даны? Исходники открытые. Я понимаю, что статья про реакт, но немного провокационно получается упомянуть нокаут и сделать вид, что там что-то непрозрачно и непонятно.

    Computed-property вычисляются при своей инициализации и биндятся на все прочие ko.observable и ko.computed, которые были вызваны при этом вычислении, если что.


  1. aldefalco
    02.06.2015 15:01
    +1

    В «реакте» смущает и напрягает гибридность кода и верстки. Очень часто верстка меняется а значит нужно переписывать каждый раз код. Так?


    1. KIVagant
      02.06.2015 15:22

      Если Facebook запилил это под свои задачи, то у них смена вёрстки явно не слишком частое явление. Видимо, они жертвуют универсальностью в пользу скорости.


    1. k12th
      02.06.2015 15:37
      +2

      Просто почаще говорите себе (и другим), что JSX — это не разметка в джаваскрипте. Многим помогает (вот сейчас в комменты набегут).


    1. Aetet
      02.06.2015 15:48

      Нет, не нужно, если уметь его готовить. Если верстка часто меняется то можно выделять компоненты, что-то типа:

      <Navigation>
        <ClickableNavlink />
      </Navigation>
      
      <Navigation>
        <NotSoClickableNavlink />
      </Navigation>
      


      Здесь мы говорим, что поведение у блока навигации будет одинаковое. Однако его содержимое мы можем передать любое. При этом мы не меняем верстку, мы меняем состав компонентов. Вот что важно. Плюс есть еще несколько подобных паттернов для переиспользования кода. Шаблоны неважны. Важно то, насколько вы сможете декомпонировать задачу и сделать переиспользуемые компоненты. Обычно реактовский компонент занимает около 100 строк. Все что выше говорит о том, что нужно проводить декомпозицию.


    1. Sunify
      03.06.2015 12:54
      +2

      «Верстка» и есть код.
      Аргументация реакта такая — разделение на «логику [отображения]» и «шаблоны» — это разделение технологий, а не отвественности. Ответственность у них одна — отображать интерфейс. Мешать бизнес-логику в компоненты, конечно же не стоит.


  1. KIVagant
    02.06.2015 15:20
    +10

    Воспоминание о 2 проблемах с перерендериванием целого приложения при смене состояния?

    Прошло.


    Говорение автора русский язык вот так? Нормалды.


  1. nuit
    02.06.2015 15:32
    +1

    Таким образом React способен вычислить минимальное количество изменений, необходимых для обновления DOM.

    React Reconciliation
    Generating the minimum number of operations to transform one tree into another is a complex and well-studied problem. The state of the art algorithms have a complexity in the order of O(n3) where n is the number of nodes in the tree.

    Since an optimal algorithm is not tractable, we implement a non-optimal O(n) algorithm...


  1. serf
    02.06.2015 18:00
    +1

    Ай да ладно никому кроме фейсбука не нужен этот React, пиар не поможет.


    1. a_l
      02.06.2015 18:29
      +2

      1. serf
        02.06.2015 18:32

        Не совсем серьезно.

        Видимо фейсбук им доплатил :)


  1. StreetStrider
    03.06.2015 16:55

    Использую Mithril. В основе лежит та же идея Virtual DOM. Нареканий нет, пока всё нравится.


    1. nuit
      03.06.2015 17:20

      исходники мифрила — наглядный пример того как не стоит писать на жаваскрипте. Самая корявая и тормозная реализация виртуального дома из всех существующих.


      1. StreetStrider
        04.06.2015 00:17

        Ага, это правда, там и кривовато и недостаток абстракций. Но в интерфейсном плане отличная либа.