Как стать автором
Обновить

Комментарии 59

Беда почти всех фреймворков: легко сделать медленное приложение, сложно сделать быстрое. А что если существует такой фреймворк, на котором сделать быстрое приложение проще, чем медленное?

Без фреймворков эта беда никуда не исчезает.

Конечно, он только усугубится.

glimmerjs — не нужно использовать ShouldComponentUpdate, т.к. свойства трекаются автоматически, нужно просто указать что оно изменяемое.

Но он по прежнему будет рендерить всё подряд, а не то, что попадает в видимую область?

Браво! По огромному спасибо автору и переводчику этой статьи. Легко читать, но сколько много полезной информации узнал.
Только я так и не понял, что мне делать с компонентами, которые привязаны через connect к redux? Что нужно проверять и нужно ли что либо проверять?
Благодарю! Рад оказаться полезным.
И спасибо за вопрос. Действительно, автор статьи не стал раскрывать тему глубоко, но сделал одно замечание, что Redux компоненты (функциональные, без state) уже чистые. Поправьте меня, если я ошибаюсь, но как я понял, имеется ввиду, что функция высшего порядка connect передаёт из store приложения данные в props компонента, и за счёт этого и достигается чистота компонента. Но, если Redux используется для компонентов (например, компонентов отдельных страниц), которые содержат другие компоненты, то к дочерним компонентам нужно при экспорте применять HOC функции для проверки на изменения их props — shouldUpdate или pure из recompose, чтобы они не перерендеривались каждый раз при изменении родительского компонента.

так вроде написано же в статье что для connect-нутых компонент надо использовать reselect, он мемоизирует данные поступаемые из стора через connect, (т.е. возвращает тот же самыйх обьект, сравнимый через ===) — и это предотвращает от перерисовки компонента

connect уже содержит в себе shouldComponentUpdate, так что ничего перепроверять не нужно.

reselect используется для других вещей, хоть про мемоизацию вы и правильно написали.

connect запускает ф-ию mapStateToProps на каждое изменение стора, и не трудно догадаться, что селекторы, описанные в ней, запускаются так же на каждое изменение стора. Если селекторы у вас дорогие (например, фильтрации массивов, сборки больших объектов и т.п.), то нужен способ избежать ненужных операций. Для этого и придумали reselect с его одноуровневым кэшем. Базовый селектор запускается так же каждый раз при изменении стора, только вот на вход он в большинстве случаев принимает другие селекторы поменьше. И трюк тут в том, что, если при выполнении этих селекторов поменьше возвращаемое от них значение не изменилось, то компонующий селектор выполняться не будет, а вернет старое значение.

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


const onClickMe = (name) => (event) => doSomething(name);
//...
<SomeComponent something={['foo', 'bar']} onClick={onClickMe('baz')} />
НЛО прилетело и опубликовало эту надпись здесь

Да, спасибо.

Напишите пожалуйста как правильно нужно вызывать this.update в ваших примерах
Лично я пишу примерно так (для контролируемых полей):

class SomeClass extends Component {

  state = {
      input: ''
  }
  ... 

  onChangeHandler = (event) => {
      this.setState({
          [event.target.name]: event.target.value
      });
  };

  render() {
      return (
          <div className="SomeClass">
              <input name="input" onChange={this.onChangeHandler} value={this.state.input} />
          </div>
      );
   }
}


Возможно есть способ лучше.

Именно так и рекомендуется в офф документации… ну или через переопределение обработчика в конструкторе.


спасибо за перевод.

Ваши примеры известно как лечить, а что с этим делать?


const onClickMe = (name) => (event) => doSomething(name)
console.log(onClickMe('baz') === onClickMe('baz'))

Я пока не придумал ничего лучше:


const onClickMeBaz = (event) => doSomething('baz')

<SomeComponent onClick={onClickMeBaz} />

Можно как-нибудь так, если с lodash:


const onClickMe = _.memoize(name => event => doSomething(name));

<SomeComponent onClick={onClickMe('baz')} />

Не боитесь утечек памяти?

А что мешает чистить кэш memoize, например, при размонтировании родительского компонента?

Тот же memoize можно завернуть в декоратор метода, пишущий эту очистку в прототип текущего класса. Тогда и руками очищать не нужно.

Приведите пример, пожалуйста.

Пишу ночью и на глазок, так что, пожалуйста, поправьте меня, если где-то не прав.
const storage = Symbol();

const Memoize = () => (target, propertyKey, descriptor) => {
    const cwm = target.prototype.componentWillMount;

    target.prototype.componentWillMount = function () {
        if (!this[storage]) {
            this[storage] = {};
        }

        if (!this[storage][propertyKey]) {
            this[storage][propertyKey] = {};
        }

        const fnStorage = this[storage][propertyKey];

        descriptor.value = function () {
            const args = Array.prototype.slice.call(arguments);
            const key = serialize(args);
            if (typeof fnStorage[key] === 'undefined') {
                fnStorage[key] = descriptor.value.apply(this, args);
            }
            return fnStorage[key];
        }.bind(this);

        cwm && cwm.call(this);
    };
};

