从零开始手写一个Redux实现

1,089 阅读17分钟

Redux概念

Redux的官网中用一句话来说明Redux是什么:

Redux是JavaScript的可预测的状态容器。

Redux和React没有直接关系,只是结合的比较好。Redux可以独立于其他框架,你也可以使用jq + redux。

Redux有三大原则,也分别对应三个非常重要的概念: store, action以及reducer

单一数据源

一个应用只有一个store,store就像一个仓库或者容器,里面存放的是整个应用的状态 state tree,就像这样

{
    count: 0
}

State是只读的

不能直接修改state,唯一想改变state的方法就是触发action。
action描述了对象的行为,也就是描述了这个对象想要做什么

{
    type: 'ADD_COUNT',
    count: 1
}

就像上面这样,action有一个type属性,用来描述这个action的行为是什么

必须使用纯函数,reducer来修改state

reducer是一个纯函数,什么是纯函数:

  1. 不改变函数输入的参数
  2. 相同的输入必然得到相同的输出

Math.random() 每次返回的值都不一样,所以它不是一个纯函数

3. 无副作用

比如,函数内部还有其他的逻辑,像读写系统文件、像服务器发送一个强求、操作dom。

也就是说,不改变外部的任何状态

reducer函数这么写:

const initState = {
    count: 0
}

function addReducer(state = initState, action) {
    switch (action.type) {
        case 'ADD_COUNT':
            return {
                ...state,
                count: state.count + action.count
            }
        default:
            return state    
    }
}

接收一个初始state,匹配到action的type类型之后,做出修改,返回一个新的state

搭建项目

在实现redux之前,得先知道redux具体的使用方法以及有哪些Api,看看redux是怎么使用的

随便找个目录,然后使用create-react-app新建一个项目

npx create-react-app redux-demo
cd redux-demo

进入项目的目录后,分别安装redux redux-logger redux-thunk

yarn add redux redux-logger redux-thunk

安装好之后,删除src目录下的其他文件,仅保留index.js。

简单的运用一下redux,为了方便(偷懒),就不再新建action、reducer和store的文件夹了,都放在一起使用

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux'

// state 初始状态
const initState = {
  count: 100
}

// reducer函数
const reducer = (state = initState, action) => {
  switch (action.type) {
    case 'PLUS':
      return {
        ...state,
        count: state.count + action.count
      }
    case 'MINUS':
      return {
        ...state,
        count: state.count - action.count
      }
    default:
      return state
  }
}

let store = createStore(reducer)

const { dispatch, getState, subscribe } = store

// 创建两个action,一个是数量加13,一个是数量减11
const plusAction = {
  type: 'PLUS',
  count: 13
}

const minusAction = {
  type: 'MINUS',
  count: 11
}


class App extends Component {
  constructor(props) {
    super(props)
    this.plus = this.plus.bind(this)
    this.minus = this.minus.bind(this)
    this.opration = this.opration.bind(this)
  }

  state = {
    count: getState().count
  }

  componentDidMount(){
    subscribe(() => {
      console.log(getState())
    })
  }

  opration(action) {
    dispatch(action)
    this.setState({
      count: getState().count
    })
  }

  plus() {
    this.opration(plusAction)
  }

  minus() {
    this.opration(minusAction)
  }

  render() {
    return (
      <div>
        <div>{this.state.count}</div>
        <div>
          <button onClick={this.plus}>+</button>
        </div>
        <div>
          <button onClick={this.minus}>-</button>
        </div>
      </div>
    )
  }

}


ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

先来看看上面这段代码的演示(白嫖的gif制作,有水印。。。):

当点击“+”时,数字会加13,当点击“-”时,数字会减11。

redux流程解析

首先,点击了按钮,触发了方法,然后调用了store.dispatch(action),接着reducer收到派发的action,开始处理数据

匹配到type为“PLUS”时,count加13;匹配到type为“MINUS”时,count减11;

返回一个新的state,使用store.getState().count拿到改变后的count值,再setState改变组件的状态。

因此,整个流程是这样的:

同时,subscribe订阅了store的状态变化,每次dispatch一个action,subscribe的回调函数都会执行

实现createStore

