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

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


咨詢
建站咨詢
