1. Redux应用场景 #

Redux 是一个流行的 JavaScript 状态管理库,它主要用于管理复杂的应用状态。以下是一些 Redux 的主要应用场景:

  1. 大型应用: 对于大型的、有许多状态需要管理的应用,Redux 可以提供一个集中的地方来存储和管理这些状态,使得状态管理变得更加可预测和容易追踪。

  2. 状态共享: 当多个组件需要共享同一份状态,或者某个状态需要在组件树的多个深层组件之间传递时,Redux 可以提供一个方便的解决方案。

  3. 复杂的状态更新逻辑: 如果你的应用有一些复杂的状态更新逻辑,例如状态的更新依赖于其他状态,或者状态的更新需要异步操作,Redux 可以帮助你将这些逻辑集中管理。

  4. 跨浏览器会话的状态持久化: Redux 配合其他库(例如 redux-persist)可以方便地实现状态的持久化,例如将状态保存到本地存储,使得即使在浏览器刷新后,应用状态也能保持不变。

  5. 开发工具: Redux 提供了强大的开发工具,如 Redux DevTools,可以非常方便地跟踪、理解和调试状态更新的过程。

2. Redux设计思想 #

Redux 是一个用于管理 JavaScript 应用状态的库。它借鉴了 Flux 架构和函数式编程的一些理念,采用了严格的单向数据流,让状态变化变得可预测和易于理解。Redux 的设计思想主要有以下几点:

  1. 单一数据源: Redux 应用只有一个单一的全局状态树(store),所有的状态都存储在这里。这让状态的访问和修改变得非常直观,并且也便于调试和持久化。

  2. 状态是只读的: 在 Redux 中,你不能直接修改状态,而只能通过派发(dispatch)一个动作(action)来描述你想要如何改变状态。这样做可以确保所有的状态变化都被集中处理,并且可以按照一定的顺序执行。

  3. 纯函数修改状态: 在 Redux 中,所有的状态变化都由纯函数(reducer)处理。reducer 函数接收当前的状态和一个动作作为参数,并返回新的状态。因为 reducer 是纯函数,所以它们没有副作用,也不依赖外部的变量或状态,这让测试变得非常简单。

Redux 的设计思想使得状态管理变得清晰和可控。通过限制如何更新状态,Redux 为复杂应用提供了结构和数据一致性。同时,由于所有的状态更新都通过动作触发,因此可以很容易地实现如撤销/重做这样的功能,或者利用 Redux DevTools 进行时间旅行调试。

3. 原生计数器 #

cra zhufengredux
cd zhufengredux
npm install redux --save

3.1 public\index.html #

public\index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
    <div id="counter">
      <p id="counter-value">0</p>
      <button id="add-btn">+</button>
      <button id="minus-btn">-</button>
    </div>
  </body>
</html>

3.2 src\index.js #

src\index.js

import { createStore} from './redux';
let counterValue = document.getElementById('counter-value');
let addBtn = document.getElementById('add-btn');
let minusBtn = document.getElementById('minus-btn');

const ADD = 'ADD';
const MINUS = 'MINUS';
let initState = { number: 0 };

const reducer = (state = initState, action) => {
    switch (action.type) {
        case ADD:
            return { number: state.number + 1 };
        case MINUS:
            return { number: state.number - 1 };
        default:
            return state;
    }
}
let store = createStore(reducer);
function render() {
    counterValue.innerHTML = store.getState().number + '';
}
store.subscribe(render);
render();
addBtn.addEventListener('click', function () {
    store.dispatch({ type: ADD });
});
minusBtn.addEventListener('click', function () {
    store.dispatch({ type: MINUS });
});

4. React计数器 #

4.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import Counter1 from './components/Counter1';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <Counter1 />
);

4.2 Counter1.js #

src\components\Counter1.js

import React, { Component } from 'react';
import { legacy_createStore as createStore} from 'redux';
const ADD = 'ADD';
const MINUS = 'MINUS';
const reducer = (state = initState, action) => {
    switch (action.type) {
        case ADD:
            return { number: state.number + 1 };
        case MINUS:
            return { number: state.number - 1 };
        default:
            return state;
    }
}
let initState = { number: 0 };
const store = createStore(reducer, initState);
export default class Counter extends Component {
    unsubscribe;
    constructor(props) {
        super(props);
        this.state = { number: 0 };
    }
    componentDidMount() {
        this.unsubscribe = store.subscribe(() => this.setState({ number: store.getState().number }));
    }
    componentWillUnmount() {
        this.unsubscribe();
    }
    render() {
        return (
            <div>
                <p>{this.state.number}</p>
                <button onClick={() => store.dispatch({ type: 'ADD' })}>+</button>
                <button onClick={() => store.dispatch({ type: 'MINUS' })}>-</button>
                <button onClick={
                    () => {
                        setTimeout(() => {
                            store.dispatch({ type: 'ADD' });
                        }, 1000);
                    }
                }>1秒后加1</button>
            </div>
        )
    }
}

