包教包會Redux

PlayerWho發表於2019-03-02

React裡父子元件可以通過props通訊,兄弟元件通訊需要把資料傳遞給父元件,再由父元件傳遞給另一個子元件。以兄弟元件通訊為需求,寫一個Redux。

問題

包教包會Redux
這是一個計數器,點選按鈕,可以讓數字加一或者減一。兩個按鈕在Counter元件裡,顯示數字的在Number元件裡。

兄弟元件通訊

  • 首先分析這個需求,點選button,改變數字,Number元件重新渲染。
    包教包會Redux
  • 可抽象為,派發一個動作,改變狀態,執行方法。
    包教包會Redux
  • 根據上兩步分析,可以看出元件通訊的核心是動作(action)、執行方法(reducer)、狀態(state)
    包教包會Redux
  • action、reducer
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
複製程式碼
export default function reducer(state = {number: 0},action) {
    switch (action.type) {
        case types.INCREMENT:
            return {
                number: state.number + 1
            };
        case types.DECREMENT:
            return {
                number: state.number - 1
            };
        default:
            return state;
    }
}
複製程式碼
  • store是個物件,負責提供getState、dispatch、subscribe三個方法。
const store = {
    listeners:[],   
    getState(){
        return this.state;
    },
    dispatch(action){
        this.state = reducer(this.state,action);
        this.listeners.forEach(listener=>listener());
    },
    subscribe(listener){
        this.listeners.push(listener);
        return function () {
            this.listeners = listeners.filter(item=>item!==listener);
        }
    }
};
store.dispatch({}); //初始化state
export default store;
複製程式碼
  • Number、Counter元件
export default class Number extends React.Component{
    componentDidMount(){
        this.unsubscribe = store.subscribe(()=>this.setState({}));
    }
    componentWillUnmount(){
        this.unsubscribe();
    }
    render(){
        return (
            <div>
                {store.getState().number}
            </div>
        )
    }
}
export default class Counter extends React.Component{
    render(){
        return (
            <div>
                <button onClick={()=>store.dispatch({type:types.INCREMENT})}>+</button>
                <button onClick={()=>store.dispatch({type:types.DECREMENT})}>-</button>
            </div>
        )
    }
}
複製程式碼

Redux

上面實現了兄弟元件的通訊,但是複用性差,而且store裡的listeners不應該被外界修改。

  • createStore,這就是Redux裡建立store的方法。
export default function createStore(reducer) {
    let state;
    let listeners = [];
    function getState() {
        return state;
    }
    function dispatch(action) {
        state =  reducer(state,action);
        listeners.forEach(listener=>listener());
    }
    dispatch({});
    function subscribe(listener) {
        listeners.push(listener);
        return function () {
            const index = listeners.indexOf(listener);
            listeners.splice(index,1);
        }
    }
    return {
        getState,dispatch,subscribe
    }
}
複製程式碼
  • 呼叫createStore,傳入reducer,返回和上一步驟一樣的store。
  • redux裡的三大原則:只有一個store;state是隻讀的,只有觸發action才能改變;使用純函式修改。我們寫自己的redux時也要遵循這些原則。

多個reducer

  • 由於store只有一個,所以對於多個reducer時,要把reducer合併。
export default function combineReducers(reducers) {
    return function (state = {},action) {
        let newState = {};
        for(const key in reducers){
            newState[key] = reducers[key](state[key],action);
        }
        return newState;
    }
};
複製程式碼
  • 呼叫combineReducers,引數是物件,物件的key可以是reducer的名字,value是reducer,返回一個函式,把函式傳給createStore,建立store。

簡化元件裡派發動作

  • 我們在派發action的時候,需要
<button onClick={()=>store.dispatch({type:types.INCREMENT})}>+</button>
複製程式碼
  • 這樣比較麻煩,如果把action直接放在例項上,會比較方便。
export default class Counter extends React.Component{
    action = bindActionCreators(actions,store.dispatch)
    render(){
        return (
            <div>
                <button onClick={this.action.increment}>+</button>
                <button onClick={this.action.decrement}>-</button>
            </div>
        )
    }
}
複製程式碼
  • 先實現actions
