Прочесывание знаний React (3): напишите свой собственный React-redux

внешний интерфейс JavaScript React.js Redux

Пример кода, пожалуйста, нажмите здесь

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

1. Создайте проект

Мы создаем новый проект с помощью create-react-app, удаляем лишние части в src и добавляем наши собственные файлы следующим образом:

# 修改后的目录结构
++ src
++++ component
++++++ Head
-------- Head.js
++++++ Body
-------- Body.js
++++++ Button
-------- Button.js
---- App.js
---- index.css
---- index.js

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

// App.js
import React, { Component } from 'react';
import Head from './component/Head/Head';
import Body from './component/Body/Body';
export default class App extends Component {
  render() {
    return (
      <div className="App">
        <Head />
        <Body />
      </div>
    );
  }
}

# Head.js
import React, { Component } from 'react';
export default class Head extends Component {
  render() {
    return (
      <div className="head">Head</div>
    );
  }
}

# Body.js
import React, { Component } from 'react';
import Button from '../Button/Button';
export default class Body extends Component {
  render() {
    return (
      <div>
        <div className="body">Body</div>
        <Button />
      </div>
    );
  }
}

# Button.js
import React, { Component } from 'react';
export default class Button extends Component {
  render() {
    return (
      <div className="button">
        <div className="btn">改变 head</div>
        <div className="btn">改变 body</div>
      </div>
    );
  }
}

Приведенный выше код не сложен, давайте напишем для них несколько стилей и, наконец, увидим эффект:

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

2. контекст

В React нам предоставляется контекстный API для решения таких вложенных сценариев (контекст подробно представленэто здесь, в React 16.3 и выше контекст обновлен, см.Смотри сюда).
Контекст предоставляет нам глобально совместно используемое состояние, которое может легко получить доступ к хранилищу компонентов верхнего уровня в любых компонентах-потомках.
Мы модифицируем наш код следующим образом:

# App.js
import PropTypes from 'prop-types';
...
export default class App extends Component {
  static childContextTypes = {
    store: PropTypes.object
  }
  getChildContext () {
    const state = {
      head: '我是全局 head',
      body: '我是全局 body',
      headBtn: '修改 head',
      bodyBtn: '修改 body'
    }
    return { store: state };
  }
  render() {
   ...
  }
}


# Head.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Head extends Component {
  static contextTypes = {
    store: PropTypes.object
  }
  constructor (props) {
    super(props)
    this.state = {};
  }
  componentWillMount(){
    this._upState();
  }
  _upState(){
    const { store } = this.context;
    this.setState({
      ...store
    })
  }
  render() {
    return (
      <div className="head">{this.state.head}</div>
    );
  }
}


# body.js
import PropTypes from 'prop-types';
...
export default class Body extends Component {
  static contextTypes = {
    store: PropTypes.object
  }
  constructor (props) {
    super(props)
    this.state = {};
  }
  componentWillMount(){
    this._upState();
  }
  _upState(){
    const { store } = this.context;
    this.setState({
      ...store
    })
  }
  render() {
    return (
      <div>
        <div className="body">{this.state.body}</div>
        <Button />
      </div>
    );
  }
}

# Button.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Button extends Component {
  static contextTypes = {
    store: PropTypes.object
  }
  constructor (props) {
    super(props)
    this.state = {};
  }
  componentWillMount(){
    this._upState();
  }
  _upState(){
    const { store } = this.context;
    this.setState({
      ...store
    })
  }
  render() {
    return (
      <div className="button">
        <div className="btn">{this.state.headBtn}</div>
        <div className="btn">{this.state.bodyBtn}</div>
      </div>
    );
  }
}

Глядя на страницу, мы видим, что к глобальному хранилищу в компоненте верхнего уровня обращались все компоненты-потомки:

Разберем шаги по использованию контекста:

