Конкуренция Redux, mobx, концентрата, посмотрите, как молодое поколение играет против предшественников

внешний интерфейс JavaScript React.js
Конкуренция Redux, mobx, концентрата, посмотрите, как молодое поколение играет против предшественников

❤ отметьте меня, если вам нравится концепт ^_^

Преамбула

redux,mobxЭто независимая структура управления состоянием, каждая со своим собственным абстрактным API, независимая от других сред пользовательского интерфейса (реагирование, vue...), в этой статье в основном рассказывается иreactЭффект контраста используется в сочетании, поэтому следующие упомянутыеredux,mobxподразумеваемыйreact-redux,mobx-reactЭто позволяет имreactбиблиотека привязки, которая работает вconcentсам дляreactЛично созданная среда разработки, управление потоком данных — это только одна из функций, а другие сопутствующие функции, улучшающие опыт разработки в ответ, могут использоваться по мере необходимости и будут удалены позже.concentвсе сreactСвязанный частичный выпускconcent-core, его позиционированиеredux,mobxаналогичный.

Итак, игроки, которые появятся в этой статье:

redux & react-redux

  • slogan
    Контейнер состояния JavaScript для предсказуемого управления состоянием

  • концепт дизайна
    Единый источник правды, использующий чистые функции для изменения состояния

mobx & mobx-react

  • лозунг:
    Простое, масштабируемое управление состоянием

  • концепт дизайна
    Все, что может быть получено из состояния приложения, должно быть получено

concent

  • лозунг:
    Предсказуемое, прогрессивное, высокопроизводительное решение для реактивной разработки с нулевым вторжением

  • концепт дизайна
    Я считаю, что метод разработки, объединяющий неизменяемость + сбор зависимостей, — это будущее реакции, улучшение характеристик компонентов реакции, меньше написания и больше работы.

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

Предварительный просмотр результата

В следующих пяти раундах конкурса есть много реальных демонстрационных кодов.Результаты сравнения сообщаются здесь заранее, чтобы читатели могли быстро понять.

конфигурация магазина concent mbox redux
поддержка разделения Yes Yes No
Нет корневого провайдера, и использование не требует явного импорта Yes No No
редукторthis Yes No Yes
Храните данные или методы без ручного сопоставления с компонентами Yes Yes No

пример редукционного счетчика
пример счетчика mobx
Пример счетчика концентрации


изменение состояния concent mbox redux
По принципу неизменности Yes No Yes
кратчайшая ссылка Yes Yes No
источник пользовательского интерфейса можно отследить Yes No No
не это Yes No Yes
Атомарные коммиты разделения и слияния Да (ленивый) Да (на основе транзакции) No

Коллекция зависимостей concent mbox redux
Поддержка сбора зависимостей во время выполнения Yes Yes No
Точный рендеринг Yes Yes No
не это Yes No No
Всего один шаг API Yes No No

пример мобкса
Пример содержания


производные данные concent mbox redux(reselect)
Автоматическое поддержание зависимостей между результатами расчета Yes Yes No
Собирать зависимости при запуске чтения результатов расчета Yes Yes No
Функция расчета без этого Yes No Yes

Пример вычисления редукса
пример вычислений mobx
Пример вычисляемого содержания


todo-mvc бой
redux todo-mvc
mobx todo-mvc
concent todo-mvc

раунд 1 - первый опыт работы со стилем кода

Как красивый мальчик в демонстрационном мире, счетчик выдвигался на сцену бессчетное количество раз. На этот раз мы все еще не исключение. Приходите к счетчику, чтобы испытать рутину разработки трех фреймворков. Используются следующие три версии.create-react-appсоздан, и смногомодульный подходОрганизовывать код и стремиться быть ближе к сценарию кода реальной среды.

редукс (действие, редуктор)

пройти черезmodelsРазделите функции по модулям на разные редюсеры, структура каталогов следующая

|____models             # business models
| |____index.js         # 暴露store
| |____counter          # counter模块相关的action、reducer
| | |____action.js     
| | |____reducer.js     
| |____ ...             # 其他模块
|____CounterCls         # 类组件
|____CounterFn          # 函数组件
|____index.js           # 应用入口文件

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

Построить счетчикaction

// code in models/counter/action
export const INCREMENT = "INCREMENT";

export const DECREMENT = "DECREMENT";

export const increase = number => {
  return { type: INCREMENT, payload: number };
};

export const decrease = number => {
  return {  type: DECREMENT, payload: number };
};

Построить счетчикreducer

// code in models/counter/reducer
import { INCREMENT, DECREMENT } from "./action";

export default (state = { count: 0 }, action) => {
  const { type, payload } = action;
  switch (type) {
    case INCREMENT:
      return { ...state, count: state.count + payload };
    case DECREMENT:
      return { ...state, count: state.count - payload };
    default:
      return state;
  }
};