export default {
    increment() {
        return {
            type: 'INCREMENT'
        }
    },
    decrement() {
        return {
            type: 'DECREMENT'
        }
    },
    changeText(value) {
        return {
            type: 'CHANGE_TEXT',
            text: value
        }
    }
}
複製程式碼
  • 再實現bindActionCreators
export default function bindActionCreators(actions,dispatch) {
    let boundActionCreators = {};
    for (const attr in actions){
        boundActionCreators[attr] = function () {
            const action = actions[attr](...arguments);
            dispatch(action);
        }
    }
    return boundActionCreators;
}
複製程式碼

React-Redux

  • 上面程式碼裡可以看出元件裡的許多程式碼是重複的,可以進一步抽象元件,最後抽象成React-Redux。
  • React-Redux裡要實現一個外層元件,負責傳遞store和渲染子元件,功能比較簡單
export default class Provider extends Component {
    static childContextTypes = {
        store:propTypes.object
    }
    getChildContext(){
        return {
            store:this.props.store
        }
    }
    render(){
        return this.props.children
    }
}
複製程式碼
  • 還要實現一個高階元件,高階元件先返回一個函式,最後返回一個元件。高階元件負責把store上的state和dispatch作為props傳遞給需要渲染的元件,還有實現生命週期函式裡的公共功能。
export default function (mapStateToProps,mapDispatchToProps) {
    return function (Component) {
        return class ProxyComponent extends React.Component{
            static contextTypes = {
                store:propTypes.object
            }
            constructor(props,context){
                super(props,context);
                this.store = context.store;
                this.state = mapStateToProps(this.store.getState());
            }
            componentDidMount(){
                const store = this.store;
                this.unsubscribe = store.subscribe(()=>this.setState(mapStateToProps(store.getState())));
            }
            componentWillUnmount(){
                this.unsubscribe();
            }
            render(){
                const actions  = bindActionCreators(mapDispatchToProps,this.store.dispatch);
                return <Component
                    {...actions}
                    {...this.state}
                />
            }
        }
    }
}
複製程式碼
  • 首頁渲染
ReactDOM.render(
    <Provider store={store}>
        <Counter/>
        <Number/>
    </Provider>, document.getElementById('root'));
複製程式碼
  • Counter、Number元件
class Counter extends React.Component{
    render(){
        return (
            <div>
                <button onClick={()=>this.props.increment()}>+</button>
                <button onClick={()=>this.props.decrement()}>-</button>
            </div>
        )
    }
}
export default connect(state=>state.counter,actions)(Counter);
class Number extends React.Component{
    render(){
        return (
            <div>
                {this.props.number}
            </div>
        )
    }
}

export default connect(state=>state.counter,actions)(Number);
複製程式碼

redux中介軟體

  • 最後實現redux中介軟體。Redux中介軟體是洋蔥模型,和Koa的中介軟體原理一樣。
    包教包會Redux
  • 開發中會有多箇中介軟體,中介軟體是函式,要把第一個中介軟體的結果作為引數傳遞給第二個中介軟體,依次執行,先實現這個compose函式
function compose(...fns) {
    if (fns.length===0) return arg=>arg;
    return fns.reduce((prev, next, index) => (...args) => prev(next(...args)));
}
export default compose;
複製程式碼
  • 應用中介軟體函式applyMiddleware
export default function applyMiddleware(...middlewares) {
    return function (createStore) {
        return function (reducers) {
            const store = createStore(reducers);
            let dispatch = store.dispatch;
            let middlewareApi = {
                dispatch:action=>dispatch(action),
                getState:store.getState
            };
            middlewares = middlewares.map(middleware=>middleware(middlewareApi));
            dispatch = compose(...middlewares)(dispatch);
            return {
                ...store,
                dispatch
            };
        };
    }
}
複製程式碼
  • 使用中介軟體,修改store
const store = applyMiddleware(thunk, logger)(createStore)(reducers);
複製程式碼

總結

Redux是管理頁面狀態和資料傳遞,從最開始元件通訊的問題,一步步的實現類似一個Redux的庫,方便我們學習Redux和理解Redux原理。

參考

相關文章