1. Укажите тип данных в компоненте верхнего уровня через childContextTypes.
2. Установить данные через getChildContext в компоненте верхнего уровня.
3. Укажите тип данных в компоненте-потомке через contextTypes.
4. Получить данные через параметр контекста в компоненте-потомке.

Выполнив описанные выше шаги, мы создали глобальное хранилище. У вас может возникнуть вопрос, почему мы определяем метод _upState в компоненте-потомке, не прописывая содержимое непосредственно в жизненном цикле, на этот вопрос не будет ответа, и ниже вы увидите, почему. Теперь давайте скомбинируем это хранилище с ранее написанным редуксом (раздел о редуксе см.Предыдущая статья, 👇 здесь.

3. React-редукс

Давайте создадим новую папку redux и завершим наш redux (значение следующего кода см.предыдущий пост):

# index.js
export * from './createStore';
export * from './storeChange';

# createStore.js
export const createStore = (state, storeChange) => {
  const listeners = [];
  let store = state || {};
  const subscribe = (listen) => listeners.push(listen);
  const dispatch = (action) => {
    const newStore = storeChange(store, action);
    store = newStore; 
    listeners.forEach(item =>  item())
  };
  const getStore = () => {
    return store;
  }
  return { store, dispatch, subscribe, getStore }
}

# storeChange.js
export const storeChange = (store, action) => {
  switch (action.type) {
    case 'HEAD':
      return { 
        ...store,  
        head: action.head
      }
    case 'BODY':
      return { 
        ...store,
        body: action.body
      }
    default:
      return { ...store }
  }
}

Через приведенный выше код мы завершили Redux. Код CreateStore.js практически такой же, как предыдущий, но он был слегка модифицирован. Заинтересованные друзья могут видеть это для себя. Теперь давайте совместимся с контекстом:

# App.js
...
import { createStore, storeChange } from './redux';

export default class App extends Component {
  static childContextTypes = {
    store: PropTypes.object,
    dispatch: PropTypes.func,
    subscribe: PropTypes.func,
    getStore: PropTypes.func
  }
  getChildContext () {
    const state = {
      head: '我是全局 head',
      body: '我是全局 body',
      headBtn: '修改 head',
      bodyBtn: '修改 body'
    }
    const { store, dispatch, subscribe, getStore } = createStore(state,storeChange)
    return { store, dispatch, subscribe, getStore };
  }
  render() {
   ...
  }
}

# Head.js
...
export default class Head extends Component {
  static contextTypes = {
    store: PropTypes.object,
    subscribe: PropTypes.func,
    getStore: PropTypes.func
  }
  ...
  componentWillMount(){
    const { subscribe } = this.context;
    this._upState();
    subscribe(() => this._upState())
  }
  _upState(){
    const { getStore } = this.context;
    this.setState({
      ...getStore()
    })
  }
  render() {
   ...
  }
}

# Body.js
...
export default class Body extends Component {
  static contextTypes = {
   // 和 Head.js 相同
  }
  ...
  componentWillMount(){
    // 和 Head.js 相同
  }
  _upState(){
   // 和 Head.js 相同
  }
  render() {
    return (
      <div>
        <div className="body">{this.state.body}</div>
        <Button />
      </div>
    );
  }
}

# Button.js
...
export default class Button extends Component {
  static contextTypes = {
    store: PropTypes.object,
    dispatch: PropTypes.func,
    subscribe: PropTypes.func,
    getStore: PropTypes.func
  }
  constructor (props) {
    super(props)
    this.state = {};
  }
  componentWillMount(){
   // 和 Head.js 相同
  }
  _upState(){
    // 和 Head.js 相同
  }
  render() {
    ...
  }
}

В приведенном выше коде мы используем метод createStore для создания глобального хранилища. И передайте сохранение, отправку и подписку через контекст, чтобы каждый компонент-потомок мог легко получить эти глобальные свойства. Наконец, мы используем setState для изменения состояния каждого компонента-потомка и добавляем функцию прослушивания для подписки.Когда хранилище изменится, пусть компонент извлечет хранилище и повторно отобразит. Здесь мы видим использование _upState, что позволяет нам легко добавлять обратные вызовы после изменений хранилища.
Наблюдая за страницей, мы обнаружили, что страница не является аномальной, и доступ к контексту все еще возможен на страницах-потомках. Значит ли это, что нам удалось совместить? Не волнуйтесь, давайте попробуем изменить данные. Мы модифицируем Button.js, чтобы добавить к кнопке событие click для смены магазина:

# Button.js
...
  changeContext(type){
    const { dispatch } = this.context;
    dispatch({ 
      type: type,
      head: '我是修改后的数据'
    });
  }
  render() {
    return (
      <div className="button">
        <div className="btn" onClick={() => this.changeContext('HEAD')}>{this.state.headBtn}</div>
        <div className="btn" onClick={() => this.changeContext('BODY')}>{this.state.bodyBtn}</div>
      </div>
    );
  }

Нажимаем кнопку и видим:

Данные успешно обновлены. На данный момент мы успешно объединили наш redux и react.

4. Оптимизация

1. Подключиться

Хотя мы реализовали комбинацию redux и react, мы видим, что в приведенном выше коде много проблем, таких как:
1) Много повторяющейся логики
В каждом компоненте-потомке мы получаем хранилище в контексте, затем обновляем соответствующее состояние, а также добавляем событие прослушивания.
2) Код вряд ли можно использовать повторно
В каждом компоненте-потомке зависимость от контекста слишком сильна. Допустим, ваш коллега хочет использовать компонент Body, но в его коде не задан контекст, тогда компонент Body недоступен.

