redux、react-redux、redux-saga總結
前言
hello大家好,我是風不識途,最近一直在整理redux系列文章,發現對於初學者不太友好,關係錯綜複雜,難倒是不太難,就是比較複雜(其實寫比較少),所以這篇帶你全面瞭解
redux、react-redux、redux-thunk還有redux-sage,immutable(多圖預警),由於知識點比較多,建議先收藏(收藏等於學會了),對你有幫助的話就給個贊👍
認識純函數
JavaScript純函數
- 函數式編程中有一個概念叫純函數,
JavaScript符合函數式編程的範式, 所以也有純函數的概念 - 在
React中,純函數的概念非常重要,在接下來我們學習的Redux中也非常重要,所以我們必須來回顧一下純函數 -
純函數的定義簡單總結一下:
- 純函數指的是, 每次給相同的參數, 一定返回相同的結果
- 函數在執行過程中, 不能產生副作用
-
純函數(
Pure Function)的注意事項:- 在純函數中不能使用隨機數
- 不能使用當前的時間或日期, 因為結果是會變的
- 不能使用或者修改全局狀態, 比如DOM,文件、數據庫等等(因為如果全局狀態改變了,它就會影響函數的結果)
- 純函數中的參數不能變化,否則函數的結果就會改變
React中的純函數
-
為什麼純函數在函數式編程中非常重要呢?
- 因為你可以安心的寫和安心的用
- 你在寫的時候保證了函數的純度,實現自己的業務邏輯即可,不需要關心傳入的內容或者函數體依賴了外部的變量
- 你在用的時候,你確定你的輸入內容不會被任意篡改,並且自己確定的輸入,一定會有確定的輸出
-
React非常靈活,但它也有一個嚴格的規則:
- 所有React組件都必須像"純函數"一樣保護它們的"props"不被更改
認識Redux
為什麼需要redux
-
JavaScript開發的應用程序, 已經變得非常複雜了:JavaScript需要管理的狀態越來越多, 越來越複雜了- 這些狀態包括服務器返回的數據, 用户操作的數據等等, 也包括一些
UI的狀態
-
管理不斷變化的
state是非常困難的:- 狀態之間相互存在依賴, 一個狀態的變化會引起另一個狀態的變化,
View頁面也有可能會引起狀態的變化 - 當程序複雜時,
state在什麼時候, 因為什麼原因發生了變化, 發生了怎樣的變化, 會變得非常難以控制和追蹤
- 狀態之間相互存在依賴, 一個狀態的變化會引起另一個狀態的變化,
React的作用
-
React只是在視圖層幫助我們解決了DOM的渲染過程, 但是state依然是留給我們自己來管理:- 無論是組件定義自己的
state,還是組件之間的通信通過props進行傳遞 - 也包括通過
Context進行數據之間的共享 React主要負責幫助我們管理視圖,state如何維護最終還是我們自己來決定
- 無論是組件定義自己的
-
Redux就是一個幫助我們管理State的容器:Redux是JavaScript的狀態容器, 提供了可預測的狀態管理
Redux除了和React一起使用之外, 它也可以和其他界面庫一起來使用(比如Vue), 並且它非常小 (包括依賴在內,只有2kb)
Redux的核心理念-Store
Redux的核心理念非常簡單-
比如我們有一個朋友列表需要管理:
- 如果我們沒有定義統一的規範來操作這段數據,那麼整個數據的變化就是無法跟蹤的
- 比如頁面的某處通過
products.push的方式增加了一條數據 - 比如另一個頁面通過
products[0].age = 25修改了一條數據
- 整個應用程序錯綜複雜,當出現
bug時,很難跟蹤到底哪裏發生的變化
Redux的核心理念-action
-
Redux要求我們通過action來更新state:- 所有數據的變化, 必須通過
dispatch來派發action來更新 action是一個普通的JavaScript對象,用來描述這次更新的type和content
- 所有數據的變化, 必須通過
-
比如下面就是幾個更新
friends的action:- 強制使用
action的好處是可以清晰的知道數據到底發生了什麼樣的變化,所有的數據變化都是可跟追蹤、可預測的 - 當然,目前我們的
action是固定的對象,真實應用中,我們會通過函數來定義,返回一個action
- 強制使用
Redux的核心理念-reducer
-
但是如何將
state和action聯繫在一起呢? 答案就是reducerreducer是一個純函數reducer做的事情就是將傳入的state和action結合起來來生成一個新的state
Redux的三大原則
-
單一數據源
- 整個應用程序的
state被存儲在一顆object tree中, 並且這個object tree只存儲在一個store Redux並沒有強制讓我們不能創建多個Store,但是那樣做並不利於數據的維護- 單一的數據源可以讓整個應用程序的
state變得方便維護、追蹤、修改
- 整個應用程序的
-
State是隻讀的
- 唯一修改
state的方法一定是觸發action, 不要試圖在其它的地方通過任何的方式來修改state - 這樣就確保了
View或網絡請求都不能直接修改state,它們只能通過action來描述自己想要如何修改state - 這樣可以保證所有的修改都被集中化處理,並且按照嚴格的順序來執行,所以不需要擔心
race condition(竟態)的問題
- 唯一修改
-
使用純函數來執行修改
- 通過
reducer將舊state和action聯繫在一起, 並且返回一個新的state - 隨着應用程序的複雜度增加,我們可以將
reducer拆分成多個小的reducers,分別操作不同state tree的一部分 - 但是所有的
reducer都應該是純函數,不能產生任何的副作用
- 通過
Redux的基本使用
Redux中核心的API
redux的安裝: yarn add redux
createStore可以用來創建store對象store.dispatch用來派發action,action會傳遞給storereducer接收action,reducer計算出新的狀態並返回它 (store負責調用reducer)store.getState這個方法可以幫助獲取store裏邊所有的數據內容store.subscribe方法可以讓讓我們訂閲store的改變,只要store發生改變,store.subscribe這個函數接收的這個回調函數就會被執行
小結
- 創建
sotore, 決定 store 要保存什麼狀態 - 創建
action, 用户在程序中實現什麼操作 - 創建
reducer, reducer 接收 action 並返回更新的狀態
Redux的使用過程
- 創建一個對象, 作為我們要保存的狀態
-
創建
Store來存儲這個state- 創建
store時必須創建reducer - 我們可以通過
store.getState來獲取當前的state
- 創建
-
通過
action來修改state- 通過
dispatch來派發action - 通常
action中都會有type屬性,也可以攜帶其他的數據
- 通過
-
修改
reducer中的處理代碼- 這裏一定要記住,
reducer是一個純函數,不能直接修改state - 後面會講到直接修改
state帶來的問題
- 這裏一定要記住,
- 可以在派發
action之前,監聽store的變化
import { createStore } from 'redux'
// 1.初始化state
const initState = { counter: 0 }
// 2.reducer純函數 不能修改傳遞的state
function reducer(state = initState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, counter: state.counter + 1 }
case 'ADD_COUNTER':
return { ...state, counter: state.counter + action.num }
default:
return state
}
}
// 3.store 參數放一個reducer
const store = createStore(reducer)
// 4.action
const action1 = { type: 'INCREMENT' }
const action2 = { type: 'ADD_COUNTER', num: 2 }
// 5.訂閲store的修改
store.subscribe(() => {
console.log('state發生了改變: ', store.getState().counter)
})
// 6.派發action
store.dispatch(action1)
store.dispatch(action2)
Redux結構劃分
- 如果我們將所有的邏輯代碼寫到一起, 那麼當
redux變得複雜時代碼就難以維護 - 對代碼進行拆分, 將
store、reducer、action、constants拆分成一個個文件
<details>
<summary>拆分目錄</summary>
</details>
Redux使用流程
Redux官方流程圖
React-Redux的使用
redux融入react代碼(案例)
-
redux融入react代碼案例:Home組件:其中會展示當前的counter值,並且有一個+1和+5的按鈕Profile組件:其中會展示當前的counter值,並且有一個-1和-5的按鈕
-
核心代碼主要是兩個:
- 在
componentDidMount中訂閲數據的變化,當數據發生變化時重新設置counter - 在發生點擊事件時,調用
store的dispatch來派發對應的action
- 在
自定義connect函數
當我們多個組件使用
redux時, 重複的代碼太多了, 比如: 訂閲state取消訂閲state或 派發action獲取state將重複的代碼進行封裝, 將不同的
state和dispatch作為參數進行傳遞
// connect.js
import React, { PureComponent } from 'react'
import { StoreContext } from './context'
/**
* 1.調用該函數: 返回一個高階組件
* 傳遞需要依賴 state 和 dispatch 來使用state或通過dispatch來改變state
*
* 2.調用高階組件:
* 傳遞該組件需要依賴 store 的組件
*
* 3.主要作用:
* 將重複的代碼抽取到高階組件中,並將該組件依賴的 state 和 dispatch
* 通過調用mapStateToProps()或mapDispatchToProps()函數
* 並將該組件依賴的state和dispatch供該組件使用,其他使用store的組件不必依賴store
*
* 4.connect.js: 優化依賴
* 目的:但是上面的connect函數有一個很大的缺陷:依賴導入的 store
* 優化:正確的做法是我們提供一個Provider,Provider來自於我們
* Context,讓用户將store傳入到value中即可;
*/
export function connect(mapStateToProps, mapDispatchToProps) {
return function enhanceComponent(WrapperComponent) {
class EnhanceComponent extends PureComponent {
constructor(props, context) {
super(props, context)
// 組件依賴的state
this.state = {
storeState: mapStateToProps(context.getState()),
}
}
// 訂閲數據發生變化,調用setState重新render
componentDidMount() {
this.unsubscribe = this.context.subscribe(() => {
this.setState({
centerStore: mapStateToProps(this.context.getState()),
})
})
}
// 組件被卸載取消訂閲
componentWillUnmount() {
this.unsubscribe()
}
render() {
// 下面的WrapperComponent相當於 home 組件(就是你傳遞的組件)
// 你需要將該組件需要依賴的state和dispatch作為props進行傳遞
return (
<WrapperComponent
{...this.props}
{...mapStateToProps(this.context.getState())}
{...mapDispatchToProps(this.context.dispatch)}
/>
)
}
}
// 取出Provider提供的value
EnhanceComponent.contextType = StoreContext
return EnhanceComponent
}
}
// home.js
// 定義組件依賴的state和dispatch
const mapStateToProps = state => ({
counter: state.counter,
})
const mapDispatchToProps = dispatch => ({
increment() {
dispatch(increment())
},
addNumber(num) {
dispatch(addAction(num))
},
})
export default connect(mapStateToProps,mapDispatchToProps)(依賴redux的組件)
react-redux使用
- 開始之前需要強調一下,
redux和react沒有直接的關係,你完全可以在React, Angular, Ember, jQuery, or vanilla JavaScript中使用Redux - 儘管這樣説,redux依然是和React或者Deku的庫結合的更好,因為他們是通過state函數來描述界面的狀態,Redux可以發射狀態的更新,讓他們作出相應。
- 雖然我們之前已經實現了
connect、Provider這些幫助我們完成連接redux、react的輔助工具,但是實際上redux官方幫助我們提供了react-redux的庫,可以直接在項目中使用,並且實現的邏輯會更加的嚴謹和高效 -
安裝
react-redux:yarn add react-redux
// 1.index.js
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
// 2.home.js
import { connect } from 'react-redux'
// 定義需要依賴的state和dispatch (函數需要返回一個對象)
export default connect(mapStateToProps, mapDispatchToProps)(About)
react-redux源碼導讀
Redux-Middleware中間件
組件中異步操作
-
在之前簡單的案例中,
redux中保存的counter是一個本地定義的數據- 我們可以直接通過同步的操作來
dispatch action,state就會被立即更新。 - 但是真實開發中,
redux中保存的很多數據可能來自服務器,我們需要進行異步的請求,再將數據保存到redux中
- 我們可以直接通過同步的操作來
- 網絡請求可以在
class組件的componentDidMount中發送,所以我們可以有這樣的結構:
redux中異步操作
-
上面的代碼有一個缺陷:
- 我們必須將網絡請求的異步代碼放到組件的生命週期中來完成
-
為什麼將網絡請求的異步代碼放在
redux中進行管理?- 後期代碼量的增加,如果把網絡請求異步函數放在組件的生命週期裏,這個生命週期函數會變得越來越複雜,組件就會變得越來越大
- 事實上,網絡請求到的數據也屬於狀態管理的一部分,更好的一種方式應該是將其也交給
redux來管理
-
但是在
redux中如何可以進行異步的操作呢?- 使用中間件 (Middleware)
- 學習過
Express或Koa框架的童鞋對中間件的概念一定不陌生 - 在這類框架中,
Middleware可以幫助我們在請求和響應之間嵌入一些操作的代碼,比如cookie解析、日誌記錄、文件壓縮等操作
理解中間件(重點)
-
redux也引入了中間件 (Middleware) 的概念:- 這個<font color='red'>中間件的目的是在
dispatch的action和最終達到的reducer之間,擴展一些自己的代碼</font> - 比如日誌記錄、調用異步接口、添加代碼調試功能等等
- 這個<font color='red'>中間件的目的是在
-
redux-thunk是如何做到讓我們可以發送異步的請求呢?- 默認情況下的
dispatch(action),action需要是一個JavaScript的對象 redux-thunk可以讓dispatch(action函數),action<font color='red'>可以是一個函數</font>-
該函數會被調用, 並且會傳給這個函數兩個參數: 一個
dispatch函數和getState函數dispatch函數用於我們之後再次派發actiongetState函數考慮到我們之後的一些操作需要依賴原來的狀態,用於讓我們可以獲取之前的一些狀態
- 默認情況下的
redux-thunk的使用
-
安裝
redux-thunkyarn add redux-thunk
-
在創建
store時傳入應用了middleware的enhance函數- 通過
applyMiddleware來結合多個Middleware, 返回一個enhancer -
將
enhancer作為第二個參數傳入到createStore中
- 通過
-
定義返回一個函數的
action- 注意:這裏不是返回一個對象了,而是一個函數
- 該函數在
dispatch之後會被執行
<details>
<summary>查看代碼</summary>
<pre>import { createStore, applyMiddleware } from 'redux'
</pre></details>
import reducer from './reducer'
import thunk from 'redux-thunk'<br/>
const store = createStore(
reducer,
applyMiddleware(thunk) // applyMiddleware可以使用中間件模塊
)
export default store
redux-devtools
redux-devtools插件
-
我們之前講過,
redux可以方便的讓我們對狀態進行跟蹤和調試,那麼如何做到呢?redux官網為我們提供了redux-devtools的工具- 利用這個工具,我們可以知道每次狀態是如何被修改的,修改前後的狀態變化等等
-
使用步驟:
- 第一步:在瀏覽器上安裝redux-devtools擴展插件
- 第二步:在
redux中集成devtools的中間件
// store.js 開啓redux-devtools擴展
import { createStore, applyMiddleware, compose } from 'redux'
// composeEnhancers函數
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true }) || compose
// 通過applyMiddleware來結合多個Middleware,返回一個enhancer
const enhancer = applyMiddleware(thankMiddleware)
// 通過enhancer作為第二個參數傳遞createStore中
const store = createStore(reducer, composeEnhancers(enhancer))
export default store
redux-sage
generator
Generator函數是 ES6 提供的一種異步編程解決方案,語法行為與傳統函數完全不同
Generator函數有多種理解角度。語法上,首先可以把它理解成,Generator函數是一個狀態機,封裝了多個內部狀態。
// 生成器函數的定義
// 默認返回: Generator
function* foo() {
console.log('111')
yield 'hello'
console.log('222')
yield 'world'
console.log('333')
yield 'jane'
console.log('444')
}
// iterator: 迭代器
const result = foo()
console.log(result)
// 使用迭代器
// 調用next,就會消耗一次迭代器
const res1 = result.next()
console.log(res1) // {value: "hello", done: false}
const res2 = result.next()
console.log(res2) // {value: "world", done: false}
const res3 = result.next()
console.log(res3) // {value: "jane", done: false}
const res4 = result.next()
console.log(res4) // {value: undefined, done: true}
redux-sage流程
redux-saga的使用
redux-saga是另一個比較常用在redux發送異步請求的中間件,它的使用更加的靈活-
Redux-saga的使用步驟如下- 安裝
redux-sage:yarn add redux-saga -
集成
redux-saga中間件- 引入
createSagaMiddleware後, 需要創建一個sagaMiddleware - 然後通過
applyMiddleware使用這個中間件,接着創建saga.js這個文件 - 啓動中間件的監聽過程, 並且傳入要監聽的
saga
- 引入
-
saga.js文件的編寫takeEvery:可以傳入多個監聽的actionType,每一個都可以被執行(對應有一個takeLatest,會取消前面的)put:在saga中派發action不再是通過dispatch, 而是通過putall:可以在yield的時候put多個action
- 安裝
// store.js
import createSageMiddleware from 'redux-saga'
import saga from './saga'
// 1.創建sageMiddleware中間件
const sagaMiddleware = createSageMiddleware()
// 2.應用一些中間件
const enhancer = applyMiddleware(sagaMiddleware)
const store = createStore(reducer,composeEnhancers(enhancer))
sagaMiddleware.run(saga)
export default store
// saga.js
import { takeEvery, put, all } from 'redux-saga/effects'
import { FETCH_HOME_DATA } from './constant'
function* fetchHomeData(action) {
const res = yield axios.get('http://123.207.32.32:8000/home/multidata')
const banners = res.data.data.banner.list
const recommends = res.data.data.recommend.list
// dispatch action 提交action,redux-sage提供了put
yield all([
yield put(changeBannersAction(banners)),
yield put(changeRecommendAction(recommends)),
])
}
function* mySaga() {
// 參數一:要攔截的actionType
// 參數二:生成器函數
yield all([
takeEvery(FETCH_HOME_DATA, fetchHomeData),
])
}
export default mySaga
reducer代碼拆分
Reducer代碼拆分
-
我們來看一下目前我們的
reducer:- 當前這個
reducer既有處理counter的代碼,又有處理home頁面的數據 - 後續
counter相關的狀態或home相關的狀態會進一步變得更加複雜 - 我們也會繼續添加其他的相關狀態,比如購物車、分類、歌單等等
- 如果將所有的狀態都放到一個
reducer中進行管理,隨着項目的日趨龐大,必然會造成代碼臃腫、難以維護
- 當前這個
-
因此,我們可以對
reducer進行拆分:- 我們先抽取一個對
counter處理的reducer - 再抽取一個對
home處理的reducer - 將它們合併起來
- 我們先抽取一個對
Reducer文件拆分
-
目前我們已經將不同的狀態處理拆分到不同的
reducer中,我們來思考:- 雖然已經放到不同的函數了,但是這些函數的處理依然是在同一個文件中,代碼非常的混亂
- 另外關於
reducer中用到的constant、action等我們也依然是在同一個文件中;
combineReducers函數
- 目前我們合併的方式是通過每次調用
reducer函數自己來返回一個新的對象 - 事實上,
redux給我們提供了一個combineReducers函數可以方便的讓我們對多個reducer進行合併
import { combineReducers } from 'redux'
import { reducer as counterReducer } from './count'
import { reducer as homeReducer } from './home'
export const reducer = combineReducers({
counterInfo: counterReducer,
homeInfo: homeReducer,
})
-
那麼
combineReducers是如何實現的呢?- 它將我們傳遞的
reducer合併成一個對象, 最終返回一個combination函數 - 在執行
combination函數過程中, 會通過判斷前後返回的數據是否相同來決定返回之前的state還是新的state
- 它將我們傳遞的
immutableJs
數據可變形的問題
-
在
React開發中,我們總是會強調數據的不可變性:- 無論是類組件中的
state,還是reduex中管理的state - 事實上在整個
JavaScript編碼的過程中,數據的不可變性都是非常重要的
- 無論是類組件中的
-
數據的可變性引發的問題(案例):
- 我們明明沒有修改obj,只是修改了obj2,但是最終obj也被我們修改掉了
- 原因非常簡單,對象是引用類型,它們指向同一塊內存空間,兩個引用都可以任意修改
const obj1 = { name: 'jane', age: 18 }
const obj2 = obj1
obj1.name = 'kobe'
console.log(obj2.name) // kobe
-
有沒有辦法解決上面的問題呢?
- 進行對象的拷貝即可:
Object.assign或擴展運算符
- 進行對象的拷貝即可:
-
這種對象的淺拷貝有沒有問題呢?
- 從代碼的角度來説,沒有問題,也解決了我們實際開發中一些潛在風險
- 從性能的角度來説,有問題,如果對象過於龐大,這種拷貝的方式會帶來性能問題以及內存浪費
-
有人會説,開發中不都是這樣做的嗎?
- 從來如此,便是對的嗎?
認識ImmutableJS
-
為了解決上面的問題,出現了
Immutable對象的概念:Immutable對象的特點是隻要修改了對象,就會返回一個新的對象,舊的對象不會發生改變;
-
但是這樣的方式就不會浪費內存了嗎?
- 為了節約內存,又出現了一個新的算法:
Persistent Data Structure(持久化數據結構或一致性數據結構)
- 為了節約內存,又出現了一個新的算法:
-
當然,我們一聽到持久化第一反應應該是數據被保存到本地或者數據庫,但是這裏並不是這個含義:
- 用一種數據結構來保存數據
- 當數據被修改時,會返回一個對象,但是新的對象會盡可能的利用之前的數據結構而不會對內存造成浪費,如何做到這一點呢?結構共享:
- 安裝
Immutable:yarn add immutable
ImmutableJS常見API
注意:我這裏只是演示了一些API,更多的方式可以參考官網
作用:不會修改原有數據結構,返回一個修改後新的拷貝對象
-
JavaScrip和ImutableJS直接的轉換- 對象轉換成
Immutable對象:Map - 數組轉換成
Immtable數組:List - 深層轉換:
fromJS
- 對象轉換成
const im = Immutable
// 對象轉換成Immutable對象
const info = {name: 'kobe', age: 18}
const infoIM = im.Map()
// 數組轉換成Immtable數組
const names = ["abc", "cba", "nba"]
const namesIM = im.List(names)
-
ImmutableJS的基本操作:-
修改數據:
set(property, newVal)- 返回值: 修改後新的數據結構
- 獲取數據:
get(property/index) - 獲取深層
Immutable對象數據(子屬性也是Immutable對象):getIn(['recommend', 'topBanners'])
-
// set方法 不會修改infoIM原有數據結構,返回修改後新的數據結構
const newInfo2IM = infoIM.set('name', 'james')
const newNamesIM = namesIM.set(0, 'why')
// get方法
console.log(infoIM.get('name'))// -> kobe
console.log(namesIM.get(0))// -> abc
結合Redux管理數據
-
ImmutableJS重構redux- yarn add Immutable
- yarn add redux-immutable
- 使用redux-immutable中的combineReducers;
- 所有的reducer中的數據都轉換成Immutable類型的數據
FAQ
React中的state如何管理
-
目前項目中採用的state管理方案(參考即可):
- 相關的組件內部可以維護的狀態,在組件內部自己來維護
- 只要是需要共享的狀態,都交給redux來管理和維護
- 從服務器請求的數據(包括請求的操作) ,交給redux來維護
前言
hello大家好,我是風不識途,最近一直在整理redux系列文章,發現對於初學者不太友好,關係錯綜複雜,難倒是不太難,就是比較複雜(其實寫比較少),所以這篇帶你全面瞭解
redux、react-redux、redux-thunk還有redux-sage,immutable(多圖預警),由於知識點比較多,建議先收藏(收藏等於學會了),對你有用的話就給個贊👍
認識純函數
JavaScript純函數
-
函數式編程中有一個概念叫純函數,
JavaScript符合函數式編程的範式, 所以也有純函數的概念 -
在
React中,純函數的概念非常重要,在接下來我們學習的Redux中也非常重要,所以我們必須來回顧一下純函數 -
純函數的維基百科定義(瞭解即可) -
純函數的定義簡單總結一下:
* 純函數指的是, 每次給相同的參數, 一定返回相同的結果 * 函數在執行過程中, 不能產生副作用 -
**純函數( `Pure Function` )的注意事項:**
React中的純函數
-
為什麼純函數在函數式編程中非常重要呢?
* 因為你可以安心的寫和安心的用 * 你在寫的時候保證了函數的純度,實現自己的業務邏輯即可,不需要關心傳入的內容或者函數體依賴了外部的變量 * 你在用的時候,你確定你的輸入內容不會被任意篡改,並且自己確定的輸入,一定會有確定的輸出 -
React非常靈活,但它也有一個嚴格的規則:
* 所有React組件都必須像"純函數"一樣保護它們的"props"不被更改
認識Redux
為什麼需要redux
-
JavaScript開發的應用程序, 已經變得非常複雜了:* `JavaScript`**需要管理的狀態越來越多**, 越來越複雜了 * 這些狀態包括服務器返回的數據, 用户操作的數據等等, 也包括一些`UI`的狀態 -
管理不斷變化的
state是非常困難的:* **狀態之間相互存在依賴**, 一個狀態的變化會引起另一個狀態的變化, `View`頁面也有可能會引起狀態的變化 * 當程序複雜時, `state`在什麼時候, 因為什麼原因發生了變化, 發生了怎樣的變化, 會變得非常難以控制和追蹤
React的作用
-
React只是在視圖層幫助我們解決了DOM的渲染過程, 但是state依然是留給我們自己來管理:* 無論是組件定義自己的`state`,還是組件之間的通信通過`props`進行傳遞 * 也包括通過`Context`進行數據之間的共享 * `React`主要負責幫助我們管理視圖,`state`如何維護最終還是我們自己來決定

-
Redux就是一個幫助我們管理State的容器:* `Redux`是`JavaScript`的狀態容器, 提供了可預測的狀態管理 -
Redux除了和React一起使用之外, 它也可以和其他界面庫一起來使用(比如Vue), 並且它非常小 (包括依賴在內,只有2kb)
Redux的核心理念-Store
-
Redux的核心理念非常簡單 -
比如我們有一個朋友列表需要管理:
* **如果我們沒有定義統一的規範來操作這段數據,那麼整個數據的變化就是無法跟蹤的** * 比如頁面的某處通過`products.push`的方式增加了一條數據 * 比如另一個頁面通過`products[0].age = 25`修改了一條數據 -
整個應用程序錯綜複雜,當出現
bug時,很難跟蹤到底哪裏發生的變化
Redux的核心理念-action
-
Redux要求我們通過action來更新state:* **所有數據的變化, 必須通過**`dispatch`來派發`action`來更新 * `action`是一個普通的`JavaScript`對象,用來描述這次更新的`type`和`content` -
比如下面就是幾個更新
friends的action:* 強制使用`action`的好處是可以清晰的知道數據到底發生了什麼樣的變化,所有的數據變化都是可跟追蹤、可預測的 * 當然,目前我們的`action`是固定的對象,真實應用中,我們會通過函數來定義,返回一個`action`
Redux的核心理念-reducer
-
但是如何將
state和action聯繫在一起呢? 答案就是reducer* `reducer`是一個純函數 * `reducer`做的事情就是將傳入的`state`和`action`結合起來來生成一個新的`state`
Redux的三大原則
-
單一數據源
* 整個應用程序的`state`被存儲在一顆`object tree`中, 並且這個`object tree`只存儲在一個`store` * `Redux`並沒有強制讓我們不能創建多個`Store`,但是那樣做並不利於數據的維護 * 單一的數據源可以讓整個應用程序的`state`變得方便維護、追蹤、修改 -
State是隻讀的
* 唯一修改`state`的方法一定是觸發`action`, 不要試圖在其它的地方通過任何的方式來修改`state` * 這樣就確保了`View`或網絡請求都不能直接修改`state`,它們只能通過`action`來描述自己想要如何修改`state` * 這樣可以保證所有的修改都被集中化處理,並且按照嚴格的順序來執行,所以不需要擔心`race condition`(竟態)的問題 -
使用純函數來執行修改
* 通過`reducer`將舊 `state` 和 `action` 聯繫在一起, 並且返回一個新的`state` * 隨着應用程序的複雜度增加,我們可以將`reducer`拆分成多個小的`reducers`,分別操作不同`state tree`的一部分 * 但是所有的`reducer`都應該是純函數,不能產生任何的副作用
Redux的基本使用
Redux中核心的API
redux的安裝: yarn add redux
-
createStore可以用來創建store對象 -
store.dispatch用來派發action,action會傳遞給store -
reducer接收action,reducer計算出新的狀態並返回它 (store負責調用reducer) -
store.getState這個方法可以幫助獲取store裏邊所有的數據內容 -
store.subscribe方法可以讓讓我們訂閲store的改變,只要store發生改變,store.subscribe這個函數接收的這個回調函數就會被執行
小結
-
創建
sotore, 決定 store 要保存什麼狀態 -
創建
action, 用户在程序中實現什麼操作 -
創建
reducer, reducer 接收 action 並返回更新的狀態
Redux的使用過程
-
創建一個對象, 作為我們要保存的狀態
-
創建
Store來存儲這個state* 創建`store`時必須創建`reducer` * 我們可以通過 `store.getState` 來獲取當前的`state` -
通過
action來修改state* 通過`dispatch`來派發`action` * 通常`action`中都會有`type`屬性,也可以攜帶其他的數據 -
修改
reducer中的處理代碼* 這裏一定要記住,`reducer`是一個**純函數**,不能直接修改`state` * 後面會講到直接修改`state`帶來的問題 -
可以在派發
action之前,監聽store的變化
import { createStore } from 'redux'
// 1.初始化state
const initState = { counter: 0 }
// 2.reducer純函數 不能修改傳遞的state
function reducer(state = initState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, counter: state.counter + 1 }
case 'ADD_COUNTER':
return { ...state, counter: state.counter + action.num }
default:
return state
}
}
// 3.store 參數放一個reducer
const store = createStore(reducer)
// 4.action
const action1 = { type: 'INCREMENT' }
const action2 = { type: 'ADD_COUNTER', num: 2 }
// 5.訂閲store的修改
store.subscribe(() => {
console.log('state發生了改變: ', store.getState().counter)
})
// 6.派發action
store.dispatch(action1)
store.dispatch(action2)
Redux結構劃分
-
如果我們將所有的邏輯代碼寫到一起, 那麼當
redux變得複雜時代碼就難以維護 -
對代碼進行拆分, 將
store、reducer、action、constants拆分成一個個文件
拆分目錄
Redux使用流程
Redux官方流程圖
React-Redux的使用
redux融入react代碼(案例)
-
redux融入react代碼案例:* `Home`組件:其中會展示當前的`counter`值,並且有一個+1和+5的按鈕 * `Profile`組件:其中會展示當前的`counter`值,並且有一個-1和-5的按鈕

