前言
在學(xué)習(xí) React 過程中,都會接觸使用到 Redux, React-Redux ,不熟悉的小伙伴,可能疑惑有了 Redux,為什么還出現(xiàn)了 React-Redux,先帶大家了解 Redux 的使用,以及使用過程中有哪些吐槽的點,再看看 React-Redux 為啥出現(xiàn)
一、Redux
在開始之前,需要記住的是
Redux 是一款著名 JavaScript 狀態(tài)管理容器
也就是說,Redux 除了跟 React 配合使用,還可以配置 JS 、Vue 使用
1.1 設(shè)計思想
-
Redux是將整個應(yīng)用狀態(tài)存儲到一個叫做store的地方,里面存在一個狀態(tài)樹state tree - 組件可通過
store.dispatch派發(fā)行為action給store,store不會直接修改state, 而是通過用戶編寫的reducer來生成新的state,并返回給store - 其它組件通過訂閱
store中的 state 狀態(tài)變化來刷新自己的視圖
1.2 三大原則
- 整個應(yīng)用有且僅有一個 store, 其內(nèi)部的 state tree 存儲整個應(yīng)用的 state
- state 是只讀的,修改 state,只能通過派發(fā) action,為了描述 action 如何改變 state 的,需要編寫 reducer 純函數(shù)
- 單一數(shù)據(jù)源的設(shè)計讓 React 組件之間通信更加方便,也有利于狀態(tài)的統(tǒng)一管理
1.3 createStroe
通過實戰(zhàn)寫個 Counter 計數(shù)器來學(xué)習(xí)下 Redux 相關(guān)的 API
1.3.1 store
// ./src/store.js
import {createStroe} from 'react'
function reudcer(){}
let store = createStore(reudcer)
通過 createStore 方法可以創(chuàng)建一個 store, 需要傳遞一個參數(shù) reducer (ps: 后續(xù)介紹),而 store 是個對象,有以下方法可調(diào)用
- store.getState(), 獲取最新的 state tree
- store.dispatch(), 派發(fā)行為 action
- store.subscribe(), 訂閱 store 中 state 的變化
1.3.2 reducer
reducer 必須是個純函數(shù),接收 state, action 兩個參數(shù),state 是舊的狀態(tài),不可直接修改,而是需要根據(jù) action.type 不同,來生成新的 state 并返回
// ./src/store.js
import {createStroe} from 'react'
export const ADD = 'ADD'
export const MINUS = 'MINUS'
function reducer (state = {count: 0}, action) {
console.log('action', action); // {type: 'xxx'}
switch(action.type) {
case ADD:
return {count: state.count + 1}
case MINUS:
return {count: state.count - 1}
default:
return state
}
}
let store = createStore(reudcer)
export default store
注意上面代碼中,給 state 設(shè)置了初始值 {count: 0}, 接下來,會在 Counter 組件中去使用這個導(dǎo)出的 store
1.3.3 getState、dispatch、subscribe
// ./src/components/Counter.jsx
import React from 'react'
import store from '../store'
class Counter extends React.Component{
constructor(props){
super(props)
this.state = {
number: store.getState().count
}
}
render () {
return <div>
<p>{this.state.number}</p>
<button onClick={() => store.dispatch({type: 'ADD'})}>+</button>
<button onClick={() => store.dispatch({type: 'MINUS'})}>-</button>
</div>
}
}
export default Counter
在 Counter 組件中,通過 store.getState() 可獲取最新的 state, 點擊按鈕,會通過 store.dispatch 派發(fā) action 給 store (ps:請注意 action 是個對象,必須存在 type 屬性),store 內(nèi)部會將當(dāng)前 state, action 傳遞給 reducer 來生成新的 state 達到更新狀態(tài)的目的, 遺憾的是,頁面上數(shù)字并沒有發(fā)生變化
可以看到,reducer 函數(shù)中已經(jīng)接受到了 action, 此時 store 中的 state 已經(jīng)發(fā)生了變化,而頁面不更新的原因在于 Counter 沒有訂閱 store 中 state 的變化,可在代碼中加入下面代碼
class Counter extends React.Component{
componentDidMount () {
this.unSubscribe = store.subscribe(() => {
this.setState({
number: store.getState().count
})
})
}
componentWillUnmount () {
this.unSubscribe && this.unSubscribe()
}
}
使用 store.subscribe 就可實現(xiàn)訂閱,該方法接受一函數(shù),當(dāng) store 中 state 中狀態(tài)發(fā)生變化,就會執(zhí)行傳入的函數(shù),同時 store.subscribe 方法返回一個函數(shù),用于取消訂閱。
至此,Counter組件已基本實現(xiàn)了??赡苡行┬』锇榘l(fā)現(xiàn)應(yīng)用首次加載后,控制臺輸出了
action {type: "@@redux/INIT1.s.m.m.c.n"}
這是 store 為了拿到 state 的初始值 {count: 0}, 會自動派發(fā)一次 action {type: "@@redux/INIT1.s.m.m.c.n"}
熟悉“發(fā)布-訂閱”模式的小伙伴可能看得出,Redux 內(nèi)部就是使用了“發(fā)布-訂閱”模式。接下來,我們嘗試實現(xiàn)個簡陋版本 Redux
1.3.4 手寫實現(xiàn) createStroe
function createStore(reducer){
let state
const listeners = []
// 返回最新的 state
function getState () {
return state
}
// 派發(fā) action
function dispatch(action){
state = reducer(state, action)
listeners.forEach(listener => listener())
}
// 訂閱,返回取消訂閱函數(shù)
function subscribe(listener){
listeners.push(listener)
return function () {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
// 獲取state默認(rèn)值
dispatch({type: "@@redux/INIT1.s.m.m.c.n"})
// 返回 store, 一個對象
return {
getState,
dispatch,
subscribe
}
}
export default createStore
通過測試,我們簡陋版 Redux 已經(jīng)實現(xiàn)的 Counter 組件的功能
1.4 bindActionCreators
1.4.1 原理及使用
在 Counter 組件中,我們是直接使用 store.dispatch 派發(fā)action
<button onClick={() => store.dispatch({type: 'ADD'})}>+</button>
<button onClick={() => store.dispatch({type: 'MINUS'})}>-</button>
上面寫法的缺陷在于,多次重復(fù)寫了 store.dispatch, 并且 action.type 容易寫錯還不易發(fā)現(xiàn),此時 redux 提供了 bindActionCreators 功能,將派發(fā) action 的函數(shù)與 store.dispatch 進行綁定
// ./src/components/Counter.jsx
import React from 'react'
import {bindActionCreators} from 'redux'
import store from '../store'
// add 函數(shù)返回 action, 所以該函數(shù)可稱作 actionCreator
function add() {
return {type: 'ADD'}
}
function minus() {
return {type: 'MINUS'}
}
const bindAdd = bindActionCreators(add, store.dispatch)
const bindMinus = bindActionCreators(minus, store.dispatch)
class Counter extends React.Component{
// ...
render () {
return <div>
<p>{this.state.number}</p>
<button onClick={bindAdd}>+</button>
<button onClick={bindMinus}>-</button>
</div>
}
}
export default Counter
其實,??代碼中可將 bindActionCreators 邏輯抽離到單獨文件中,可在其它組件中去使用。同時,上面代碼的缺陷在與 每個函數(shù)都需要去手動綁定,并不合理,所以,bindActionCreators 支持傳入對象,將所以的 actionCreator 函數(shù)包裝成對象
// ./src/components/Counter.jsx
import React from 'react'
import {bindActionCreators} from 'redux'
import store from '../store'
// add 函數(shù)返回 action, 所以該函數(shù)可稱作 actionCreator
function add() {
return {type: 'ADD'}
}
function minus() {
return {type: 'MINUS'}
}
const actions = {add, minus}
const bindActions = bindActionCreators(actions, store.dispatch)
class Counter extends React.Component{
// ...
render () {
return <div>
<p>{this.state.number}</p>
<button onClick={ bindActions.add }>+</button>
<button onClick={ bindActions.minus }>-</button>
</div>
}
}
export default Counter
1.4.2 手寫實現(xiàn)
function bindActionCreators (actionCreater, dispatch) {
// actionCreater 可以是函數(shù)/對象
if (typeof actionCreater === 'function') {
return function (...args) {
return dispatch(actionCreater(...args))
}
} else {
let bindActionCreaters = {}
Object.keys(actionCreater).forEach(key => {
bindActionCreaters[key] = function (...args) {
return dispatch(actionCreater(...args))
}
})
return bindActionCreaters
}
}
export default bindActionCreaters
1.5 combineReducers
1.5.1 原理及使用
當(dāng)一個應(yīng)用包含多個模塊,將所以模塊的 state 放在并不合理,更好的做法是按照模塊進行劃分,每個模塊有各自的 reducer、action,最終通過 Redux 中的 combineReducers 合并成一個大的 reducer
// src\store\reducers\index.js
import {combineReducers} from 'redux';
import counter1 from './counterReducer1';
import counter2 from './counterReducer2';
export default combineReducers({
x: counter1,
y: counter2
});
// src/store/reducers/counterReducer1.js
import * as types from '../action-types';
export default function (state= {count: 0},action){
switch(action.type){
case types.ADD1:
return state.count + 1;
case types.MINUS1:
return state.count - 1;
default:
return state;
}
}
// src/store/reducers/counterReducer2.js
import * as types from '../action-types';
export default function (state= {count: 0},action){
switch(action.type){
case types.ADD2:
return state.count + 1;
case types.MINUS2:
return state.count - 1;
default:
return state;
}
}
combineReducers 方法接受一個對象,屬性key 可任意設(shè)置,屬性value對應(yīng)每個模塊的 reducer 函數(shù), 返回最終的一個合并之后的 reducer 方法。
通過 reducer 合并之后,store 中的 state tree 也會按照模塊進行劃分
store.getState()
{
x: {count: 0}
y: {count: 0}
}
這樣,在組件中,使用 state 需要修改成下面這樣
import store from '../store';
export default class Counter extends Component {
constructor(props) {
super(props);
this.state = {
value: store.getState().x.count
}
}
//...
}
當(dāng)組件中派發(fā) action 時,action 會傳遞到 combineReducers 返回的函數(shù)中,在該函數(shù)中,會調(diào)用每個模塊各自的 reducer 生成各自新的 state, 最終將所以 state 合并之后,去更新 store 中的 state
1.5.2 手寫實現(xiàn)
function combineReducers(reducers){
// 返回合并之后的 reducer 函數(shù)
return function (state, action){
const nextState = {}
Object.keys(reducers).forEach(key => {
nextState[key] = reducers[key](state[key], action)
})
return nextState
}
}
可以看出,主要派發(fā) action,每個模塊的的 reducer 函數(shù)都會執(zhí)行的
1.6 小結(jié)
可以看出,在 React 組件中使用 store, 都需要手動去引入 store 文件, 手動訂閱 store 中狀態(tài)的變化,這是不合理的,接下來,我們看下 react-redux 是如何解決的
二、React-Redux
2.1 原理及使用
react-redux 提供一個 Provider 組件,通過 Provider 組件,可以向其子組件、孫組件傳遞 store, 而不需要每個組件都手動引入
// ./src/index.js
import { Provider } from 'react-redux'
import store from './store'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
在后代組件 Counter1 中,可使用 react-redux 提供 connect 函數(shù),將 store 與 Counter1 組件的 props 進行關(guān)聯(lián)
import React from 'react'
import { connect } from 'react-redux'
import action from '../store/actions/Counter1'
class Counter1 extends React.Component{
render () {
return <div>
<p>{ this.props.count }</p>
<button onClick={ this.props.add }>+</button>
<button onClick={ this.props.minus }>-</button>
</div>
}
}
const mapStateToProps = state => state
const mapDispatchToProps = {
...action
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter1)
從上面代碼中,可以看出在 Counter1 組件內(nèi)部,屬性或方法都是通過 props 訪問,我們完全可以將 Counter1 組件轉(zhuǎn)換成函數(shù)組件(無狀態(tài)組件),通過函數(shù)組件外部都是一個容器組件(有狀態(tài)組件)進行包裹,所有 connect(mapStateToProps, mapDispatchToProps)(Counter1) 最終返回的就是一個容器組件,接下來我們看下如何手寫一個 react-redux
2.2 手寫實現(xiàn)
想跨組件傳遞 store,react-redux 內(nèi)部使用了 React Context API
創(chuàng)建一個 ReactReduxContext 上下文對象
// src/react-redux/Context.js
import React from 'react'
export const ReactReduxContext = React.createContext(null)
export default ReactReduxContext
在 Proveider 組件中,需要使用 ReactReduxContext 對象中提供的 Provider 組件
// src/react-redux/Provider.js
import React from 'react'
import {ReactReduxContext} from './Context'
class Provider extends React.Component{
constructor(props) {
super(props)
}
render () {
return <ReactReduxContext.Provider value={{ store: this.props.store }}>
{this.props.children}
</ReactReduxContext.Provider>
}
}
export default Provider
而 connect 方法,接收 mapStateToProps, mapDispatchToProps 兩個參數(shù),返回一個函數(shù),返回的函數(shù)接收 自定義組件(例如 Counter1 ),函數(shù)執(zhí)行后,返回最終的容器組件
// src/react-redux/connect.js
import React from 'react'
import {bindActionCreators} from 'redux'
import {ReactReduxContext} from './Context'
function connect(mapStateToProps, mapDispatchToProps) {
return function (WrappedComponent) {
// 返回最終的容器組件
return class extends React.Component{
static contextType = ReactReduxContext
constructor(props, context){
super(props)
this.state = mapStateToProps(context.store.getState())
}
shouldComponentUpdate() {
if (this.state === mapStateToProps(this.context.store.getState())) {
return false;
}
return true;
}
componentDidMount () {
this.unsubscribe = this.context.subscribe(() => {
this.setState(mapStateToProps(this.context.store.getState()))
})
}
componentWillUnmount (){
this.unsubscribe && this.unsubscribe()
}
render(){
const actions = bindActionCreators(
mapDispatchToProps,
this.context.store.dispatch
)
return <WrappedComponent {...this.state} {...this.props} {...actions}/>
}
}
}
}
export default connect
可以看出,connect 方法中,有 bindActionCreators 綁定 action 與 store.dispatch, 有訂閱 store 中的 state 變化,這些都是我們只使用 redux ,需要在 react 組件中需要手動去寫的,幸運的是,現(xiàn)在 react-redux 幫我們?nèi)ジ闪?/p>
三、總結(jié)
通過上面的分享,我們終于知道,為什么 react 應(yīng)用中需要同時引入 redux 和 react-redux 了