RSS

Redux实现分析

一、React独自做数据管理的一些问题

我们在用React做项目的时候,随着项目规模的扩大和业务逻辑越来越复杂,如果我们只用React独立做数据管理,会出现下列一些问题。

  1. 数据请求和业务逻辑混在了一起

     import React, { Component, PropTypes } from 'react';
    
     export default class PostList extends Component {
       constructor(props) {
         super(props);
    
         this.state = {
           loading: true,
           error: null,
           posts: []
         };
       }
    
       componentDidMount() {
         fetch('/api/post/list').then(posts => {
           this.setState({ loading: false,  posts });
         }).catch(error => {
           this.setState({ loading: false, error })
         });
       }
    
       render() {
         if (this.state.loading) {
           return (<span>Loading... ... </span>);
         } else if (this.state.error) {
           return (<span>Error: </span>);
         } else {
           return (
             <ul className="post-box">
               this.state.posts.map((post, i) => {
                 <li key=post- className='post-item'>
                   <p className='post-item-name'></p>
                   <p className='post-item-content'></p>
                 </li>
               });
             </ul>
           );
         }
       }
     }
    
  2. 非同一个树路径组件之间的数据交互变得复杂

    在任何一个React项目中,所有组件的关系是一个组件树的组织关系,同一个树路径下的组件之间的数据交互可以用props属性传递,那么不是同一个树路径下的组件之间的数据交互需要借助于它们共同的祖先组件。随着项目的复杂和各个组件之间的交互的复杂这将变得不可维护。

  import React, { Component } from 'react';

  class PostBox extends Component {
    constructor(props) {
      super(props);

      this.state = { posts: [] };
    }

    handleSubmit(value) {
      this.setState({ posts: [value, ...this.state.posts] });
    }

    render() {
      return (
        <div>
          <PostList posts= />
          <PostForm onSubmit=::this.handleSubmit />
        </div>
      );
    }
  }

  function PostList({ posts }) {
    return (
      <ul className="post-box">
        posts.map((post, i) => {
          <li key=post- className='post-item'>
            <p className='post-item-name'></p>
            <p className='post-item-content'></p>
          </li>
        });
      </ul>
    ); 
  }

  class PostForm extends Component {
    constructor(props) {
      super(props);

      this.handleChange = this.handleChange.bind(this);
      this.state = { value: '' };
    }

    handleChange(e) {
      this.setState({ value: e.target.value });
    }

    render() {
      return (
        <div>
          <textarea value= onChange=></textarea>
          <button onClick=this.props.onSubmit.bind(this, this.state.value)>提交</button>
        </div>
      );
    }
  }
  1. 容器型组件的职责变得负责

    针对以上第二点,实际上很多时候共同的祖先组件就是一个容器型组件,只负责接收子组件的事件数据,然后更新所有子组件内容,容器组件的所有子组件都是一个展示型组件,一个纯函数组件。实际上这种做法是可取的,至少我们把负责的数据处理和交互的逻辑放到了一处集中管理,让更多的组件变成纯函数组件只负责展示。但是这样的容器组件依然是把业务逻辑融合到了组件当中,而且多个容器逐渐之间也难免会有数据交互,所以按照这个思路,我们归根结底是需要一个专注于业务逻辑的,最高抽象的根容器组件,更进一步根容器组件完成可以从组件中脱离出来,变成一个只做业务数据处理的框架,Redux就是这样的框架。

  2. Redux的特点

    基于以上,我们知道Redux至少具有以下特点: a. 统一集中的数据管理; b. 处理所有组件的事件数据,更新自身的数据; c. 将自身的数据通过props传给所有组件。

二、Redux中经典代码赏析

