React - redux && react-redux 的使用及基本原理

  • Redux 以数据存储中心 Store 为核心,修改数据、初始化数据等通过 ReducersReducers 操作完成后会通过组件对 Store 的订阅告知组件数据更新了,当然组件也可以发起对数据的修改,组件通过发送 Action,派发行为给 Store ,然后通知Reducers 进行相应操作
  • Redux 是不存在异步操作的
  • 在使用之前要先安装 Redux,当前使用版本:
"redux": "^4.0.5"
  • 举个计数器例子认识下最基本的Redux ,在 src 目录下创建一个 store.js
    计数器.jpg
// src/store.js

import {createStore} from 'redux';

// 初始化状态 state = 0
// 状态的操作
const counterReducer = (state = 0, action) => {
  switch(action.type){
    case 'add':
      return state + 1;
    case 'minus':
      return state - 1;
    default:
      return state;
  } 
}

// 创建一个store
const store = createStore(counterReducer)

export default store;
  • 然后来使用这个 store,写到这的时候发现页面中的加减按钮不生效,原因是组件要实现对 store 的操作,必须订阅 store 的状态更新
// src/components/ReduxTest.js

import React from 'react';
// 引入store
import store from '../store'

class ReduxTest extends React.Component{
  render () {
    return (
      <div>
        {/* 获取状态 */}
        <p>{store.getState()}</p>

        {/* 通过dispatch派发action */}
        <div>
          <button onClick={() => store.dispatch({type: 'minus'})}>-</button>
          <button onClick={() => store.dispatch({type: 'add'})}>+</button>
        </div>
      </div>
    )
  }
}

export default ReduxTest
  • 在没有使用其他插件的情况下,我们对 index.js 添加部分内容来实现订阅,添加完成后计数器就生效了
// index.js

// 添加store.subscribe方法,在该方法中再次执行ReactDOM的渲染,实现对store的订阅
store.subscribe(() => {
  ReactDOM.render(
    // <React.StrictMode>
      <App />,
    // </React.StrictMode>,
    document.getElementById('root')
  );
})
  • 最基本的 Redux 可以看出用起来还是比较繁琐的,每个组件都要引入 storeindex.js 中还要频繁更新 DOM,为了解决这些问题可以使用相对更友好的插件库,这里安装下 react-redux,这个库主要提供了2个 api,分别是 Providerconnect,安装完成后我们来对上面的例子做重构
"react-redux": "^7.2.0"
// 1. 首先去掉index.js中的订阅
// index.js

// 添加store.subscribe方法,在该方法中再次执行ReactDOM的渲染,实现对store的订阅
// store.subscribe(() => {
//   ReactDOM.render(
//     // <React.StrictMode>
//       <App />,
//     // </React.StrictMode>,
//     document.getElementById('root')
//   );
// }) 
// 2. 修改使用 ReduxTest.js 的方式
// App.js

import store from './store'
import {Provider} from 'react-redux'

{/* 上下文的形式隔代传递数据 */}
<Provider store={store}>
  <ReduxTest></ReduxTest>
 </Provider>
// 3. 重构计数器组件
// src/components/ReactTest.js

import React from 'react';
// connect函数连接 react-redux 和 redux
import {connect} from 'react-redux';

// 映射函数
const mapStateToProps = state => ({num: state});
// 简化store.dispatch的操作
const mapDispatchToProps = {
  add: () => ({type: 'add'}),
  minus: () => ({type: 'minus'})
}

// 装饰器写法:
@connect(mapStateToProps, mapDispatchToProps)
class ReduxTest extends React.Component{
  render () {
    const {num, add, minus} = this.props
    return (
      <div>
        {/* 获取状态 */}
        <p>{num}</p>

        {/* 通过dispatch派发action */}
        <div>
          <button onClick={minus}>-</button>
          <button onClick={add}>+</button>
        </div>
      </div>
    )
  }
}

export default ReduxTest
  • 接着再看如何让 Redux 支持异步,需要中间件的支持,在 Action 被派发至 Store 之前先通过中间件的处理,在中间件处理中可以对 Action 做很多操作,比如日志记录等,我们使用一个相对简单的中间件 redux-thunkredux-thunk 实现异步的基本原理是判断当 action 是个函数时,先执行这个函数,也同时安装一个 redux-logger 用于日志记录,然后对计数器组件做一些变化
"redux-thunk": "^2.3.0"
"redux-logger": "^3.0.6"
// 1. 修改store.js 
// src/store.js  修改以下部分内容,其他不变

// applyMiddleware 应用中间件方法
import {createStore, applyMiddleware} from 'redux';
// 引入中间件
import logger from 'redux-logger'
import thunk from 'redux-thunk'

// 创建一个store
const store = createStore(
  counterReducer, 
  // 按照需要使用的中间件的顺序传递参数
  applyMiddleware(logger, thunk)
)
// 2. 修改计数器组件,新增异步操作
// src/components/ReactTest.js  修改部分内容

// 新增asyncAdd异步操作
const mapDispatchToProps = {
  add: () => ({type: 'add'}),
  minus: () => ({type: 'minus'}),
  // asyncAdd方法return一个方法
  asyncAdd: () => dispatch => {
    // 异步操作
    setTimeout(()=> {
      dispatch({type: 'add'})
    },2000)
  }
}

