[Серия React] практическая реализация react-redux

JavaScript React.js
[Серия React] практическая реализация react-redux

что такое реакция-редукс

react-reduxдаreduxофициальныйReactбиблиотека привязки. Это помогает нам соединить слой пользовательского интерфейса и уровень данных. Цель этой статьи не в том, чтобы представитьreact-reduxиспользовать, а реализовать простойreact-redux, я надеюсь, что смогу вам помочь.

Сначала подумайте об этом, если вы не используетеreact-redux,нашreactКак совместить в проектеreduxразвивать.

каждый требуетreduxКомбинируя используемые компоненты, нам всем нужно сделать следующее:

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

следующим образом:

import React from 'react';
import store from '../store';
import actions from '../store/actions/counter';
/**
 * reducer 是 combineReducer({counter, ...})
 * state 的结构为 
 * {
 *      counter: {number: 0},
 *      ....
 * }
 */
class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: store.getState().counter.number
        }
    }
    componentDidMount() {
        this.unsub = store.subscribe(() => {
            if(this.state.number === store.getState().counter.number) {
                return;
           	}
            this.setState({
                number: store.getState().counter.number
            });
        });
    }
    render() {
        return (
            <div>
                <p>{`number: ${this.state.number}`}</p>
                <button onClick={() => {store.dispatch(actions.add(2))}}>+</button>
                <button onClick={() => {store.dispatch(actions.minus(2))}}>-</button>
            <div>
        )
    }
    componentWillUnmount() {
        this.unsub();
    }
}

Если у нас есть много компонентов в нашем проекте, которые необходимо объединить сreduxКомбинированное использование, то эти компоненты нужно многократно писать эту логику. Очевидно, нам нужно найти способ переиспользовать эту часть логики, иначе будет казаться, что мы тупые. мы знаем,reactКомпоненты среднего и высокого уровня могут реализовать повторное использование логики.

используется в [Counterкод] (GitHub.com/Иветт Л.А. Ю/Б…) серединаmyreact-redux/counter, рекомендуется сначалаcloneКод, конечно, если вы считаете, что эта статья хороша, поставьте звезду, чтобы поощрить ее.

логическое мультиплексирование

существуетsrcСоздайте новый в каталогеreact-reduxПапка, последующие файлы создаются в этой папке.

Создайте файл connect.js

файл, созданный вreact-redux/componentsПод папкой:

Мы будем писать повторяющуюся логикуconnectсередина.

import React, { Component } from 'react';
import store from '../../store';

export default function connect (WrappedComponent) {
    return class Connect extends Component {
        constructor(props) {
            super(props);
            this.state = store.getState();
        }
        componentDidMount() {
            this.unsub = store.subscribe(() => {
                this.setState({
                    this.setState(store.getState());
                });
            });
        }
        componentWillUnmount() {
            this.unsub();
        }
        render() {
            return (
                <WrappedComponent {...this.state} {...this.props}/>
            )
        }
    }
}

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

Для этого добавим два новых параметра:mapStateToPropsа такжеmapDispatchToProps, эти два параметра отвечают за сообщениеconnectтребуемый компонентstateСодержимое и действия, которые необходимо отправить.

mapStateToProps и mapDispatchToProps

мы знаемmapStateToPropsа такжеmapDispatchToPropsКакова роль, но пока нам не ясно, в каком формате эти два параметра должны быть переданыconnectиспользовать.