function serialize(args) {
	const argsAreValid = args.every(arg => {
		return typeof arg === 'number' || typeof arg === 'string';
	});
	if (!argsAreValid) {
		throw Error('Arguments to memoized function can only be strings or numbers');
	}
	return JSON.stringify(args);
}


Суть в том, чтобы заинжектиться в метод в прототипе класса и создать там хранилище на его инстансе, поэтому почти все через function. Вроде бы даже получилось без componentWillUnmount, так как хранилище — обычное поле на инстансе, и очистится вместе с самим инстансом. Ну а даже если нет, можно руками delete'нуть в componentWillUnmount, прописанным в прототип.
Так же можно поиграться с полями в дексрипторе и добавить обработку initializer и get, все-равно все в замыкании болтается.

Поправка: descriptor.value надо в переменную сохранять, иначе вечная рекурсия будет.


Ну и раз предполагается что в языке есть декораторы — то и вместо slice.call(arguments) надо ...args использовать.

рекурсия
Да, конечно, упустил.

надо ...args использовать.
По поводу args — сделано умышленно, так как во время написания прообраза нужен был es2015, а там babel переводит эту конструкцию в цикл.
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
    args[_key2] = arguments[_key2];
}

Мелочь конечно, но ради перфоманса и, раз уж это все-равно закрытый сервисный код, выбран был call. Apply же используется из-за того, что нужен контекст this.

Вы так пишите, как будто использованный вами Array.prototype.slice.call(arguments) не содержит внутри такой же цикл!


Кстати, вы в курсе что передача объекта arguments наружу (даже в slice.call) может выключить оптимизацию? Babel не просто так цикл использует...

Кстати, вы в курсе
Хм, теперь в курсе :)

Edit: но ведь наружу-то ничего не утекает, arguments передаются на 1 уровень глубже и все?
Ну ок, в serialize еще. Но там, только stringify, тоже ничего не протекает.

"Наружу" означает "за пределы единицы оптимизации". Оптимизатор же не знает что за значение лежит в Array.prototype.slice и что делает его метод call.

Логично, надо переписать, как руки дойдут.

Хочется решение для функциональных компонентов.

Обернуть в HoC, наверно.

Не совсем понимаю, как это. Непосредственно внутри SFC функцию не замемоизировать — каждый раз новая, даже с memoize. Значит нужно выносить — либо на уровень модуля и там мемоизировать. Либо наверх в стейтфул-компонент (класс).

Плюс, SFC не имеет фазы unmount, так что утечки гарантированы.
const pure = (fn: Function) => {
  class Wrapper extends React.PureComponent {
    componentWillMount() {
      // this.onClick 
    }
    render () {
      return fn(this.props, this.context)
    }
  }
  Wrapper.displayName = `pure(${fn.name})`
  return Wrapper
}

const myComponent = pure(({ onClick }) => {
  return (<div onClick={onClick}>Hello!</div>)
})

На примере HoC для PureComponent. Как прицепить onClick к инстансу HoC. Только надо как-то мемоизировать.

Поясните, каким образом возникают утечки памяти?

Утечки будут, если вы не контролируете количество первых аргументов в каррируемую мемоизирующую функцию, например
class {
  @Memoize
  onClick = id => event => {
  }
}

Если у вас потенциально неограниченное количество айдишников, то на каждый из них будет бесконтрольно выделяться по функции. Как вариант, прописать функцию очистки в дексриптор (через тот же символ) и в самой задекорированном методе вызывать ее «на себе».
Вот тут есть пример этого случая, а не описанного выше.

Edit: но тут есть проблемы с react-hot-loader, так как он по факту заменяет все функции на обертки, и достучаться до clearer нет возможности. Хорошо хоть только в дев-режиме.
Ссылку на декоратор скинул, а пример-то зажал:
class Foo extends React.Component {
  render() {
    const {list} = this.props;
    return (
      <div>
        {list.map(item => (
          <button onClick={this.onClick(item.id)}>{item.name}</button>
        ))}
      </div>
    );
  }
  
  @Memoize
  onClick = id => event => {
    //memory leak - need to cleanup
    this.onClick[MEMOIZE_CLEAR_FUNCTION](id);
  }
}
НЛО прилетело и опубликовало эту надпись здесь

Ещё нашёл такое:


import memoize from 'lru-memoize';

let multiply = (a, b, c) => a * b * c;

multiply = memoize(10)(multiply); // with limit