// 添加异步操作的对应按钮
<div>
  <button onClick={minus}>-</button>
  <button onClick={add}>+</button>
  <button onClick={asyncAdd}>async +</button>
</div>
  • 完成以上的支持异步的代码之后,我们来观察不同操作的日志情况,日志记录了方法名称、操作前后的值变化等


    同步操作记录.jpg

    异步操作记录.jpg
  • 实现异步操作之后,来对 store.js 的部分做个架构分层,实际开发中可能会有多个 Store 分管不同模块的数据,在 src 目录下新建一个 store 文件夹

    store文件夹结构.jpg

// src/store/index.js

// applyMiddleware 应用中间件方法
import {createStore} from 'redux';
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import {counterReducer} from './count.redux'

const store = createStore(
  applyMiddleware(logger, thunk)
);

export default store;
// src/store/count.redux.js

export const counterReducer = (state=0, action) => {
  switch(action.type){
    case 'add':
      return state + 1;
    case 'minus':
      return state - 1;
    default:
      return state;
  }
}

// action creator
export const add = () => ({type: 'add'})
export const minus = () => ({type: 'minus'})
// asyncAdd方法return一个方法
export const asyncAdd = () => dispatch => {
  // 异步操作
  setTimeout(()=> {
    dispatch({type: 'add'})
  },1500)
}
  • 修改好 store.js 的架构分层之后,下面就是使用了,接着修改在计数器组件中使用 Store 的部分
import React from 'react';
import {connect} from 'react-redux';
// 将action creator导入
import {add, minus, asyncAdd} from '../store/count.redux'

const mapStateToProps = state => ({num: state});
// count.redux之后
const mapDispatchToProps = {add, minus, asyncAdd}

@connect(mapStateToProps, mapDispatchToProps)
class ReduxTest extends React.Component{
  render () {
    const {num, add, minus, asyncAdd} = this.props
    return (
      <div>
        {/* 获取状态 */}
        <p>{num}</p>

        {/* 通过dispatch派发action */}
        <div>
          <button onClick={minus}>-</button>
          <button onClick={add}>+</button>
          <button onClick={asyncAdd}>async +</button>
        </div>
      </div>
    )
  }
}

export default ReduxTest
  • 重构结构完成之后,继续 store 添加新的模块,实现下多个 store 如何使用
// src/store/index.js

// 引入combineReducers
import {createStore, applyMiddleware, combineReducers} from 'redux';

const store = createStore(
  // combineReducers将多个模块的reducer合并
  // 在使用具体某个reducer的时候要注意命名空间
  combineReducers({counter: counterReducer}), 
  applyMiddleware(logger, thunk)
);
  • 关于 redux 的原理可以了解下,使用 redux 的时候通常先通过调用 createStore 方法初始化一个 store,如下:
const store = createStore(
  // combineReducers将多个模块的reducer合并
  // 在使用具体某个reducer的时候要注意命名空间
  combineReducers({user}),
  applyMiddleware(logger, thunk)
);
  1. createStore 接收2个参数,一个是 reducer,当有多个 reducer 的时候,使用 combineReducers,另一个我们叫 enhancer,可以理解为强化器
  2. 在上面的代码使用中,这里的 enhancer 就是 applyMiddleware 中间件函数,enhancer存在的时候会首先用于强化我们的 store
export function createStore(reducer, enhancer) {
  if(enhancer) {
    return enhancer(createStore)(reducer)
  }
}
  1. store 的基本 api 分别是:getStatesubscribedispatchgetState 的原理其实就是 return currenState 返回当前状态值,subscribe 主要用于组件对 store 的监听,dispatch 用于执行 reducer ,根据 action 的具体指令执行对应的方法对状态进行更新,然后返回全新的状态,执行完毕后通知所有监听器执行更新
export function createStore(reducer, enhancer) {
  if(enhancer) {
    return enhancer(createStore)(reducer)
  }

  // 当前状态值、监听器(监听store变化并执行的回调函数)
  let currentState = []
  let currentListeners = []

  function getState () {
    return currentState
  }

  function subscribe (listener) {
    currentListeners.push(listener)
  }

  function dispatch (action) {
    currentState = reducer(currentState, action)
    currentListeners.forEach(...)
    return action
  }

  return {getState, subscribe, dispatch}
}
  1. applyMiddleware 接收若干个中间件,并且是按顺序接收的,然后中间件是希望放在 Actionreducer 之前执行的,通过一些方法将若干个中间件组成一个中间件链
export function applyMiddleware (...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = store.dispatch

    // 当前基本api都先赋值给中间件
    const midApi = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }

    // 组成中间件链
    const middlewareChain = middlewares.map(middleware => middleware(midApi))
    // compose方法可以将以方法组成的数组变成函数层层嵌套,也就是函数复合
    diapatch = compose(...middlewareChain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}
  • react-redux 的原理主要涉及到新增的2个 api,分别是 connectprovider
  1. connect 主要用于将 stateaction 相关内容绑定至组件,原理上主要是使用了上下文 contextstate 进行隔代传递
  2. provider 做的主要就是上下文提供,将 store 传给需要的组件
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343