深刻学习和理解 Redux

本文首发于 vivo互联网技术 微信公众号 
连接:https://mp.weixin.qq.com/s/jhgQXKp4srsl9_VYMTZXjQ
做者:曾超javascript

Redux官网上是这样描述Redux,Redux is a predictable state container for JavaScript apps.(Redux是JavaScript状态容器,提供可预测性的状态管理)。 目前Redux GitHub有5w多star,足以说明 Redux 受欢迎的程度。前端

1、Why Redux

在说为何用 Redux 以前,让咱们先聊聊组件通讯有哪些方式。常见的组件通讯方式有如下几种:java

  • 父子组件:props、state/callback回调来进行通讯react

  • 单页面应用:路由传值git

  • 全局事件好比EventEmitter监听回调传值github

  • react中跨层级组件数据传递Context(上下文)

在小型、不太复杂的应用中,通常用以上几种组件通讯方式基本就足够了。express

但随着应用逐渐复杂,数据状态过多(好比服务端响应数据、浏览器缓存数据、UI状态值等)以及状态可能会常常发生变化的状况下,使用以上组件通讯方式会很复杂、繁琐以及很难定位、调试相关问题。redux

所以状态管理框架(如 Vuex、MobX、Redux等)就显得十分必要了,而 Redux 就是其中使用最广、生态最完善的。数组

2、Redux Data flow

在一个使用了 Redux 的 App应用里面会遵循下面四步:浏览器

第一步:经过store.dispatch(action)来触发一个action,action就是一个描述将要发生什么的对象。以下:

{ type: 'LIKE_ARTICLE', articleId: 42 }
{ type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
{ type: 'ADD_TODO', text: '金融前端.' }

第二步:Redux会调用你提供的  Reducer函数。

第三步:根 Reducer 会将多个不一样的 Reducer 函数合并到单独的状态树中。

第四步:Redux store会保存从根 Reducer 函数返回的完整状态树。

所谓一图胜千言,下面咱们结合 Redux 的数据流图来熟悉这一过程。

深刻学习和理解 Redux

3、Three Principles(三大原则)

一、Single source of truth:单一数据源,整个应用的state被存储在一个对象树中,而且只存在于惟一一个store中。

二、State is read-only:state里面的状态是只读的,不能直接去修改state,只能经过触发action来返回一个新的state。

三、Changes are made with pure functions:要使用纯函数来修改state。

4、Redux源码解析

Redux 源码目前有js和ts版本,本文先介绍 js 版本的 Redux 源码。Redux 源码行数很少,因此对于想提升源码阅读能力的开发者来讲,很值得前期来学习。

Redux源码主要分为6个核心js文件和3个工具js文件,核心js文件分别为index.js、createStore.js、compose.js、combineRuducers.js、bindActionCreators.js和applyMiddleware.js文件。

接下来咱们来一一学习。

一、index.js

index.js是入口文件,提供核心的API,如createStore、combineReducers、applyMiddleware等。

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}

二、createStore.js

createStore是 Redux 提供的API,用来生成惟一的store。store提供getState、dispatch、subscibe等方法,Redux 中的store只能经过dispatch一个action,经过action来找对应的 Reducer函数来改变。

export default function createStore(reducer, preloadedState, enhancer) {
...
}

从源码中能够知道,createStore接收三个参数:Reducer、preloadedState、enhancer。

Reducer是action对应的一个能够修改store中state的纯函数。

preloadedState表明以前state的初始化状态。

enhancer是中间件经过applyMiddleware生成的一个增强函数。store中的getState方法是获取当前应用中store中的状态树。

/**
 * Reads the state tree managed by the store.
 *
 * @returns {any} The current state tree of your application.
 */
function getState() {
  if (isDispatching) {
    throw new Error(
      'You may not call store.getState() while the reducer is executing. ' +
        'The reducer has already received the state as an argument. ' +
        'Pass it down from the top reducer instead of reading it from the store.'
    )
  }
  return currentState
}

dispatch方法是用来分发一个action的,这是惟一的一种能触发状态发生改变的方法。subscribe是一个监听器,当一个action被dispatch的时候或者某个状态发生改变的时候会被调用。

三、combineReducers.js

/**
 * Turns an object whose values are different reducer functions, into a single
 * reducer function. It will call every child reducer, and gather their results
 * into a single state object, whose keys correspond to the keys of the passed
 * reducer functions.
 */