关于Redux的源码比较简单,但是有很多经典的代码,充分体现了函数式编程的精髓,下面逐一赏析。

  1. store的enhancer,在createStore方法的参数里面我们可以传入自己的enhancer方法来对store进行代理,加强,或者做其他我们需要做的处理。
  // createStore.js 如果我们传入enhancer,会调用这段代码返回创建的store
  return enhancer(createStore)(reducer, preloadedState)

  // 不难看出enhancer方法的形式应该是
  function enhancer(createStore) {
    return (reducer, preloadedState) {
      let store = createStore(reducer, preloadedState);
      // 对store做我们需要的处理 TODO
      return store;
    }
  }
  1. store处理的中间件
  // 中间件的形式
  const logger = { getState, dispatch } => next => action => {
    console.log('dispatching', action);
    let result = next(action);
    console.log('next state', getState());
    return result
  };

  // 多个中间件通过applyMiddleware方法处理后返回一个createStore的enhancer方法
  // applyMiddleware.js
  export default function applyMiddleware(...middlewares) {
    return createStore => (...args) => {
        const store = createStore(...args)

        // 给中间件暴露store的getState和dispatch方法
        const middlewareAPI = {
          getState: store.getState,
          dispatch: (...args) => dispatch(...args)
        }

        // 所有的中间件map调用一次,
        // 返回一个携带代理Store也就是middlewareAPI的方法数组
        // [next => action => {}, next => action => {}]
        const chain = middlewares.map(middleware => middleware(middlewareAPI))

        // compose: funcs.reduce((a, b) => (...args) => a(b(...args)))
        // 将数组中的方法连接调用,前一个方法的返回值作为后一个方法的参数
        // 见3的分析
        dispatch = compose(...chain)(store.dispatch)

        // 返回重写(原型链覆盖)过dispatch的store,
        // 这样我们就可以通过中间件拦截store的,做一些我们的处理
        // 比如处理异步action的中间件redux-thunk
        return {
          ...store,
          dispatch
        }
    }
  }
  1. compose 方法的分析

    compose方法就是将数组中的多个方法连接调用

  function m1(next) { return function(action) {} }
  function m2(next) { return function(action) {} }
  function m3(next) { return function(action) {} }

  // compose.js 的实现,先转换成非箭头函数可能比较直观 
  // funcs: [m1, m2, m3]
  funcs.reduce(function(a, c) {
    return function(...args) {
      a(c(...args))
    }
  })

  // ES6 Array.reduce 函数调用过程,
  // reduce为数组中的每一个元素依次执行callback函数
  // 接受四个参数:
  // accumulator 累计器
  // currentValue 当前值
  // currentIndex 当前索引
  // array 数组

  // 回调函数第一次执行时,accumulator 和currentValue的取值有两种情况:如果调用reduce()时提供了initialValue,accumulator取值为initialValue,currentValue取数组中的第一个值;如果没有提供 initialValue,那么accumulator取数组中的第一个值,currentValue取数组中的第二个值。

  // 所以,compose里面的回调函数没有提供initialValue,
  // 第一次调用
  // a: m1, c: m2
  // 返回:
  function a_new(...args) {
    m1(m2(...args))
  }

  // 第二次调用
  // a: a_new, c: m3
  // 返回:
  function a_new2(...args) {
    // a_new(m3(...args)) 
    m1(m2(m3(...args)))
  }

  // 分析 dispatch = compose(...chain)(store.dispatch)
  // compose(...chain)的结果:
  function(...args) {
    m1(m2(m3(...args)))
  }
  // compose(...chain)(store.dispatch):
  dispatch = m1(m2(m3(store.dispatch)))

  // m3(store.dispatch),m3的next参数其实就是store.dispatch,返回一个新函数
  function(action) {}
  // 这个函数又将作为m2的next参数,相当于dispatch,一个内部包含store.dispatch调用
  // 的dispatch,所以我们必须在每个中间件内部调用这个next(action)
  // 才会最终调用store的dispatch

  // 以此类推 以下调用返回的就是一个function(action) 
  // {},即内部层层包裹的新的dispatch函数
  m1(m2(m3(store.dispatch))) 
  1. 异步action以及redux-thunk中间件

    redux里面的actionCreator都是一个返回普通的方法,然后通过绑定dispatch最终调用它。这样我们在actionCreator层面就完成屏蔽了dispatch层面的逻辑,让action变得很纯粹。

  // is a actionCreator
  function loading(key, data) {
      return {
          type: types.LOADING,
          key: key,
          data: data
      }
  }

  // bindActionCreators.js
  // 将dispatch绑定到普通actionCreator,
  // 返回的新方法作为actionCreator内部就拥有了dispatch
  function bindActionCreator(actionCreator, dispatch) {
    return function() {
      return dispatch(actionCreator.apply(this, arguments))
    }
  }

但是如果我们的actionCreator很多时候需要做异步的数据请求,因为是异步,我们无法像普通的actionCreator一样在内部直接返回action。必须等到异步回调后手动调用dispatch方法。

  function fetch() {
    let url = '/api/global';
      return dispatch => {
          return get(url).then(json => {
              dispatch({
                  type: types.GLOBAL,
                  data: json
              });
          });
      };
  }

那么,这个actionCreator的内部dispatch参数是哪儿来的呢?实际上也不难想到,我们在bindActionCreator的时候用dispatch直接调用我们actionCreator的返回值,这在普通的actionCreator返回的是action对象的时候没有问题,但是如果我们的actionCreator返回的是一个function,我们就直接调用这个function,然后把dispatch传给它就行了。这在redux-thunk中间件中给出了实现:

  function createThunkMiddleware(extraArgument) {
    return ({ dispatch, getState }) => next => action => {
      if (typeof action === 'function') {
        return action(dispatch, getState, extraArgument);
      }
      return next(action);
    };
  }
  1. reducer以及store的创建

    redux通过reducer构建store的数据结构,并且通过reducer更新store的数据,这个实现也很巧妙。

  // reducer 就是一个普通的方法,每次接收旧的state数据和action,如果匹配到action
  // 就返回新的state数据,这样我们的store的dispatch也很简单
  function r1(state=[], action) {
    switch (action.type) {
        case 'add':
            return [action.data, ...state];
        case 'delete':
          const newState = [...state];
          newState.splice(0, 1);
          return newState;
        case 'other':
          // TODO
        default:
            return state;
    }
  }

  // store的dispatch很简单,类似
  function dispatch(action) {
    currentState = currentReducer(currentState, action)
  }

当然我们的项目通常数据类型,数据结构不止一个,如果都放到一个reducer里面会变得异常的臃肿,我们可以将不同的数据在不同的reducer里面处理,这个时候我们就需要将多个reducer组合成一个reducer提供给createStore方法。

  // combineReducers.js 简化之后
  export default function combineReducers(reducers) {
    // 源码做了一些reducer的检查和过滤,这里忽略。直接拷贝过来。
    const finalReducerKeys = [...reducers];
    // 返回一个reducer
    return function combination(state = {}, action) {
      let hasChanged = false
      const nextState = {}
      for (let i = 0; i < finalReducerKeys.length; i++) {
        const key = finalReducerKeys[i]
        const reducer = finalReducers[key]
        const previousStateForKey = state[key]
        const nextStateForKey = reducer(previousStateForKey, action)
        nextState[key] = nextStateForKey
        hasChanged = hasChanged || nextStateForKey !== previousStateForKey
      }
      return hasChanged ? nextState : state
    }
  }

以上就是所有redux的核心代码,代码虽然不多,但是很多地方设计和实现都非常的巧妙和经典,充分体现了函数式编程的精髓,值得深入学习和细细品味。