React里父子组件可以通过props通信,兄弟组件通信需要把数据传递给父组件,再由父组件传递给另一个子组件。以兄弟组件通信为需求,写一个Redux。
问题
兄弟组件通信
- 首先分析这个需求,点击button,改变数字,Number组件重新渲染。
- 可抽象为,派发一个动作,改变状态,执行方法。
- 根据上两步分析,可以看出组件通信的核心是动作(action)、执行方法(reducer)、状态(state)
- action、reducer
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
代码语言:javascript复制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的中间件原理一样。
开发中会有多个中间件,中间件是函数,要把第一个中间件的结果作为参数传递给第二个中间件,依次执行,先实现这个compose函数
代码语言:javascript复制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原理。