export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
     ...
  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)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      //判断state是否发生改变
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    //根据是否发生改变,来决定返回新的state仍是老的state
    return hasChanged ? nextState : state
  }
}

从源码能够知道,入参是 Reducers,返回一个function。combineReducers就是将全部的 Reducer合并成一个大的 Reducer 函数。核心关键的地方就是每次 Reducer 返回新的state的时候会和老的state进行对比,若是发生改变,则hasChanged为true,触发页面更新。反之,则不作处理。

四、bindActionCreators.js

/**
 * Turns an object whose values are action creators, into an object with the
 * same keys, but with every function wrapped into a `dispatch` call so they
 * may be invoked directly. This is just a convenience method, as you can call
 * `store.dispatch(MyActionCreators.doSomething())` yourself just fine.
 */
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }
    ...
    ...
  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

bindActionCreator是将单个actionCreator绑定到dispatch上,bindActionCreators就是将多个actionCreators绑定到dispatch上。

bindActionCreator就是将发送actions的过程简化,当调用这个返回的函数时就自动调用dispatch,发送对应的action。

bindActionCreators根据不一样类型的actionCreators作不一样的处理,actionCreators是函数就返回函数,是对象就返回一个对象。主要是将actions转化为dispatch(action)格式,方便进行actions的分离,而且使代码更加简洁。

五、compose.js

/**
 * Composes single-argument functions from right to left. The rightmost
 * function can take multiple arguments as it provides the signature for
 * the resulting composite function.
 *
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 * from right to left. For example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

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

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

compose是函数式变成里面很是重要的一个概念,在介绍compose以前,先来认识下什么是 Reduce?官方文档这么定义reduce:reduce()方法对累加器和数组中的每一个元素(从左到右)应用到一个函数,简化为某个值。compose是柯里化函数,借助于Reduce来实现,将多个函数合并到一个函数返回,主要是在middleware中被使用。

六、applyMiddleware.js

/**
 * Creates a store enhancer that applies middleware to the dispatch method
 * of the Redux store. This is handy for a variety of tasks, such as expressing
 * asynchronous actions in a concise manner, or logging every action payload.
 */
export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    ...
    ...
    return {
      ...store,
      dispatch
    }
  }
}

applyMiddleware.js文件提供了middleware中间件重要的API,middleware中间件主要用来对store.dispatch进行重写,来完善和扩展dispatch功能。

那为何须要中间件呢?

首先得从Reducer提及,以前 Redux三大原则里面提到了reducer必须是纯函数,下面给出纯函数的定义:

  • 对于同一参数,返回同一结果

  • 结果彻底取决于传入的参数

  • 不产生任何反作用

至于为何reducer必须是纯函数,能够从如下几点提及?

  • 由于 Redux 是一个可预测的状态管理器,纯函数更便于 Redux进行调试,能更方便的跟踪定位到问题,提升开发效率。

  • Redux 只经过比较新旧对象的地址来比较两个对象是否相同,也就是经过浅比较。若是在 Reducer 内部直接修改旧的state的属性值,新旧两个对象都指向同一个对象,若是仍是经过浅比较,则会致使 Redux 认为没有发生改变。但要是经过深比较,会十分耗费性能。最佳的办法是 Redux返回一个新对象,新旧对象经过浅比较,这也是 Reducer是纯函数的重要缘由。

Reducer是纯函数,可是在应用中仍是会须要处理记录日志/异常、以及异步处理等操做,那该如何解决这些问题呢?

这个问题的答案就是中间件。能够经过中间件加强dispatch的功能,示例(记录日志和异常)以下:

const store = createStore(reducer);
const next = store.dispatch;

// 重写store.dispatch
store.dispatch = (action) => {
    try {
        console.log('action:', action);
        console.log('current state:', store.getState());
        next(action);
        console.log('next state', store.getState());
    } catch (error){
        console.error('msg:', error);
    }
}

5、从零开始实现一个简单的Redux

既然是要从零开始实现一个Redux(简易计数器),那么在此以前咱们先忘记以前提到的store、Reducer、dispatch等各类概念,只需牢记Redux是一个状态管理器。

首先咱们来看下面的代码:

let state = {
    count : 1
}
//修改以前
console.log (state.count);
//修改count的值为2
state.count = 2;
//修改以后
console.log (state.count);

咱们定义了一个有count字段的state对象,同时能输出修改以前和修改以后的count值。但此时咱们会发现一个问题?就是其它若是引用了count的地方是不知道count已经发生修改的,所以咱们须要经过订阅-发布模式来监听,并通知到其它引用到count的地方。所以咱们进一步优化代码以下:

let state = {
    count: 1
};
//订阅
function subscribe (listener) {
    listeners.push(listener);
}
function changeState(count) {
    state.count = count;
    for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i];
        listener();//监听
    }
}

此时咱们对count进行修改,全部的listeners都会收到通知,而且能作出相应的处理。可是目前还会存在其它问题?好比说目前state只含有一个count字段,若是要是有多个字段是否处理方式一致。同时还须要考虑到公共代码须要进一步封装,接下来咱们再进一步优化:

const createStore = function (initState) {
    let state = initState;
    //订阅
    function subscribe (listener) {
        listeners.push(listener);
    }
    function changeState (count) {
        state.count = count;
        for (let i = 0; i < listeners.length; i++) {
            const listener = listeners[i];
            listener();//通知
        }
    }
    function getState () {
        return state;
    }
    return {
        subscribe,
        changeState,
        getState
    }
}

咱们能够从代码看出,最终咱们提供了三个API,是否是与以前Redux源码中的核心入口文件index.js比较相似。可是到这里尚未实现Redux,咱们须要支持添加多个字段到state里面,而且要实现Redux计数器。

let initState = {
    counter: {
        count : 0
    },
    info: {
        name: '',
        description: ''
    }
}
let store = createStore(initState);
//输出count
store.subscribe(()=>{
    let state = store.getState();
    console.log(state.counter.count);
});
//输出info
store.subscribe(()=>{
    let state = store.getState();
    console.log(`${state.info.name}:${state.info.description}`);
});

经过测试,咱们发现目前已经支持了state里面存多个属性字段,接下来咱们把以前changeState改造一下,让它能支持自增和自减。

//自增
store.changeState({
    count: store.getState().count + 1
});
//自减
store.changeState({
    count: store.getState().count - 1
});
//随便改为什么
store.changeState({
    count: 金融
});

咱们发现能够经过changeState自增、自减或者随便改,但这其实不是咱们所须要的。咱们须要对修改count作约束,由于咱们在实现一个计数器,确定是只但愿能进行加减操做的。因此咱们接下来对changeState作约束,约定一个plan方法,根据type来作不一样的处理。

function plan (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + 1
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1
      }
    default:
      return state
  }
}
let store = createStore(plan, initState);
//自增
store.changeState({
    type: 'INCREMENT'
});
//自减
store.changeState({
    type: 'DECREMENT'
});

咱们在代码中已经对不一样type作了不一样处理,这个时候咱们发现不再能随便对state中的count进行修改了,咱们已经成功对changeState作了约束。咱们把plan方法作为createStore的入参,在修改state的时候按照plan方法来执行。到这里,恭喜你们,咱们已经用Redux实现了一个简单计数器了。

这就实现了 Redux?这怎么和源码不同啊

而后咱们再把plan换成reducer,把changeState换成dispatch就会发现,这就是Redux源码所实现的基础功能,如今再回过头看Redux的数据流图是否是更加清晰了。

深刻学习和理解 Redux

6、Redux Devtools

Redux devtools是Redux的调试工具,能够在Chrome上安装对应的插件。对于接入了Redux的应用,经过 Redux devtools能够很方便看到每次请求以后所发生的改变,方便开发同窗知道每次操做后的来龙去脉,大大提高开发调试效率。

深刻学习和理解 Redux

如上图所示就是 Redux devtools的可视化界面,左边操做界面就是当前页面渲染过程当中执行的action,右侧操做界面是State存储的数据,从State切换到action面板,能够查看action对应的 Reducer参数。切换到Diff面板,能够查看先后两次操做发生变化的属性值。

7、总结

Redux 是一款优秀的状态管理器,源码短小精悍,社区生态也十分红熟。如经常使用的react-redux、dva都是对 Redux 的封装,目前在大型应用中被普遍使用。这里推荐经过Redux官网以及源码来学习它核心的思想,进而提高阅读源码的能力。

相关文章
相关标签/搜索