сливатьсяreducerструктураstore, и внедряется в корневой компонент

mport { createStore, combineReducers } from "redux";
import  countReducer  from "./models/counter/reducer";

const store = createStore(combineReducers({counter:countReducer}));

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

используйте соединение, чтобы соединить пользовательский интерфейс сstore

import React from "react";
import { connect } from "react-redux";
import { increase, decrease } from "./redux/action";

@connect(
  state => ({ count: state.counter.count }),// mapStateToProps
  dispatch => ({// mapDispatchToProps
    increase: () => dispatch(increase(1)),
    decrease: () => dispatch(decrease(1))
  }),
)
class Counter extends React.Component {
  render() {
    const { count, increase, decrease } = this.props;
    return (
      <div>
        <h1>Count : {count}</h1>
        <button onClick={increase}>Increase</button>
        <button onClick={decrease}>decrease</button>
      </div>
    );
  }
}

export default Counter;

В приведенном выше примере записывается компонент класса, и на данный момент горячийhook,redux v7Также выпущен соответствующий APIuseSelector,useDispatch

import * as React from "react";
import { useSelector, useDispatch } from "react-redux";
import * as counterAction from "models/counter/action";

const Counter = () => {
  const count = useSelector(state => state.counter.count);
  const dispatch = useDispatch();
  const increase = () => dispatch(counterAction.increase(1));
  const decrease = () => dispatch(counterAction.decrease(1));

  return (
    <>
      <h1>Fn Count : {count}</h1>
      <button onClick={increase}>Increase</button>
      <button onClick={decrease}>decrease</button>
    </>
  );
};

export default Counter;

Отрисовывая эти два счетчика,Посмотрите пример редукса

function App() {
  return (
      <div className="App">
        <CounterCls/>
        <CounterFn/>
      </div>
  );
}

mobx(store, inject)

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

import someStore from 'models/foo';// 是一个已经实例化的store实例

@observer
class Comp extends React.Component{
    foo = someStore;
    render(){
        this.foo.callFn();//调方法
        const text = this.foo.text;//取数据
    }
}

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

|____models             # business models
| |____index.js         # 暴露store
| |____counter          # counter模块相关的store
| | |____store.js     
| |____ ...             # 其他模块
|____CounterCls         # 类组件
|____CounterFn          # 函数组件
|____index.js           # 应用入口文件

Построить счетчикstore

import { observable, action, computed } from "mobx";

class CounterStore {
  @observable
  count = 0;

  @action.bound
  increment() {
    this.count++;
  }

  @action.bound
  decrement() {
    this.count--;
  }
}

export default new CounterStore();

объединить всеstoreдля根store, и внедряется в корневой компонент

// code in models/index.js
import counter from './counter';
import login from './login';

export default {
  counter,
  login,
}

// code in index.js
import React, { Component } from "react";
import { render } from "react-dom";
import { Provider } from "mobx-react";
import store from "./models";
import CounterCls from "./CounterCls";
import CounterFn from "./CounterFn";

render(    
    <Provider store={store}>
      <App />
    </Provider>, 
    document.getElementById("root")
);

Создайте компонент класса

import React, { Component } from "react";
import { observer, inject } from "mobx-react";

@inject("store")
@observer
class CounterCls extends Component {
  render() {
    const counter = this.props.store.counter;
    return (
      <div>
        <div> class Counter {counter.count}</div>
        <button onClick={counter.increment}>+</button>
        <button onClick={counter.decrement}>-</button>
      </div>
    );
  }
}

export default CounterCls;

Создайте функциональный компонент

import React from "react";
import { useObserver, observer } from "mobx-react";
import store from "./models";

const CounterFn = () => {
  const { counter } = store;
  return useObserver(() => (
      <div>
        <div> class Counter {counter.count}</div>
        <button onClick={counter.increment}>++</button>
        <button onClick={counter.decrement}>--</button>
      </div>
  ));
};

export default CounterFn;

Отобразите эти два счетчика,Посмотрите пример mobx

function App() {
  return (
      <div className="App">
        <CounterCls/>
        <CounterFn/>
      </div>
  );
}

concent(reducer, register)

Concent, как и redux, имеет одно глобальное корневое состояние.RootStore, ключ первого уровня в корневом состоянии используется в качестве пространства имен модуля, и модуль согласия должен быть настроенstate,остатокreducer,computed,watch,initОн необязателен и может быть настроен по мере необходимости.Если все модули магазина пишутся в одном месте, самый простой вариантconcentПримеры следующие

import { run, setState, getState, dispatch } from 'concent';
run({
    counter:{// 配置counter模块
        state: { count: 0 }, // 【必需】定义初始状态, 也可写为函数 ()=>({count:0})
        // reducer: { ...}, // 【可选】修改状态的方法
        // computed: { ...}, // 【可选】计算函数
        // watch: { ...}, // 【可选】观察函数
        // init: { ...}, // 【可选】异步初始化状态函数
    }
});