export default multiply;
Ну, кстати, чем не решение

Может, тут мне ответят...


При использовании связки react-mobx можно написать вот так:


const SubComponent = observer((props) => props.fn(...props.args));
const sub = fn => React.createElement(SubComponent, { fn : fn, args : [] });
const subFn = fn => (...args) => React.createElement(SubComponent, { fn : fn, args : args });

        <thead>
            <tr>
                {Children.map(children, subFn((field, index) =>
                    <DatagridHeaderCell
                        key={index}
                        field={field}
                        currentSort={this.props.currentSort}
                        updateSort={this.updateSort}
                    />
                ))}
            </tr>
        </thead>

Это избавит родительский компонент от перерисовки при изменении параметров, влияющих на поддерево.


Какие недостатки у такого решения?

Ну так все эти обертки, введенные лишь для того, чтобы пробросить в observable аргументы в виде объекта, чтобы mobx начал трекать их изменения, выглядят как лютый костыль.

Э… вы сейчас про какие именно обертки?

SubComponent и subFn. Вместо того, чтобы просто передать field и index в SubComponent, его нужно обернуть в компонент, который принимает объект, чтобы вызвать на нем observable, потому как в противном случае, mobx с observable не будет трекать изменения field и index. Это кажется самым большим костылем.

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


Не вижу причин считать это большим костылем чем создание отдельного DatagridBody с похожей целью.

Пожалуйста, прочитайте внимательнее тот пункт документации, на который дали ссылку.

Ну и что вы меня доки читать отправляете? Мне-то понятно, что имеется в виду под:

MobX can do a lot, but it cannot make primitive values observable (although it can wrap them in an object see boxed observables). So not the values that are observable, but the properties of an object. This means that observer actually reacts to the fact that you dereference a value. So in our above example, the Timer component would not react if it was initialized as follows:

React.render(<Timer timerData={timerData.secondsPassed} />, document.body)


It is the property secondsPassed that will change in the future, so we need to access it in the component. Or in other words: values need to be passed by reference and not by value.


Вы вынуждены спускать в компоненты observable-объекты, вместо обычных значений, потому что в противном случае mobx превращается в тыкву.

А, я понял причину недопонимания. Там документация кривая...


Если передать дочернему компоненту значение — он и правда не сможет за ним наблюдать (что логично!).


Но факт обращения к этому значению будет запомнен для родителя, что в свою очередь приведет к уже его рендеру.

Ну вот.
Если передать дочернему компоненту значение — он и правда не сможет за ним наблюдать (что логично!).
Т.е. появляются какие-то крайние случаи, где что-то идет не так, как ожидается, об этом нужно помнить, тогда как mobx продается под маркой «сел и поехал, и не думай ни о чем».

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

Да, но это не выходит за рамки обычных для React принципов! props заполняются родителем, и он же должен обеспечивать их актуальность.


Если мы передали дочернему компоненту число 5, а потом оно стало не 5 а 6 — то мы должны уведомить его об этом изменении.

А если это число лежит в объекте, должны ли мы его (дочерний компонент) об этом уведомлять? Или положимся на магию, что попытка доступа в нужное поле выполнит подписку на изменения и запустит перерендер дочернего компонента? Где та грань, когда нужно что-то спускать простым значением, а что-то объектом? Все спускать объектом? Ну так вот он и костыль.

Все просто же. Есть декоратор observer — магия включена (для текущего компонента, но не для его родителей или детей). Нет observer — магия выключена.

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

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

Не нужно!


Если дочерний компонент принимает в качестве свойства мутабельный объект некоторого типа — то реагирование на изменения в нем является его ответственностью. Точка. И не важно, будет ли он использовать магию mobx-react чтобы достичь желаемого эффекта.


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

Знаете, я тут подумал еще, и, да, вы правы, никаких нарушений нет. Видимо, полностью перестроенные на схему state => ui мозги не дают покоя. И, действительно, никогда не знаешь внутри компонента, принимающего observable, что именно произойдет при установке конкретного свойства, и кого это поаффектит во всем приложении. Да, гибкость выше, но и последствия могут быть серьезней, и гораздо труднее отлавливать источник изменений.

В любом случае, это уже выходит за рамки изначального обсуждения. Так как mobx все-равно накладывает ограничения на структуру данных, передаваемых в компонент, чтобы все правильно затрекалось. Как и ограничения redux, требующего полной неизменяемости.
Что со мной не так?

Не вижу чекбоксов Network, JS Profile, Paint. И нет разворота User Timing.

Вы проверяете на localhost, с ?react_perf в конце URL и версией React не ниже 15.4?

Спасибо! С параметром ?react_perf появился разворот User Timing. Но по-прежнему не вижу чекбоксов: Network, JS Profile, Paint.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации