1. Redux应用场景 #

随着 JavaScript 单页应用开发日趋复杂,管理不断变化的 state 非常困难.Redux的出现就是为了解决state里的数据问题。 在React中,数据在组件中是单向流动的。数据从一个方向父组件流向子组件(通过props),由于这个特征,两个非父子关系的组件(或者称作兄弟组件)之间的通信就比较麻烦。

2. Redux设计思想 #

3. Redux三大原则 #

4. Redux概念解析 #

4.1 Store #

4.2 State #

Store对象包含所有数据。如果想得到某个时点的数据,就要对Store生成快照。这种时间点的数据集合,就叫做State。 当前时刻的State,可以通过store.getState()拿到。

import { createStore } from 'redux';
const store = createStore(fn);
const state = store.getState();

Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。

4.3 Action #

State的变化,会导致View的变化。但是,用户接触不到 State,只能接触到View。所以,State的变化必须是 View导致的。Action 就是 View 发出的通知,表示State 应该要发生变化了。 Action是一个对象。其中的type属性是必须的,表示 Action 的名称。其他属性可以自由设置,社区有一个规范可以参考。

const action = {
  type: 'ADD_TODO',
  payload: '学习redux'
};

上面代码中,Action 的名称是ADD_TODO,它携带的信息是字符串学习redux。 可以这样理解,Action描述当前发生的事情。改变State的唯一办法,就是使用 Action。它会运送数据到 Store

4.4 Action Creator #

View要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator

const ADD_TODO = '添加 TODO';
function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

const action = addTodo('学习Redux');

上面代码中,addTodo函数就是一个 Action Creator。

4.5 store.dispatch() #

store.dispatch()是 View 发出 Action 的唯一方法。

import { createStore } from 'redux';
const store = createStore(fn);

store.dispatch({
  type: 'ADD_TODO',
  payload: '学习Redux'
});

上面代码中,store.dispatch接受一个 Action 对象作为参数,将它发送出去。 结合 Action Creator,这段代码可以改写如下。

store.dispatch(addTodo('学习Redux'))

4.6 Reducer #

Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。 Reducer 是一个纯函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。

const reducer = function (state, action) {
  // ...
  return new_state;
};

5. redux源码 #

const createStore = (reducer) => {
  let state;
  let listeners = [];

  const getState = () => state;

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach(listener => listener());
  };

  const subscribe = (listener) => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter(l => l !== listener);
    }
  };

  dispatch({});

  return { getState, dispatch, subscribe };
};

6. 案例 #

6.1 原生计数器 #

html代码

<div id="counter"></div>
  <button id="addBtn">+</button>
  <button id="minusBtn">-</button>

js代码

function createStore(reducer) {
    var state;
    var listeners = [];
    var getState = () => state;
    var dispatch = (action) => {
        state = reducer(state, action);
        listeners.forEach(l=>l());
    }
    var subscribe = (listener) => {
        listeners.push(listener);
        return () => {
            listeners = listeners.filter((l) => l != listener)
        }
    }
    dispatch();
    return {
        getState, dispatch, subscribe
    }
}
var reducer = (state = 0, action) => {
    if (!action) return state;
    console.log(action);
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state;
    }
}
var store = createStore(reducer);
store.subscribe(function () {
    document.querySelector('#counter').innerHTML = store.getState();
});

document.querySelector('#addBtn').addEventListener('click', function () {
    store.dispatch({type: 'INCREMENT'});
});
document.querySelector('#minusBtn').addEventListener('click', function () {
    store.dispatch({type: 'DECREMENT'});
});

6.2 redux计数器 #

import {createStore} from 'redux';

6.3 react组件计数器 #

html代码

<div id="counter"></div>
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux';
class Counter extends React.Component {
    render() {
        return (
            <div>
                <h1>{this.props.value}</h1>
                <button onClick={this.props.onIncrement}>+</button>
                <button onClick={this.props.onDecrement}>-</button>
            </div>
        )
    }
}
const reducer = (state = 0, action) => {
        switch (action.type) {
            case 'INCREMENT':
                return state + 1;
            case 'DECREMENT':
                return state - 1;
            default:
                return state;
        }
    };

const store = createStore(reducer);

const render = () => {
        ReactDOM.render(
            <Counter
                value={store.getState()}
                onIncrement={() => store.dispatch({type: 'INCREMENT'})}
                onDecrement={() => store.dispatch({type: 'DECREMENT'})}
            />,
            document.getElementById('counter')
        );
    };

render();

store.subscribe(render);

7. combineReducers #

function createStore(reducer) {
    var state;
    var listeners = [];
    var getState = () => state;
    var dispatch = (action) => {
        state = reducer(state, action);
        listeners.forEach(l => l());
    }
    var subscribe = (listener) => {
        listeners.push(listener);
        return () => {
            listeners = listeners.filter((l) => l != listener)
        }
    }
    dispatch();
    return {
        getState, dispatch, subscribe
    }
}
var hour = (state = 0, action) => {
    if (!action) return state;
    if (action.type == 'HOUR') {
        return state + 1;
    }
    return state;
}
var minute = (state = 0, action) => {
    if (!action) return state;
    if (action.type == 'MINUTE') {
        return state + 1;
    }
    return state;
}
var second = (state = 0, action) => {
    if (!action) return state;
    if (action.type == 'SECOND') {
        return state + 1;
    }
    return state;
}
function combineReducers(reducers) {
    return function (state = {}, action) {
        return Object.keys(reducers).reduce((curr, key) => {
            curr[key] = reducers[key](state[key], action);
            return curr;
        }, {});
    }
}
var reducer = combineReducers({
    hour, minute, second
});

var store = createStore(reducer);
store.subscribe(function () {
    var state = store.getState();
    document.querySelector('#hour').innerHTML = state.hour;
    document.querySelector('#minute').innerHTML = state.minute;
    document.querySelector('#second').innerHTML = state.second;
});

document.querySelector('#hourBtn').addEventListener('click', function () {
    store.dispatch({type: 'HOUR'});
});
document.querySelector('#minuteBtn').addEventListener('click', function () {
    store.dispatch({type: 'MINUTE'});
});
document.querySelector('#secondBtn').addEventListener('click', function () {
    store.dispatch({type: 'SECOND'});
});
import {createStore, combineReducers} from 'redux';
var hour = (state, action) => {
    if (action.type == 'HOUR') {
        return state + 1;
    }
    return state;
}
var minute = (state, action) => {
    if (action.type == 'MINUTE') {
        return state + 1;
    }
    return state;
}
var second = (state, action) => {
    if (action.type == 'SECOND') {
        return state + 1;
    }
    return state;
}
var reducer = combineReducers({
    hour,
    minute,
    second
});

8. react-redux #

8.1 UI 组件 #

React-Redux 将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)。 UI 组件有以下几个特征。

下面就是一个 UI 组件的例子。

const Title =
  value => <h1>{value}</h1>;

因为不含有状态,UI 组件又称为"纯组件",即它纯函数一样,纯粹由参数决定它的值。

8.2 容器组件 #

容器组件的特征恰恰相反。

8.3 connect() #

React-Redux 提供connect方法,用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来。

import { connect } from 'react-redux'
const App = connect()(Counter)

上面代码中,Counter是 UI 组件,CounterApp就是由 React-Redux 通过connect方法自动生成的容器组件。 但是,因为没有定义业务逻辑,上面这个容器组件毫无意义,只是 UI 组件的一个单纯的包装层。为了定义业务逻辑,需要给出下面两方面的信息。

因此,connect方法的完整 API 如下。

import { connect } from 'react-redux'

const CounterApp = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

上面代码中,connect方法接受两个参数:mapStateToPropsmapDispatchToProps。 它们定义了 UI 组件的业务逻辑。 前者负责输入逻辑,即将state映射到 UI 组件的参数(props) 后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。

8.4 mapStateToProps() #

mapStateToProps是一个函数。它的作用就是像它的名字那样,建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系。 作为函数,mapStateToProps执行后应该返回一个对象,里面的每一个键值对就是一个映射。请看下面的例子。

function mapStateToProps(state) {
  return {
    value: state.count
  }
}

上面代码中,mapStateToProps是一个函数,它接受state作为参数,返回一个对象。这个对象有一个value属性,代表 UI 组件的同名参数

mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。 mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象。

使用ownProps作为参数后,如果容器组件的参数发生变化,也会引发 UI 组件重新渲染。 connect方法可以省略mapStateToProps参数,那样的话,UI 组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。

8.5 mapDispatchToProps() #

mapDispatchToProps是connect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。 如果mapDispatchToProps是一个函数,会得到dispatch和ownProps(容器组件的props对象)两个参数。

function mapDispatchToProps(dispatch) {
  return {
    onIncreaseClick: () => dispatch(increaseAction)
  }
}

从上面代码可以看到,mapDispatchToProps作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。

8.6 组件 #

connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。 一种解决方法是将state对象作为参数,传入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级级将state传下去就很麻烦。 React-Redux 提供Provider组件,可以让容器组件拿到state。

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import counter from './reducers'
import CounterApp from './components/CounterApp'

let store = createStore(counter);

render(
  <Provider store={store}>
    <CounterApp />
  </Provider>,
  document.getElementById('root')
)

上面代码中,Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。 它的原理是React组件的context属性,请看源码。

class Provider extends Component {
  getChildContext() {
    return {
      store: this.props.store
    };
  }
  render() {
    return this.props.children;
  }
}

Provider.childContextTypes = {
  store: React.PropTypes.object
}

上面代码中,store放在了上下文对象context上面。然后,子组件就可以从context拿到store,代码大致如下。

import React from 'react';
export function connect(mapStateToProps, mapDispatchToState) {
    return (component) => {
        class Proxy extends React.Component {
            constructor() {
                super();
                this.state = mapStateToProps(this.context.store.getState());
            }

            componentWillMount() {
                this.unsubscribe = this.context.store.subscribe(() => {
                    this.setState({...mapStateToProps(this.context.store.getState())});
                })
            }

            componentWillUnMount() {
                this.unsubscribe();
            }

            return() {
                return
                <component {...this.state} {...mapDispatchToState(this.context.store.dispatch)}></component>
            }
        }
        return Proxy;
    }
}

React-Redux自动生成的容器组件的代码,就类似上面这样,从而拿到store。

8.7 react-redux计数器 #

class Counter extends Component {
  render() {
    const { value, onIncreaseClick } = this.props
    return (
      <div>
        <span>{value}</span>
        <button onClick={onIncreaseClick}>Increase</button>
      </div>
    )
  }
}
function mapStateToProps(state) {
  return {
    value: state.count
  }
}
const increaseAction = { type: 'increase' }

function mapDispatchToProps(dispatch) {
  return {
    onIncreaseClick: () => dispatch(increaseAction)
  }
}

const CounterApp = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)
// Reducer
function counter(state = { count: 0 }, action) {
  const count = state.count
  switch (action.type) {
    case 'increase':
      return { count: count + 1 }
    default:
      return state
  }
}

const store = createStore(counter);

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

https://github.com/zhufengnodejs/zhufeng-redux-lessons