前言
在学习 React 过程中,都会接触使用到 Redux, React-Redux ,不熟悉的小伙伴,可能疑惑有了 Redux,为什么还出现了 React-Redux,先带大家了解 Redux 的使用,以及使用过程中有哪些吐槽的点,再看看 React-Redux 为啥出现
一、Redux
在开始之前,需要记住的是
Redux 是一款著名 JavaScript 状态管理容器
也就是说,Redux 除了跟 React 配合使用,还可以配置 JS 、Vue 使用
1.1 设计思想
-
Redux
是将整个应用状态存储到一个叫做store
的地方,里面存在一个状态树state tree
- 组件可通过
store.dispatch
派发行为action
给store
,store
不会直接修改state
, 而是通过用户编写的reducer
来生成新的state
,并返回给store
- 其它组件通过订阅
store
中的 state 状态变化来刷新自己的视图
1.2 三大原则
- 整个应用有且仅有一个 store, 其内部的 state tree 存储整个应用的 state
- state 是只读的,修改 state,只能通过派发 action,为了描述 action 如何改变 state 的,需要编写 reducer 纯函数
- 单一数据源的设计让 React 组件之间通信更加方便,也有利于状态的统一管理
1.3 createStroe
通过实战写个 Counter 计数器来学习下 Redux 相关的 API
1.3.1 store
// ./src/store.js
import {createStroe} from 'react'
function reudcer(){}
let store = createStore(reudcer)
通过 createStore
方法可以创建一个 store
, 需要传递一个参数 reducer (ps: 后续介绍),而 store 是个对象,有以下方法可调用
- store.getState(), 获取最新的 state tree
- store.dispatch(), 派发行为 action
- store.subscribe(), 订阅 store 中 state 的变化
1.3.2 reducer
reducer 必须是个纯函数,接收 state
, action
两个参数,state 是旧的状态,不可直接修改,而是需要根据 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
设置了初始值 {count: 0}
, 接下来,会在 Counter
组件中去使用这个导出的 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
{this.state.number}
}
}
export default Counter
在 Counter
组件中,通过 store.getState() 可获取最新的 state, 点击按钮,会通过 store.dispatch 派发 action 给 store (ps:请注意 action 是个对象,必须存在 type 属性),store 内部会将当前 state, action 传递给 reducer 来生成新的 state 达到更新状态的目的, 遗憾的是,页面上数字并没有发生变化
可以看到,reducer 函数中已经接受到了 action, 此时 store 中的 state 已经发生了变化,而页面不更新的原因在于 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
就可实现订阅,该方法接受一函数,当 store
中 state
中状态发生变化,就会执行传入的函数,同时 store.subscribe
方法返回一个函数,用于取消订阅。
至此,Counter组件已基本实现了。可能有些小伙伴发现应用首次加载后,控制台输出了
action {type: "@@redux/INIT1.s.m.m.c.n"}
这是 store
为了拿到 state
的初始值 {count: 0}
, 会自动派发一次 action {type: "@@redux/INIT1.s.m.m.c.n"}
熟悉“发布-订阅”模式的小伙伴可能看得出,Redux
内部就是使用了“发布-订阅”模式。接下来,我们尝试实现个简陋版本 Redux
1.3.4 手写实现 createStroe
function createStore(reducer){
let state
const listeners = []
// 返回最新的 state
function getState () {
return state
}
// 派发 action
function dispatch(action){
state = reducer(state, action)
listeners.forEach(listener => listener())
}
// 订阅,返回取消订阅函数
function subscribe(listener){
listeners.push(listener)
return function () {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
// 获取state默认值
dispatch({type: "@@redux/INIT1.s.m.m.c.n"})
// 返回 store, 一个对象
return {
getState,
dispatch,
subscribe
}
}
export default createStore
通过测试,我们简陋版 Redux 已经实现的 Counter 组件的功能
1.4 bindActionCreators
1.4.1 原理及使用
在 Counter 组件中,我们是直接使用 store.dispatch 派发action
上面写法的缺陷在于,多次重复写了 store.dispatch, 并且 action.type 容易写错还不易发现,此时 redux 提供了 bindActionCreators 功能,将派发 action 的函数与 store.dispatch 进行绑定
// ./src/components/Counter.jsx
import React from 'react'
import {bindActionCreators} from 'redux'
import store from '../store'
// add 函数返回 action, 所以该函数可称作 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
{this.state.number}
}
}
export default Counter
其实,代码中可将 bindActionCreators 逻辑抽离到单独文件中,可在其它组件中去使用。同时,上面代码的缺陷在与 每个函数都需要去手动绑定,并不合理,所以,bindActionCreators 支持传入对象,将所以的 actionCreator 函数包装成对象
// ./src/components/Counter.jsx
import React from 'react'
import {bindActionCreators} from 'redux'
import store from '../store'
// add 函数返回 action, 所以该函数可称作 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
{this.state.number}
}
}
export default Counter
1.4.2 手写实现
function bindActionCreators (actionCreater, dispatch) {
// actionCreater 可以是函数/对象
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 原理及使用
当一个应用包含多个模块,将所以模块的 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 可任意设置,属性value对应每个模块的 reducer 函数, 返回最终的一个合并之后的 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
}
}
//...
}
当组件中派发 action
时,action
会传递到 combineReducers
返回的函数中,在该函数中,会调用每个模块各自的 reducer
生成各自新的 state
, 最终将所以 state
合并之后,去更新 store
中的 state
1.5.2 手写实现
function combineReducers(reducers){
// 返回合并之后的 reducer 函数
return function (state, action){
const nextState = {}
Object.keys(reducers).forEach(key => {
nextState[key] = reducers[key](state[key], action)
})
return nextState
}
}
可以看出,主要派发 action,每个模块的的 reducer 函数都会执行的
1.6 小结
可以看出,在 React 组件中使用 store, 都需要手动去引入 store 文件, 手动订阅 store 中状态的变化,这是不合理的,接下来,我们看下 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(
,
document.getElementById('root')
);
在后代组件 Counter1 中,可使用 react-redux 提供 connect 函数,将 store 与 Counter1 组件的 props 进行关联
import React from 'react'
import { connect } from 'react-redux'
import action from '../store/actions/Counter1'
class Counter1 extends React.Component{
render () {
return
{ this.props.count }
}
}
const mapStateToProps = state => state
const mapDispatchToProps = {
...action
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter1)
从上面代码中,可以看出在 Counter1 组件内部,属性或方法都是通过 props 访问,我们完全可以将 Counter1 组件转换成函数组件(无状态组件),通过函数组件外部都是一个容器组件(有状态组件)进行包裹,所有 connect(mapStateToProps, mapDispatchToProps)(Counter1) 最终返回的就是一个容器组件,接下来我们看下如何手写一个 react-redux
2.2 手写实现
想跨组件传递 store,react-redux 内部使用了 React Context API
创建一个 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
{this.props.children}
}
}
export default Provider
而 connect 方法,接收 mapStateToProps, mapDispatchToProps 两个参数,返回一个函数,返回的函数接收 自定义组件(例如 Counter1 ),函数执行后,返回最终的容器组件
// 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
}
}
}
}
export default connect
可以看出,connect 方法中,有 bindActionCreators 绑定 action 与 store.dispatch, 有订阅 store 中的 state 变化,这些都是我们只使用 redux ,需要在 react 组件中需要手动去写的,幸运的是,现在 react-redux 帮我们去干了
三、总结
通过上面的分享,我们终于知道,为什么 react 应用中需要同时引入 redux 和 react-redux 了