文章目录
Redux是一款著名的JavaScript状态管理容器。
也就是说,Redex除了和React配合使用,还可以配置JS、Vue使用。
1. 设计思想
- Redux是将整个应用状态存储到一个叫做
store
的地方,里面存在一个状态树state tree
- 组件可通过
store.dispatch
派发行为action
给store
,store不会直接修改state,而是通过用户编写的reducer
来生成新的state,并返回给store - 其他组件通过订阅
store
中的state
状态变化来刷新自己的视图
2. 三大原则
- 整个应用有且仅有一个store,其内部的state tree存储整个应用的state
- state是只读的,修改state只能通过派发action,为了描述action如何改变state,编写纯函数reducer(原始状态值,工作)
- 单一数据源的设计让React组件之间通信更加方便,也有利于状态的统一管理。
3. createStore(reducer)
Redux 提供createStore这个函数,用来生成 Store。
// 目录:./src/store.js
import {createStore} from 'redux';
function reducer(){}
let store = createStore(reudcer)
3.1 store
通过 createStore 方法可以创建一个 store, 需要传递一个参数 reducer。
store提供的方法:
store.getState()
:获取最新的 state treestore.dispatch()
: 派发行为 actionstore.subscribe()
: 订阅 store 中 state 的变化
3.2 reducer(state,action)
- reducer必须是一个纯函数,接收state、action两个参数。
- state是旧的状态,不可直接修改,需要根据action.type的不同,生成新的state并返回。
// 目录:./src/store.js
import {createStore} from 'redux';
export const ADD = 'ADD';
export const MINUS = "MINUS";
function reducer(state={count:0},action){
console.log('action:',action);
switch (action.type){
case ADD:
return {count:state.count+1};
case MINUS:
return {count:state.count-1};
default:
return state;
}
}
let store = createStore(reducer);
export default store;
3.3 getState()
dispatch()
subscribe()
//目录: ./src/components/Counter.js
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 (
<>
<p>{this.state.number}</p>
<button onClick={() => store.dispatch({type: 'ADD'})}>+</button>
<button onClick={() => store.dispatch({type: 'MINUS'})}>-</button>
</>
)
}
}
export default Counter;
在Counter组件中,通过store.getState()获取最新的state。
点击按钮,会通过store.dispatch()派发action给store(注意:action是一个对象,必须存在type属性),store内部会将当前的state action传递给reducer来生成新的state达到更新状态的目的。
但页面上的数字并没有发生变化。
可以看到,reducer函数中已经接受了action,此时store中的state已经发生了变化,而页面不更新的原因在于Counter没有订阅store中state的变化。
类组件实现:
//目录: ./src/components/Counter.js
import React from 'react';
import store from '../store';
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
number: store.getState().count
}
}
componentDidMount() {
this.unSubscribe = store.subscribe(() => {
this.setState({number: store.getState().count})
})
}
componentWillUnmount() {
this.unSubscribe && this.unSubscribe();
}
render(){
return (
<>
<p>{this.state.number}</p>
<button onClick={() => store.dispatch({type: 'ADD'})}>+</button>
<button onClick={() => store.dispatch({type: 'MINUS'})}>-</button>
</>
)
}
}
export default Counter;
函数组件实现:
//目录: ./src/components/Counter.js
import React from 'react';
import store from '../store';
import {useState,useEffect} from 'react';
function Counter (){
let [number,setNumber] = useState(store.getState().count);
useEffect(() => {
const unSubscribe = store.subscribe(() => {
setNumber(store.getState().count)
})
return () => {
unSubscribe();
}
},[])
return (
<>
<p>{number}</p>
<button onClick={() => store.dispatch({type: 'ADD'})}>+</button>
<button onClick={() => store.dispatch({type: 'MINUS'})}>-</button>
</>
)
}
export default Counter;
使用store.subscribe()
就可以实现订阅,该方法接受一函数,当store中state的状态发生改变,就会执行传入的函数。
store.subscribe()
方法返回一个函数,用于取消订阅。
首次加载后,控制台输出了:
这是store
为了拿到state的初始值{count:0},会自动派发一次action {type: “@@redux/INIT1.s.m.m.c.n”}
redux内部使用"发布-订阅"模式
3.4 bindActionCreators()
在 Counter 组件中,我们是直接使用 store.dispatch 派发action
<button onClick={ ()=> store.dispatch({type: 'ADD'})}>count++</button>
<button onClick={ ()=> store.dispatch({type: 'MINUS'})}>count--</button>
上面写法的缺陷在于,多次重复写了 store.dispatch, 并且 action.type 容易写错还不易发现,此时 redux 提供了 bindActionCreators 功能,将派发 action 的函数与 store.dispatch 进行绑定。
import React from 'react';
import store from '../store';
import {useState,useEffect} from 'react';
import {bindActionCreators} from "redux";
function Counter (){
let [number,setNumber] = useState(store.getState().count);
useEffect(() => {
const unSubscribe = store.subscribe(() => {
setNumber(store.getState().count)
})
return () => {
unSubscribe();
}
},[])
function add() {
return {type:'ADD'}
}
function minus(){
return {type:'MINUS'}
}
const bindAdd = bindActionCreators(add,store.dispatch);
const bindMinus = bindActionCreators(minus,store.dispatch);
return (
<>
<p>{number}</p>
<button onClick={bindAdd}>+</button>
<button onClick={bindMinus}>-</button>
</>
)
}
export default Counter;
其实,代码中可将 bindActionCreators 逻辑抽离到单独文件中,可在其它组件中去使用。
同时,上面代码的缺陷在与 每个函数都需要去手动绑定,并不合理,所以,bindActionCreators 支持传入对象,将所以的 actionCreator 函数包装成对象,代码如下:
import React from 'react';
import store from '../store';
import {useState,useEffect} from 'react';
import {bindActionCreators} from "redux";
function Counter (){
let [number,setNumber] = useState(store.getState().count);
useEffect(() => {
const unSubscribe = store.subscribe(() => {
setNumber(store.getState().count)
})
return () => {
unSubscribe();
}
},[])
function add() {
return {type:'ADD'}
}
function minus(){
return {type:'MINUS'}
}
const actions = {add,minus};
const bindActions = bindActionCreators(actions,store.dispatch);
return (
<>
<p>{number}</p>
<button onClick={bindActions.add}>+</button>
<button onClick={bindActions.minus}>-</button>
</>
)
}
export default Counter;
4. 购物车案例
举例一个场景:
我在逛淘宝商城,点进了一个淘宝店家的商品A的详情页,加进购物车;又点进了天猫店家的商品B,加入购物车。
最终我打开购物车发现有A B两件商品。假设淘宝商品详情页和天猫商品是两个独立的组件。
要实现上述的场景,购物车就应该是一个共享组件。此时,可以使用redux对购物车进行统一管理。
- 目录结构
- store.js
import {createStore} from 'redux';
// 声明一个实际操作函数,相应操作
const cardReducer = (state={goods:[]},action) => {
switch(action.type){
case 'add':
return {...state,goods:[...state.goods,action.good]}
case 'delete':
let deletedGoodIdx = action.good.index;
let newGoods = [...state.goods];
newGoods.splice(deletedGoodIdx,1);
return {...state,goods:newGoods};
default:
return state;
}
}
// 创建store实例并导出
const store = createStore(cardReducer);
export default store;
- 淘宝----TBItem.js
import store from '../store/store';
// 淘宝商品详情组件
function TBItem(){
return (
<>
<h2>这是淘宝详情页</h2>
<p>当前购物车内商品数量: {store.getState().goods.length}</p>
<button
onClick={() => {
store.dispatch(
{
type:'add',
good:{
title:`淘宝商品${Date.now()}`,
price: 100
}
})
}}>添加购物车
</button>
</>
)
}
export default TBItem;
- 天猫—TMItem.js
import store from '../store/store';
// 天猫商品详情组件
function TMItem(){
return (
<>
<h2>这是天猫详情页</h2>
<p>当前购物车商品数量:{store.getState().goods.length}</p>
<button
onClick={() => {
store.dispatch(
{
type:'add',
good:{
title:`天猫商品${Date.now()}`,
price: 100
}
})
}}>添加购物车
</button>
</>
)
}
export default TMItem;
- 购物车—Cart.js
import store from '../store/store';
function Cart(){
let goods = store.getState().goods;
return (
<>
<ul>
{
goods.map((good,index)=> {
return (
<li key={ index }>
<span>{ good.title }</span>
<button onClick={ ()=>
store.dispatch({
type:'delete',
good:{index}
})}>删除</button>
</li>
)
})
}
</ul>
</>
)
}
export default Cart;
- App.js
import {NavLink,Route,Routes} from 'react-router-dom';
import TBItem from './components/TBItem';
import TMItem from './components/TMItem';
import Cart from './components/Cart';
function App() {
return (
<div className="App">
<div>
<NavLink to={'/tb'}>淘宝</NavLink>
<br/>
<NavLink to={'/tm'}>天猫</NavLink>
</div>
<Routes>
<Route path={'/tb'} element={<TBItem/>}/>
<Route path={'/tm'} element={<TMItem/>}/>
</Routes>
<Cart/>
</div>
);
}
export default App;
- index.js
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import store from './store/store';
import {BrowserRouter} from 'react-router-dom';
const render = () => {
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
}
render();
store.subscribe(render);
最终效果展示:
-
刚开始进入
-
点击进入淘宝界面添加商品
-
点击进入天猫界面添加商品
由此可以发现淘宝天猫添加的商品都在购物车中都能展现。
实现了使用redux对淘宝天猫状态的统一管理。
5. 小结
可以看出,在 React 组件中使用 store, 都需要手动去引入 store 文件, 手动订阅 store 中状态的变化,这是不合理的。
下篇博客,我们看 react-redux 是如何解决的。