寫這篇文章是因為我所有能搜尋到的文章都太!復!雜!了!,一上來就做了個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))
}
複製程式碼
綜上,這些看似高大上的外掛只是把一些麻煩的方法給你封裝好了,如果你不喜歡的話,不用也是完全沒有問題的。