const count = getState('counter').count;// count is: 0
// count is: 1,如果有组件属于该模块则会被触发重渲染
setState('counter', {count:count + 1});

// 如果在了counter.reducer下定义了changeCount方法
// dispatch('counter/changeCount')

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

import { useConcent, register } from 'concent';

function FnComp(){
    const { state, setState, dispatch } = useConcent('counter');
    // return ui ...
}

@register('counter')
class ClassComp extends React.Component(){
    render(){
        const { state, setState, dispatch } = this.ctx;
        // return ui ...
    }
}

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

|____models             # business models
| |____index.js         # 配置store各个模块
| |____counter          # counter模块相关
| | |____state.js       # 状态
| | |____reducer.js     # 修改状态的函数
| | |____index.js       # 暴露counter模块
| |____ ...             # 其他模块
|____CounterCls         # 类组件
|____CounterFn          # 函数组件
|____index.js           # 应用入口文件
|____runConcent.js      # 启动concent 

Построить счетчикstateа такжеreducer

// code in models/counter/state.js
export default {
  count: 0,
}

// code in models/counter/reducer.js
export function increase(count, moduleState) {
  return { count: moduleState.count + count };
}

export function decrease(count, moduleState) {
  return { count: moduleState.count - count };
}

Два способа настройки магазина

  • Настраивается в функции запуска
import counter from 'models/counter';

run({counter});
  • пройти черезconfigureконфигурация интерфейса,runИнтерфейс отвечает только за запуск концентрата
// code in runConcent.js
import { run } from 'concent';
run();

// code in models/counter/index.js
import state from './state';
import * as reducer from './reducer';
import { configure } from 'concent';

configure('counter', {state, reducer});// 配置counter模块

Создайте функциональный компонент

import * as React from "react";
import { useConcent } from "concent";

const Counter = () => {
  const { state, dispatch } = useConcent("counter");
  const increase = () => dispatch("increase", 1);
  const decrease = () => dispatch("decrease", 1);

  return (
    <>
      <h1>Fn Count : {state.count}</h1>
      <button onClick={increase}>Increase</button>
      <button onClick={decrease}>decrease</button>
    </>
  );
};

export default Counter;

Этот функциональный компонент мы следуем традиционномуhookстиль записи, то есть каждый рендеринг выполняетhookфункция, используяhookБазовый интерфейс, возвращаемый функцией, снова определяет функцию действия, соответствующую текущим бизнес-требованиям.

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

import * as React from "react";
import { useConcent } from "concent";

export const setup = ctx => {
  return {
    // better than ctx.dispatch('increase', 1);
    increase: () => ctx.moduleReducer.increase(1),
    decrease: () => ctx.moduleReducer.decrease(1)
  };
};

const CounterBetter = () => {
  const { state, settings } = useConcent({ module: "counter", setup });
  const { increase, decrease } = settings;
  // return ui...
};

export default CounterBetter;

Создайте компонент класса, повторно используйтеsetupлогика в

import React from "react";
import { register } from "concent";
import { setup } from './CounterFn';

@register({module:'counter', setup})
class Counter extends React.Component {
  render() {
    // this.state 和 this.ctx.state 取值效果是一样的
    const { state, settings } = this.ctx;
     // return ui...
  }
}

export default Counter;

Отобразите эти два счетчика,Посмотрите пример концентрата

function App() {
  return (
    <div className="App">
      <CounterCls />
      <CounterFn />
    </div>
  );
}

Обзор и резюме

Этот раунд показывает различную организацию кода и структуру 3 пар фреймворков при определении многомодульного состояния.

  • reduxпройти черезcombineReducersСотрудничатьProviderОберните корневой компонент, одновременно получая рукописный вводmapStateToPropsа такжеmapActionToPropsЧтобы помочь компоненту получить данные и методы хранилища
  • mobxпутем объединения несколькихsubStoreКstoreобъект и подходитProviderОбертывая корневой компонент, данные и методы хранилища можно получить напрямую.
  • concentпройти черезrunЦентрализованная конфигурация интерфейса илиconfigureКонфигурация разделения интерфейса, данные и методы хранения могут быть получены напрямую
конфигурация магазина concent mbox redux
поддержка разделения Yes Yes No
Нет корневого провайдера и нет необходимости явно импортировать, где он используется Yes No No
редукторthis Yes No Yes
Храните данные или методы без ручного сопоставления с компонентами Yes Yes No

раунд 2 - изменение состояния

Стили модификации трех фреймворков совершенно разные.reduxСуществуют строгие ограничения на методы модификации состояния, поэтому все варианты поведения модификации состояния должны быть отправлены.action, затем нажмите соответствующийreducerСинтезировать новые состояния.

