Redux的特點
- 統一的狀態管理,一個應用中只有一個倉庫(store)
- 倉庫中管理了一個狀態樹(statetree)
- 倉庫不能直接修改,修改只能通過派發器(dispatch)派發一個動作(action)
- 更新state的邏輯封裝到reducer中
Redux能做什麼?
隨著JavaScript單頁應用開發日趨複雜,管理不斷變化的state非常困難,Redux的出現就是為了解決state裡的資料問題。在React中,資料在元件中是單向流動的,資料從一個方向父元件流向子元件(通過props),由於這個特徵,兩個非父子關係的元件(或者稱作兄弟元件)之間的通訊就比較麻煩
redux中各物件的說明
store
store是一個資料倉儲,一個應用中store是唯一的,它裡面封裝了state狀態,當使用者想訪問state的時候,只能通過store.getState來取得state物件,而取得的物件是一個store的快照,這樣就把store物件保護起來。
action
action描述了一個更新state的動作,它是一個物件,其中type屬性是必須有的,它指定了某動作和要修改的值:
{type: UPDATE_TITLE_COLOR, payload: 'green'}
複製程式碼
actionCreator
如果每次派發動作時都寫上長長的action物件不是很方便,而actionCreator就是建立action物件的一個方法,呼叫這個方法就能返回一個action物件,用於簡化程式碼。
dispatch
dispatch是一個方法,它用於派發一個動作action,這是唯一的一個能夠修改state的方法,它內部會呼叫reducer來呼叫不同的邏輯基於舊的state來更新出一個新的state。
reducer
reducer是更新state的核心,它裡面封裝了更新state的邏輯,reducer由外界提供(封裝業務邏輯,在createStore時傳入),並傳入舊state物件和action,將新值更新到舊的state物件上返回。
使用redux的流程
- 定義動作型別:
const INCREAMENT='INCREAMENT';
複製程式碼
- 定義專案的預設狀態,傳入reducer
let initState={...};
function reducer(state=initState,action){
//...
}
複製程式碼
- 編寫reducer,實現更新state的具體邏輯
function reducer(state=initState,action){
let newState;
switch(action.type){
//...
}
return newState;
}
複製程式碼
- 建立容器,傳入reducer
let store=createStore(reducer);
複製程式碼
- 訂閱需要的方法,當state改變會自動更新
store.subcribe(function(){});
複製程式碼
- 在需要更新state的地方呼叫dispatch即可
store.dispatch(/*某個action*/);
複製程式碼
可以看到通過以上幾個步驟,就可以使用redux,且不侷限於某種“框架”中,redux是一個設計思想,只要符合你的需求就可以使用redux。
在React中使用Redux
以下編寫一個待辦事項的小功能,描述如下:
- 可以讓使用者新增待辦事項(todo)
- 可以統計出還有多少項沒有完成
- 使用者可以勾選某todo置為已完成
- 可篩選檢視條件(顯示全部、顯示已完成、顯示未完成)
小專案的目錄結構:
專案根結點
┗━ 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個:
- TodoHeader 用於展示未辦數量和新增待辦
- TodoList 按條件展示待辦項列表
- TodoFooter 功能按鈕(顯示全部、未完成、已完成)
統計未完成的事項
此功能的核心就是把所有“未完成”的數量統計出來,在編寫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的原始碼,其實也並不複雜。