import { connect } from 'react-redux';
....
//connect 的使用
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
  • mapStateToProps сообщаетconnect, состояние, к которому компонент должен привязаться.

    mapStateToPropsНужно выбрать состояние, в котором нуждается компонент, из всего состояния, но вызватьconnect, мы не можем получитьstore,ноconnectДоступно внутриstore, для этого будемmapStateToPropsопределяется как функция вconnectвызывая его внутренне, будетstoreсерединаstateперейти к нему, а затем передать результат, возвращенный функцией, компоненту в качестве свойства. компонент черезthis.props.XXXчтобы получить. следовательно,mapStateToPropsФормат должен выглядеть следующим образом:

    //将 store.getState() 传递给 mapStateToProps
    mapStateToProps = state => ({
        number: state.counter.number
    });
    
  • mapDispatchToProps сообщаетconnect, действие, к которому компонент должен привязаться.

    Напомним, что действие отправляется в компоненте:store.dispatch({actions.add(2)}).connectПосле упаковки мы все еще должны иметь возможность отправлять действия, определенноthis.props.XXX()такой формат.

    Например, счетчик увеличивается, вызываетthis.props.add(2), необходимо раздатьstore.dispatch({actions.add(2)}),следовательноaddатрибут, соответствующий контент(num) => { store.dispatch({actions.add(num)}) }. Свойства, передаваемые компоненту, выглядят следующим образом:

    {
        add: (num) => {
            store.dispatch(actions.add(num))
        },
        minus: (num) => {
            store.dispatch(actions.minus(num))
        }
    }
    

    а такжеmapStateToPropsто же самое при звонкеconnect, мы не можем получитьstore.dispatch, так что нам тоже нужноmapDispatchToPropsразработан как функция, вconnectвнутренний вызов, так чтоstore.dispatchперейти к нему. так,mapStateToPropsОн должен быть в следующем формате:

    //将 store.dispacth 传递给 mapDispatchToProps
    mapDispatchToProps = (dispatch) => ({
        add: (num) => {
            dispatch(actions.add(num))
        },
        minus: (num) => {
            dispatch(actions.minus(num))
        }
    })
    

Пока что мы выяснилиmapStateToPropsа такжеmapDispatchToPropsФормат - время дальнейшего совершенствованияconnect.

подключить версию 1.0

import React, { Component } from 'react';
import store from '../../store';

