Почему я перешел с Redux на Mobx

Vue.js React.js Redux MobX
Почему я перешел с Redux на Mobx

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

Проблемы с Redux

Predictable state container for JavaScript apps

Это позиционирование Redux для себя, но с ним много проблем.
Прежде всего, что сделал REDUX? Посмотрите на исходный код Redux,createStoreЕсть только одна функция, которая возвращает 4 замыкания.dispatchПросто сделай одно, позвониreducerтогда позвониsubscribeизlistener,некоторые изstateНеизмермость или изменение состояния все контролируются пользователем. Redux не знает, изменилось ли состояние, не говоря уже о том, где состояние изменилось. Таким образом, если уровень просмотра должен знать, какая часть должна быть обновлена, она может пройти только грязную проверку.

посмотри сноваreact-reduxЧто вы сделали, повесили коллбэк на store.subscribe и вызывали его каждый раз, когда происходит подпискаconnectпройти вmapStateToPropsа такжеmapDispatchToProps, затем грязное обнаружениеpropsкаждого предмета. Конечно, мы можем использовать характеристики неизменяемых данных, чтобы уменьшить количество реквизитов и, таким образом, уменьшить количество грязных обнаружений, но как могут так хорошо появляться реквизиты из одного и того же поддерева?

Поэтому, если подключено n компонентов, всякий раз, когда отправляется действие, независимо от степени детализации обновления, будет происходить грязное обнаружение с временной сложностью O (n).

// Redux 3.7.2 createStore.js

// ...
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
// ...

Что еще хуже, каждый раз, когда редюсер выполняет Redux, прослушиватель вызывается напрямую. строка, является грязной Накладные расходы на обнаружение в сочетании с накладными расходами уровня представления могут сделать общую производительность очень плохой, даже если пользовательский ввод часто требует обновления только одного «входа». Чем больше масштаб приложения, тем хуже производительность. (Здесь приложение относится к одной странице. Одна страница здесь не означает одну страницу SPA, потому что в случае маршрутизатора все компоненты вырезанной страницы не размонтируются)

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

Mobx

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

В настоящее время Mobx (3.x) и Vue (2.x) используют тот же принцип отзывчивости, заимствуя картинку из документа Vue:

Создайте Watcher для каждого компонента, добавьте хуки в геттер и сеттер данных, при рендеринге компонента (например, вызов метода рендера) будет срабатывать геттер, а затем добавьте в зависимости Watcher, соответствующий этому компоненту данных, связанных с геттером (например, Set). Когда сеттер срабатывает, вы можете знать, что данные изменились, а затем соответствующий Watcher одновременно перерисовывает компонент.

Таким образом, данные, требуемые каждым компонентом, точно известны, поэтому при изменении данных вы можете точно знать, какие компоненты необходимо перерисовать, а процесс перерисовки при изменении данных занимает O(1) временную сложность.

Следует отметить, что в Mobx данные должны быть объявлены наблюдаемыми.

import React from 'react';
import ReactDOM from 'react-dom';
import { observable, action } from 'mobx';
import { Provider, observer, inject } from 'mobx-react';

class CounterModel {
    @observable
    count = 0

    @action
    increase = () => {
        this.count += 1;
    }
}

const counter = new CounterModel();

@inject('counter') @observer
class App extends React.Component {
    render() {
        const { count, increase } = this.props.counter;

        return (
            <div>
                <span>{count}</span>
                <button onClick={increase}>increase</button>
            </div>
        )
    }
}

ReactDOM.render(
    <Provider counter={counter}>
        <App />
    </Provider>
);

представление

в этомстатья, автор использовал чертежную доску 128 * 128, чтобы проиллюстрировать проблему.
Как использует Mobxgetterа такжеsetter(Возможна параллель, основанная наProxyверсия) для сбора данных о зависимостях экземпляров компонентов, поэтому каждый раз, когда обновляется точка,MobxЗная, какие компоненты необходимо обновить, временная сложность принятия решения о том, какой компонент обновить, составляет O (1), в то время какReduxПроверяй каждого по грязномуconnectкомпоненты, чтобы получить, какие компоненты необходимо обновить, есть n компонентовconnectВременная сложность этого процесса составляет O(n), что в конечном итоге отражается в инструменте Perf как время выполнения JavaScript.

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

Mobx State Tree

Мобкс не идеален. Mobx не требует, чтобы данные находились в дереве, поэтому преобразовать данные Mobx или записать каждое изменение данных непросто. На основе Mobx родилось дерево состояний Mobx. Как и Redux, дерево состояний Mobx требует, чтобы данные находились в дереве, что упрощает визуализацию и отслеживание данных, что является благом для разработки. В то же время Mobx State Tree позволяет очень легко получить точные определения типов TypeScript, что нелегко сделать Redux. Он также обеспечивает проверку безопасности типов во время выполнения.