Что касается этих проблем, мы можем решить их с помощью компонентов более высокого порядка (для проблем с компонентами более высокого порядка, пожалуйста,кликните сюдаилиздесь), мы можем инкапсулировать логику повторяющегося кода и назовем этот инкапсулированный метод connect . Это просто имя, вам не нужно об этом беспокоиться, вы можете называть его ааа, если хотите.
Создаем новый файл подключения в папке redux:

# connect.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export const connect = (Comp) => {
  class Connect extends Component {
    render(){
      return (
        <div className="connect">
          <Comp />
        </div>
      );
    }
  }
  return Connect;
}

Мы видели, что соединение — это компонент более высокого порядка, который принимает компонент и возвращает обработанный компонент. Давайте используем компонент Head, чтобы убедиться, что этот компонент более высокого порядка доступен:

# Head.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from '../../redux';
class Head extends Component {
 ...
}
export default connect(Head);

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

Исходя из этого, можем ли мы заставить connect делать больше вещей, например, дать ему все о контексте, мы пытаемся преобразовать connect и Head следующим образом:

# connect.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export const connect = (Comp) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object,
      dispatch: PropTypes.func,
      subscribe: PropTypes.func,
      getStore: PropTypes.func
    }
    constructor (props) {
      super(props)
      this.state = {};
    }
    componentWillMount(){
      const { subscribe } = this.context;
      this._upState();
      subscribe(() => this._upState())
    }
    _upState(){
      const { getStore } = this.context;
      this.setState({
        ...getStore()
      })
    }
    render(){
      return (
        <div className="connect">
          <Comp {...this.state} />
        </div>
      );
    }
  }
  return Connect;
}

# Head.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from '../../redux';
class Head extends Component {
  render() {
    return (
      <div className="head">{this.props.head}</div> // 从 props 中取值
    );
  }
}
export default connect(Head);

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

# Body.js
...
class Body extends Component {
  render() {
    return (
      <div>
        <div className="body">{this.props.body}</div>
        <Button />
      </div>
    );
  }
}
export default connect(Body)