可以发现,使用的dispatch,getState等API都是来自store,而store来自createStore。
所以,先来看看createStore是怎么实现的

  • createStore接收一个reducer参数,返回一个store
  • store是一个对象,包含了dispatch、getState和subscribe等方法
  • subscribe订阅了store的状态变化,每次dispatch一个action,subscribe的回调函数都会执行,是发布订阅模式

既然state的状态修改都是依赖于store提供的方法,而store又是createStore函数返回,那么,不难知道,state存在于createStore函数中

调用store的dispatch,state会改变,就说明dispatch函数内部,调用了reducer函数来改变了state,每次调用都会改变state的值

这其实是一个闭包,根据闭包的特性,state的值一直都会被保存下来

src目录新建一个文件夹redux,里面新建createStore.js

// createStore.js 

const createStore = reduer => {
    let state;
    // subscibe结合dispatch是一个发布订阅模式
    // 需要一个订阅的数组
    let listners = []

    const subscribe = callback => {
        // 将所有要执行的函数存起来,订阅完成,等待被调用
        listners.push(callback)
    }

    const dispatch = action => {

    }

    // getState返回state
    const getState = () => state

    // 向外暴露的store对象 
    return {
        subscribe,
        dispatch,
        getState,
    }
}

一个基本结构就搭成了。

// dispatch接收一个action
// dispatch要调用reducer函数来修改state
// dispatch也是一个发布者,将已经订阅的列表依次执行
const dispatch = action => {
    state = reducer(state, action)
    listners.forEach(listner => {
        listner()
    })

}

顺便补充两点实现

  1. 在store.dispatch发出之后,到reducer返回state之前,有几个需要注意的点:
 (1) 不能在reducer中使用store.getState()
 
 (2) 不能在reducer中派发action,即store.dispatch()
 
 (3 )不能在reducer中订阅,即store.subscribe  
 
  总结为一句话:reducer中不能嵌套调用store的方法  
  1. createStore需要初始化一下,也就是执行一次dispatch。

    这样做的目的在没有调用store.dispatch时,第一次使用store.getState()可以拿到reducer返回的初 始值(因为reducer里,action的type匹配不到会返回传入的默认的initState),不然拿到的会是undefined。

完整的createStore:

const ActionTypes = {
  INIT: `@@redux/INIT${Math.random()}`,
}

const createStore = reduer => {
    let state;

    // 设置一个变量,标志是否正在dispatch
    let isDispatching = false
    // subscibe结合dispatch是一个发布订阅模式
    // 需要一个订阅的数组
    let listners = []

    const subscribe = callback => {

      // 不允许在reducer内部订阅store
      if(isDispatching) {
            throw new Error('You may not call store.subscribe() while the reducer is executing. ' + 'If you would like to be notified after the store has been updated, subscribe from a ' + 'component and invoke store.getState() in the callback to access the latest state. ' + 'See https://redux.js.org/api-reference/store#subscribelistener for more details.');
      }
      // 将所有要执行的函数存起来,订阅完成,等待被调用
      listners.push(callback)
    }

    // dispatch接收一个action
    // dispatch要调用reducer函数来修改state
    // dispatch也是一个发布者,将已经订阅的列表依次执行
    const dispatch = action => {

      // 不允许在reducer内部派发action
      if(isDispatching) {
        throw new Error('Reducers may not dispatch actions.') 
      }
      try {
        isDispatching = true
        state = reducer(state, action)
      }finally {
        isDispatching = false
      }

      listners.forEach(listner => {
            listner()
      })

    }

    // getState返回state
    const getState = () => {
      
      // 不允许在reducer内部获取调用此方法
      if(isDispatching) {
        throw new Error('You may not call store.subscribe() while the reducer is executing. ' + 'If you would like to be notified after the store has been updated, subscribe from a ' + 'component and invoke store.getState() in the callback to access the latest state. ' + 'See https://redux.js.org/api-reference/store#subscribelistener for more details.');
      }

      return state
    }
    
    // 初始化store,第一次调用store.getState()可以拿到reducer返回的默认值
    dispatch({ type: ActionTypes.INIT })

    // 向外暴露的store对象 
    return {    
        subscribe,
        dispatch,
        getState,
    }
}