5.bindActionCreators #

bindActionCreators 是一个 Redux 的函数,用于把 action creators 绑定到 dispatch 函数,让你可以直接调用它们,而不是每次都要调用 dispatch(actionCreator())

这个函数特别有用,当你需要把 action creators 传给一个组件,然后让它能够直接调用 action 创建函数来触发 action,而无需直接调用 dispatch 函数。

react-redux 库的 connect 函数中,你也可以直接传入一个包含了多个 action creators 的对象,connect 函数会自动帮你完成绑定操作:

5.1 Counter1.js #

src\components\Counter1.js

import React, { Component } from 'react';
+import { legacy_createStore as createStore,bindActionCreators} from 'redux';
const ADD = 'ADD';
const MINUS = 'MINUS';
const reducer = (state = initState, action) => {
    switch (action.type) {
        case ADD:
            return { number: state.number + 1 };
        case MINUS:
            return { number: state.number - 1 };
        default:
            return state;
    }
}
let initState = { number: 0 };
const store = createStore(reducer, initState);
+function add() {
+    return { type: 'ADD' };
+}
+function minus() {
+    return { type: 'MINUS' };
+}
+const actions = { add, minus };
+const boundActions = bindActionCreators(actions, store.dispatch);
export default class Counter extends Component {
    unsubscribe;
    constructor(props) {
        super(props);
        this.state = { number: 0 };
    }
    componentDidMount() {
        this.unsubscribe = store.subscribe(() => this.setState({ number: store.getState().number }));
    }
    componentWillUnmount() {
        this.unsubscribe();
    }
    render() {
        return (
            <div>
                <p>{this.state.number}</p>
+               <button onClick={boundActions.add}>+</button>
+               <button onClick={boundActions.minus}>-</button>
                <button onClick={
                    () => {
+                       setTimeout(boundActions.add, 1000);
                    }
                }>1秒后加1</button>
            </div>
        )
    }
}

6. combineReducers.js #

combineReducers 接受一个对象作为参数,该对象的每个键值代表不同的状态切片,并对应一个处理该状态切片的 reducer 函数。返回的 reducer 函数在调用时会将传入的 state 对象切分成与之匹配的部分,并将每个部分传递给相应的 reducer 函数。

通过使用 combineReducers,你可以更容易地将逻辑划分为模块化的方式,每个模块都有自己的 reducer 和相关状态,这有助于提高代码的可读性和可维护性。

6.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
+import Counter1 from './components/Counter1';
+import Counter2 from './components/Counter2';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
+    <div>
+        <Counter1/>
+        <Counter2/>
+    </div>
);

6.2 action-types.js #

src\store\action-types.js

export const ADD1 = 'ADD1';
export const MINUS1 = 'MINUS1';

export const ADD2 = 'ADD2';
export const MINUS2 = 'MINUS2';

6.3 counter1.js #

src\store\reducers\counter1.js

import * as types from '../action-types';
let initialState = { number: 0 }
export default function (state=initialState, action) {
    switch (action.type) {
        case types.ADD1:
            return { number: state.number + 1 };
        case types.MINUS1:
            return { number: state.number - 1 };
        default:
            return state;
    }
}

6.4 counter2.js #

src\store\reducers\counter2.js

import * as types from '../action-types';
let initialState = { number: 0 };
export default function (state=initialState, action) {
    switch (action.type) {
        case types.ADD2:
            return { number: state.number + 1 };
        case types.MINUS2:
            return { number: state.number - 1 };
        default:
            return state;
    }
}

6.5 reducers\index.js #

src\store\reducers\index.js

import { combineReducers} from 'redux';
import counter1 from './counter1';
import counter2 from './counter2';
let rootReducer = combineReducers({
    counter1,
    counter2
});
export default rootReducer;

6.6 store\index.js #

src\store\index.js

import { legacy_createStore as createStore  } from 'redux';
import reducer from './reducers';
const store = createStore(reducer, { counter1: { number: 0 }, counter2: { number: 0 } });
export default store;

