隨着JavaScript單頁應用開發日趨複雜,管理不斷變化的state很是困難,Redux的出現就是爲了解決state裏的數據問題。在React中,數據在組件中是單向流動的,數據從一個方向父組件流向子組件(經過props),因爲這個特徵,兩個非父子關係的組件(或者稱做兄弟組件)之間的通訊就比較麻煩javascript
store是一個數據倉庫,一個應用中store是惟一的,它裏面封裝了state狀態,當用戶想訪問state的時候,只能經過store.getState來取得state對象,而取得的對象是一個store的快照,這樣就把store對象保護起來。html
action描述了一個更新state的動做,它是一個對象,其中type屬性是必須有的,它指定了某動做和要修改的值:java
{type: UPDATE_TITLE_COLOR, payload: 'green'}
複製代碼
若是每次派發動做時都寫上長長的action對象不是很方便,而actionCreator就是建立action對象的一個方法,調用這個方法就能返回一個action對象,用於簡化代碼。node
dispatch是一個方法,它用於派發一個動做action,這是惟一的一個可以修改state的方法,它內部會調用reducer來調用不一樣的邏輯基於舊的state來更新出一個新的state。react
reducer是更新state的核心,它裏面封裝了更新state的邏輯,reducer由外界提供(封裝業務邏輯,在createStore時傳入),並傳入舊state對象和action,將新值更新到舊的state對象上返回。redux
const INCREAMENT='INCREAMENT';
複製代碼
let initState={...};
function reducer(state=initState,action){
//...
}
複製代碼
function reducer(state=initState,action){
let newState;
switch(action.type){
//...
}
return newState;
}
複製代碼
let store=createStore(reducer);
複製代碼
store.subcribe(function(){});
複製代碼
store.dispatch(/*某個action*/);
複製代碼
能夠看到經過以上幾個步驟,就可使用redux,且不侷限於某種「框架」中,redux是一個設計思想,只要符合你的需求就可使用redux。數組
如下編寫一個待辦事項的小功能,描述以下:bash
小項目的目錄結構:架構
項目根結點
┗━ components 存放組件
┗━ todo-header.js
┗━ todo-list.js
┗━ todo-footer.js
┗━ index.js
┗━ store 保存redux的相關文件
┗━ actions 定義action
┗━ action-type 定義動做類型
┗━ action-types.js
┗━ index.js
┗━ reducers 定義reducer
┗━ index.js
┗━ index.js 默認文件用於導出store
┗━ index.html 模版頁面
複製代碼
以上4個功能咱們使用redux結合react來實現。框架
組件拆分爲3個:
此功能的核心就是把全部「未完成」的數量統計出來,在編寫redux程序時,首先定義好默認state,默認state是寫在reducer中的:
//定義默認狀態
let initState = {
todos: [
{
id: parseInt(Math.random() * 10000000),
isComplete: false,
title: '學習redux'
}, {
id: parseInt(Math.random() * 10000000),
isComplete: true,
title: '學習react'
}, {
id: parseInt(Math.random() * 10000000),
isComplete: false,
title: '學習node'
}
]
};
複製代碼
在reducer目錄下建立一個index.js,因爲這4個功能點過於簡單,沒必要拆分爲多個reducer,所以全部的功能都在這一個index.js文件中完成。這樣還能夠減小combineReducer這個步驟。
以上定義了一個默認state對象,它裏面有3條數據,描述了待辦事項的內容。
因爲目前沒有具體的功能邏輯,咱們建立一個空的reducer:
function reducer(state=initState,action){
return state;
}
export default reducer;
複製代碼
能夠看到,傳入了默認的initState,這樣就能夠基於舊的state對象來做更新,每次reducer都會根據原state更新出一個新的state返回。
以後就能夠建立倉庫(store),引用剛剛寫好的reducer,並把store返回給頂層組件使用:
import {createStore} from 'redux';
import reducer from './reducers';
let store = createStore(reducer);//傳入reducer
export default store;
複製代碼
在store目錄下的index.js默認導出store對象,方便組件引入。
在根組件中引入store對象,它是全部組件的容器,所以它要作全部組件的store提供者的角色,因此它的任務要把store提供給全部子組件使用,這就須要react-redux包提供的一個組件:Provider
:
Provider也是一個組件,它只有一個屬性:store
,傳入建立好的store對象便可:
import {Provider} from 'react-redux';
import store from '../store';
//其它代碼略...
ReactDOM.render(<Provider store={store}> <div> <TodoHeader/> <TodoList/> <TodoFooter/> </div> </Provider>, document.querySelector('#root'));
複製代碼
這樣就意味着Provider
包裹的全部組件均可合法的取到store。
如今數據已經提供,還須要子組件來接收,一樣接收store數據react-redux包也爲咱們提供了一個方法:connect
。
connect這個方法很是奇妙,它的功能很是強大,它能夠把倉庫中state數據注入到組件的屬性(this.props)中,這樣子組件就能夠經過屬性的方式拿到倉庫中的數據。 首先定義一個頭組件,用於顯示未完成的數量:
import React from 'react';
import ReactDOM from 'react-dom';
class TodoHeader extends React.Component {
//代碼略...
}
複製代碼
下面使用connect方法將state數據注入到TodoHeader組件中:
import {connect} from 'react-redux';
let ConnectedTodoHeader = connect((state) => ({
...state
}))(TodoHeader);
複製代碼
能夠看到它的寫法很怪,connect是一個高階函數(函數返回函數),它的最終返回值是一個組件,這個組件(ConnectedTodoHeader)最終「鏈接」好了頂層組件Provider提供的store數據。
connect的第一個參數是一個函數,它的返回是一個對象,返回的對象會綁定到目標組件的屬性上,函數參數state就是store.getState的返回值,使用它就能夠取到全部state上的數據,目前state就是todos的3條待辦事項
而高階函數傳入的參數就是要注入的組件,這裏是TodoHeader,這樣在TodoHeader組件中就能夠經過this.props.todos
取到待辦事項的數據。
這樣就能夠編寫好咱們的第一個統計功能,下面附上代碼:
class TodoHeader extends React.Component {
//取得未完成的todo數量
getUnfinishedCount() {
//this.props.todos就是從connect傳入的state數據
return this.props.todos.filter((i) => {
return i.isComplete === false;
}).length;
}
render() {
return (<div> <div>您有{this.getUnfinishedCount()}件事未完成</div> </div>);
}
}
//導出注入後的組件
export default connect((state) => ({
...state//此時的state就是todos:[...]數據
}))(TodoHeader);
複製代碼
能夠看到,經過connect取得state注入到組件屬性上,便可編寫邏輯完成功能。
接下來完成添加待辦項的功能,用戶在一個文本框中輸入待辦項,把數據添加到倉庫中,並更新視圖。
因爲有用戶的操做了,咱們須要編寫動做(Action),Action須要一個具體的動做類型,咱們在action-types.js中建立須要動做類型便可:
//添加待辦事項
export const ADD_TODO = 'ADD_TODO';
複製代碼
能夠看到它很是簡單,就定義了一個動做類型,也就是一個描述Action動做的指令,導出它給reducer來使用。
接下來編寫ActionCreator,它是一個函數,只返回用剛剛這個指令生成的Action對象:
import {ADD_TODO} from './action-type/action-types';
let actions = {
addTodo: function(payload) {
return {type: ADD_TODO, payload};
}
};
export default actions;//導出ActionCreators
複製代碼
能夠看到引入了action-type,addTodo返回了一個形如{type:XXX, payload:XXX}
的一個Action對象。這就是一個標準的Action對象的形式,第二個參數payload就是用戶傳入的參數。
注意在導出時必定要將ActionCreator函數包到一個對象中返回,這樣redux內部會經過bindActionCreators將dispatch的功能封裝到每一個函數中,這樣在connect鏈接時極大的方便了用戶的操做,稍候會看到。
下面編寫reducer,它裏面封裝了「添加待辦項」的邏輯:
import {ADD_TODO} from '../actions/action-type/action-types';
//部分代碼略...
function reducer(state = initState, action) {
let newState;
switch (action.type) {
case ADD_TODO:
newState = {
todos: [
...state.todos,
action.payload
]
};
break;
default:
newState = state;
break;
}
return newState;
}
複製代碼
以上經過switch語句的一個分支,判斷動做類型是否是「添加待辦」這個功能(ADD_TODO),這樣在原state對象的基礎上追加這條數據便可。
注意,每次reducer都返回一個新的對象,不要直接在原state.todos.push這條數據,由於reducer是一個純函數。
...
是ES6的寫法,意爲展開運算符,它是將原state.todos的數據展開,並在後面添加一條新數據,至關於合併操做。
好了,到此處理數據的部分已經寫好,又到了注入組件的工做了,建立展現待辦的組件TodoList:
import React from 'react';
import {connect} from 'react-redux';
class TodoList extends React.Component {
//代碼略...
}
export default connect((state) => ({
...state
}))(TodoList);
複製代碼
再次經過connect方法將state數據注入到組件 (TodoList)的屬性上,讓組件內部能夠經過this.props取得state數據。
下面編寫展現待辦項的功能:
class TodoList extends React.Component {
getTodos() {
return this.props.todos.map((todo, index) => {
return (<li key={index}> <input type="checkbox" checked={todo.isComplete}/> { todo.isComplete ? <del>{todo.title}</del> : <span>{todo.title}</span> } <button type="button" data-id={todo.id}>刪除</button> </li>); }); } render() { return (<div> <ul> {this.getTodos()} </ul> </div>); } } 複製代碼
在組件中定義一個getTodos方法用於循環全部待辦項,能夠看到經過this.props.todos便可拿到connect傳入的數據,並在render中調用getTodos渲染便可。
如今能夠初探整個小項目的邏輯,咱們取數據再也不是經過一層一層的組件傳遞了,而是全部的數據操做都交由redux來解決,組件只負責展現數據。
接下來實現更改一條待辦項的狀態,當用戶給一條待辦打勾就記爲已完成,不然置爲未完成。
仍是同樣,新建一個action-type:
//更改待辦項的完成狀態
export const TOGGLE_COMPLETE = 'TOGGLE_COMPLETE';
複製代碼
建立actionCreator,引入這個action-type:
let actions = {
//更改完成狀態,此處payload傳id
toggleComplete: function(payload) {
return {type: TOGGLE_COMPLETE, payload};
}
//其它略...
};
複製代碼
因爲用戶勾選一條記錄,應傳入id做爲惟一標識,所以這裏的payload參數就是待辦項的id。
payload並非必定要叫payload能夠更改變量名,如todoId
,redux中管個這變量叫載荷,所以這裏使用payload。
一樣在reducer中再加一個swtich分支,判斷TOGGLE_COMPLETE:
function reducer(state = initState, action) {
let newState;
switch (action.type) {
case TOGGLE_COMPLETE:
newState = {
//循環每一條待辦,把要修改的記錄更新
todos: state.todos.map(item => {
if (item.id == action.payload) {
item.isComplete = !item.isComplete;
}
return item;
})
};
break;
//其它代碼略...
default:
newState = state;
break;
}
return newState;
}
複製代碼
能夠看到此次是修改某一條記錄的isComplete屬性,所以使用map函數循環,找到id爲action.payload的那一條,修改isComplete的狀態。
仍要注意,不要使用slice函數去修改原state,必定要返回一個基於state更新後的新對象,map函數的執行結果就是返回一個新數組,所以使用map符合這裏的需求。
接下來爲組件的checkbox元素添加事件,當用戶勾選時,調用對應的Action toggleComplete動做便可完成邏輯:
//引入actionCreators
import actions from '../store/actions';
//其它 代碼略...
class TodoList extends React.Component {
todoChange = (event) => {
//當onChange事件發生時,調用toggleComplete動做
this.props.toggleComplete(event.target.value);
}
getTodos() {
return this.props.todos.map((todo, index) => {
return (<li key={index}> <input type="checkbox" value={todo.id} checked={todo.isComplete} onChange={this.todoChange}/> { todo.isComplete ? <del>{todo.title}</del> : <span>{todo.title}</span> } <button type="button" data-id={todo.id}>刪除</button> </li>); }); } render() { //略... } } export default connect((state) => ({ ...state }), actions)(TodoList); //第二個參數傳入actionCreators 複製代碼
這裏的connect函數傳入了第二個參數,它是一個actionCreator對象,同理因爲組件中須要調用Action派發動做以實現某個邏輯,好比這裏就是組件須要更新待辦項的狀態,則「功能」也是由redux傳給組件的。
這樣組件裏的this.props就能夠拿到actionCreator的方法,以調用邏輯: this.props.toggleComplete()
。
如今能夠看到connect函數的強大之處,不論是數據state和功能actionCreators,都是由redux傳給須要調用的組件。redux在內部自動處理了更新組件、數據傳遞的工做,咱們開發者沒必要再爲組件之間的通訊花費精力了。
咱們的從此的工做就是按照redux的架構定義好動做(Action)和reducer,也就是業務邏輯,而其它繁複的工做都由redux來完成。
刪除待辦項的功能相似,再也不詳述。
篩選查看條件須要預先定義好3個狀態,即查看所有(all)只查看未完成(uncompleted)和查看已完成(completed)。
所以,咱們修改初始化的狀態,讓它默認爲「查看所有」:
//定義默認狀態
let initState = {
//display用於控制待辦項列表的顯示
display:'all',
todos: [
//略...
]
};
複製代碼
一樣的套路,建立action-type:
//更改顯示待辦項的狀態
export const CHANGE_DISPLAY = 'CHANGE_DISPLAY';
複製代碼
建立actionCreator:
//部分代碼略...
let actions = {
//更改顯示待辦項的狀態,
//payload爲如下3個值(all,uncompleted,completed)
changeDisplay: function(payload) {
return {type: CHANGE_DISPLAY, payload};
}
};
複製代碼
爲reducer增長CHANGE_DISPLAY的邏輯:
//部分代碼略...
function reducer(state = initState, action) {
let newState;
switch (action.type) {
case CHANGE_DISPLAY:
newState = {
display: action.payload,
todos: [...state.todos]
};
break;
default:
newState = state;
break;
}
return newState;
}
複製代碼
在組件中,根據display條件過濾待辦項的數據便可,這裏抽出一個方法filterDisplay
來實現:
class TodoList extends React.Component {
//按display條件過濾數據
filterDisplay() {
return this.props.todos.filter(item => {
switch (this.props.display) {
case 'completed':
return item.isComplete;
case 'uncompleted':
return !item.isComplete;
case 'all':
default:
return true;
}
});
}
getTodos() {
return this.filterDisplay().map((todo, index) => {
//略...
});
}
render() {
//略...
}
}
export default connect((state) => ({
...state
}), actions)(TodoList);
複製代碼
以上仍是由connect方法注入數據到組件,根據狀態的display條件過濾出符合條件的數據便可。
到此,所有的功能已實現。
運行效果:
這個例子雖簡單卻完整的展現了redux的使用,真正項目開發時只要遵循redux的「套路」便可。
要需瞭解redux的更深層邏輯原理,就要讀redux的源碼,其實也並不複雜。