在redux目录下新建一个index.js,把createStore引入index.js,后续的其他方法也会引入到这个文件,作为统一向外暴露的文件

export { createStore } from './createStore'

将src/index.js里createStore替换成自己实现的

// import { createStore } from 'redux'
import { createStore } from './redux'

再来看看效果,发现和之前一样,说明createStore没有问题。

这里还有个小问题,其实createStore一共可以接收三个参数。

createStore(reducer, preloadedState, enhancer)

enhancer后面我们会讲到,先来看下preloadedState。

preloadedState是传递给reducer的一个默认参数,之前是在index.js声明了一个initState,同时让reducer的state参数拥有一个默认值initState。

其实也可以传给createStore,这样也是一样的效果。

const reducer = (state = {}, action) => {...}

const initState = {
  count: 100
}

let store = createStore(reducer, initState)

实现applyMiddleware

一般我们在使用applyMiddleware的时候,都是搭配中间一起使用的。

createStore(reducer, applyMiddleware(中间件))

下面使用一个例子,引入redux-logger来看下

import logger from 'redux-logger'

...
...

let store = createStore(reducer, initState, applyMiddleware(logger))

createStore第二个参数和第三个参数在这里说明一下
上面已经说过,第二参数是preloadedState,传给reducer的初始参数,那第三个参数是做什么的?
根据官方的说明,createStore的第三个参数定义:

type enhancer = (next: StoreCreator) => StoreCreator

也就是说 enchancer接收一个createStore,然后返回一个新的createStore

调用这个createStore,得到的也是一个store对象

也就是说,enhancer是一个函数,这个函数接收createStore为参数,做了某些事之后,返回了新的createStore。

那么问题来了,我们看到过很多示例,都是这样使用的:

createStore(reducer, enhancer)

可是第二个参数不是preloadedState吗? 其实看完源码就知道针对第二个参数有一个判断:

if (typeof preloadedState === 'function' && typeof enhancer === 'function' || typeof enhancer === 'function' && typeof arguments[3] === 'function') {
    throw new Error('It looks like you are passing several store enhancers to ' + 'createStore(). This is not supported. Instead, compose them ' + 'together to a single function.');
}

if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState;
    preloadedState = undefined;
}

if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
        throw new Error('Expected the enhancer to be a function.');
    }

    return enhancer(createStore)(reducer, preloadedState);
}

if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.');
}
  • 如果第二个参数preloadedState和第三个参数enhancer都为函数,或者enhancer为函数并且还有第四个参数也为函数,报错
  • 如果preloadedState是函数并且enhancer为undefined,也就是enhancer没有传或者手动传了undefined,则将preloadedState赋值给enhancer,preloadedState值就相当于没有,赋值为undefined
  • 如果enhancer有值,而且还不是函数,报错;如果同时也是函数,那么就执行这个enhancer函数,传入createStore,返回一个createStore函数,再调用并且传入reducer和preloadedState
  • 如果reducer不是函数,报错

看到这里就明白了第二个参数可以传reducer的初始state,也可以直接传enhancer函数,所以createStore(reducer, enhancer)就相当于createStore(reducer, undefined, enhancer)

enhancer的形式已经知道是什么样子了,接收createStore,返回新的newCreateStore,再调用这个返回的newCreateStore得到新的store

先改写createStore