mobxОн способен реагировать и может быть изменен напрямую, но также вызывает проблемы с неотслеживаемыми методами модификации данных, что приводит кmobx-state-treeчтобы соответствовать ограничениям для изменения поведения данных.

concentИзменения полностью соблюденыreactзапись модификацииsetStateстиль, а затем инкапсулировать на этой основеdispatch,invoke,syncРяд API-интерфейсов, и независимо от того, какой API-интерфейс вызывается, может не только отслеживать полную связь модификации данных, но также включать источник запуска модификации данных.

redux(dispatch)

синхронное действие

export const changeFirstName = firstName => {
  return {
    type: CHANGE_FIRST_NAME,
    payload: firstName
  };
};

Асинхронное действие, с помощьюredux-thunkЧто нужно сделать

// code in models/index.js, 配置thunk中间件
import  thunk  from "redux-thunk";
import { createStore, combineReducers, applyMiddleware } from "redux";
const store = createStore(combineReducers({...}), applyMiddleware(thunk));

// code in models/login/action.js
export const CHANGE_FIRST_NAME = "CHANGE_FIRST_NAME";

const delay = (ms = 1000) => new Promise(r => setTimeout(r, ms));
// 工具函数,辅助写异步action
const asyncAction = asyncFn => {
  return dispatch => {
    asyncFn(dispatch).then(ret => {
      if(ret){
        const [type, payload] = ret;
        dispatch({ type, payload });
      }
    }).catch(err=>alert(err));
  };
};

export const asyncChangeFirstName = firstName => {
  return asyncAction(async (dispatch) => {//可用于中间过程多次dispatch
    await delay();
    return [CHANGE_FIRST_NAME, firstName];
  });
};

версия mobx (это.XXX)

Синхронные и асинхронные действия

import { observable, action, computed } from "mobx";

const delay = (ms = 1000) => new Promise(r => setTimeout(r, ms));

class LoginStore {
  @observable firstName = "";

  @observable lastName = "";

  @action.bound
  changeFirstName(firstName) {
    this.firstName = firstName;
  }

  @action.bound
  async asyncChangeFirstName(firstName) {
    await delay();
    this.firstName = firstName;
  }

  @action.bound
  changeLastName(lastName) {
    this.lastName = lastName;
  }
}

export default new LoginStore();

Изменить напрямую

const LoginFn = () => {
  const { login } = store;
  const changeFirstName = e => login.firstName = e.target.value;
  // ...    
}

Изменить действием

const LoginFn = () => {
  const { login } = store;
  const const changeFirstName = e => login.changeFirstName(e.target.value);
  // ...    
}

concent(dispatch,setState,invoke,sync)

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

Синхронизироватьreducerс асинхроннымreducer

// code in models/login/reducer.js
const delay = (ms = 1000) => new Promise(r => setTimeout(r, ms));

export function changeFirstName(firstName) {
  return { firstName };
}

export async function asyncChangeFirstName(firstName) {
  await delay();
  return { firstName };
}

export function changeLastName(lastName) {
  return { lastName };
}

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

// reducerFn(payload:any, moduleState:{}, actionCtx:IActionCtx)
// 当lazy调用此函数时,任何一个函数出错了,中间过程产生的所有状态都不会提交到store
export async changeFirstNameAndLastName([firstName, lastName], m, ac){
    await ac.dispatch(changeFirstName, firstName);
    await ac.dispatch(changeFirstName, lastName);
    // return {someNew:'xxx'};//可选择此reducer也返回新的片断状态
}

// 视图处
function UI(){
    const ctx useConcent('login');
    // 触发两次渲染
    const normalCall = ()=>ctx.mr.changeFirstNameAndLastName(['first', 'last']);
    // 触发一次渲染
    const lazyCall = ()=>ctx.mr.changeFirstNameAndLastName(['first', 'last'], {lazy:true});
    
    return (
        <>
            <button onClick={handleClick}> normalCall </button>
            <button onClick={handleClick}> lazyCall </button>
        </>
    )
}

пример lazyReducer

Неленивый процесс вызова

ленивый процесс вызова

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

  • setState
function FnUI(){
    const {setState} = useConcent('login');
    const changeName = e=> setState({firstName:e.target.name});
    // ... return ui
}

@register('login')
class ClsUI extends React.Component{
    changeName = e=> this.setState({firstName:e.target.name})
    render(){...}
}
  • invoke
function _changeName(firstName){
    return {firstName};
}

function FnUI(){
    const {invoke} = useConcent('login');
    const changeName = e=> invoke(_changeName, e.target.name);
    // ... return ui
}

@register('login')
class ClsUI extends React.Component{
    changeName = e=> this.ctx.invoke(_changeName, e.target.name)
    render(){...}
}
  • sync

Дополнительные сведения о синхронизации см. в файле App2-1-sync.js.

