详解JavaScript状态容器Redux

编辑: admin 分类: javascript 发布时间: 2022-03-11 来源:互联网
目录
  • 一、Why Redux
  • 二、Redux Data flow
  • 三、Three Principles(三大原则)
  • 四、Redux源码解析
    • 4.1、index.js
    • 4.2、createStore.js
    • 4.3、combineReducers.js
    • 4.4、bindActionCreators.js
    • 4.5、compose.js
    • 4.6、applyMiddleware.js
  • 五、从零开始实现一个简单的Redux
    • 六、Redux Devtools
      • 七、总结

        一、Why Redux

        在说为什么用 Redux 之前,让我们先聊聊组件通信有哪些方式。常见的组件通信方式有以下几种:

        • 父子组件:props、state/callback回调来进行通信
        • 单页面应用:路由传值
        • 全局事件比如EventEmitter监听回调传值
        • react中跨层级组件数据传递Context(上下文)

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

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

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

        二、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 的数据流图来熟悉这一过程。

        三、Three Principles(三大原则)

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

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

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

        四、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文件。

        接下来我们来一一学习。

        4.1、index.js

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

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

        4.2、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的时候或者某个状态发生改变的时候会被调用。

        4.3、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,触发页面更新。反之,则不做处理。

        4.4、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的分离,并且使代码更加简洁。

        4.5、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中被使用。

        4.6、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是纯函数,但是在应用中还是会需【本文由http://www.nextecloud.cn/hk.html首发,转载请保留出处,谢谢】要处理记录日志/异常、以及异步处理等操作,那该如何解决这些问题呢?

        这个问题的答案就是中间件。可以通过中间件增强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);
            }
        }

        五、从零开始实现一个简单的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 Devtools

        Redux devtools是Redux的调试工具,可以在Chrome上安装对应的插件。对于接入了Redux的应用,通过 Redux devtools可以很方便看到每次请求之后所发生的改变,方便开发同学知道每次操作后的前因后果,大大提升开发调试效率。

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

        七、总结

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

        以上就是详解JavaScript状态容器Redux的详细内容,更多关于JavaScript状态容器Redux的资料请关注海外IDC网其它相关文章!