const ActionTypes = {
  INIT: `@@redux/INIT${Math.random()}`,
}
const createStore = (reducer, preloadedState, enhancer, ...args) => {
  let state;
  // 设置一个变量,标志是否正在dispatch
  let isDispatching = false
  // subscibe结合dispatch是一个发布订阅模式
  // 需要一个订阅的数组
  let listners = []

  if (typeof reducer !== 'function') {
      return
  }

  if ((typeof preloadedState === 'function' && typeof enhancer === 'function') || (typeof enhancer === 'function' && typeof args[0] === 'function')) {
      return
  }

  if (typeof preloadedState === 'function') {
      if (typeof enhancer === 'undefined') {
          enhancer = preloadedState
          preloadedState = undefined
      }
  }

  if (typeof enhancer !== 'undefined') {
      if (typeof enhancer !== 'function') {
          return
      }
      let newCreateStore = enhancer(createStore)
      let newStore = newCreateStore(reducer, preloadedState)
      return newStore
  }
  state = preloadedState
  const subscribe = callback => {
      // 不允许在reducer内部订阅store
      if (isDispatching) {
          throw new Error('You may not call store.subscribe() while the reducer is executing. ' + 'If you would like to be notified after the store has been updated, subscribe from a ' + 'component and invoke store.getState() in the callback to access the latest state. ' + 'See https://redux.js.org/api-reference/store#subscribelistener for more details.');
      }
      // 将所有要执行的函数存起来,订阅完成,等待被调用
      listners.push(callback)
  }

  // dispatch接收一个action
  // dispatch要调用reducer函数来修改state
  // dispatch也是一个发布者,将已经订阅的列表依次执行
  const dispatch = action => {
      // 不允许在reducer内部派发action
      if (isDispatching) {
          throw new Error('Reducers may not dispatch actions.')
      }
      try {
          isDispatching = true
          state = reducer(state, action)
      } finally {
          isDispatching = false
      }
      listners.forEach(listner => {
          listner()
      })
  }

  // getState返回state
  const getState = () => {
      // 不允许在reducer内部获取调用此方法
      if (isDispatching) {
          throw new Error('You may not call store.subscribe() while the reducer is executing. ' + 'If you would like to be notified after the store has been updated, subscribe from a ' + 'component and invoke store.getState() in the callback to access the latest state. ' + 'See https://redux.js.org/api-reference/store#subscribelistener for more details.');
      }
      return state
  }

  // 初始化store,第一次调用store.getState()可以拿到reducer返回的默认值
  dispatch({ type: ActionTypes.INIT })

  // 向外暴露的store对象 
  return {
      subscribe,
      dispatch,
      getState,
  }
}

export default createStore

再来看applyMiddleware,中间件的使用都是传到applyMiddleware里,最后再传给createStore,像这样:

createStore(reducer, applyMiddleware(logger))

createStore(reducer, applyMiddleware(thunk))

createStore(reducer, applyMiddleware(saga))

这说明了applyMiddleware这个函数接收的参数是中间件,返回的是enhancer函数

来结合redux-logger的源码看下:

const logger = ({getState}) => next => action => {
    console.group('action', action.type)
    console.info('prev state', getState())
    console.info('action', action)
    let result = next(action)
    console.log('next state', getState())
    console.groupEnd()
    return result
}

分析上面的代码:

let result = next(action)

这个next函数接收的一个action,根据之前打印的logger能够看出state发生改变是在next(action)之后,而redux里想要改变state就得先dispatch一个action

因此,这个next其实就是一个dispatch函数

而这个函数内部返回的函数也接收一个参数action,因此返回的也是一个dispatch函数,newDispatch

  • logger函数接收一个参数,getState是被解构出来的,实际上接收的是store
  • 返回一个函数,接收了一个next(next为dispatch)作为参数,返回一个dispatch函数
  • dispatch函数接收一个action,并且在dispatch函数内部打印了一些log

分析完logger函数的流程,其实可以发现中间件做的事情很简单,就是增强dispatch函数的功能

正式实现applyMiddleware

const applyMiddleware = middleware => {
    // applyMiddleware返回的是一个enhancer函数
    // enhancer函数接收createStore作为参数,返回一个新的newCreateStore
    // newCreateStore接收reducer,返回新的newStore
    return createStore => {
        return (reducer, preloadedState) => {
            // 调用createStore,拿到store
            let store = createStore(reducer, preloadedState) 

            // 将store传入中间件函数,执行中间件的逻辑,拿到一个新函数
            const newFunc = middleware(store)

            // 新函数接收一个dispatch,返回一个dispatch
            const newDispatch = newFunc(dispatch)

            // 返回dispatch更新后的store
            return { ...store, dispatch: newDispatch }
        }
    }
} 

将redux替换成自己的文件

// import { createStore, applyMiddleware } from 'redux'
import { createStore, applyMiddleware } from './redux'

运行一下结果,仍然可以正常打印出来结果。

compose

applyMiddleware功能虽然已经实现,但是还不支持多个中间件的场景,例如我们会传入多个中间件:

createStore(reducer, applyMiddleware(logger,saga,thunk))

那么applyMiddleware内部是怎么处理这么多中间件的呢?
源码里使用了compose组合函数

const compose = (...args) => {
    if (!args.length) {
        return arg => arg
    }

    if (args.length === 1) {
        return args[0]
    }

    return args.reduce((prev, curr) => (...args) => prev(curr(...args)))
}

首先,要拿到一个 dispatch => newDispatch的形式,也就是将每个中间件都执行一遍,拿到返回的那个函数

const applyMiddleware = ()...middlewares) => {
    // applyMiddleware返回的是一个enhancer函数
    // enhancer函数接收createStore作为参数,返回一个新的newCreateStore
    // newCreateStore接收reducer,返回新的newStore
    return createStore => {
        return (reducer, preloadedState) => {
            // 调用createStore,拿到store
            let store = createStore(reducer, preloadedState) 

            // 将store传入中间件函数,执行每一个中间件的逻辑,拿到每一个中间返回的新函数
            let funcList = middlewares.map(middleware => middleware(store))

            // 然后执行组合函数
            const newFunc = compose(...funcList)

            // 新函数接收一个dispatch,返回一个dispatch
            const newDispatch = newFunc(store.dispatch)

            // 返回dispatch更新后的store
            return { ...store, dispatch: newDispatch }
        }
    }
} 

上面的组合函数就是函数套娃,一个函数套另一个函数的调用结果,拿applyMiddleware来看,最终要返回一个新的disptach函数

假设现在有三个中间件,分别为

全部执行完依次为:

let fn1 = dispatch => newDispatch1

let fn2 = dispatch => newDispatch2

let fn3 = dispatch => newDispatch3

执行compose的流程为:

  • (fn1, fn2) => (...args) => fn1(fn2(...args)),执行结果保存为memoFunc
  • 下次执行的是(memoFunc, fn3) => (...args) => memoFunc(fn3(...args))
  • 执行完会返回下面的最终返回值,其实是从右向左依次执行完所有的函数
  • (memoFunc, fn3) => (...args) => memoFunc(fn3(...args))执行完的结果为 (...args) => (memoFunc(fn3(...args)))
  • 将args替换成dispatch函数
  • dispatch => memoFunc(fn3(dispatch))
  • fn3(dispatch)的返回值是newDispatch3
  • dispatch => memoFunc(newDispatch3)
  • 再将memoFunc展开,dispatch => fn1(fn2(newDispatch3))
  • fn2(newDispatch3)返回newDispatch2,dispatch => fn1(newDispatch2)
  • 最终返回dispatch => newDispatch1,这个是compose函数值
  • 执行compose函数时,会将所有的dispatch函数都执行一遍,所以每个中间件的逻辑都得以执行

这里在引入一个redux-thunk中间件,同时执行两个中间件看看效果

import thunk from 'redux-thunk'

...
...

let store = createStore(reducer, initState, applyMiddleware(logger, thunk))

异步的action

redux-thunk是一个处理异步逻辑的中间件,它允许我们在action到reducer中间做一些异步操作
来看下redux源码,src目录下新建一个redux-thunk文件夹,里面新建一个index.js
简化后的thunk代码,其实只有短短几行代码:

const thunk = ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
        return action(dispatch, getState)
    }
    return next(action)
}

export default thunk

观看源码的时候,发现在createStore里,传递给中间件的dispatch是返回的newDispatch,结合thunk再来看,就很好理解了。

先来改下createStore函数

const applyMiddleware = (...middlewares) => {
    // applyMiddleware返回的是一个enhancer函数
    // enhancer函数接收createStore作为参数,返回一个新的newCreateStore
    // newCreateStore接收reducer,返回新的newStore
    return createStore => {
        return (reducer, preloadedState) => {
            // 调用createStore,拿到store
            let store = createStore(reducer, preloadedState) 

            // 声明一个新的dispatch
            let newDispatch; 

            // 声明一个需要传入中间件的结构
            // dispatch为返回的新的dispatch
            const middlewareApi = {
                getState: store.getState,
                dispatch(action){
                    return newDispatch(action)
                }
            }

            // 将middlewareApi传入中间件函数,执行每一个中间件的逻辑,拿到每一个中间返回的新函数
            let funcList = middlewares.map(middleware => middleware(middlewareApi))

            // 然后执行组合函数
            const newFunc = compose(...funcList)

            // 新函数接收一个dispatch,返回一个dispatch
            newDispatch = newFunc(store.dispatch)

            // 返回dispatch更新后的store
            return { ...store, dispatch: newDispatch }
        }
    }
} 