function FnUI(){
    const {sync, state} = useConcent('login');
    return  <input value={state.firstName} onChange={sync('firstName')} />
}

@register('login')
class ClsUI extends React.Component{
    changeName = e=> this.ctx.invoke(_changeName, e.target.name)
    render(){
        return  <input value={this.state.firstName} onChange={this.ctx.sync('firstName')} />
    }
}

Помните, мы упомянули об этом, чтобы сконцентрироваться, прежде чем началось сравнение 2 раунда:Возможность проследить не только полную связь модификации данных, но и источник запуска модификации данных, что это значит, потому что каждый компонент содержания имеетctxиметь уникальный идентификаторccUniqueKeyИдентифицирует текущий экземпляр компонента, который{className}_{randomTag}_{seq}Генерируется автоматически, то есть имя класса (не указан тип компонента?CClass, ?CCFrag, ?CCHook) плюс случайная метка плюс автоматически увеличивающийся серийный номер, если вы хотите намеренно отслеживать и изменять исходный пользовательский интерфейс, вам необходимо поддерживать его вручнуюtag,ccClassKeyДа, а потомconcent-plugin-redux-devtoolдля достижения нашей цели.

function FnUI(){
    const {sync, state, ccUniqueKey} = useConcent({module:'login', tag:'xxx'}, 'FnUI');
    // tag 可加可不加,
    // 不加tag,ccUniqueKey形如: FnUI_xtst4x_1
    // 加了tag,ccUniqueKey形如: FnUI_xxx_1
}

@register({module:'login', tag:'yyy'}, 'ClsUI')
class ClsUI extends React.Component{...}

доступconcent-plugin-redux-devtoolПосле этого вы увидите, что любая модификация действия Action будет содержать полеccUniqueKey.

Обзор и резюме

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

Для метода обновления статуса сравнитеredux, когда все наши процессы действия сжаты до кратчайшего, нет действия-->редьюсер такого звена, не важно выделяется функция хранения или функция побочного эффекта(rematch,dvaи т.п. извлеченные понятия), отдайте эти понятияjsСам синтаксис будет более удобным и понятным, если вам нужны чистые функции, просто напишитеexport function, вам нужно написать функцию побочного эффектаexport async function.

В сравненииmobx, все это базовая функция, которую можно разобрать в любом сочетании, нетthis, полностью ориентированный на ФП, дающийinputожидалoutput, этот метод также более удобен для тестового контейнера.

изменение состояния concent mbox redux
По принципу неизменности Yes No Yes
кратчайшая ссылка Yes Yes No
источник пользовательского интерфейса можно отследить Yes No No
не это Yes No Yes
Атомарные коммиты разделения и слияния Да (ленивый) Да (на основе транзакции) No

раунд 3 - сбор зависимостей

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

начать говорить снова依赖收集Прежде, давайте резюмируемreactОригинальный механизм рендеринга, когда состояние компонента меняется, если его пользовательский компонент не поддерживается вручнуюshouldComponentUpdateПри оценке он всегда будет отображать все сверху вниз, аreduxизcconnectинтерфейс берет на себяshouldComponentUpdateПоведение, когда действие запускает модификацию действия, все подключенные компоненты будутmapStateToPropsПолученное состояние и текущая датаmapStateToPropsРезультирующее состояние поверхностно сравнивается, чтобы решить, следует ли обновлять завернутые дочерние компоненты.

В эпоху крюков это обеспечиваетReact.memoПользователям необходимо блокировать такие «совместно связанные» обновления, но пользователи обязаны пройтиprimitiveтип данных или неизменная ссылка наprops,иначеReact.memoНеглубокое сравнение вернет false.

ноreduxОдна проблема заключается в том, что если определенное состояние больше не используется в представлении в определенный момент, оно должно не рендериться, а рендериться,mobxКонцепция переноса наименьшего подмножества данных, полученных пользовательским интерфейсом во время выполнения, элегантно решает эту проблему, ноconcentИдя дальше, сокрытие поведения коллекции зависимостей является более элегантным, и пользователю не нужно знатьobservableи других связанных терминов и понятий, определенное значение рендеринга, которое вы принимаете, имеет точку зависимости от этого значения, а следующее рендеринг не имеет определенного значения.stateKeyПоведение значения должно быть удалено из зависимости, котораяvueмолодец, что сделалreactИмеет более элегантные, более комплексные механизмы сбора зависят,concentТакже было приложено немало усилий.

редукционная версия (не поддерживается)

Разрешение сбора зависимостей неreduxПервоначальное намерение его рождения, здесь мы можем только молча пригласить его в область кандидатов и участвовать в следующем раунде конкурса.

версия mobx (вычисляется, useObserver)

с помощью декоратора илиdecorateФункция помечает свойство, которое нужно наблюдать или вычислять.

import { observable, action, computed } from "mobx";

