學習 Redux 使用及其原理

目標

  1. 掌握 redux
  2. 掌握 react-redux 及其中間件
  3. 實現(xiàn) redux、react-redux 及其中間件

資源

  1. redux
  2. react-redux

起步

Redux

Redux 是 JavaScript 應用的狀態(tài)容器。它保證程序行為一致性且易于測試。

安裝 redux

npm i redux -S

redux 上手

redux 較難上手,是因為上來就有太多的概念需要學習,用一個累加器舉例

  1. 需要一個 store 來存儲數(shù)據(jù)
  2. store 里的 reducer 初始化 state 并定義 state 修改規(guī)則
  3. 通過 dispatch 一個 action 來提交對數(shù)據(jù)的修改
  4. action 提交到 reducer 函數(shù)里,根據(jù)傳入的 action 的 type,返回新的 state
// src/store/index.js
import { createStore } from 'redux'

const counterReducer = function(state = 0, action) {
  switch (action.type) {
    case 'add':
      return state + 1
    case 'minus':
      return state - 1
    default:
      // 初始化
      return state
  }
}

const store = createStore(counterReducer)

export default store

// ReduxTest.js
import React, { Component } from 'react'
import store from '../store'

export default class ReduxTest extends Component {
  componentDidMount() {
    // 訂閱狀態(tài)變更
    store.subscribe(() => {
      this.forceUpdate()
    })
  }

  render() {
    return (
      <div>
        {store.getState()}
        <div>
          <button onClick={() => store.dispatch({ type: 'add' })}>+</button>
          <button onClick={() => store.dispatch({ type: 'minus' })}>-</button>
        </div>
      </div>
    )
  }
}

redux 是單項非響應式的
1.createStore 創(chuàng)建 store
2.reducer 初始化、修改狀態(tài)函數(shù)
3.getState 獲取狀態(tài)值
4.dispatch 提交更新
5.subscribe 變更訂閱

react-redux

每次都重新調用 render 和 getState 太 low 了,想用更 react 的方式來寫,需要 react-redux 的支持

npm i react-redux -S

提供了兩個 api

  1. Provider 為后代組件提供 store
  2. connect 為組件提供數(shù)據(jù)和變更方法
// index.js 入口文件
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './store'
import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={store}>
    <App title="hello React" />
  </Provider>,
  document.getElementById('root')
)

// store/index.js
import { createStore } from 'redux'

const counterReducer = function(state = 0, action) {
  const num = action.payload || 1
  switch (action.type) {
    case 'add':
      return state + num
    case 'minus':
      return state - num
    default:
      // 初始化
      return state
  }
}

const store = createStore(counterReducer)

export default store

// ReduxTest.js
import React, { Component } from 'react'
// import store from '../store'
import { connect } from 'react-redux'

// 參數(shù)1:mapStateToProps = (state) => { return { num:state } }
// 參數(shù)2:mapDispatchToProps = dispatch => { return { add: () => { dispatch({ type: 'add' }) } } }
@connect(
  state => ({ num: state }),
  {
    add: num => ({ type: 'add', payload: num }), // action creator
    minus: () => ({ type: 'minus' }) // action creator
  }
)
class ReduxTest extends Component {
  //   componentDidMount() {
  //     // 訂閱狀態(tài)變更
  //     store.subscribe(() => {
  //       this.forceUpdate()
  //     })
  //   }
  render() {
    return (
      <div>
        {/* {store.getState()} */}
        {this.props.num}
        <div>
          {/* <button onClick={() => this.props.dispatch({ type: 'add' })}>
            +
          </button>
          <button onClick={() => this.props.dispatch({ type: 'minus' })}>
            -
          </button> */}
          <button onClick={() => this.props.add(2)}>+</button>
          <button onClick={() => this.props.minus()}>-</button>
        </div>
      </div>
    )
  }
}

export default ReduxTest

重構水果列表案例為 redux 版

中間件

異步

react 默認只支持同步,實現(xiàn)異步任務比如延遲,網(wǎng)絡請求,需要中間件的支持,比如我們試用最簡單的 redux-thunk 和 redux-logger

npm i redux-thunk redux-logger -S

// store/index.js
import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'

const counterReducer = function(state = 0, action) {
  const num = action.payload || 1
  switch (action.type) {
    case 'add':
      return state + num
    case 'minus':
      return state - num
    default:
      // 初始化
      return state
  }
}
// applyMiddleware 引入中間件
// 中間件之間有依賴,被依賴的放前面
const store = createStore(counterReducer, applyMiddleware(logger, thunk))

export default store

// ReduxTest.js
@connect(
  state => ({ num: state }),
  {
    add: num => ({ type: 'add', payload: num }), // action creator
    minus: () => ({ type: 'minus' }), // action creator
    // 異步調用函數(shù)
    asyncAdd: () => dispatch => {
      setTimeout(() => {
        // 異步
        dispatch({ type: 'add' })
      }, 1000)
    }
  }
)

<button onClick={() => this.props.asyncAdd()}>+</button>

代碼優(yōu)化

模塊化

抽離 reducer 和 action,創(chuàng)建 store/counter.js

export const add = num => ({ type: 'add', payload: num }) // action creator
export const minus = () => ({ type: 'minus' }) // action creator
// 異步返回的是函數(shù)
export const asyncAdd = () => dispatch => {
  // 異步調用在這里
  setTimeout(() => {
    dispatch({ type: 'add' })
  }, 1000)
}

export const counterReducer = function(state = 0, action) {
  const num = action.payload || 1
  switch (action.type) {
    case 'add':
      return state + num
    case 'minus':
      return state - num
    default:
      // 初始化
      return state
  }
}

store/index.js 修改后

import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import { counterReducer } from './counter'

// applyMiddleware 引入中間件
// 中間件之間有依賴,被依賴的放前面
const store = createStore(counterReducer, applyMiddleware(logger, thunk))

export default store

ReduxTest.js

import { add, minus, asyncAdd } from '../store/counter'

@connect(
  state => ({ num: state }),
  {
    add,
    minus,
    asyncAdd
  }
)

combineReducers

store/index.js

import { createStore, applyMiddleware, combineReducers } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import { counterReducer } from './counter'

// applyMiddleware 引入中間件
// 中間件之間有依賴,被依賴的放前面
const store = createStore(
  combineReducers({ counter: counterReducer }),
  applyMiddleware(logger, thunk)
)

export default store

ReduxTest.js

@connect(
  state => ({ num: state.counter }),
  {
    add,
    minus,
    asyncAdd
  }
)

Redux 原理

核心實現(xiàn)

  • 存儲狀態(tài) state
  • 獲取狀態(tài) getState
  • 更新狀態(tài) dispatch
  • 變更訂閱 subscribe

store/kredux.js

// 閉包
export function createStore(reducer) {
  let currentState = undefined
  const currentListeners = []
  function getState() {
    return currentState
  }

  function dispatch(action) {
    // 修改
    currentState = reducer(currentState, action)
    // 變更通知
    currentListeners.forEach(cb => cb())

    return action
  }

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

  // 初始化狀態(tài)
  dispatch({ type: '@ArchieChiu' })

  // 暴露
  return {
    getState,
    dispatch,
    subscribe
  }
}

使用 kredux,KReduxTest.js

import React, { Component } from 'react'
import { createStore } from '../store/kredux'

const counterReducer = function(state = 0, action) {
  const num = action.payload || 1
  switch (action.type) {
    case 'add':
      return state + num
    case 'minus':
      return state - num
    default:
      // 初始化
      return state
  }
}

const store = createStore(counterReducer)

export default class KReduxTest extends Component {
  componentDidMount() {
    store.subscribe(() => this.forceUpdate())
  }
  render() {
    return (
      <div>
        {store.getState()}
        <div>
          <button onClick={() => store.dispatch({ type: 'add' })}>+</button>
          <button onClick={() => store.dispatch({ type: 'minus' })}>-</button>
        </div>
      </div>
    )
  }
}

實現(xiàn) applyMiddleware

kredux.js

export function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 先完成之前 createStore 工作
    const store = createStore(...args)
    let dispatch = store.dispatch
    // 強化dispatch
    const midApi = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // [fn1(dispatch), fn2(dispatch)] => fn(dispatch) {}
    const chain = middlewares.map(mw => mw(midApi))
    // 強化 dispatch,讓它可以按順序執(zhí)行中間件函數(shù)
    dispatch = compose(...chain)(store.dispatch)

    // 返回全新 store,更新強化過的 dispatch 函數(shù)
    return {
      ...store,
      dispatch
    }
  }
}

export function compose(...fns) {
  if (fns.length === 0) {
    return arg => arg
  }
  if (fns.length === 1) {
    return fns[0]
  }
  // 將參數(shù) 函數(shù)數(shù)組 聚合成 一個函數(shù) [fn1, fn2] => fn2(fn1())
  return fns.reduce((left, right) => (...args) => right(left(...args)))
}

自定義中間件

logger

KReduxTest.js

import { applyMiddleware } from '../store/kredux'

// 自定義中間件
// 有兩個參數(shù):getState,dispatch
function logger() {
  // 返回真正的中間件任務執(zhí)行函數(shù)
  return dispatch => action => {
    // 執(zhí)行中間件任務
    console.log(`${action.type} 執(zhí)行了?。?!`)

    // 執(zhí)行下一個中間件
    return dispatch(action)
  }
}

//使用
const store = createStore(counterReducer, applyMiddleware(logger))

thunk

KReduxTest.js

const thunk = function({ getState }) {
  return dispatch => action => {
    // thunk邏輯:處理函數(shù)action
    if (typeof action === 'function') {
      return action(dispatch, getState)
    }

    // 不是函數(shù)直接跳過
    return dispatch(action)
  }
}