6.7 actions\counter1.js #

src\store\actions\counter1.js

import * as types from '../action-types';
const actions = {
    add1() {
        return { type: types.ADD1 };
    },
    minus1() {
        return { type: types.MINUS1 };
    }
}
export default actions;

6.8 actions\counter2.js #

src\store\actions\counter2.js

import * as types from '../action-types';
const actions = {
    add2() {
        return { type: types.ADD2 };
    },
    minus2() {
        return { type: types.MINUS2 };
    }
}
export default actions;

6.9 Counter1.js #

src\components\Counter1.js

import React, { Component } from 'react';
+import { bindActionCreators } from 'redux';
+import actions from '../store/actions/counter1';
+import store from '../store';
+const boundActions = bindActionCreators(actions, store.dispatch);
export default class Counter extends Component {
    unsubscribe;
    constructor(props) {
        super(props);
        this.state = { number: 0 };
    }
    componentDidMount() {
+       this.unsubscribe = store.subscribe(() => this.setState({ number: store.getState().counter1.number }));
    }
    componentWillUnmount() {
        this.unsubscribe();
    }
    render() {
        return (
            <div>
+               <p>{this.state.number}</p>
+               <button onClick={boundActions.add1}>+</button>
+               <button onClick={boundActions.minus1}>-</button>
            </div>
        )
    }
}

6.10 Counter2.js #

src\components\Counter2.js

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import actions from '../store/actions/counter2';
import store from '../store';
const boundActions = bindActionCreators(actions, store.dispatch);
export default class Counter extends Component {
    unsubscribe;
    constructor(props) {
        super(props);
        this.state = { number: 0 };
    }
    componentDidMount() {
        this.unsubscribe = store.subscribe(() => this.setState({ number: store.getState().counter2.number }));
    }
    componentWillUnmount() {
        this.unsubscribe();
    }
    render() {
        return (
            <div>
                <p>{this.state.number}</p>
                <button onClick={boundActions.add2}>+</button>
                <button onClick={boundActions.minus2}>-</button>
            </div>
        )
    }
}

7. react-redux #

react-redux 是一个库,提供了两个主要的接口 Providerconnect,用于将 React 应用和 Redux store 连接起来。它使得我们能够在 React 组件中访问和使用 Redux store 中的数据。

Provider:它是一个 React 组件,负责提供 Redux store 给子组件。我们通常在应用的最外层使用这个组件,并且只使用一次。这使得我们的 Redux store 变成了应用的 context 的一部分,可以被应用中的其他组件访问。

connect:这是 react-redux 的一个函数,用于连接 React 组件和 Redux store。connect 函数接受四个参数(都是可选的):mapStateToPropsmapDispatchToPropsmergePropsoptions。它返回一个新的函数,这个新函数需要接受一个 React 组件作为参数,并返回一个新的已连接 Redux store 的 React 组件。

react-redux 提供了一种简洁、易于理解的方式来集成 React 和 Redux,使得 React 组件可以更方便地从 Redux store 中读取数据以及派发 actions。

7.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import Counter1 from './components/Counter1';
import Counter2 from './components/Counter2';
+import store from './store';
+import { Provider } from 'react-redux';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
+    <Provider store={store}>
        <Counter1/>
        <Counter2/>
+    </Provider>
);

7.2 Counter1.js #

src\components\Counter1.js

import React, { Component } from 'react';
+import actions from '../store/actions/counter1';
+import { connect } from '../react-redux';
class Counter1 extends Component {
    render() {
+        let { number, add1, minus1 } = this.props;
        return (
            <div>
+               <p>{number}</p>
+               <button onClick={add1}>+</button>
+               <button onClick={minus1}>-</button>
            </div>
        )
    }
}
let mapStateToProps = (state) => state.counter1;
export default connect(
    mapStateToProps,
    actions
)(Counter1)

7.3 Counter2.js #

src\components\Counter2.js

import React from 'react';
+import { useSelector,useDispatch} from 'react-redux';
import actions from '../store/actions/counter2';
const Counter2 =  () => {
+  const counter2 = useSelector(state => state.counter2);
+  const dispatch = useDispatch();
        return (
            <div>
                <p>{counter2.number}</p>
+               <button onClick={()=>dispatch(actions.add2())}>+</button>
+               <button onClick={()=>dispatch(actions.minus2())}>-</button>
            </div>
        )
}
export default Counter2;