Комментарии 59
Беда почти всех фреймворков: легко сделать медленное приложение, сложно сделать быстрое. А что если существует такой фреймворк, на котором сделать быстрое приложение проще, чем медленное?
Всем mobx!
Только я так и не понял, что мне делать с компонентами, которые привязаны через 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')} />
Да, спасибо.
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, например, при размонтировании родительского компонента?
Приведите пример, пожалуйста.
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 уровень глубже и все?
Хочется решение для функциональных компонентов.
Обернуть в HoC, наверно.
Плюс, 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);
}
}
Может, тут мне ответят...
При использовании связки 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
начал трекать их изменения, выглядят как лютый костыль.Э… вы сейчас про какие именно обертки?
Нет, вы ошибаетесь. По умолчанию 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 чтобы достичь желаемого эффекта.
Информация о том, какие типы значений ожидает компонент увидеть в своих свойствах — часть публичного интерфейса.
В любом случае, это уже выходит за рамки изначального обсуждения. Так как mobx все-равно накладывает ограничения на структуру данных, передаваемых в компонент, чтобы все правильно затрекалось. Как и ограничения redux, требующего полной неизменяемости.
Не вижу чекбоксов Network, JS Profile, Paint. И нет разворота User Timing.
React медленный, React быстрый: оптимизация React-приложения на практике