import React from 'react';
import ReactDOM from 'react-dom';
import { types } from 'mobx-state-tree';
import { Provider, observer, inject } from 'mobx-react';

const CountModel = types.model('CountModel', {
    count: types.number
}).actions(self => ({
    increase() {
        self.count += 1;
    }
}));

const store = CountModel.create({
    count: 0
});

@inject(({ store }) => ({ count: store.count, increase: store.increase }))
class App extends React.Component {
    render() {
        const { count, increase } = this.props;

        return (
            <div>
                <span>{count}</span>
                <button onClick={increase}>increase</button>
            </div>
        )
    }
}

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>
);

Mobx State Tree также предоставляетsnapshotСледовательно, хотя данные самого MST являются переменными, он все же может обеспечить эффект неизменяемых данных. Официально предоставленоsnaptshotЕго можно использовать непосредственно в сочетании с инструментами разработки Redux, что удобно для разработки, при этом официал также предусматривает использование данных MST в качестве хранилища Redux, конечно, для встраивания MST в Redux можно использовать и снэпшоты. хранить как данные (аналогично популярной в Redux) роли Immutable.js).

// 连接Redux的开发工具
// ...
connectReduxDevtools(require("remotedev"), store);
// ...

// 直接作为一个Redux store使用
// ...
import { Provider, connect } from 'react-redux';

const store = asReduxStore(store);

@connect(// ...)
function SomeComponent() {
    return <span>Some Component</span>
}

ReactDOM.render(
    <Provider store={store}>
        <App />
    <Provider />,
    document.getElementById('foo')
);

// ...

Более того, в MST изменяемые данные и неизменяемые данные (снапшот) можно преобразовывать друг в друга, и вы можете в любой момент применить к данным снэпшот.

applySnapshot(counter, {
    count: 12345
});

Кроме того, официальный также обеспечивает поддержку асинхронных действий. Из-за ограничений JavaScript трудно отслеживать асинхронные операции.Даже если используется асинхронная функция, ее выполнение невозможно отследить.Хотя данные манипулируются в асинхронной функции, асинхронная функция также помечается как действие, но она будет неправильно оценено, что данные были изменены вне действия. В прошлом асинхронные действия можно было выполнять только путем объединения нескольких действий, тогда как Vue реализуется путем разделения действий и мутаций. Генераторы используются в дереве состояний Mobx, поэтому асинхронные операции могут выполняться внутри функции действия и могут отслеживаться.

// ...

SomeModel.actions(self => ({
    someAsyncAction: process(function* () {
        const a = 1;
        const b = yield foo(a); // foo必须返回一个Promise
        self.bar = b;
    })
}));

// ...

Суммировать

Эксплойт Mobxgetterа такжеsetterДля сбора зависимостей данных компонентов, чтобы точно знать, какие компоненты необходимо перерисовывать при изменении данных.Когда масштаб интерфейса становится больше, часто возникает много мелких обновлений, хотя адаптивный дизайн будет иметь дополнительные накладные расходы, Когда интерфейс большой, эти накладные расходы намного меньше, чем выполнение грязной проверки для каждого компонента, поэтому в этом случае Mobx легко получит лучшую производительность, чем Redux. И когда все данные изменились, реализация на основе грязной проверки будет иметь лучшую производительность, чем реактивные вроде Mobx, но такие случаи редки. В то же время некоторые бенчмарки не являются лучшей практикой, а их результаты не отражают реальной ситуации.

Однако из-заReactОн сам по себе предоставляет механизм для сокращения бесполезного рендеринга за счет использования неизменяемых структур данных (таких как PureComponent, функциональные компоненты). шаблон наблюдателя переменной не так удобен, когда дело доходит до структуры данных. Поэтому рекомендуется использовать Redux и Immutable.js с React, пока не возникнут проблемы с производительностью.

The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.

немного практики

Из-за ограничений JavaScript некоторые объекты не являются собственными объектами, а другие библиотеки проверки типов могут привести к неожиданным результатам. Например, в Mobx массив является не массивом, а массивоподобным объектом, который должен отслеживать присвоение индексов данных. Напротив, в Vue массив является массивом, но необходимо использовать назначение индекса массива.spliceдля продолжения, иначе он не может быть обнаружен.

По принципу Mobx для достижения точных обновлений по требованию необходимо запускать геттер в нужном месте.Проще всего деконструировать данные используемые рендером только в рендере.mobx-reactНачиная с 4.0,injectСтруктуры в принятой функции карты также отслеживаются, поэтому вы можете использовать что-то вродеreact-reduxписьма. Обратите внимание, что до версии 4.0 функция карты внедрения не отслеживалась.

У Reactive есть дополнительные накладные расходы, которые могут повлиять на производительность при рендеринге больших объемов данных (например, длинных списков), поэтому используйте его с умом.observable.ref,observable.shallow(Мобкс),types.frozen(Дерево состояний Mobx).

Эта статья была впервые опубликована вБлог о технологиях Youzan.