「HearLing」React學(xué)習(xí)之路-redux、react-redux
前言
這篇文章零基礎(chǔ)也可以看,我盡量寫得簡(jiǎn)單易懂了,如果覺得理解起來有點(diǎn)費(fèi)力,也可以先去官網(wǎng)入門。目前我還只是初學(xué)React,只能算入門,如果有哪里說的不對(duì)的,歡迎評(píng)論區(qū)指正,萬分感激~
一、Redux因何產(chǎn)生?
首先說它為什么出現(xiàn)
1.趨勢(shì)所致:JavaScript 單頁應(yīng)用開發(fā)日趨復(fù)雜,「JavaScript 需要管理比任何時(shí)候都要多的 state (狀態(tài))。」
2.管理不斷變化的 state 非常困難:如果一個(gè) model 的變化會(huì)引起另一個(gè) model 變化,那么當(dāng) view 變化時(shí),就可能引起對(duì)應(yīng) model 以及另一個(gè) model 的變化,依次地,可能會(huì)引起另一個(gè) view 的變化。「state 在什么時(shí)候,由于什么原因,如何變化已然不受控制。」
二、Redux是干什么的?
說到底它也只是個(gè)工具,了解一個(gè)工具最開始當(dāng)然是要了解它是做啥的咯。
官網(wǎng)對(duì)它的定義:Redux 是 JavaScript 狀態(tài)容器,提供可預(yù)測(cè)化的狀態(tài)管理。
詳細(xì)一些:
Redux會(huì)將整個(gè)應(yīng)用狀態(tài)(其實(shí)也就是數(shù)據(jù))存儲(chǔ)到 StoreStore里面保存一棵狀態(tài)樹( state tree)組件改變state的唯一方法是通過調(diào)用store的 dispatch方法,觸發(fā)一個(gè)action,這個(gè)action被對(duì)應(yīng)的reducer處理,于是state完成更新組件可以派發(fā)(dispatch)行為(action)給store,而不是直接通知其它組件 其它組件可以通過訂閱store中的狀態(tài)(state)來刷新自己的視圖
可以結(jié)合這張圖看:

三、Redux怎么用?
官網(wǎng)實(shí)例(todo)
「State:」 用了一個(gè)普通對(duì)象描述應(yīng)用中的State,沒有setter(修改器方法)
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}
「Action:」 想更新state中的數(shù)據(jù),例如增加todo,需要發(fā)起一個(gè)action。Action就是一個(gè)普通的JavaScript對(duì)象,描述發(fā)生了什么的指示器
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }
強(qiáng)制使用 action 來描述所有變化帶來的好處是可以清晰地知道應(yīng)用中到底發(fā)生了什么。如果一些東西改變了,就可以知道為什么變。
「Reducer:」 把 action 和 state 串起來,reducer 只是一個(gè)接收 state 和 action,并返回新的 state 的函數(shù)。
//編寫很多小函數(shù)來分別管理 state 的一部分
function visibilityFilter(state = 'SHOW_ALL', action) {
if (action.type === 'SET_VISIBILITY_FILTER') {
return action.filter;
} else {
return state;
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }]);
case 'TOGGLE_TODO':
return state.map((todo, index) =>
action.index === index ?
{ text: todo.text, completed: !todo.completed } :
todo
)
default:
return state;
}
}
//reducer 調(diào)用上兩個(gè) reducer,進(jìn)而管理整個(gè)應(yīng)用的 state
function todoApp(state = {}, action) {
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
};
}
手寫實(shí)戰(zhàn)(TodoList)
感興趣的可以看一下codesandbox-TodoList例子可能會(huì)比較慢。
todo這種例子還是比較簡(jiǎn)單的,相當(dāng)于入門,理解Redux工作。
四、react-redux
可以看到上面我們并沒有使用到react-redux,雖然能實(shí)現(xiàn)功能,但細(xì)心會(huì)發(fā)現(xiàn)我是直接拿的store,組件多的話個(gè)個(gè)拿store,這樣不好。我來總結(jié)一下不用react-redux可能會(huì)遇到頭痛的問題比如:
1.store并不是那么顯而易見,一旦組件層級(jí)變得更復(fù)雜,這個(gè)store就會(huì)變得很難控制。
2.邏輯組件看上去很亂,不清晰的原因state和dispatch沒有各自寫在一起,重復(fù)代碼有點(diǎn)多,不直觀。
3.React 組件從 Redux store 中讀取數(shù)據(jù),向 store 中分發(fā) actions 更新數(shù)據(jù)還不夠方便。
Provider
這個(gè)還是很好理解的,就是把store直接集成到React應(yīng)用的頂層props里面,好處是,所有組件都可以在react-redux的控制之下,所有組件都能訪問到Redux中的數(shù)據(jù)。
<Provider store={store}>
<App />
</Provider>,
connect
技術(shù)上講,容器組件就是使用store.subscribe() 從 Redux state 樹中讀取部分?jǐn)?shù)據(jù),并通過 props 來把這些數(shù)據(jù)提供給要渲染的組件。 為啥要用它,簡(jiǎn)單來說節(jié)省工作,沒有他得手工開發(fā)容器組件,并為了性能而手動(dòng)實(shí)現(xiàn) React 性能優(yōu)化建議中的 shouldComponentUpdate 方法。 使用 React Redux 庫的 connect() 方法來生成,這個(gè)方法做了性能優(yōu)化來避免很多不必要的重復(fù)渲染。
connect的使用
代碼如下:
const App = connect(mapStateToProps, mapDispatchToProps)(Counter);
export default App;
mapStateToProps
理解這個(gè)單詞mapStateToProps:把state映射到props中去,state就是redux的state啦,props就是react的props咯。
代碼:
// Map Redux state to component props
function mapStateToProps(state) {
return {
value: state.count
}
}
然后在組件中使用this.props.value就能完成渲染
class Counter extends Component {
render() {
const { value, onIncreaseClick } = this.props;
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
);
}
}
export default Counter;
mapDispatchToProps
理解這個(gè)單詞mapDispatchToProps:map 各種dispatch 變成props。
// Map Redux actions to component props
function mapDispatchToProps(dispatch) {
return {
onIncreaseClick: () => dispatch(increaseAction)
}
}
class Counter extends Component {
render() {
const { value, onIncreaseClick } = this.props;
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
);
}
}
export default Counter;
同理也是可以通過this.props.onIncreaseClick調(diào)用dispatch,這樣就不需要在代碼中運(yùn)行dispatch了。
connect、provider應(yīng)用實(shí)例
看了上面的介紹,應(yīng)該能比較清楚的了解connect是干什么的了,然后也基本能明白怎么做了,但還是沒有寫哥實(shí)例更清楚直白的了:
簡(jiǎn)單的點(diǎn)擊增加count的實(shí)例,應(yīng)該還有許多需要優(yōu)化的地方,這里就學(xué)明白connect和provider就好了。
復(fù)雜一點(diǎn)的todolist的實(shí)例這里用了hooks、connect、provider沒有用react-redux里的hooks鉤子(如果有看不懂的話可以學(xué)學(xué)hooks或者等我有時(shí)間再出一個(gè)class改寫成hooks的文章,還是很簡(jiǎn)單的,只要你專心學(xué))
五、Hooks下的redux
如果項(xiàng)目開發(fā)是用的hooks,那很好,你又省了許多力氣,比如計(jì)數(shù)器這個(gè)這種「簡(jiǎn)單」的狀態(tài)管理例子,幾行代碼解決。
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
但是我們能完全不用redux狀態(tài)管理了嘛?哈哈哈怎么可能呢
對(duì)于已經(jīng)使用redux的:首先在redux沒有給出對(duì)hooks較好支持之前,大多不會(huì)為了hooks來完全重構(gòu)項(xiàng)目吧,順便一講重構(gòu)可能造成的問題: 失去很多connect()提供的自動(dòng)引用緩存,可能導(dǎo)致性能問題,除非用大量的useCallback()來包裹 如果代碼依賴于mapStateToProps中的ownProps,那么你可能會(huì)使用redux hooks編寫更多代碼,而不能直接拿到這個(gè)屬性。 不能像以前那樣在mapDispatchToProps中,為action creator提供依賴注入 對(duì)于有可能是復(fù)雜應(yīng)用的:許多公司的項(xiàng)目大部分都是用的redux管理狀態(tài),他的許多優(yōu)點(diǎn)比如單一數(shù)據(jù)源、數(shù)據(jù)共享、事務(wù)狀態(tài)、數(shù)據(jù)狀態(tài)I/O和副作用隔離、狀態(tài)回溯以及一系列輔助工具帶來的強(qiáng)大調(diào)試能力等等,使得用redux來管理數(shù)據(jù)流成為更好的選擇。 react-redux發(fā)布了新的版本,與之前的contextAPI分離,提供對(duì)hooks的支持,那這不就更香了
新的redux帶來的改變
「不再需要使用」 mapStateToProps,mapDispatchToProps和connect來維護(hù)單獨(dú)的container組件和UI組件,而是在組件中直接使用redux提供的hooks,讀取redux中的state。可以將任何現(xiàn)有的自定義「hooks與redux集成」,而不是將通過hooks創(chuàng)建的state,作為參數(shù)傳遞給其他hooks。
redux對(duì)hooks的支持
首先介紹幾個(gè)核心:
「useSelector:」 用于從Redux存儲(chǔ)的state中提取值并訂閱該state。 「useDispatch:」 除了讀取store中的state,還能dispatch actions更新store中的state。 「useStore:」 用于獲取創(chuàng)建的store實(shí)例。
光看簡(jiǎn)介還不是很清楚,一個(gè)個(gè)來說:
useSelector
看它的介紹,就很像mapStateToProps,但是
不提供ownProps API,最好用useCallback或useMemo來獲取 和useEffect一樣,如果不提供第二個(gè)參數(shù),每次組件更新就會(huì)重新計(jì)算
那可能會(huì)存在一些擔(dān)憂,會(huì)不會(huì)新的沒有之前用的mapStateToProps好用呢?那來看看他的一些好處吧:
當(dāng)然是配合hooks寫代碼更「簡(jiǎn)潔」 性能上延續(xù)redux以前的性能優(yōu)化邏輯,「比較props」,如果當(dāng)前的props跟老的props相同,則組件將不會(huì)重新渲染。 批處理更新,使得多個(gè)useSelector()重新計(jì)算出state,組件只會(huì)重新渲染一次,「不用擔(dān)心useSelector重復(fù)渲染問題」。
首先看看以前是怎么寫的:
//before
// React component
class Counter extends Component {
render() {
const { value, onIncreaseClick } = this.props;
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
);
}
}
export default Counter;
// Connected Component
// Map Redux state to component props
function mapStateToProps(state) {return {value: state.count}}
// Map Redux actions to component props
function mapDispatchToProps(dispatch) {return {onIncreaseClick: () => dispatch(increaseAction)}}
// Connected Component
const App = connect(mapStateToProps,mapDispatchToProps)(Counter)
export default App
然后讓我們用新的useSelect改寫之前寫得計(jì)數(shù)器:
//after
const Counter = props=> {
const { count } = useSelector(
(state) => ({
count: state.count
})
);
return (
<div>
<span>{count}</span>
</div>
);
}
export default Counter;
useDispatch
之前是使用mapDispatchToProps:
//before
// Map Redux actions to component props
function mapDispatchToProps(dispatch) {
return {
onIncreaseClick: () => dispatch(increaseAction)
}
}
現(xiàn)在使用useDispatch,可以直接在組件中使用,以匿名函數(shù)形式:
//after
const dispatch = useDispatch();
return (
<div>
<button onClick={()=>dispatch(increaseAction)}>Increase</button>
</div>
);
由于匿名函數(shù)的性質(zhì),每次重新渲染獲得新的引用,如果作為props傳遞給子組件,那么子組件每次都要重新渲染。
優(yōu)化的意見是在useCallback中創(chuàng)建這個(gè)匿名函數(shù):
//after
import React, { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import increaseAction from "./store/action";
const Counter = props=> {
const { count } = useSelector(
(state) => ({
count: state.count
})
);
const dispatch = useDispatch();
const onIncreaseClick = useCallback(
() => dispatch(increaseAction),[dispatch]
);
return (
<div>
<span>{count}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
);
}
export default Counter;
useStore
在任何需要訪問store的應(yīng)用中,都可以通過usestore來獲取。如果出于某種原因,比如說單元測(cè)試時(shí),想要獲取不同的store,我們可以將store通過新的contextAPI傳遞進(jìn)組件樹中,就像下面這樣:
import React from 'react';
import { useStore } from 'react-redux';
import OtherProvider from './OtherProvider';
const Component = props => {
const store = useStore();
return <OtherProvider store={store}>{props.children}</OtherProvider>
}
實(shí)戰(zhàn)
接著上面已經(jīng)改成hooks的todolist但是還是用的connect的實(shí)例,來重新用react-redux的useSelector和useDispatch實(shí)現(xiàn)。
基本思想前面介紹的差不多來,這里我就不敗代碼,為了更直觀還是用sandbox雖然不是很快:
SandBox ------ useSelector、useDispatch實(shí)戰(zhàn)TodoList
Hooks下的redux總結(jié)
為什么還是要用redux?簡(jiǎn)單來說:Redux 提供了應(yīng)對(duì)大型應(yīng)用的代碼組織和調(diào)試能力,在程序出錯(cuò)時(shí), 能幫你快速定位問題。
對(duì)于一些場(chǎng)景的需求hooks沒法解決:
需要保存或者加載狀態(tài) 跨組件共享狀態(tài) 需要與其他組件共享業(yè)務(wù)邏輯或數(shù)據(jù)處理過程
配合hooks新的redux帶來的不一樣的改變:通過使用useSelector、useDispatch和useStore搭配這hooks寫確實(shí)也是個(gè)不錯(cuò)的嘗試。
總結(jié)
作為一個(gè)之前vue技術(shù)棧轉(zhuǎn)react技術(shù)棧的菜鳥來說,還是踩了一些的坑的:比如在有了vuex的基礎(chǔ)之后,然后有沒有理解清楚理解redux,很容易覺得他兩差不多,但實(shí)際還是有挺多區(qū)別的,也是我深入學(xué)習(xí)redux的一個(gè)導(dǎo)火索。
簡(jiǎn)單的說一下:在 Vuex 中,$store 被直接注入到了組件實(shí)例中,因此可以比較靈活的使用:
使用 dispatch 和 commit 提交更新
通過 mapState 或者直接通過 this.$store 來讀取數(shù)據(jù)
組件中既可以 dispatch action 也可以 commit updates
在 Redux 中:
我們每一個(gè)組件都需要顯示的用 connect 把需要的 props 和 dispatch 連接起來。 Redux 中只能進(jìn)行 dispatch,并不能直接調(diào)用 reducer 進(jìn)行修改。從實(shí)現(xiàn)原理上來說,最大的區(qū)別是兩點(diǎn):
Redux 使用的是不可變數(shù)據(jù),而Vuex的數(shù)據(jù)是可變的。Redux每次都是用新的state替換舊的state,而Vuex是直接修改。
Redux 在檢測(cè)數(shù)據(jù)變化的時(shí)候,是通過 diff 的方式比較差異的,而Vuex其實(shí)和Vue的原理一樣,是通過 getter/setter來比較的。
挖坑
???? 后續(xù)可能大概率還會(huì)更新這篇文章,還有些沒寫到,希望這篇文章對(duì)于你學(xué)習(xí)redux有所幫助哦~