那么再来看thunk函数,它的内部对action做了处理,如果action是一个函数,那么先执行这个action,并且传入的是dispatch和getState。

当具有副作用的action函数执行完成后,再调用dispatch,因为传入的是newDispatch,所以执行这个newDispatch的时候又回到了这个中间件,此时action是一个plain object,那么会继续执行next(action),也就是dispatch(action),继而调用reducer更改state的状态。

来模拟一个异步的action,获取数据,获取成功之后reducer能更改state,最终打印出来异步的数据

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// import { createStore, applyMiddleware } from 'redux'
// import logger from 'redux-logger'
import { createStore, applyMiddleware } from './redux'
import logger from './redux-logger'
import thunk from 'redux-thunk'



const reducer = (state = {}, action) => {
  switch (action.type) {
    case 'PLUS':
      return {
        ...state,
        count: state.count + action.count
      }
    case 'MINUS':
      return {
        ...state,
        count: state.count - action.count
      }
    default:
      return state
  }
}



const initState = {
  count: 100
}

let store = createStore(reducer, initState, applyMiddleware(logger, thunk))

const { dispatch, getState, subscribe } = store

// 创建两个action,一个是数量加13,一个是数量减11
const plusAction = {
  type: 'PLUS',
  count: 13
}

const minusAction = {
  type: 'MINUS',
  count: 11
}

// 从thunk的源码就可以知道,异步的action会接收一个dispatch  
// 作用是执行完异步逻辑如请求接口后,再次调用dispatch,传入一个object对象的action  
// 这个dispatch触发后,会调用reducer来更改state
const asyncAction = dispatch => {
  setTimeout(() => {
    dispatch({
      type: 'Async',
      username: '彭于晏',
      size: 18
    })
  }, 3000)
}

class App extends Component {
  constructor(props) {
    super(props)
    this.plus = this.plus.bind(this)
    this.minus = this.minus.bind(this)
    this.opration = this.opration.bind(this)
    this.getAsyncInfo = this.getAsyncInfo.bind(this)
  }

  state = {
    count: getState().count
  }

  opration(action) {
    dispatch(action)
    this.setState({
      count: getState().count
    })
  }

  plus() {
    this.opration(plusAction)
  }

  minus() {
    this.opration(minusAction)
  }

  getAsyncInfo(){
    this.opration(asyncAction)
  }

  render() {
    return (
      <div>
        <div>{this.state.count}</div>
        <div>
          <button onClick={this.plus}>+</button>
        </div>
        <div>
          <button onClick={this.minus}>-</button>
        </div>
        <div>
          <button onClick={this.getAsyncInfo}>获取异步数据</button>
        </div>
      </div>
    )
  }

}


ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

我们模拟了一个3s的异步获取数据,来看下打印:

打印的效果是,3s后,打印出了异步获取的state状态

所以对于异步action来说,整个流程是这样:

View -> dispatch action(side effects function) -> 处理异步逻辑,拿到数据 -> dispatch action(plain object) -> reducer -> state -> View

实现combineReducers

总是发现哪里不对,原来是忘了combineReducers

我们不可能只在项目里写一个reducer函数,因为开发人员根据不同的功能划分,需要完成不同的模块
如果此时都是用了redux管理状态,那么就需要根据具体的功能来对reducer进行更具体的功能划分。

这个时候,combineReducers就登场了

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware, combineReducers } from 'redux'
// import logger from 'redux-logger'
// import { createStore, applyMiddleware } from './redux'
import logger from './redux-logger'
// import thunk from 'redux-thunk'
import thunk from './redux-thunk'

const countState = {
  count: 100
}

const moneyState = {
  count: 100
}

