写这篇文章是因为我所有能搜索到的文章都太!复!杂!了!,一上来就做了个todo list,并且使用了一大堆react-redux已经封装好的方法,所有的一切对我来说都是黑盒的,并且藕合度非常低,我根本不知道为什么这样写最后就会那样,有时候甚至这样写根本不能得到那样的结果,然而由于我并不知道中间发生了什么,所以只能去网上搜到底哪里出了错,往往还搜不到解决方案!!所以这里写了个最简单的例子,并且一步一步从原始写法到封装写法,以便理解封装那些方法的作用。
先搭建最简单的redux
redux的作用无需多言,因为react只负责view的部分,而不管理组件间的交互和时间传递,redux就是来解决这一问题的。
redux组成部分
创建一个redux需要先定义一些静态的东西,就好比我们现在要用乐高搭个变形金刚,先把你要的形状的乐高准备好。1.数据 2.动作 3.处理数据用的方法
1 . 创建初始state(数据)
state里你可以放任何你需要的数据,他就是个普通对象
const initialState={
name:'test',
count:1
}
2 . 创建actions (动作)
action描述要发起的动作类型,以及完成这个动作需要的参数,所以type是action必要的属性,其他的属性就随意定义你需要的,这里我定义了三个动作,加(addCount),减(minusCount),改变name(changeName)
//描述动作类型的常量
const ADD_COUNT='ADD_COUNT';
const MINUS_COUNT='MINUS_COUNT';
const CHANGE_NAME='CHANGE_NAME';
//actions
//加动作
function addCount(count){
return {
type:ADD_COUNT,
count: count
}
}
//减动作
function minusCount(count){
return {
type:MINUS_COUNT,
count:count
}
}
//改变name
function changeName(name){
return {
type:'CHANGE_NAME',
name:name
}
}
3 . reducer (处理数据用的方法)
reducer处理action动作并返回新的state
//reducer
function countReducer(state = initialState, action) {
switch (action.type) {
case ADD_COUNT:
return Object.assign({},state,{count:state.count+action.count});
case MINUS_COUNT:
return Object.assign({},state,{count:state.count-action.count});
case CHANGE_NAME
return Object.assign({},state,{name:action.name});
default:
return Object.assign({},state);
}
}
现在,所有的乐高都准备好了,我们可以“组装”了。
4 .store(组装)
redux提供一个createStore方法,利用我们刚定义的state和reducer来生成一个store
import { createStore} from 'redux';
let store= createStore(countReducer,initialState);
store提供四个方法dispach,subscribe,getState,replaceReducer 分别用来触发动作,订阅动作,获取当前的state,更换reducer。
5 . 结合react
react可以看成redux的使用者,他需要用redux来发起事件,订阅事件。
下面定义了两个组件App 和 Count。其中Count是App的子组件,在render中把store作为props传递给App,使得App可以使用store提供的方法.
import {render} from 'react-dom';
import {Component} from 'react';
class Count extends Component{
constructor(props){
super(props);
}
render(){
return(
<div >
Hello, Im a count: {this.props.count}
</div>
);
}
}
class App extends Component{
constructor(props){
super(props);
this.state=this.props.store.getState();
}
componentDidMount(){
let _this = this;
let store=this.props.store
//订阅store发起的所有事件,获取新的state用来更新自身的state
store.subscribe(function(){
_this.setState(store.getState());
});
}
add(count){
//发起addCount事件
this.props.store.dispatch(addCount(count));
}
render(){
return (
<div>
<Count store={this.store} count={this.state.count}/>
<button onClick={this.add.bind(this,3)}>add</button>
</div>
)
}
}
let store= createStore(countReducer,initialState);
render(
<App store={store}/>
,
document.getElementById('container')
);
到这里,我们的变形金刚已经可以动起来了,可以发起事件,也可以订阅事件并更新界面。不出意外的话,每次点击add页面上的数字都能加3了呢。
问题
但是现在还有些问题:
1.如果count的子组件需要使用store,我们得把store作为子组件的props层层传递下去
2.现在App可以通过store拿到state中所有的值,也就是state中有任何更新都会导致App重新渲染,但在实际项目中,一个react组件往往只需要state中的某些值。当然你可以在subscribe中拿到新的state后判断是否需要的属性发生了改变,然后再去更新界面来规避这个问题,现在,有个插件react-redux把这些都做好了。
redux-react提供了两个方法Provider,connect。Provider有一个必要的参数store,它使得所有通过connect生成的子组件能从props中获得store提供的方法。
首先改写App和render,将App作为Provider的子组件,并使用connect对原来的App进行改装
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {connect} from 'react-redux';
class App extends Component{
addCount(count){
//dispatch是被connect注入到props中的
this.props.dispatch(addCount(count))
}
changeName(name){
this.props.dispatch(changeName(name));
}
render(){
console.log('app render');
return (
<div>
<Count count={this.props.count}/>
<button onClick={this.addCount.bind(this,3)}>add</button>
<button onClick={this.changeName.bind(this,'hello')}>changeName</button>
</div>
)
}
}
//把state中的值注入到组件的props中
function mapStateToProps(state){
return {
//仅把count放到App的props中,在App中就可以使用this.props.count来访问count了
//并且只有当state中的count发生改变时才会引起app的重新渲染
count:state.count;
}
}
//用connect生成的新组件覆盖原App,实际上我们在Provider里使用的是这个App,这一点非常重要
App=connect(mapStateToProps)(App);
let store= createStore(countReducer,initialState);
render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('container')
)
其中mapStateToProps方法以之前定义的state作为入参,返回值可以是整个state,也可以是你需要的部分数据,这里仅仅把count传递给了props,只有count值改变才会引起App的重新渲染,另外这个例子加了一个changeName的按钮,并在render方法里打了log来观察App的重新渲染,点击changeName导致了name值改变,但可以在调试窗口里看见并没有打印“app render”,说明name的改变并没有引起App的重新渲染。
通过connect我们已经能够在App中使用this.props.dispatch来发起事件了,但是我们并没有和一开始一样在App中写subscribe订阅事件,却依然能监听到addCount,这是因为connect把订阅事件也封装好了,它的源码是这样的:
Connect.prototype.componentDidMount = function componentDidMount() {
this.trySubscribe();
};
trySubscribe中做了比较计算,只有被mapStateToProps映射到props上的值改变时,才会做setState操作来发起重新渲染。
另外,connect还提供了mapDispatchToProps方法把dispatch事件传递给props:
function mapDispatchToProps(dispatch){
return {
addCount:function(count){
dispatch(addCount(count));
}
}
}
App=connect(mapStateToProps,mapDispatchToProps)(App);
这和在App中直接写add方法是一样的,这样我们就可以在App中使用onClick={this.props.addCount}来发起事件了,另外redux还提供一个方法bindActionCreators,把dispatch也给封装好了,所以上面的mapDispatchToProps还可以写成这样:
import {bindActionCreators} from 'redux';
function mapDispatchToProps(dispatch){
return {
addCount:bindActionCreators(addCount,dispatch)
}
}
这样看来bindActionCreators这个方法用处似乎不大,但是当你有一组事件都要放入组件的props时,用它就方便很多,你只要把所有定义好的action放在一个对象中传递给他就好了:
function mapDispachToProps(dispatch){
//actions是一个对象,里面包含了一组action
return bindActionCreators(actions,dispatch);
}
bindActionCreators的源码非常简单,当actions是一个对象,里面包含一组动作,bindActionCreators就返回一个对象,类似这样:
{
addCount:function(){
return dispatch(actions.addCount.apply(undefined, arguments))
},
minusCount:function(){
return dispatch(actions.minusCount.apply(undefined, arguments))
}
}
当actions只是一个单独的动作,比如addCount,bindActionCreators的返回结果是一个方法:
function(){
return dispatch(addCount.apply(undefined, arguments))
}
综上,这些看似高大上的插件只是把一些麻烦的方法给你封装好了,如果你不喜欢的话,不用也是完全没有问题的。