-
核心代碼主要是兩個:
* 在 `componentDidMount`中訂閲數據的變化,當數據發生變化時重新設置 `counter` * 在發生點擊事件時,調用`store`的`dispatch`來派發對應的`action`
自定義connect函數
當我們多個組件使用
redux時, 重複的代碼太多了, 比如: 訂閲state取消訂閲state或 派發action獲取state將重複的代碼進行封裝, 將不同的
state和dispatch作為參數進行傳遞
// connect.js
import React, { PureComponent } from 'react'
import { StoreContext } from './context'
/**
- 1.調用該函數: 返回一個高階組件
- 傳遞需要依賴 state 和 dispatch 來使用state或通過dispatch來改變state
* - 2.調用高階組件:
- 傳遞該組件需要依賴 store 的組件
* - 3.主要作用:
- 將重複的代碼抽取到高階組件中,並將該組件依賴的 state 和 dispatch
- 通過調用mapStateToProps()或mapDispatchToProps()函數
- 並將該組件依賴的state和dispatch供該組件使用,其他使用store的組件不必依賴store
* - 4.connect.js: 優化依賴
- 目的:但是上面的connect函數有一個很大的缺陷:依賴導入的 store
- 優化:正確的做法是我們提供一個Provider,Provider來自於我們
- Context,讓用户將store傳入到value中即可;
*/
export function connect(mapStateToProps, mapDispatchToProps) {
return function enhanceComponent(WrapperComponent) {
class EnhanceComponent extends PureComponent {
constructor(props, context) {
super(props, context)
// 組件依賴的state
this.state = {
storeState: mapStateToProps(context.getState()),
}
}
// 訂閲數據發生變化,調用setState重新render
componentDidMount() {
this.unsubscribe = this.context.subscribe(() => {
this.setState({
centerStore: mapStateToProps(this.context.getState()),
})
})
}
// 組件被卸載取消訂閲
componentWillUnmount() {
this.unsubscribe()
}
render() {
// 下面的WrapperComponent相當於 home 組件(就是你傳遞的組件)
// 你需要將該組件需要依賴的state和dispatch作為props進行傳遞
return (
<WrapperComponent
{...this.props}
{...mapStateToProps(this.context.getState())}
{...mapDispatchToProps(this.context.dispatch)}
/>
)
}
}
// 取出Provider提供的value
EnhanceComponent.contextType = StoreContext
return EnhanceComponent
}
}
// home.js
// 定義組件依賴的state和dispatch
const mapStateToProps = state => ({
counter: state.counter,
})
const mapDispatchToProps = dispatch => ({
increment() {
dispatch(increment())
},
addNumber(num) {
dispatch(addAction(num))
},
})
export default connect(mapStateToProps,mapDispatchToProps)(依賴redux的組件)
react-redux使用
-
開始之前需要強調一下,
redux和react沒有直接的關係,你完全可以在React, Angular, Ember, jQuery, or vanilla JavaScript中使用Redux -
儘管這樣説,redux依然是和React或者Deku的庫結合的更好,因為他們是通過state函數來描述界面的狀態,Redux可以發射狀態的更新,讓他們作出相應。
-
雖然我們之前已經實現了
connect、Provider這些幫助我們完成連接redux、react的輔助工具,但是實際上redux官方幫助我們提供了react-redux的庫,可以直接在項目中使用,並且實現的邏輯會更加的嚴謹和高效 -
安裝
react-redux:* `yarn add react-redux`
// 1.index.js
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
// 2.home.js
import { connect } from 'react-redux'
// 定義需要依賴的state和dispatch (函數需要返回一個對象)
export default connect(mapStateToProps, mapDispatchToProps)(About)
react-redux源碼導讀
Redux-Middleware中間件
組件中異步操作
-
在之前簡單的案例中,
redux中保存的counter是一個本地定義的數據* 我們可以直接通過同步的操作來`dispatch action`,`state`就會被立即更新。 * 但是真實開發中,`redux`中保存的**很多數據可能來自服務器**,我們需要進行**異步的請求**,再將數據保存到`redux`中 -
網絡請求可以在
class組件的componentDidMount中發送,所以我們可以有這樣的結構:
redux中異步操作
-
上面的代碼有一個缺陷:
* 我們必須將**網絡請求**的異步代碼放到組件的生命週期中來完成 -
為什麼將網絡請求的異步代碼放在
redux中進行管理?* 後期代碼量的增加,如果把網絡請求異步函數放在組件的生命週期裏,這個生命週期函數會變得越來越複雜,組件就會變得越來越大 * 事實上,**網絡請求到的數據也屬於狀態管理的一部分**,更好的一種方式應該是將其也交給`redux`來管理
-
但是在
redux中如何可以進行異步的操作呢?* **使用中間件 (Middleware)** * 學習過`Express`或`Koa`框架的童鞋對中間件的概念一定不陌生 * 在這類框架中,`Middleware`可以幫助我們在**請求和響應之間嵌入一些操作的代碼**,比如cookie解析、日誌記錄、文件壓縮等操作
理解中間件(重點)
-
redux也引入了中間件 (Middleware) 的概念:* 這個中間件的目的是在`dispatch`的`action`和最終達到的`reducer`之間,擴展一些自己的代碼 * 比如日誌記錄、**調用異步接口**、添加代碼調試功能等等
-
redux-thunk是如何做到讓我們可以發送異步的請求呢?* 默認情況下的`dispatch(action)`,`action`需要是一個`JavaScript`的對象 * `redux-thunk`可以讓`dispatch`(`action`函數), `action`**可以是一個函數** * 該函數會被調用, 並且會傳給這個函數兩個參數: 一個`dispatch`函數和`getState`函數 * `dispatch`函數用於我們之後再次派發`action` * `getState`函數考慮到我們之後的一些操作需要依賴原來的狀態,用於讓我們可以獲取之前的一些狀態
redux-thunk的使用
-
安裝
redux-thunk* `yarn add redux-thunk` -
在創建
store時傳入應用了middleware的enhance函數* 通過`applyMiddleware`來結合多個`Middleware`, 返回一個`enhancer` * 將`enhancer`作為第二個參數傳入到`createStore`中  -
定義返回一個函數的
action* 注意:這裏不是返回一個對象了,而是一個**函數** * 該函數在`dispatch`之後會被執行