# Button.js
...
class Button extends Component {
  changeContext(type, value){
    const { dispatch } = this.context;  // context 已经不存在了
    dispatch({ 
      type: type,
      head: value
    });
  }
  render() {
    return (
      <div className="button">
        <div className="btn" onClick={() => this.changeContext('HEAD', '我是改变的数据1')}>{this.props.headBtn}</div>
        <div className="btn" onClick={() => this.changeContext('HEAD', '我是改变的数据2')}>{this.props.bodyBtn}</div>
      </div>
    );
  }
}
export default connect(Button)

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

# Button.js
    ...
 const { dispatch } = this.props;  // 从 props 中取值
  ... 
  
# connect.js
...
export const connect = (Comp) => {
  class Connect extends Component {
   ...
    constructor (props) {
      super(props)
      this.state = {
        dispatch: () => {}
      };
    }
    componentWillMount(){
      const { subscribe, dispatch } = this.context; // 取出 dispatch 
      this.setState({
        dispatch
      })
      this._upState();
      subscribe(() => this._upState())
    }
   ...
  }
  return Connect;
}

На данный момент вроде бы все решено. Давайте подытожим, что мы на самом деле сделали:

1) Мы инкапсулировали подключение и передали ему все связанные операции подключения.
2) Мы модифицировали компоненты-потомки, чтобы они получали данные из реквизита и больше не зависели от контекста.

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

# connect.js
...
export const connect = (Comp, propsType) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object,
      dispatch: PropTypes.func,
      subscribe: PropTypes.func,
      getStore: PropTypes.func,
      ...propsType
    }
    ...
  }
  return Connect;
}

# Head.js
...
const propsType = {
  store: PropTypes.object,
}
export default connect(Head, propsType);

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

2. Провайдер

Мы видим, что во всех компонентах-потомках операции, связанные с контекстом, были разделены, но в App.js по-прежнему присутствует контент, связанный с контекстом. На самом деле контекст используется в приложении только для хранения хранилища, чтобы дочерние компоненты могли получать из него данные. Затем мы можем полностью обновить состояние через компонент контейнера, отделить эту часть грязной работы от компонента приложения и обновить ее до нового компонента контейнера. Нам просто нужно передать ему хранилище, которое нужно сохранить в контексте.
По предыдущей идее создаем новый Provider под папкой redux, и выносим из приложения весь код, который не имеет отношения к бизнесу:

# Provider
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { createStore, storeChange } from '../redux';
export class Provider extends Component {
  static childContextTypes = {
    store: PropTypes.object,
    dispatch: PropTypes.func,
    subscribe: PropTypes.func,
    getStore: PropTypes.func
  }
  getChildContext () {
    const state = this.props.store;
    const { store, dispatch, subscribe, getStore } = createStore(state,storeChange)
    return { store, dispatch, subscribe, getStore };
  }
  render(){
    return (
      <div className="provider">{this.props.children}</div>
    );
  }
}

# App.js 
...
export default class App extends Component {
  render() {
    return (
      <div className="App">
        <Head />
        <Body />
      </div>
    );
  }
}

# index.js
...
import { Provider } from './redux'
const state = {
  head: '我是全局 head',
  body: '我是全局 body',
  headBtn: '修改 head',
  bodyBtn: '修改 body'
}
ReactDOM.render(
  <Provider store={state}>
    <App />
  </Provider>, 
  document.getElementById('root')
);

Обновленный компонент приложения также стал очень освежающим.
Мы определяем глобальное хранилище в index.js и вставляем его в контекст через компонент-контейнер Provider, чтобы можно было легко получить все компоненты-потомки.В компоненте App нам нужно только обратить внимание на конкретную бизнес-логику.

последние слова

В этой статье я завершил свой собственный react-redux с помощью нескольких простых примеров кода.Конечно, приведенный выше код слишком прост, есть много проблем, и он немного отличается от нашей обычно используемой библиотеки react-redux.Мы сосредоточимся на понимание некоторых из их внутренних принципов.
Если описание неверно, поправьте меня!