新聞中心
?:既然不需要,為什么需要了解?

創(chuàng)新互聯(lián)公司是專業(yè)的思茅網(wǎng)站建設(shè)公司,思茅接單;提供成都網(wǎng)站建設(shè)、網(wǎng)站建設(shè),網(wǎng)頁(yè)設(shè)計(jì),網(wǎng)站設(shè)計(jì),建網(wǎng)站,PHP網(wǎng)站建設(shè)等專業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進(jìn)行思茅網(wǎng)站開(kāi)發(fā)網(wǎng)頁(yè)制作和功能擴(kuò)展;專業(yè)做搜索引擎喜愛(ài)的網(wǎng)站,專業(yè)的做網(wǎng)站團(tuán)隊(duì),希望更多企業(yè)前來(lái)合作!
?:不了解你怎么知道不需要呢?
?:這話沒(méi)毛病,學(xué)它!
Redux 是一個(gè)可預(yù)測(cè)的狀態(tài)管理容器,也是 react 中最流行的一個(gè)狀態(tài)管理工具,無(wú)論是工作或面試只要你使用了 react 都需要掌握它。核心理念是在全局維護(hù)了一個(gè)狀態(tài),稱為 store,為應(yīng)用系統(tǒng)提供了全局狀態(tài)管理的能力,使得跨組件通信變得更簡(jiǎn)單。
Redux 抽象程度很高,關(guān)注的是 “哲學(xué)設(shè)計(jì)”,開(kāi)發(fā)者最關(guān)心的是 “如何實(shí)現(xiàn)”,做為初學(xué)者盡管看了官網(wǎng) API 介紹但面對(duì)實(shí)際項(xiàng)目時(shí)還是發(fā)現(xiàn)無(wú)從入手,特別是面對(duì)一些新名詞 store、state、action、dispatch、reducer、middlwware 時(shí),有些小伙伴表示我就認(rèn)識(shí) state...
本篇在介紹一些 Redux 的概念后會(huì)重構(gòu)上一節(jié) useReducer + useContext 實(shí)現(xiàn)的 Todos,介紹如何在 React 中應(yīng)用 Redux,從實(shí)踐中學(xué)習(xí)。
Redux 數(shù)據(jù)流轉(zhuǎn)過(guò)程
Redux 通過(guò)一系列規(guī)范約定來(lái)約束應(yīng)用程序如何根據(jù) action 來(lái)更新 store 中的狀態(tài),下圖展示了 Redux 數(shù)據(jù)流轉(zhuǎn)過(guò)程,也是 Redux 的主要組成部分:
- View:Redux 不能單獨(dú)工作,需要結(jié)合 React/Vue/Angular 等 View 層框架工作,通常 Redux 主要應(yīng)用于 React 框架中,渲染時(shí)頁(yè)面從 Redux store 中獲取數(shù)據(jù)渲染展現(xiàn)給用戶。
- Action:當(dāng)頁(yè)面想改變 store 里的數(shù)據(jù),通過(guò) dispatch 方法派發(fā)一個(gè) action 給 store(例如,請(qǐng)求接口響應(yīng)之后派發(fā) action 改變數(shù)據(jù)狀態(tài)),這里的 action 是 store 唯一的信息來(lái)源,做為一個(gè)信息的載體存在。
- Store:store 是鏈接 action 和 reducer 的橋梁,它在收到 action 后會(huì)把之前的 state 和 action 一起發(fā)給 reducer。
- Reducer:reducer 主要責(zé)任是計(jì)算下一個(gè)狀態(tài),因此它在接收到之前的 state 和 action 之后會(huì)返回新的數(shù)據(jù)給到 store(這里要保證 reducer 是一個(gè)純函數(shù)),最終 store 更新自己數(shù)據(jù)告訴頁(yè)面,回到 View 層頁(yè)面自動(dòng)刷新。
圖片來(lái)源:redux application data flow
Immutable
在 reducer 純函數(shù)中不允許直接修改 state 對(duì)象,每次都應(yīng)返回一個(gè)新的 state。原生 JavaScript 中我們要時(shí)刻記得使用 ES6 的擴(kuò)展符 ... 或 Object.assign() 函數(shù)創(chuàng)建一個(gè)新 state,但是仍然是一個(gè)淺 copy,遇到復(fù)雜的數(shù)據(jù)結(jié)構(gòu)我們還需要做深拷貝返回一個(gè)新的狀態(tài),總之你要保證每次都返回一個(gè)新對(duì)象,一方面深拷貝會(huì)造成性能損耗、另一方面難免會(huì)忘記從而直接修改原來(lái)的 state。
Immutable 數(shù)據(jù)一旦創(chuàng)建,對(duì)該數(shù)據(jù)的增、刪、改操作都會(huì)返回一個(gè)新的 immutable 對(duì)象,保證了舊數(shù)據(jù)可用同時(shí)不可變。
Immutable 實(shí)現(xiàn)的原理是 Persistent Data Structure(持久化數(shù)據(jù)結(jié)構(gòu)),也就是使用舊數(shù)據(jù)創(chuàng)建新數(shù)據(jù)時(shí),要保證舊數(shù)據(jù)同時(shí)可用且不變。同時(shí)為了避免 deepCopy 把所有節(jié)點(diǎn)都復(fù)制一遍帶來(lái)的性能損耗,Immutable 使用了 Structural Sharing(結(jié)構(gòu)共享),即如果對(duì)象樹(shù)中一個(gè)節(jié)點(diǎn)發(fā)生變化,只修改這個(gè)節(jié)點(diǎn)和受它影響的父節(jié)點(diǎn),其它節(jié)點(diǎn)則進(jìn)行共享。參考 Immutable 詳解及 React 中實(shí)踐
請(qǐng)看下面動(dòng)畫(huà):
在本文中整個(gè) redux store 狀態(tài)樹(shù)都采用的是 Immutable 數(shù)據(jù)對(duì)象,同時(shí)使用時(shí)也應(yīng)避免與普通的 JavaScript 對(duì)象混合使用,從下面例子中可以學(xué)習(xí)到一些常用的 API 使用,更詳細(xì)的介紹參考官網(wǎng)文檔 immutable-js.com/docs。
項(xiàng)目結(jié)構(gòu)
React + Redux 項(xiàng)目的組織結(jié)構(gòu),在第一次寫(xiě)項(xiàng)目時(shí)也犯了困惑,你如果在社區(qū)中搜索會(huì)發(fā)現(xiàn)很多種聲音,例如,按類型劃分(類似于 MVC 這樣按不同的角色劃分)、頁(yè)面功能劃分、Ducks(將 actionTypes、actionCreators、reducer 放在一個(gè)文件里),這里每一種的區(qū)別也可以單獨(dú)寫(xiě)篇文章討論了,本節(jié)采用的方式是按頁(yè)面功能劃分,也是筆者剛開(kāi)始寫(xiě) React + Redux 時(shí)的一種目錄組織方式。沒(méi)有最佳的方式,選擇適合于你的方式。
按照一個(gè)頁(yè)面功能對(duì)應(yīng)一個(gè)文件夾劃分,pages/todos 文件夾負(fù)責(zé)待辦事項(xiàng)功能,如果頁(yè)面復(fù)雜可在頁(yè)面組件內(nèi)創(chuàng)建一個(gè) pages/todos/components 文件夾,redux 相關(guān)的 action、reducer 等放在 page/todos/store 文件夾中。
src/
├── App.css
├── App.js
├── index.css
├── index.js
├── components
├── pages
│ └── todos
│ ├── components
│ │ ├── Todo.jsx
│ │ └── TodoAdd.jsx
│ ├── index.jsx
│ └── store
│ ├── actionCreators.js
│ ├── constants.js
│ ├── index.js
│ └── reducer.js
├── reducers
│ └── todos-reducer.js
├── routes
│ └── index.js
└── store
├── index.js
└── reducer.js
Todos View 層展示組件
View 層我們定義為 “展示組件”,負(fù)責(zé) UI 渲染,至于渲染時(shí)用到的數(shù)據(jù)如何獲取交由后面的容器組件負(fù)責(zé)。以下 Todo、TodoAdd、Todos 三個(gè)組件中使用到的數(shù)據(jù)都是從 props 屬性獲取,后面容器組件鏈接 React 與 Redux 時(shí)會(huì)再講。
Todo 組件
組件位置src/pages/todos/components/Todo.jsx
import { useState } from "react";
/**
* Todo component
* @param {Number} props.todo.id
* @param {String} props.todo.content
* @param {Function} props.editTodo
* @param {Function} props.removeTodo
* @returns
*/
const Todo = ({ todo, editTodo, removeTodo }) => {
console.log('Todo render');
const [isEdit, setIsEdit] = useState(false);
const [content, setContent] = useState(todo.get('content'));
return
{
!isEdit ? <>
{todo.get('content')}
> : <>
setContent(e.target.value) } />
>
}
}
export default Todo;Todos 組件
組件位置src/pages/todos/index.jsx。
import { useState } from "react";
import { actionCreators } from '../store';
/**
* Add todo component
* @param {Function} props.addTodo
* @returns
*/
const TodoAdd = ({ addTodo }) => {
console.log('TodoAdd render');
const [content, setContent] = useState('');
return
setContent(e.target.value)} />
};
export default TodoAdd;唯一數(shù)據(jù)源Store
一個(gè) React + Redux 的應(yīng)用程序中只有一個(gè) store,是應(yīng)用程序的唯一數(shù)據(jù)源,類似于在我們應(yīng)用中抽象出一個(gè)狀態(tài)樹(shù),與組件一一關(guān)聯(lián)。這也是一種集中式管理應(yīng)用狀態(tài)的方式,也是和 React hooks 提供的 useState/useReducer 一個(gè)重大區(qū)別之處。
創(chuàng)建 store
通過(guò) redux 的 createStore() 方法創(chuàng)建 store,支持預(yù)設(shè)一些初始化狀態(tài)。
代碼位置src/store/index.js。
import { createStore, compose } from 'redux';
import reducer from './reducer';
const store = createStore(reducer, /* preloadedState, */);
export default store;reducer 拆分與組裝
當(dāng)應(yīng)用復(fù)雜時(shí)我們通常會(huì)拆分出多個(gè)子 reducer 函數(shù),每個(gè) reducer 處理自己負(fù)責(zé)的 state 數(shù)據(jù)。例如,按頁(yè)面功能劃分項(xiàng)目結(jié)構(gòu),每個(gè)頁(yè)面/公共組件都可以維護(hù)自己的 reducer。
有了拆分,對(duì)應(yīng)還有組合,redux 為我們提供了 combineReducers 函數(shù)用于合并多個(gè) reducer。因?yàn)槲覀兊?state 是一個(gè) Immutable 對(duì)象,而 redux 提供的 combineReducers 只支持原生 JavaScript 對(duì)象,不能操作 Immutable 對(duì)象,我們還需要借助另外一個(gè)中間件 **redux-immutable** 從 state 取出 Immutable 對(duì)象。
可以為 reducer 函數(shù)指定不同的 key 值,這個(gè) key 值在組件從 store 獲取 state 時(shí)會(huì)用到,下文 “容器組件鏈接 React 與 Redux” 中會(huì)使用到。
代碼位置src/store/reducer.js。
import { combineReducers } from 'redux-immutable';
import { reducer as todosReducer } from '../pages/todos/store';
import { reducer as otherComponentReducer } from '../pages/other-component/store';
const reducer = combineReducers({
todosPage: todosReducer,
otherComonpent: otherComponentReducer, // 其它組件的 reducer 函數(shù),在這里依次寫(xiě)
});
export default reducer;為 todos 組件創(chuàng)建 store 文件
代碼位置:src/pages/todos/store/index.js。
import * as constants from './constants';
import * as actionCreators from './actionCreators';
import reducer from './reducer';
export {
reducer,
constants,
actionCreators,
};
constants
代碼位置:src/pages/todos/store/constants.js。
export const TODO_LIST = 'todos/TODO_LIST';
export const TODO_LIST_ADD = 'todos/TODO_LIST_ADD';
export const TODO_LIST_EDIT = 'todos/TODO_LIST_EDIT';
export const TODO_LIST_REMOVE = 'todos/TODO_LIST_REMOVE';
創(chuàng)建 action creator 與引入中間件
action 是 store 唯一的信息來(lái)源,action 的數(shù)據(jù)結(jié)構(gòu)要能清晰描述實(shí)際業(yè)務(wù)場(chǎng)景,通常 type 屬性是必須的,描述類型。我的習(xí)慣是放一個(gè) payload 對(duì)象,描述類型對(duì)應(yīng)的數(shù)據(jù)內(nèi)容。
一般會(huì)通過(guò) action creator 創(chuàng)建一個(gè) action。例如,以下為一個(gè)獲取待辦事項(xiàng)列表的 action creator,這種寫(xiě)法是同步的。
function getTodos() {
return {
type: 'TODO_LIST',
payload: {}
}
}在實(shí)際的業(yè)務(wù)中,異步操作是必不可少的,而 store.dispatch 方法只能處理普通的 JavaScript 對(duì)象,如果返回一個(gè)異步 function 代碼就會(huì)報(bào)錯(cuò)。通常需要結(jié)合 redux-thunk 中間件使用,實(shí)現(xiàn)思路是** action creator 返回的異步函數(shù)先經(jīng)過(guò) redux-thunk 處理,當(dāng)真正的請(qǐng)求響應(yīng)后,在發(fā)送一個(gè) dispatch(action) 此時(shí)的 action 就是一個(gè)普通的 JavaScript 對(duì)象了**。
Redux 的中間件概念與 Node.js 的 Web 框架 Express 類似,通用的邏輯可以抽象出來(lái)做為一個(gè)中間件,一個(gè)請(qǐng)求先經(jīng)過(guò)中間件處理后 -> 到達(dá)業(yè)務(wù)處理邏輯 -> 業(yè)務(wù)邏輯響應(yīng)之后 -> 響應(yīng)再到中間件。redux 里的 action 好比 Web 框架收到的請(qǐng)求。
代碼位置:src/store/index.js。修改 store 文件,引入中間件使得 action 支持異步操作。
import { createStore, compose, applyMiddleware } from 'redux'; // 導(dǎo)入 compose、applyMiddleware
import chunk from 'redux-thunk'; // 導(dǎo)入 redux-thunk 包
import reducer from './reducer';
const store = createStore(reducer, /* preloadedState, */ compose(
applyMiddleware(chunk),
));
export default store;創(chuàng)建本次 todos 需要的 action creator,實(shí)際業(yè)務(wù)中增、刪、改、查我們會(huì)調(diào)用服務(wù)端的接口查詢或修改數(shù)據(jù),為了模擬異步,我們簡(jiǎn)單點(diǎn)使用 Promise 模擬異步操作。
代碼位置:src/pages/todos/store/actionCreators.js。
import { TODO_LIST, TODO_LIST_ADD, TODO_LIST_REMOVE, TODO_LIST_EDIT } from './constants';
const randomID = () => Math.floor(Math.random() * 10000);
// 獲取待辦事項(xiàng)列表
export const getTodos = () => async dispatch => {
// 模擬 API 異步獲取數(shù)據(jù)
const todos = await Promise.resolve([
{
id: randomID(),
content: '學(xué)習(xí) React',
},
{
id: randomID(),
content: '學(xué)習(xí) Node.js',
}
]);
const action = {
type: TODO_LIST,
payload: {
todos
}
};
dispatch(action);
}
// 添加待辦事項(xiàng)
export const addTodo = (content) => async dispatch => {
const result = await Promise.resolve({
id: randomID(),
content,
});
const action = {
type: TODO_LIST_ADD,
payload: result
};
dispatch(action);
}
// 編輯待辦事項(xiàng)
export const editTodo = (id, content) => async dispatch => {
const result = await Promise.resolve({ id, content });
const action = {
type: TODO_LIST_EDIT,
payload: result,
};
dispatch(action);
}
// 移除待辦事項(xiàng)
export const removeTodo = id => async dispatch => {
const result = await Promise.resolve({ id });
const action = {
type: TODO_LIST_REMOVE,
payload: result,
};
dispatch(action);
}
reducer 純函數(shù)
reducer 根據(jù) action 的響應(yīng)決定怎么去修改 store 中的 state。編寫(xiě) reducer 函數(shù)沒(méi)那么復(fù)雜,倒要切記該函數(shù)始終為一個(gè)純函數(shù),應(yīng)避免直接修改 state。reducer 純函數(shù)要保證以下兩點(diǎn):
- 同樣的參數(shù),函數(shù)的返回結(jié)果也總是相同的。例如,根據(jù)上一個(gè) state 和 action 也會(huì)返回一個(gè)新的 state,類似這樣的結(jié)構(gòu) (previousState, action) => newState。
- 函數(shù)執(zhí)行沒(méi)有任何副作用,不受外部執(zhí)行環(huán)境的影響。例如,不會(huì)有任何的接口調(diào)用或修改外部對(duì)象。
需要注意一點(diǎn)是在第一次調(diào)用時(shí) state 為 undefined,這時(shí)需使用 initialState 初始化 state。
代碼位置:src/pages/todos/store/reducer.js。
import { fromJS } from 'immutable';
import { TODO_LIST, TODO_LIST_ADD, TODO_LIST_REMOVE, TODO_LIST_EDIT } from './constants';
export const initialState = fromJS({
todos: [],
});
const reducer = (state = initialState, action = {}) => {
switch (action.type) {
case TODO_LIST: {
return state.merge({
todos: state.get('todos').concat(fromJS(action.payload.todos)),
});
}
case TODO_LIST_ADD: {
return state.set('todos', state.get('todos').push(fromJS({
id: action.payload.id,
content: action.payload.content,
})));
}
case TODO_LIST_EDIT: {
return state.merge({
todos: state.get('todos').map(item => {
if (item.get('id') === action.payload.id) {
const newItem = { ...item.toJS(), content: action.payload.content };
return fromJS(newItem);
}
return item;
})
})
}
case TODO_LIST_REMOVE: {
return state.merge({
todos: state.get('todos').filter(item => item.get('id') !== action.payload.id),
})
}
default: return state;
}
};
export default reducer;容器組件鏈接 React 與 Redux
Redux 做為一個(gè)狀態(tài)管理容器,本身并沒(méi)有與任何 View 層框架綁定,當(dāng)在 React 框架中使用 Redux 時(shí)需安裝 react-redux npm i react-redux -S 庫(kù)。
容器組件
react-redux 提供的 connect 函數(shù),可以把 React 組件和 Redux 的 store 鏈接起來(lái)生成一個(gè)新的容器組件(這里有個(gè)經(jīng)典的設(shè)計(jì)模式 “高階組件”),數(shù)據(jù)如何獲取就是容器組件需要負(fù)責(zé)的事情,在獲取到數(shù)據(jù)后通過(guò) props 屬性傳遞到展示組件,當(dāng)展示組件需要變更狀態(tài)時(shí)調(diào)用容器組件提供的方法同步這些狀態(tài)變化。
總結(jié)下來(lái),容器組件需要做兩件事:
- 從 Redux 的 store 中獲取數(shù)據(jù)給到展示組件,對(duì)應(yīng)下例 mapStateToProps() 方法。
- 提供方法供展示組件同步需要變更的狀態(tài),對(duì)應(yīng)下例 mapDispatchToProps() 方法。
// 創(chuàng)建容器組件代碼示例
import { connect } from 'react-redux';
import ExampleComponent from './ExampleComponent'
const mapStateToProps = (state) => ({ // 從全局狀態(tài)取出數(shù)據(jù)映射到展示組件的 props
todos: state.getIn(['todosComponent', 'todos']),
});
const mapDispatchToProps = (dispatch) => ({ // 把展示組件變更狀態(tài)需要用到的方法映射到展示組件的 props 上。
getTodos() {
dispatch(actionCreators.getTodos());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ExampleComponent);
上例,當(dāng) redux store 中的 state 變化時(shí),對(duì)應(yīng)的 mapStateToProps 函數(shù)會(huì)被執(zhí)行,如果 mapStateToProps 函數(shù)新返回的對(duì)象與之前對(duì)象淺比較相等(此時(shí),如果是類組件可以理解為 shouldComponentUpdate 方法返回 false),展示組件就不會(huì)重新渲染,否則重新渲染展示組件。
展示組件與容器組件之間的關(guān)系可以自由組合,可以單獨(dú)創(chuàng)建一個(gè) container 文件,來(lái)包含多個(gè)展示組件,同樣也可以在展示組件里包含容器組件。在我們的示例中,也比較簡(jiǎn)單是在展示組件里返回一個(gè)容器組件,下面開(kāi)始修改我們展示組件。
修改 Todo 組件
組件位置src/pages/todos/components/Todo.jsx。
在我們的 Todo 組件中,參數(shù) todo 是由上層的 Todos 組件傳遞的這里并不需要從 Redux 的 store 中獲取 state,只需要修改狀態(tài)的函數(shù)就可以了,connect() 函數(shù)第一個(gè)參數(shù) state 可以省略,這樣 state 的更新也就不會(huì)引起該組件的重新渲染了。
import { connect } from 'react-redux';
const Todo = ({ todo, editTodo, removeTodo }) => {...} // 中間代碼省略
const mapDispatchToProps = (dispatch) => ({
editTodo(id, content) {
dispatch(actionCreators.editTodo(id, content));
},
removeTodo(id) {
dispatch(actionCreators.removeTodo(id));
}
});
export default connect(null, mapDispatchToProps)(Todo);修改 TodoAdd 組件
組件位置src/pages/todos/components/TodoAdd.jsx。
import { connect } from 'react-redux';
const TodoAdd = ({ addTodo }) => {...}; // 中間代碼省略
const mapDispatchToProps = (dispatch) => ({
addTodo(content) {
dispatch(actionCreators.addTodo(content));
},
});
export default connect(null, mapDispatchToProps)(TodoAdd);修改 Todos 組件
組件位置src/pages/todos/components/Todos.jsx。
import { connect } from 'react-redux';
const Todos = ({ todos, getTodos }) => { ... } // 中間代碼省略
const mapStateToProps = (state) => ({
todos: state.getIn(['todosPage', 'todos']),
});
const mapDispatchToProps = (dispatch) => ({
getTodos() {
dispatch(actionCreators.getTodos());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Todos);創(chuàng)建 MyRoutes 組件
有了 Page 相應(yīng)的也有路由,創(chuàng)建 MyRoutes 組件,代碼位置 src/routes/index.js。
import {
BrowserRouter, Routes, Route
} from 'react-router-dom';
import Todos from '../pages/todos';
const MyRoutes = () => {
return (
} />
);
};
export default MyRoutes;Provider 組件傳遞 store
通過(guò) react-redux 的 connect 函數(shù)創(chuàng)建的容器組件可以獲取 redux store,那么有沒(méi)有想過(guò)容器組件又是如何獲取的 redux store?
在 React 狀態(tài)管理 - Context 一篇中介紹過(guò),使用 React.createContext() 方法創(chuàng)建一個(gè)上下文(MyContext),之后通過(guò) MyContext 提供的 Provider 組件可以傳遞 value 屬性供子組件使用。react-redux 也提供了一個(gè) Provider 組件,正是通過(guò) context 傳遞 store 供子組件使用,所以我們使用 redux 時(shí),一般會(huì)把 Provider 組件做為根組件,這樣被 Provider 根組件包裹的所有子組件都可以獲取到 store 中的存儲(chǔ)的狀態(tài)。
創(chuàng)建 App.js 組件,組件位置:src/app.js。
import { Provider } from
網(wǎng)站欄目:React 狀態(tài)管理 - 你可能不需要 Redux,但你需要了解它!
標(biāo)題網(wǎng)址:http://fisionsoft.com.cn/article/coeocgi.html


咨詢
建站咨詢