const delay = (ms = 1000) => new Promise(r => setTimeout(r, ms));

class LoginStore {
  @observable firstName = "";

  @observable lastName = "";

  @computed
  get fullName(){
    return `${this.firstName}_${this.lastName}`
  }

  @computed
  get nickName(){
    return `${this.firstName}>>nicknick`
  }

  @computed
  get anotherNickName(){
    return `${this.nickName}_another`
  }
}

export default new LoginStore();

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

  • Зависит только от результата расчета, метода записи компонента класса
@inject("store")
@observer
class LoginCls extends Component {
  state = {show:true};
  toggle = ()=> this.setState({show:!this.state.show})
  render() {
    const login = this.props.store.login;
    return (
      <>
        <h1>Cls Small Comp</h1>
        <button onClick={this.toggle}>toggle</button>
        {this.state.show ? <div> fullName:{login.fullName}</div>: ""}
      </>
    )
  }
}
  • Зависит только от результата расчета, функциональная составляющая записывается
import { useObserver } from "mobx-react";

// show为true时,当前组件读取了fullName,
// fullName由firstName和lastName计算而出
// 所以他的依赖是firstName、lastName
// 当show为false时,当前组件无任何依赖
export const LoginFnSmall = React.memo((props) => {
  const [show, setShow] = React.useState(true);
  const toggle = () => setShow(!show);
  const { login } = store;

  return useObserver(() => {
    return (
      <>
        <h1>Fn Small Comp</h1>
        <button onClick={toggle}>toggle</button>
        {show ? <div> fullName:{login.fullName}</div>: ""}
      </>
    )
  });
});

Нет никакой разницы между зависимостью от состояния и зависимостью от результата вычисления.this.props.loginПолучение соответствующих результатов по вышеизложенному приводит к зависимости пользовательского интерфейса от данных.

Посмотрите пример mobx

concent(state,moduleComputed)

Никаких декораторов не нужно отмечать наблюдаемые свойства и вычисленные результаты, простоjsonобъекты и функции, фаза выполнения автоматически преобразуется вProxyобъект.

Результат расчета зависит от

// code in models/login/computed.js
// n: newState, o: oldState, f: fnCtx

// fullName的依赖是firstName lastName
export function fullName(n, o, f){
  return `${n.firstName}_${n.lastName}`;
}

// nickName的依赖是firstName
export function nickName(n, o, f){
  return `${n.firstName}>>nicknick`
}

// anotherNickName基于nickName缓存结果做二次计算,而nickName的依赖是firstName
// 所以anotherNickName的依赖是firstName,注意需将此函数放置到nickName下面
export function anotherNickName(n, o, f){
  return `${f.cuVal.nickName}_another`;
}
  • Зависит только от результата расчета, метода записи компонента класса
@register({ module: "login" })
class _LoginClsSmall extends React.Component {
  state = {show:true};
  render() {
    const { state, moduleComputed: mcu, syncBool } = this.ctx;

    // show为true时实例的依赖为firstName+lastName
    // 为false时,则无任何依赖
    return (
      <>
        <h1>Fn Small Comp</h1>
        <button onClick={syncBool("show")}>toggle</button>
        {state.show ? <div> fullName:{mcu.fullName}</div> : ""}
      </>
    );
  }
}
  • Зависит только от результата расчета, функциональная составляющая записывается
export const LoginFnSmall = React.memo(props => {
  const { state, moduleComputed: mcu, syncBool } = useConcent({
    module: "login",
    state: { show: true }
  });

  return (
    <>
      <h1>Fn Small Comp</h1>
      <button onClick={syncBool("show")}>toggle</button>
      {state.show ? <div> fullName:{mcu.fullName}</div> : ""}
    </>
  );
});

а такжеmobxТочно так же нет разницы между зависимостью от состояния и зависимостью от результата расчета.ctx.stateПолучение соответствующих результатов на вышеизложенном производит зависимость пользовательского интерфейса от данных, и каждый рендерингconcentВсе динамически собирают последние зависимости текущего экземпляра, в экземпляреdidUpdateФазы перемещают зависимости, которые ушли.

  • зависимости жизненного цикла

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

export const setupSm = ctx=>{
  // 当firstName改变时,组件渲染渲染完毕后会触发
  ctx.effect(()=>{
    console.log('fisrtName changed', ctx.state.fisrtName);
  }, ['firstName'])
}

