redux源码解读(简单易懂版)

863 阅读6分钟

写这篇文章是因为我所有能搜索到的文章都太!复!杂!了!,一上来就做了个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))
}

综上,这些看似高大上的插件只是把一些麻烦的方法给你封装好了,如果你不喜欢的话,不用也是完全没有问题的。