查看代碼
redux-devtools
redux-devtools插件
-
我們之前講過,
redux可以方便的讓我們對狀態進行跟蹤和調試,那麼如何做到呢?* `redux`官網為我們提供了`redux-devtools`的工具 * 利用這個工具,我們可以知道每次狀態是如何被修改的,修改前後的狀態變化等等 -
使用步驟:
* 第一步:在瀏覽器上安裝[redux-devtools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd/related?utm_source=chrome-ntp-icon)擴展插件 * 第二步:在`redux`中集成`devtools`的中間件
// store.js 開啓redux-devtools擴展
import { createStore, applyMiddleware, compose } from 'redux'
// composeEnhancers函數
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true }) || compose
// 通過applyMiddleware來結合多個Middleware,返回一個enhancer
const enhancer = applyMiddleware(thankMiddleware)
// 通過enhancer作為第二個參數傳遞createStore中
const store = createStore(reducer, composeEnhancers(enhancer))
export default store
redux-sage
generator
Generator函數是 ES6 提供的一種異步編程解決方案,語法行為與傳統函數完全不同
Generator函數有多種理解角度。語法上,首先可以把它理解成,Generator函數是一個狀態機,封裝了多個內部狀態。
// 生成器函數的定義
// 默認返回: Generator
function* foo() {
console.log('111')
yield 'hello'
console.log('222')
yield 'world'
console.log('333')
yield 'jane'
console.log('444')
}
// iterator: 迭代器
const result = foo()
console.log(result)
// 使用迭代器
// 調用next,就會消耗一次迭代器
const res1 = result.next()
console.log(res1) // {value: "hello", done: false}
const res2 = result.next()
console.log(res2) // {value: "world", done: false}
const res3 = result.next()
console.log(res3) // {value: "jane", done: false}
const res4 = result.next()
console.log(res4) // {value: undefined, done: true}
redux-sage流程
redux-saga的使用
-
redux-saga是另一個比較常用在redux發送異步請求的中間件,它的使用更加的靈活 -
Redux-saga的使用步驟如下1. 安裝`redux-sage`: `yarn add redux-saga` 2. 集成`redux-saga`中間件 * 引入 `createSagaMiddleware` 後, 需要創建一個 `sagaMiddleware` * 然後通過 `applyMiddleware` 使用這個中間件,接着創建 `saga.js` 這個文件 * 啓動中間件的監聽過程, 並且傳入要監聽的`saga` 3. `saga.js`文件的編寫 * `takeEvery`:可以傳入多個監聽的`actionType`,每一個都可以被執行(對應有一個`takeLatest`,會取消前面的) * `put`:在`saga`中派發`action`不再是通過`dispatch`, 而是通過`put` * `all`:可以在`yield`的時候`put`多個`action`
// store.js
import createSageMiddleware from 'redux-saga'
import saga from './saga'
// 1.創建sageMiddleware中間件
const sagaMiddleware = createSageMiddleware()
// 2.應用一些中間件
const enhancer = applyMiddleware(sagaMiddleware)
const store = createStore(reducer,composeEnhancers(enhancer))
sagaMiddleware.run(saga)
export default store
// saga.js
import { takeEvery, put, all } from 'redux-saga/effects'
import { FETCH_HOME_DATA } from './constant'
function* fetchHomeData(action) {
const res = yield axios.get('http://123.207.32.32:8000/hom...
const banners = res.data.data.banner.list
const recommends = res.data.data.recommend.list
// dispatch action 提交action,redux-sage提供了put
yield all([
yield put(changeBannersAction(banners)),
yield put(changeRecommendAction(recommends)),
])
}
function* mySaga() {
// 參數一:要攔截的actionType
// 參數二:生成器函數
yield all([
takeEvery(FETCH_HOME_DATA, fetchHomeData),
])
}
export default mySaga
reducer代碼拆分
Reducer代碼拆分
-
我們來看一下目前我們的
reducer:* 當前這個`reducer`既有處理`counter`的代碼,又有處理`home`頁面的數據 * 後續`counter`相關的狀態或`home`相關的狀態會進一步變得更加複雜 * 我們也會繼續添加其他的相關狀態,比如購物車、分類、歌單等等 * 如果將所有的狀態都放到一個`reducer`中進行管理,隨着項目的日趨龐大,必然會造成代碼臃腫、難以維護 -
因此,我們可以對
reducer進行拆分:* 我們先抽取一個對`counter`處理的`reducer` * 再抽取一個對`home`處理的`reducer` * 將它們合併起來
Reducer文件拆分
-
目前我們已經將不同的狀態處理拆分到不同的
reducer中,我們來思考:* 雖然已經放到不同的函數了,但是這些函數的處理依然是在同一個文件中,代碼非常的混亂 * 另外關於`reducer`中用到的`constant`、`action`等我們也依然是在同一個文件中;
combineReducers函數
-
目前我們合併的方式是通過每次調用
reducer函數自己來返回一個新的對象 -
事實上,
redux給我們提供了一個combineReducers函數可以方便的讓我們對多個reducer進行合併
import { combineReducers } from 'redux'
import { reducer as counterReducer } from './count'
import { reducer as homeReducer } from './home'
export const reducer = combineReducers({
counterInfo: counterReducer,
homeInfo: homeReducer,
})
-
那麼
combineReducers是如何實現的呢?* 它將我們傳遞的`reducer`合併成一個對象, 最終返回一個`combination`函數 * 在執行`combination`函數過程中, 會通過判斷前後返回的數據是否相同來決定返回之前的`state`還是新的`state`
immutableJs
數據可變形的問題
-
在
React開發中,我們總是會強調數據的不可變性:* 無論是類組件中的`state`,還是`reduex`中管理的`state` * 事實上在整個`JavaScript`編碼的過程中,數據的不可變性都是非常重要的 -
數據的可變性引發的問題(案例):
* 我們明明沒有修改obj,只是修改了obj2,但是最終obj也被我們修改掉了 * 原因非常簡單,對象是引用類型,它們指向同一塊內存空間,兩個引用都可以任意修改
const obj1 = { name: 'jane', age: 18 }
const obj2 = obj1
obj1.name = 'kobe'
console.log(obj2.name) // kobe
-
有沒有辦法解決上面的問題呢?
* 進行對象的拷貝即可:`Object.assign`或擴展運算符 -
這種對象的淺拷貝有沒有問題呢?
* 從代碼的角度來説,沒有問題,也解決了我們實際開發中一些潛在風險 * 從性能的角度來説,有問題,如果對象過於龐大,這種拷貝的方式會帶來性能問題以及內存浪費 -
有人會説,開發中不都是這樣做的嗎?
* 從來如此,便是對的嗎?
認識ImmutableJS
-
為了解決上面的問題,出現了
Immutable對象的概念:* `Immutable`對象的特點是隻要修改了對象,就會返回一個新的對象,舊的對象不會發生改變; -
但是這樣的方式就不會浪費內存了嗎?
* 為了節約內存,又出現了一個新的算法:`Persistent Data Structure`(持久化數據結構或一致性數據結構) -
當然,我們一聽到持久化第一反應應該是數據被保存到本地或者數據庫,但是這裏並不是這個含義:
* 用一種數據結構來保存數據 * 當數據被修改時,會返回一個對象,但是**新的對象會盡可能的利用之前的數據結構而不會對內存造成浪費**,如何做到這一點呢?結構共享:
-
安裝
Immutable:yarn add immutable
ImmutableJS常見API
注意:我這裏只是演示了一些API,更多的方式可以參考官網
作用:不會修改原有數據結構,返回一個修改後新的拷貝對象
-
JavaScrip和ImutableJS直接的轉換* 對象轉換成`Immutable`對象:`Map` * 數組轉換成`Immtable`數組:`List` * 深層轉換:`fromJS`
const im = Immutable
// 對象轉換成Immutable對象
const info = {name: 'kobe', age: 18}
const infoIM = im.Map()
// 數組轉換成Immtable數組
const names = ["abc", "cba", "nba"]
const namesIM = im.List(names)
-
ImmutableJS的基本操作:* 修改數據:`set(property, newVal)` * 返回值: 修改後新的數據結構 * 獲取數據:`get(property/index)` * 獲取深層`Immutable`對象數據(子屬性也是`Immutable`對象): `getIn(['recommend', 'topBanners'])`
// set方法 不會修改infoIM原有數據結構,返回修改後新的數據結構
const newInfo2IM = infoIM.set('name', 'james')
const newNamesIM = namesIM.set(0, 'why')
// get方法
console.log(infoIM.get('name'))// -> kobe
console.log(namesIM.get(0))// -> abc
結合Redux管理數據
-
ImmutableJS重構redux* yarn add Immutable * yarn add redux-immutable -
使用redux-immutable中的combineReducers;
-
所有的reducer中的數據都轉換成Immutable類型的數據
FAQ
React中的state如何管理
-
目前項目中採用的state管理方案(參考即可):
* 相關的組件內部可以維護的狀態,在組件內部自己來維護 * 只要是需要共享的狀態,都交給redux來管理和維護 * 從服務器請求的數據(包括請求的操作) ,交給redux來維護