// 类组件里使用
export const LoginFnSmall = React.memo(props => {
  console.log('Fn Comp ' + props.tag);
  const { state, moduleComputed: mcu, sync } = useConcent({
    module: "login",setup: setupSm, state: { show: true }
  });
  //...
}

// 函数组件里使用
@register({ module: "login", setup:setupSm })
class _LoginClsSmall extends React.Component {...}

Посмотрите пример концентрата

Подробнее о ctx.effect

Обзор и резюме

В этом раунде сбора зависимостейconcentФорма набора зависимостей и форма представления компонентов , иmobxРазница очень большая, и весь процесс сбора зависимостей не задействует никаких других избыточных API, иmboxтребуетсяcomputedИзмените поле Getter, которое необходимо использовать в компоненте функцииuseObserverСтатус пакета возвращается в пользовательский интерфейс,concentОбратите больше внимания на все это функция, устраняющаяthisэто ключевое слово, используяfnCtxКонтекст функции передает вычисленные результаты, явно различаяstateа такжеcomputedдержит объект-контейнер.

Коллекция зависимостей concent mbox redux
Поддержка сбора зависимостей во время выполнения Yes Yes No
Точный рендеринг Yes Yes No
не это Yes No No
Всего один шаг API Yes No No

раунд 4 - производные данные

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

мы все это знаемvueЭта концепция была встроена, открывая необязательныйcomputedдля обработки вычислений и кэширования полученных данных,reactНет такого понятия,reduxне дает такой возможности, ноreduxОткрытый механизм промежуточного программного обеспечения позволяет сообществу найти точку входа для поддержки этой возможности, поэтому здесь мы сосредоточимся наreduxКогда дело доходит до вычислений, я имею в виду то, что де-факто стало популярной стандартной библиотекой.reslect.

mobxа такжеconcentУ всех есть собственная вычислительная поддержка, мы в вышеуказанномКоллекция зависимостейпродемонстрировано в раундеmobxа такжеconcentПроизводный код данных , поэтому этот раунд только дляreduxПример записи производных данных

redux(reselect)

последний релиз редуксаv7версия, которая предоставляет два API,useDispatchа такжеuseSelector, использование следует предыдущемуmapStateToStateа такжеmapDispatchToPropsПолностью эквивалентно, наш пример будет использовать как компоненты класса, так и компоненты функции для демонстрации.

определить селектор

import { createSelector } from "reselect";

// getter,仅用于取值,不参与计算
const getFirstName = state => state.login.firstName;
const getLastName = state => state.login.lastName;

// selector,等同于computed,手动传入计算依赖关系
export const selectFullName = createSelector(
  [getFirstName, getLastName],
  (firstName, lastName) => `${firstName}_${lastName}`
);

export const selectNickName = createSelector(
  [getFirstName],
  (firstName) => `${firstName}>>nicknick`
);

export const selectAnotherNickName = createSelector(
  [selectNickName],
  (nickname) => `${nickname}_another`
);

Компонент класса получает селектор

import React from "react";
import { connect } from "react-redux";
import * as loginAction from "models/login/action";
import {
  selectFullName,
  selectNickName,
  selectAnotherNickName
} from "models/login/selector";

@connect(
  state => ({
    firstName: state.login.firstName,
    lastName: state.login.lastName,
    fullName: selectFullName(state),
    nickName: selectNickName(state),
    anotherNickName: selectAnotherNickName(state),
  }), // mapStateToProps
  dispatch => ({
    // mapDispatchToProps
    changeFirstName: e =>
      dispatch(loginAction.changeFirstName(e.target.value)),
    asyncChangeFirstName: e =>
      dispatch(loginAction.asyncChangeFirstName(e.target.value)),
    changeLastName: e => dispatch(loginAction.changeLastName(e.target.value))
  })
)
class Counter extends React.Component {
  render() {
    const {
      firstName,
      lastName,
      fullName,
      nickName,
      anotherNickName,
      changeFirstName,
      asyncChangeFirstName,
      changeLastName
    } = this.props;
    return 'ui ...'
  }
}

export default Counter;

Функциональный компонент получает селектор

import * as React from "react";
import { useSelector, useDispatch } from "react-redux";
import * as loginAction from "models/login/action";
import {
  selectFullName,
  selectNickName,
  selectAnotherNickName
} from "models/login/selector";

const Counter = () => {
  const { firstName, lastName } = useSelector(state => state.login);
  const fullName = useSelector(selectFullName);
  const nickName = useSelector(selectNickName);
  const anotherNickName = useSelector(selectAnotherNickName);
  const dispatch = useDispatch();
  const changeFirstName = (e) => dispatch(loginAction.changeFirstName(e.target.value));
  const asyncChangeFirstName = (e) => dispatch(loginAction.asyncChangeFirstName(e.target.value));
  const changeLastName = (e) => dispatch(loginAction.changeLastName(e.target.value));

  return 'ui...'
  );
};

export default Counter;

Пример онлайн-данных, полученных с помощью Redux

mobx (вычисляемый декоратор)

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

concent (полученный непосредственно модулем Computed)

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

Обзор и резюме

в сравнении сmobxнепосредственно изthis.pops.someStoreПолучать,concentнепосредственно изctx.moduleComputedСуществует дополнительный процесс ручного ведения расчетных зависимостей или сопоставления и выбора результатов.Я думаю, с первого взгляда понятно, какой метод разработчики более охотно используют.

