redux原始碼解讀(簡單易懂版)

一塊晶片發表於2018-10-09

寫這篇文章是因為我所有能搜尋到的文章都太!復!雜!了!,一上來就做了個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))
}
複製程式碼

綜上,這些看似高大上的外掛只是把一些麻煩的方法給你封裝好了,如果你不喜歡的話,不用也是完全沒有問題的。

相關文章