const countReducer = (state = countState, action) => {
  switch (action.type) {
    case 'PLUS':
      return {
        ...state,
        count: state.count + action.count
      }
    case 'MINUS':
      return {
        ...state,
        count: state.count - action.count
      }
    case 'Async':
      return {
        ...state,
        ...action.payload
      }
    default:
      return state
  }
}

const moneyReducer = (state = moneyState, action) => {
  switch (action.type) {
    case 'SAVE':
      return {
        ...state,
        count: state.count + action.count
      }
    case 'TAKE':
      return {
        ...state,
        count: state.count - action.count
      }
    default:
      return state
  }
}

let rootReducer = combineReducers({
  countReducer,
  moneyReducer,
})


let store = createStore(rootReducer, applyMiddleware(logger, thunk))

const { dispatch, getState, subscribe } = store

// 创建两个action,一个是数量加13,一个是数量减11
const saveAction = {
  type: 'SAVE',
  count: 10000,
}

const takeAction = {
  type: 'TAKE',
  count: 1,
}

// 创建两个action,一个是数量加10000,一个是数量减1
const plusAction = {
  type: 'PLUS',
  count: 13
}

const minusAction = {
  type: 'MINUS',
  count: 11
}

const asyncAction = newDispatch => {
  setTimeout(() => {
    newDispatch({
      type: 'Async',
      payload: {
        username: '彭于晏',
        size: 18,
      }
    })
  }, 3000)
}

class App extends Component {
  constructor(props) {
    super(props)
    this.plus = this.plus.bind(this)
    this.minus = this.minus.bind(this)
    this.opration = this.opration.bind(this)
    this.getAsyncInfo = this.getAsyncInfo.bind(this)
    this.take = this.take.bind(this)
    this.save = this.save.bind(this)
  }

  state = {
    count: getState().countReducer.count
  }


  opration(action) {
    dispatch(action)
    this.setState({
      count: getState().countReducer.count
    })
  }

  plus() {
    this.opration(plusAction)
  }

  minus() {
    this.opration(minusAction)
  }

  save() {
    this.opration(saveAction)
  }

  take() {
    this.opration(takeAction)
  }

  getAsyncInfo() {
    this.opration(asyncAction)
  }

  render() {
    return (
      <div>
        <div>{this.state.count}</div>
        <div>
          <button onClick={this.plus}>+</button>
        </div>
        <div>
          <button onClick={this.minus}>-</button>
        </div>
        <div>
          <button onClick={this.getAsyncInfo}>获取异步数据</button>
        </div>
        <div>
          <button onClick={this.save}>存钱</button>
        </div>
        <div>
          <button onClick={this.take}>取钱</button>
        </div>
      </div>
    )
  }

}

分别点击存钱和取钱,得到的打印结果:

可以看到,经过conbineReducer处理过的state,最终变成了我们传入这个函数的键名 -> 对应的state

redux目录下新建一个combineReducers.js

const combineReducers = reducerList => {
    if (typeof reducerList !== 'object') {
        return
    }

    // 检测state是否发生变化的标志
    let hasChanged = false

    // combineReducers的主要步骤
    // combineReducers的返回值会传入createStore,因此这个返回值是一个reducer函数
    // 声明一个新的空对象newState
    // 遍历reducerList,拿到遍历出来每一个reducer,将每一个reducer都执行一遍
    // 执行完拿到的state,赋予newState[key],如: newState['countReducer'] = countReducer执行结果  newState['moneyReducer'] = moneyReducer执行结果
    // 返回newState,返回的newState将作为下次dispatch时的初始state
    return (state = {}, action) => {
        const newState = {}

        for (let key in reducerList) {
            let currentReducer = reducerList[key]


            // prevState也就是上一次dispatch返回的state
            // 比如第一次点击了存钱,调用store.dispatch(saveAction) 拿到最终的state值为 
            /**
            * { 
            *    countReducer: {
            *      count: 100
            *    }, 
            *    moneyReducer: {
            *      count: 10100
            *    }
            * }
            */
            // 当第二次store.dispatch 初始的的state值就为上面的结构,此时在遍历时,state[key]就能得到每个reducer下的state
            // 再进行赋值的时候,就看可以按照key更改当前key对应的state
            let prevState = state[key]

            newState[key] = currentReducer(prevState, action)

            // 检测state是否发生变化
            hasChanged = hasChanged || newState[key] !== prevState
        }

        // 还是检测state是否发生变化,再对比最终的state和传入的reducerList的长度是否一致
        hasChanged = hasChanged || reducerList.length !== Object.keys(state).length

        // 如果state发生了变化,返回newState,作为dispatch中更改后的state值
        // 如果reducer里返回的是原来的state,就返回原先的state
        return hasChanged ? newState : state
    }

}