// 使用
const store = createStore(counterReducer, applyMiddleware(logger, thunk))

react-redux 原理

實現(xiàn) kreact-redux

核心任務:

  • 實現(xiàn)一個高階函數(shù)工廠 connect,可以根據(jù)傳入狀態(tài)映射規(guī)則函數(shù)派發(fā)器映射規(guī)則來映射需要的屬性,可以處理變更檢測和刷新任務
  • 實現(xiàn)一個 Provider 組件可以傳遞 store

范例:kstore

kstore/kreact-redux.js

import React from 'react'
import PropTypes from 'prop-types'
import { bindActionCreators } from './kredux'

export const connect = (
  mapStateToProps = state => state,
  mapDispatchToProps = {}
) => WrapComponent => {
  return class ConnectComponent extends React.Component {
    static contextTypes = {
      store: PropTypes.object
    }
    constructor(props, context) {
      super(props, context)
      this.state = {
        props: {}
      }
    }
    componentDidMount() {
      const { store } = this.context
      store.subscribe(() => this.update())
      this.update()
    }
    update() {
      const { store } = this.context
      const stateProps = mapStateToProps(store.getState())
      const dispatchProps = bindActionCreators(
        mapDispatchToProps,
        store.dispatch
      )
      this.setState({
        props: {
          ...this.state.props,
          ...stateProps,
          ...dispatchProps
        }
      })
    }
    render() {
      return <WrapComponent {...this.state.props} />
    }
  }
}
export class Provider extends React.Component {
  static childContextTypes = {
    store: PropTypes.object
  }
  getChildContext() {
    return { store: this.store }
  }
  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }
  render() {
    return this.props.children
  }
}

實現(xiàn) bindActionCreators

添加一個 bindActionCreators 能轉換 actionCreator 為派發(fā)函數(shù)

kstore/kredux.js

function bindActionCreator(creator, dispatch) {
  return (...args) => dispatch(creator(...args))
}
export function bindActionCreators(creators, dispatch) {
  return Object.keys(creators).reduce((ret, item) => {
    ret[item] = bindActionCreator(creators[item], dispatch)
    return ret
  }, {})
}

kstore/index.js

import { createStore, applyMiddleware } from './kredux'
import logger from './kredux-logger'
import thunk from './kredux-thunk'
import { counterReducer } from './counter'

// applyMiddleware 引入中間件
// 中間件之間有依賴,被依賴的放前面
const store = createStore(counterReducer, applyMiddleware(logger, thunk))

export default store

kstore/counter.js

export const add = num => ({ type: 'add', payload: num }) // action creator
export const minus = num => ({ type: 'minus', payload: num }) // action creator
// 異步返回的是函數(shù)
export const asyncAdd = num => dispatch => {
  // 異步調用在這里
  setTimeout(() => {
    dispatch({ type: 'add', payload: num })
  }, 1000)
}

export const counterReducer = function(state = 0, action) {
  const num = action.payload || 1
  switch (action.type) {
    case 'add':
      return state + num
    case 'minus':
      return state - num
    default:
      // 初始化
      return state
  }
}

kstore/kredux-logger.js

// 自定義中間件
// 有兩個參數(shù):getState,dispatch
export default function() {
  // 返回真正的中間件任務執(zhí)行函數(shù)
  return dispatch => action => {
    // 執(zhí)行中間件任務
    console.log(`${action.type} 執(zhí)行了?。?!`)

    // 執(zhí)行下一個中間件
    return dispatch(action)
  }
}

kstore/kredux-thunk.js

// 自定義中間件
// 有兩個參數(shù):getState,dispatch
export default function({ getState }) {
  return dispatch => action => {
    // thunk邏輯:處理函數(shù)action
    if (typeof action === 'function') {
      return action(dispatch, getState)
    }

    // 不是函數(shù)直接跳過
    return dispatch(action)
  }
}

使用

// KReactReduxTest.js
import React, { Component } from 'react'
import { connect } from '../kstore/kreact-redux'
import { add, minus, asyncAdd } from '../kstore/counter'

@connect(
  state => ({ num: state }),
  {
    add,
    minus,
    asyncAdd
  }
)
class KReactReduxTest extends Component {
  render() {
    return (
      <div>
        {this.props.num}
        <div>
          <button onClick={() => this.props.add(2)}>+</button>
          <button onClick={() => this.props.minus()}>-</button>
          <button onClick={() => this.props.asyncAdd()}>+</button>
        </div>
      </div>
    )
  }
}

export default KReactReduxTest

// index.js 入口
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
// import store from './store'
// import { Provider } from 'react-redux'
import store from './kstore'
import { Provider } from './kstore/kreact-redux'

ReactDOM.render(
  <Provider store={store}>
    <App title="hello React" />
  </Provider>,
  document.getElementById('root')
)
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容