export default function connect (mapStateToProps, mapDispatchToProps) {
    return function wrapWithConnect (WrappedComponent) {
        return class Connect extends Component {
            constructor(props) {
                super(props);
                this.state = mapStateToProps(store.getState());
                this.mappedDispatch = mapDispatchToProps(store.dispatch);
            }
            componentDidMount() {
                this.unsub = store.subscribe(() => {
                    const mappedState = mapStateToProps(store.getState());
                    //TODO 做一层浅比较,如果状态没有改变,则不setState
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
                )
            }
        }
    }
}

мы знаем,connectкак естьreact-reduxметоды, предоставляемые библиотекой, поэтому мы не можем напрямуюconnect.jsимпортироватьstore,этоstoreдолжны использоватьсяreact-reduxпоступление заявки.reactСуществует два типа передачи данных: через атрибутыpropsили через объект контекстаcontext,пройти черезconnectОбернутые компоненты распределяются по приложению, аcontextОн предназначен для обмена данными, которые являются «глобальными» для дерева компонентов.

нам нужно поставитьstoreпомещатьcontext, чтобы можно было получить все компоненты-потомки корневого компонентаstore. Этой части контента мы можем конечно написать соответствующий код в приложении, но очевидно, что эти коды повторяются в каждом приложении. Поэтому мы инкапсулируем эту часть вreact-reduxвнутренний.

Здесь мы используем старыйContext APIдля записи (учитывая код, который мы реализовали в ветке react-redux 4.x, поэтому мы используем устаревший контекстный API).

Provider

нам нужно предоставитьProviderКомпонент, его функция — получение перевода от приложенияstore, держисьcontext, так что его компоненты-потомки могут быть получены через объект контекстаstore.

Создайте новый файл Provider.js

файл, созданный вreact-reduxВ папке:

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class Provider extends Component {
    static childContextTypes = {
        store: PropTypes.shape({
            subscribe: PropTypes.func.isRequired,
            dispatch: PropTypes.func.isRequired,
            getState: PropTypes.func.isRequired
        }).isRequired
    }
    
    constructor(props) {
        super(props);
        this.store = props.store;
    }

    getChildContext() {
        return {
            store: this.store
        }
    }

    render() {
        /**
         * 早前返回的是 return Children.only(this.props.children)
         * 导致Provider只能包裹一个子组件,后来取消了此限制
         * 因此此处,我们直接返回 this.props.children
         */
        return this.props.children
    }
}
Создайте новый файл index.js

файл, созданный вreact-reduxПод содержанием:

Этот файл делает только одну вещь, т.connectа такжеProviderэкспорт

import connect from './components/connect';
import Provider from './components/Provider';

export {
    connect,
    Provider
}

Использование провайдера

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

import React, { Component } from 'react';
import { Provider } from '../react-redux';
import store from './store';
import Counter from './Counter';

export default class App extends Component {
    render() {
        return (
            <Provider store={store}>
                <Counter />
            </Provider>
        )
    }
}

Слишком далеко,ProviderИсходный код и использование были ясно объяснены, но соответствующиеconnectТребуются также некоторые модификации, для общности нужно начать сcontextподнимись и получиstore, заменив предыдущий импорт.

подключить версию 2.0

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default function connect(mapStateToProps, mapDispatchToProps) {
    return function wrapWithConnect(WrappedComponent) {
        return class Connect extends Component {
            //PropTypes.shape 这部分代码与 Provider 中重复,因此后面我们可以提取出来
            static contextTypes = {
                store: PropTypes.shape({
                    subscribe: PropTypes.func.isRequired,
                    dispatch: PropTypes.func.isRequired,
                    getState: PropTypes.func.isRequired
                }).isRequired
            }

            constructor(props, context) {
                super(props, context);
                this.store = context.store;
                //源码中是将 store.getState() 给了 this.state
                this.state = mapStateToProps(this.store.getState());
                this.mappedDispatch = mapDispatchToProps(this.store.dispatch);
            }
            componentDidMount() {
                this.unsub = this.store.subscribe(() => {
                    const mappedState = mapStateToProps(this.store.getState());
                    //TODO 做一层浅比较,如果状态没有改变,则无需 setState
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
                )
            }
        }
    }
}

использоватьconnectассоциацияCounterа такжеstoreданные в .

import React, { Component } from 'react';
import { connect } from '../react-redux';
import actions from '../store/actions/counter';

class Counter extends Component {
    render() {
        return (
            <div>
                <p>{`number: ${this.props.number}`}</p>
                <button onClick={() => { this.props.add(2) }}>+</button>
                <button onClick={() => { this.props.minus(2) }}>-</button>
            </div>
        )
    }
}

const mapStateToProps = state => ({
    number: state.counter.number
});

const mapDispatchToProps = (dispatch) => ({
    add: (num) => {
        dispatch(actions.add(num))
    },
    minus: (num) => {
        dispatch(actions.minus(num))
    }
});


export default connect(mapStateToProps, mapDispatchToProps)(Counter);

store/actions/counter.js определяется следующим образом:

import { INCREMENT, DECREMENT } from '../action-types';

const counter = {
    add(number) {
        return {
            type: INCREMENT,
            number
        }
    },
    minus(number) {
        return {
            type: DECREMENT,
            number
        }
    }
}
export default counter;

До сих пор нашreact-reduxБиблиотека готова к использованию, но осталось проработать много деталей:

  • mapDispatchToPropsОпределение немного громоздко писать и недостаточно лаконично ты помнишьreduxсерединаbindActionCreators, с помощью этого метода мы можем разрешить передачуactionCreatorДатьconnect, затем вconnectКонвертировать внутри.

  • connectа такжеProviderсерединаstoreизPropTypeПравила могут быть извлечены, чтобы избежать избыточности кода

  • mapStateToPropsа такжеmapDispatchToPropsМожно указать значение по умолчаниюmapStateToPropsПо умолчаниюstate => ({}); не относитсяstate;

    mapDispatchToPropsЗначение по умолчаниюdispatch => ({dispatch}),Будуstore.dispatchМетоды передаются как свойства обернутому свойству.

  • В настоящее время мы проходим толькоstore.getState()ДатьmapStateToProps, но вполне вероятно, что фильтрация нужногоstateКогда его необходимо обрабатывать в соответствии с свойствами самого компонента, вы также можете пройти свойства компонента, чтобыmapStateToProps, по той же причине, также передает свои свойства вmapDispatchToProps.

подключить версию 3.0

мы будемstoreПравила PropType извлекаются и помещаются вutils/storeShape.jsв файле.

Код поверхностного сравнения находится вutils/shallowEqual.jsВ файле не указана общая функция поверхностного сравнения, если вам интересно, вы можете прочитать код напрямую.

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import storeShape from '../utils/storeShape';
import shallowEqual from '../utils/shallowEqual';
/**
 * mapStateToProps 默认不关联state
 * mapDispatchToProps 默认值为 dispatch => ({dispatch}),将 `store.dispatch` 方法作为属性传递给组件
 */
const defaultMapStateToProps = state => ({});
const defaultMapDispatchToProps = dispatch => ({ dispatch });

export default function connect(mapStateToProps, mapDispatchToProps) {
    if(!mapStateToProps) {
        mapStateToProps = defaultMapStateToProps;
    }
    if (!mapDispatchToProps) {
        //当 mapDispatchToProps 为 null/undefined/false...时,使用默认值
        mapDispatchToProps = defaultMapDispatchToProps;
    }
    return function wrapWithConnect(WrappedComponent) {
        return class Connect extends Component {
            static contextTypes = {
                store: storeShape
            };
            constructor(props, context) {
                super(props, context);
                this.store = context.store;
                //源码中是将 store.getState() 给了 this.state
                this.state = mapStateToProps(this.store.getState(), this.props);
                if (typeof mapDispatchToProps === 'function') {
                    this.mappedDispatch = mapDispatchToProps(this.store.dispatch, this.props);
                } else {
                    //传递了一个 actionCreator 对象过来
                    this.mappedDispatch = bindActionCreators(mapDispatchToProps, this.store.dispatch);
                }
            }
            componentDidMount() {
                this.unsub = this.store.subscribe(() => {
                    const mappedState = mapStateToProps(this.store.getState(), this.props);
                    if (shallowEqual(this.state, mappedState)) {
                        return;
                    }
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
                )
            }
        }
    }
}

Теперь нашconnectразрешатьmapDispatchToPropsэто функция илиactionCreatorsобъект, вmapStateToPropsа такжеmapDispatchToPropsпо умолчанию илиnullтакже может хорошо работать.

Но есть еще одна проблема,connectВсе возвращаемые имена компонентовConnect, который нелегко отлаживать. Таким образом, мы можем добавитьdisplayName.

подключить версию 4.0

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import storeShape from '../utils/storeShape';
import shallowEqual from '../utils/shallowEqual';
/**
 * mapStateToProps 缺省时,不关联state
 * mapDispatchToProps 缺省时,设置其默认值为 dispatch => ({dispatch}),将`store.dispatch` 方法作为属性传递给组件
 */ 
const defaultMapStateToProps = state => ({});
const defaultMapDispatchToProps = dispatch => ({ dispatch });

function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

export default function connect(mapStateToProps, mapDispatchToProps) {
    if(!mapStateToProps) {
        mapStateToProps = defaultMapStateToProps;
    }
    if(!mapDispatchToProps) {
        //当 mapDispatchToProps 为 null/undefined/false...时,使用默认值
        mapDispatchToProps = defaultMapDispatchToProps;
    }
    return function wrapWithConnect (WrappedComponent) {
        return class Connect extends Component {
            static contextTypes = storeShape;
            static displayName = `Connect(${getDisplayName(WrappedComponent)})`;
            constructor(props) {
                super(props);
                //源码中是将 store.getState() 给了 this.state
                this.state = mapStateToProps(store.getState(), this.props);
                if(typeof mapDispatchToProps === 'function') {
                    this.mappedDispatch = mapDispatchToProps(store.dispatch, this.props);
                }else{
                    //传递了一个 actionCreator 对象过来
                    this.mappedDispatch = bindActionCreators(mapDispatchToProps, store.dispatch);
                }
            }
            componentDidMount() {
                this.unsub = store.subscribe(() => {
                    const mappedState = mapStateToProps(store.getState(), this.props);
                    if(shallowEqual(this.state, mappedState)) {
                        return;
                    }
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
                )
            }
        }
    }
}

Слишком далеко,react-reduxВ принципе мы это реализовали, но код не идеален, например,refпропущенный вопрос, компонентpropsПри изменении пересчитатьthis.stateа такжеthis.mappedDispatch, без дальнейшей оптимизации производительности и т. д. На этой основе можно действовать дальше.

react-reduxКод из магистральной ветки уже используетсяhooksПерепишите, если будет время позже, будет выведена новая версия разбора кода.

Наконец, используйте наши собственные письменныеreact-reduxа такжеreduxнаписаноTodoДемо, функция нормальная, код вhttps://github.com/YvetteLau/Blogсерединаmyreact-redux/todoВниз.

Прикрепите старый и новыйcontext APIКак использовать:

context

На данный момент существует две версииcontext API, старый API будет поддерживаться во всех выпусках 16.x, но будет удален в будущих выпусках.

контекстный API (новый)

const MyContext = React.createContext(defaultValue);

СоздаватьContextобъект. когдаReactсделать подписку на этоContextКомпонент объекта, этот компонент будет сопоставляться с ближайшим к себе в дереве компонентовProviderчитать текущийcontextстоимость.

Примечание: только если дерево, в котором находится компонент, не совпадаетProviderкогда этоdefaultValueпараметры вступят в силу.

использовать
Context.js

Сначала создайте объект Context

import React from 'react';

const MyContext = React.createContext(null);

export default MyContext;
Корневые компоненты (Panel.js)
  • Установите контент, которым нужно поделиться в<MyContext.Provider>изvaluein (т.е. значение контекста)
  • подкомпоненты<MyContext.Provider>пакет
import React from 'react';
import MyContext from './Context';
import Content from './Content';

class Pannel extends React.Component {
    state = {
        theme: {
            color: 'rgb(0, 51, 254)'
        }
    }
    render() {
        return (
            // 属性名必须叫 value
            <MyContext.Provider value={this.state.theme}>
                <Content />
            </MyContext.Provider>
        )
    }
}
Компонент-потомок ( Content.js )

компонент класса

  • определениеClass.contextType: static contextType = ThemeContext;
  • пройти черезthis.contextПолучать<ThemeContext.Provider>серединаvalueсодержание (т.contextстоимость)
//类组件
import React from 'react';
import ThemeContext from './Context';

class Content extends React.Component {
    //定义了 contextType 之后,就可以通过 this.context 获取 ThemeContext.Provider value 中的内容
    static contextType = ThemeContext;
    render() {
        return (
            <div style={{color: `2px solid ${this.context.color}`}}>
                //....
            </div>
        )
    }
}

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

  • дочерние элементы завернуты в<ThemeContext.Consumer>середина
  • <ThemeContext.Consumer>Дочерний элемент — функция, входной параметрcontextстоимость(Providerкоторый предоставилvalue). вот{color: XXX}
import React from 'react';
import ThemeContext from './Context';

export default function Content() {
    return (
        <ThemeContext.Consumer>
            {
                context => (
                    <div style={{color: `2px solid ${context.color}`}}>
                        //....
                    </div>
                )
            }
        </ThemeContext.Consumer>
    )
}

контекстный API (старый)

использовать
  • определить корневой компонентchildContextTypes(проверятьgetChildContextтип возврата)
  • определениеgetChildContextметод
Корневой компонент ( Pannel.js )
import React from 'react';
import PropTypes from 'prop-types';
import Content from './Content';

class Pannel extends React.Component {
    static childContextTypes = {
        theme: PropTypes.object
    }
    getChildContext() {
        return { theme: this.state.theme }
    }
    state = {
        theme: {
            color: 'rgb(0, 51, 254)'
        }
    }
    render() {
        return (
            // 属性名必须叫 value
            <>
                <Content />
            </>
        )
    }
}
Компонент-потомок ( Content.js )
  • определить компоненты-потомкиcontextTypes(объявление и проверка типа состояния, которое необходимо получить)
  • Переданный контент контекста можно получить через this.context.
import React from 'react';
import PropTypes from 'prop-types';

class Content extends React.Component {
    static contextTypes = {
        theme: PropTypes.object
    };
    render() {
        return (
            <div style={{color: `2px solid ${this.context.theme.color}`}}>
                //....
            </div>
        )
    }
}

Ссылка на ссылку:


Подпишитесь на официальный аккаунт и присоединитесь к группе технического обмена