производные данные concent mbox redux(reselect)
Автоматическое поддержание зависимостей между результатами расчета Yes Yes No
Собирать зависимости при запуске чтения результатов расчета Yes Yes No
Функция расчета без этого Yes No Yes

раунд 5 - Настоящий TodoMvc

Вышеупомянутые 4 раунда объединяют яркие примеры кода, обобщают характеристики и стили кодирования трех фреймворков. Я полагаю, что читатели ожидают увидеть примеры кода, которые ближе к производственной среде, чтобы увидеть различия, поэтому давайте, наконец, использоватьTodoMvcПриходите к концу этого конкурса функций, я надеюсь на ваше понимание и опытconcent, нанеизменный & Коллекция зависимостейТур по программированию на React.

redux-todo-mvc

Посмотрите демонстрацию redux-todo-mvc

связанный с действием

связанный с редуктором

вычисленный

mobx-todo-mvc

Посмотрите демо mobx-todo-mvc

связанный с действием

вычисленный

concent-todo-mvc

Ознакомьтесь с демо-версией концентрата todo-mvc.

связанный с редуктором

вычисленный связанный

end

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

import React from "react";
import "./styles.css";
import { run, useConcent, defWatch } from 'concent';

run({
  login:{
    state:{
      name:'c2',
      addr:'bj',
      info:{
        sex: '1',
        grade: '19',
      }
    },
    reducer:{
      selectSex(sex, moduleState){
        const info = moduleState.info;
        info.sex = sex;
        return {info};
      }
    },
    computed: {
      funnyName(newState){
        // 收集到funnyName对应的依赖是 name
        return `${newState.name}_${Date.now()}`
      },
      otherFunnyName(newState, oldState, fnCtx){
        // 获取了funnyName的计算结果和newState.addr作为输入再次计算
        // 所以这里收集到otherFunnyName对应的依赖是 name addr
        return `${fnCtx.cuVal.funnyName}_${newState.addr}`
      }
    },
    watch:{
      // watchKey name和stateKey同名,默认监听name变化
      name(newState, oldState){
        console.log(`name changed from ${newState.name} to ${oldState.name}`);
      },
      // 从newState 读取了addr, info两个属性的值,当前watch函数的依赖是 addr, info
      // 它们任意一个发生变化时,都会触发此watch函数
      addrOrInfoChanged: defWatch((newState, oldState, fnCtx)=>{
        const {addr, info} = newState;
        if(fnCtx.isFirstCall)return;// 仅为了收集到依赖,不执行逻辑
        console.log(`addr is${addr}, info is${JSON.stringify(info)}`);
      }, {immediate:true})
    }
  }
})

function UI(){
  console.log('UI with state value');
  const {state, sync, dispatch} = useConcent('login');
  return (
    <div>
      name:<input value={state.name} onChange={sync('name')} />
      addr:<input value={state.addr} onChange={sync('addr')} />
      <br />
      info.sex:<input value={state.info.sex} onChange={sync('info.sex')} />
      info.grade:<input value={state.info.grade} onChange={sync('info.grade')} />
      <br />
      <select value={state.info.sex} onChange={(e)=>dispatch('selectSex', e.target.value)}>
        <option value="male">male</option>
        <option value="female">female</option>
      </select>
    </div>
  );
}

function UI2(){
  console.log('UI2 with comptued value');
  const {state, moduleComputed, syncBool} = useConcent({module:'login', state:{show:true}});
  return (
    <div>
      {/* 当show为true的时候,当前组件的依赖是funnyName对应的依赖 name */}
      {state.show? <span>dep is name: {moduleComputed.funnyName}</span> : 'UI2 no deps now'}
      <br/><button onClick={syncBool('show')}>toggle show</button>
    </div>
  );
}

function UI3(){
  console.log('UI3 with comptued value');
  const {state, moduleComputed, syncBool} = useConcent({module:'login', state:{show:true}});
  return (
    <div>
      {/* 当show为true的时候,当前组件的依赖是funnyName对应的依赖 name addr */}
      {state.show? <span>dep is name,addr: {moduleComputed.otherFunnyName}</span> : 'UI3 no deps now'}
      <br/><button onClick={syncBool('show')}>toggle show</button>
    </div>
  );
}

export default function App() {
  return (
    <div className="App">
      <h3>try click toggle btn and open console to see render log</h3>
      <UI />
      <UI />
      <UI2 />
      <UI3 />
    </div>
  );
}

❤ отметьте меня, если вам нравится концепт ^_^

Edit on CodeSandbox

https://codesandbox.io/s/concent-guide-xvcej

Edit on StackBlitz

https://stackblitz.com/edit/cc-multi-ways-to-wirte-code

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