export default combineReducers

将combineReducer导入到redux/index.js文件,并且作为成员导出

import createStore from './createStore'
import applyMiddleware from './applyMiddleware'
import combineReducers from './combineReducers'

export { createStore, applyMiddleware, combineReducers }

将combineReducers替换成自己实现的,看看效果:

// import { createStore, applyMiddleware, combineReducers } from 'redux'
import { createStore, applyMiddleware, combineReducers } from './redux'

分别点击了存钱和取钱两个按钮,右边log的打印日志可以看到已经实现了多个reducer的状态合并

bindActionCreators

bindActionCreators是结合react-redux的mapDispatchToProps来一起使用的,所以本章就不再展开,放到之后的react-redux实现一文里去实现

总结

  • Redux是一个可预测JavaScript的状态管理容器
  • 为什么要使用redux?

在React中,使用Redux的主要优势之一是它可以帮你处理应用的共享状态。如果多个组件需要访问同一状态,也就是所谓的“共享状态”时,一般情况下开发者会将该状态提升到附近的父组件,但是如果该父组件的下的子组件层级很深,那么就需要一层一层的向下传递,这样就会让开发者感到十分头疼。

并且,在一层一层向下传递的过程中,中间几个组件压根就不需要这些“共享状态”

但是使用了Redux,开发者就可以在应用的任何地方去访问这些“共享状态”,大大提升了开发效率,更有利于去管理这些状态。

  • Redux只是和React结合的比较好,这并不代表Redux就是React的必需品,它甚至可以在jq里使用。

  • Redux也并非一定要使用,如果开发的应用比较小,那么完全就没有能用到Redux的地方,一些需要管理的状态直接本地缓存就搞定了。

  • Redux有三大原则

  • 单一数据源:也就是一个应用只有一个store,所有的状态都存放在store的 state tree中
  • state状态只读:不能直接修改state tree的值,必须使用dispatch函数派发一个action来完成
  • 使用纯函数reducer来更改state,redcuer接收一个初始的state和action,返回一个新的state。

纯函数有以下特点:

  • 不能修改传入的参数
  • 相同的输入必须有相同的输出
  • 不能有任何副作用,包括读写文件系统,打印log,发送请求,更改dom等操作
  • combineReducers

combineReducers可以接收多个reducer,会将所有的reducer集中到一个对象里,按照传入的key添加到对象中,每个reducer执行后返回的state会被赋值到对应的key。

  • createStore

它可以接收三个参数,第一个是reducer,第二个是初始化的state对象,第三个是enhancer增强器函数

如果第二个参数传的是enhancer函数,如applyMiddleware('thunk'),那么它实际上就是createStore(reducer, undefined, applyMiddleware('thunk'))

createStore中包含有redux非常重要的三个api

  • subscribe,接收一个callback,订阅store的状态变化,一旦调用了dispatch,就会执行callback
  • dispatch,调用reducer函数来更改state,同时执行所有的订阅事件,所以dispatch和subscribe实际上就是一个发布订阅者模式
  • getState,返回当前的state tree
  • applyMiddleware,也是一个非常重要的API,它提供了redux的中间件的接入接口

applyMiddleware返回的是一个enhancer函数,内部的逻辑是在dispatch一个action到reducer之间可以写执行中间件的逻辑,比如打印,执行异步的action等

applyMiddleware也可以接收多个中间件,内部会使用compose组合函数来将所有的中间件逻辑串起来执行。

  • Redux的中间件设计原则

Redux的中间件在设计时,基本都是一个格式, ({dispatch,getState}) => next => action => {/* 中间件的逻辑 */} 它的逻辑会在applyMiddleware函数内部被调用