React-Native 之 redux 与 react-redux

前言


  • 本文 有配套视频,可以酌情观看。
  • 文中内容因各人理解不同,可能会有所偏差,欢迎朋友们联系我讨论。
  • 文中所有内容仅供学习交流之用,不可用于商业用途,如因此引起的相关法律法规责任,与我无关,如文中内容对您造成不便,烦请联系 277511806@qq.com 处理,谢谢。
  • 转载麻烦注明出处,谢谢。

redux简介


  • 简单来说,redux 就是帮我们统一管理了 react 组件的 state 状态。

  • 为什么要使用 redux 统一管理 state 呢?没有 redux 我们依旧可以开发 APP,但是当 APP 的复杂度到达一定程度的时候,摆在我们面前的就是 难以维护 的代码(其中包含组件大量的异步回调,数据处理等等),但是使用 redux 也会增加我们整个项目的复杂度,这就需要我们在两者之间进行权衡了,对于这一部分,redux 开发者给我们下面几个参考点:

    • 以下几种情况不需要使用 redux

      • 整体 UI 很简单,没有太多交互。

      • 不需要与服务器进行大量交互,也没有使用 WebSocket。

      • 视图层只从单一来源获取数据。

    • 以下几种情况可考虑使用 redux

      • 用户的交互复杂。

      • 根据层级用户划分功能。

      • 多个用户之间协作。

      • 与服务器大量交互,或使用了 WebSocket。

      • 视图层需要从多个来源获取数据。

      • 遇到 React 无法解决的问题。

    • 总结以上内容:redux 适用于 多交互,多数据源,复杂程度高的工程中。

  • 也就是说,当我们的组件出现 某个状态需要共享需要改变另一个组件状态 等传值比较不容易的情况。就可以考虑 redux ,当然还有其他 redux 的替代产品供我们使用。

译注:

WebSocket:被称为下一代客户端与服务端的异步通信方法。取代了单个的TCP套接字,使用ws或wss协议,可用于任意的客户端和服务器程序。WebSocket目前由W3C进行标准化。主要的优点是服务器和客户端可以彼此相互推送信息,允许跨域通信。

redux 必要知识


  • 使用 redux 之前,基本的东西还是要都懂的,数据流向介绍:
redux工作流程.png
  • Action:行为。它的作用就是将我们更新组件的 状态(state) 的每个动作抽象为一个行为,它有一个必须的参数 type,定义了 Action(行为) 的名称,其他参数可自定义。写法:

        {
            type: 'TEST_ACTION',
            key1: 'value',
                ...
            keyN: value
        }
    
  • 因为 Action 是个对象,所以,我们需要创建这个对象,那创建这个对象的方法叫做 ActionCreator,写法:

        function testAction(key1: ?string, ..., keyN: ?string) {
            return {
                type: "TEST_ACTION",
                key1: key1,
                ...
                keyN: keyN
            }
        }
    
  • Reducer:reducer 的作用就是根据传入的 Action行为和旧的 state对象,返回一个新的 state ,然后组件会根据 state 刷新。当我们确定了组件的 state 对象结构 和 action 行为的时候就可以编写 reducer 中的内容。写法:

        function testReducer(state, action) {
            let key1 = action.key1;
            switch(action.type) {
                case TEST_ACTION:
                    return {
                    ...state,
                    key1: key1 + '变化'
                };
                
                default:
                    return state;
            }
        };
        
        export default testReducer;
    
  • 当然我们的工程中可能会有多个 reducer 的情况,通过 combineReducers 可以将多个 reducer 合成统一管理。

        import { combineReducers } from 'redux';
        import testReducer1 from './testReducer1';
        import testReducer2 from './testReducer2';
        
        export default = combineReducers({
            testReducer1,
            testReducer2
        });
    
  • reducer 是一个纯函数(同样的输入,必须有同样的输出,需要遵循 3 个约束):

    • 不可修改传入的参数。

    • 一定要干净,没有API请求,没有变量修改,单纯执行计算,没有特殊情况。

    • 调用非纯函数(Date.now()、Math.random()等),每次都会得到不同结果导致数据错误等安全问题。

    • 当传入的 state 与 旧state 相比没有区别,返回的 新state也应该一摸一样。

  • Store:当 reducer 返回了新的 state 后,这个 state 怎么传到组件和存储就成了问题,redux 就是把这个状态统一放到 store 中进行管理。

        import { createStore } from 'redux';
        const store = createStore(reducers);
    
  • 上面的代码根据 reducers 创建了一个 store方法集(它并不是一个对象),然后再 store 中提供一些方法供我们使用:

        // 获取当前 state
        store.getState()
        
        // 发送action,根据我们前面 注册的reducers 处理state
        store.dispath(action)
        
        // 替换当前 state 中的 reducer
        store.replaceReducer(nextReducer) 
        
        // 添加监听
        store.subscribe(listener)
    
  • 另外 redux 有 5个 全局方法:

    • createStore:创建一个readux store 来存储应用中所有的state,应用中只能存在一个 store

          createStore(reducer, [initialState],enhancer);
      
    • combineReducers:把多个reducer函数作为value的object,合并成一个reducers函数,然后就可以通过reducers调用各个子reducer,state 对象的结构由传入的多个 reducer 的 key 决定。

          combineReducers(...reducers)
      
    • ...middlewares:每个 middleware 接受 store 的 dispatch 和 getState 函数作为命名参数,并返回一个函数。

      • 该函数会被传入被称为 next 的下一个 middleware 的 dispatch 方法,并返回一个接受 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,也可不调用。

      • 调用链的最后一个 middleware 会接受真实的 store 的 dispatch 方法作为 next 参数,并结束调用链。所以 middleware 的函数为 ({ getState, dispatch }) => next => action。

      • 返回值:一个应用了 middleware 后的 store enhancer。这个store enhancer 就是一个函数,并且需要应用到 createStore。它会返回一个应用了 middleware 的新 createStore。

    • bindActionCreators:把 actionCreators 转曾拥有同名 keys 的对象,让 dispatch 把每个 actionCreator 包装起来,这样就可以直接调用它们。唯一使用 bindActionCreators 的场景是需要把 actionCreator 往下传到一个组件上,却不想让这个组件察觉到 redux 的存在,而且不希望把 redux store 或者 dispatch 传给它。

          // actionCreators:一个 actionCreators 或 键值是 actionCreators 的对象
          // dispatch:一个 dispatch 函数, 由 store 提供
          bindActionCreators(actionCreators, dispatch)
      
      • 返回值:一个与原对象类似的对象,只不过这个对象中的每个函数值都直接 dispatch action。如果传入的是个函数,返回的也是函数。
    • compose(...fuctions):当需要多个 store 增强器 依次执行的时候使用它。compose 在应用常见的两个用法:

          // 1
          let buildStore = compose(
              applymiddleware(thunk)
          )(createStore)
          
          // 2
          let initStore = compose(
              applymiddleware(thunk)
          )
      
      • 参数1(arguments):合成多个函数。每个函数接受一个函数作为参数,然后返回一个函数。

      • 参数2(Function):从右往左把接受到的函数合成后的终极函数。

  • 可能刚接触,还不能很好理解,这边我们换个方式来理解,如下图:

举个栗子.png
  • 更多关于 redux 的内容(如 redux数据异步处理等)可前往 官方文档 阅读查看,这边不讲这么多,只要了解上面的这些就可以了。

react-redux 需要知道的那些事


  • 终于进入正题了,为了在 react-native 中使用 redux,开发者提供了 react-redux,基础工作原理不变,只不过多了些方法和参数,所以这边就需要继续了解一下,以下内容整理自官方文档:

  • <Provider store>:使组件层级中的 connect() 方法能够得到 redux store。正常情况下,我们的根组件应该嵌套在 <Provider> 中才能使用 connect() 方法。

    • 属性(store):工程中唯一的 redux store。

    • 属性(children):组件层级的根组件。

  • connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]):链接 react组件 和 redux store。

    • 参数(mapStateToProps(state, [ownProps]): stateProps):定义了这个参数,组件会监听 redux store 的变化,在任何情况下,只要 redux store 发送变化, mapStateToProps 函数就会被调用。也就是说:mapStateToProps负责返回需要传递给子组件的 state。

      • 这个函数必须返回一个纯对象,这个对象会与组件的props合并,如果省略这个参数,组件将监听不到 redux store 。

      • 如果指定改回调函数中的第二个参数 ownProps,这个参数的值为传递到组件的props,而且只要组件接到新的 props,mapStateToProps 也会被调用。

    • 参数(mapDispatchToProps(dispatch, [ownProps]): dispatchProps):负责返回一个 dispatchProps,dispatchProps 是actionCreator的key和dispatch(action)的组合。

      • 如果传递一个对象,那么每个定义在该对象的函数都将被当做 redux action creator,而且这个对象会与 redux store 绑定在一起,其中所定义的方法名将作为属性名,合并到组件的 props 中。

      • 如果传递的是一个函数,该函数将接收一个 dispatch 函数,然后由我们自己决定如何返回一个对象,这个对象通过 dispatch 函数与 action creator 以某种方式绑定在一起(提示:你也许会用到 Redux 的辅助函数bindActionCreators())。

      • 如果你省略这个 mapDispatchToProps 参数,默认情况下,dispatch 会注入到你的组件 props 中。

      • 如果指定了该回调函数中第二个参数 ownProps,该参数的值为传递到组件的 props,而且只要组件接收到新props,mapDispatchToProps 也会被调用。

    • 参数(mergeProps(stateProps, dispatchProps, ownProps): props (Function)):如果指定了这个参数,mapStateToProps() 与 mapDispatchToProps() 的执行结果和组件自身的 props 将传入到这个回调函数中。该回调函数返回的对象将作为 props 传递到被包装的组件中。你也许可以用这个回调函数,根据组件的 props 来筛选部分的 state 数据,或者把 props 中的某个特定变量与 action creator 绑定在一起。如果你省略这个参数,默认情况下返回 Object.assign({}, ownProps, stateProps,dispatchProps) 的结果。

    • 参数(options (Object)) 如果指定这个参数,可以定制 connector 的行为。

      • [pure = true] (Boolean): 如果为 true,connector 将执行 shouldComponentUpdate 并且浅对比mergeProps 的结果,避免不必要的更新,前提是当前组件是一个“纯”组件,它不依赖于任何的输入或 state 而只依赖于 props 和 Redux store 的 state。默认值为 true。
      • [withRef = false] (Boolean): 如果为 true,connector 会保存一个对被包装组件实例的引用,该引用通过 getWrappedInstance() 方法获得。默认值为 false。
    • 返回值:根据配置信息,返回一个注入了 state 和 action creator 的 React 组件。

      • 静态属性:WrappedComponent (Component): 传递到 connect() 函数的原始组件类。

      • 静态方法:组件原来的静态方法都被提升到被包装的 React 组件。

      • 实例方法:getWrappedInstance(): ReactComponent;仅当 connect() 函数的第四个参数 options 设置了 { withRef: true } 才返回被包装的组件实例。

注:

  • 函数将被调用两次。第一次是设置参数,第二次是组件与 Redux store 连接 connect(mapStateToProps, mapDispatchToProps, mergeProps)(MyComponent)。

  • connect 函数不会修改传入的 React 组件,返回的是一个新的已与 Redux store 连接的组件,而且你应该使用这个新组件。

  • mapStateToProps 函数接收整个 Redux store 的 state 作为 props,然后返回一个传入到组件 props 的对象。该函数被称之为 selector。参考使用 reselect 高效地组合多个 selector ,并对 收集到的数据进行处理。

  • bindActionCreators 的作用就是将 Actions 和 dispatch 组合起来生成 mapDispatchToProps 需要生成的内容。

  • 是不是又懵圈了?那其实没必要想得太复杂,只不过是组件这边进行了2次包装,其他并没有太大的改变,这边给各位客官又画了张图帮忙理解:
Snip20170419_1.png

使用前准备


  • 使用 redux 之前,我们还是需要配置一下是吧,很简单,我们只需要执行以下步骤:

    • 使用 终端 打开需要使用 redux 的工程主目录:

          // 比如我们的
          cd Desktop/Test
      
    • 导入 redux库

          npm install --save redux
      
    • 我喜欢直接介绍实用的,所以这边我们要直接介绍 react-redux ,不磨磨唧唧一大堆有的没的,所以我们还需要:

          npm install --save react-redux
      
    • 这里先不讲 中间件,尽量不然这些东西干扰我们。

    • 好了,这样我们就可以开始在 react-native 中 使用 redux 了。

react-redux 使用


  • 既然已经了解了redux和react-redux相关的东西,那这边就通过一个小Demo来实际演练一下,UI结构如下:
UI结构.png
  • 首先,根据 redux官方文档的示例 我们可以看出官方建议我们将组件分成 containers(容器组件)components(模块视图组件)redux 三大块。所以我们这边文件的层级如下图所示:
目录结构.png
  • 接着,我们再来完成视图部分,然后根据视图部分确定哪些需要 redux 支持,再来生成相应的 actionreducer 文件。

    • 首先,是 Main 文件,作为我们的容器组件放到 containers 文件夹内,Main 中的内容:

          import React, { Component } from 'react';
          import {
              StyleSheet,
              Text,
              View,
              TouchableOpacity,
          } from 'react-native';
          
          export default class Main extends Component {
              render() {
                  return (
                      <View style={styles.container}>
                          {/* 需要改变的组件 */}
          
                          {/* 按钮 */}
                          <TouchableOpacity>
                              <Text>改变文字按钮</Text>
                          </TouchableOpacity>
                      </View>
                  );
              }
          }
      
          const styles = StyleSheet.create({
              container: {
                  flex: 1,
                  justifyContent: 'center',
                  alignItems: 'center',
                  backgroundColor: '#F5FCFF',
              },
          });
      
    • 那里面我们需要将 Text 作为视图组件独立出来,所以将视图组件 TestText 放到 components 文件夹中,TestText 中的内容:

          export default class TestText extends Component {
              render() {
                  return (
                      <Text>Welcome to React Native</Text>
                  );
              }
          }
      
  • 视图部分我们搭建完成,那么我们接着就是确定需要哪些 action(行为),那前面提到了,我们是要点击按钮的时候让文字发生改变,也就是说我们当前需要一个改变文字的行为,那我们就将这个行为命名为 CHANGE_TEXT,那么我们需要初始化这个 action 这个对象,也就是前面我们提到的 action creator

        export const CHANGE_TEXT = 'CHANGE_TEXT';
    
        // 初始化 CHANGE_TEXT 对象
        export const changeText = (text) => {
            return {
                type: CHANGE_TEXT,
                text
            }
        };
    
  • action 文件配置完毕后,我们就可以根据需求来编写 reducer 文件了,reducer 文件就是起到更新 state 的作用嘛,所以我们将改变 文字 的逻辑放到这里,当reducer 匹配到当前的点击行为为 CHANGE_TEXT 时,就执行相应的操作,返回一个新的 state 给我们使用,如果匹配不到,那么就默认返回一个不变的新 state

        import { CHANGE_TEXT, changeText } from '../action/action';
    
        const mainReducer = (state = changeText('welcome to React Native'), action) => {
        
            const newState = state;
            const text = action.text;
        
            // 判断 action 类型
            switch (action.type) {
                case CHANGE_TEXT:
                    return {
                        ...newState,
                        text: '改变了' + text
                    };
        
                default:
                    return {
                        ...newState,
                        text:state.text
                    }
            }
        };
        
        export default mainReducer;
    
  • 配置完 actionreducer 两个文件后,紧接着我们就可以根据 reducer 来初始化 store 了:

        import Reducer from '../reducer/reducer';
        import { createStore } from 'redux';
        
        export default () => {
        
            // 根据 reducer 初始化 store
            const store = createStore(Reducer);
        
            return store;
        }
    
  • redux 的东西已经都配置完成了,接着就剩下使用了,所以接下来要解决的问题就是怎么发送行为,怎么接收 state(状态),上面提到了,store 其实是个方法集,我们的 发送行为 和 接收状态 方法都在 store 中,所以只要拿到 store,所以只要拿到 store 就能进行这两个操作。

  • 那怎么拿到 store 呢?在官方文档中,清楚地告诉我们,Provider 的任务就是将 store 传给 connect,而 connect 的作用是将我们的组件进行第二次包装,将操作数据的函数和数据的状态包装到 props 中,所以,首先,我们需要对我们的 Main 文件进行第一次包装,我们再新建一个 index 文件来对 Main 文件进行包装:

        import React, { Component } from 'react';
    
        // 引用外部文件
        import { Provider } from 'react-redux';
        import Main from './Main';
        import configureStore from '../redux/store/store';
        
        // 调用 store 文件中的 mainReducer常量中保存的方法
        const store = configureStore();
        
        export default class Root extends Component {
            render() {
                return(
                    // 第一层包装,为了让 main 能够拿到 store
                    <Provider store={store}>
                        <Main />
                    </Provider>
                )
            }
        }
    
  • 包装完成后,我们的 Main 文件就可以获得 store 了,那接着就是进行第二次包装了,通过 connect 生成新组件:

        import React, { Component } from 'react';
        import {
            StyleSheet,
            Text,
            View,
            TouchableOpacity,
        } from 'react-native';
        
        
        import { connect } from 'react-redux';
        import { changeText } from '../redux/action/action';
        import TestText from '../components/TestText';
        
        
        class Main extends Component {
            render() {
        
                // 通过 props 拿到保存的 onChangeText
                const { onChangeText } = this.props;
        
                return (
                    <View style={styles.container}>
                        {/* 需要改变的组件 */}
                        <TestText {...this.props} />
        
                        {/* 按钮 */}
                        <TouchableOpacity
                            onPress={onChangeText}
                        >
                            <Text>改变文字按钮</Text>
                        </TouchableOpacity>
                    </View>
                );
            }
        }
        
        const styles = StyleSheet.create({
            container: {
                flex: 1,
                justifyContent: 'center',
                alignItems: 'center',
                backgroundColor: '#F5FCFF',
            },
        });
        
        // 获取 state 变化
        const mapStateToProps = (state) => {
            return {
                // 获取 state 变化
            }
        };
        
        // 发送行为
        const mapDispatchToProps = (dispatch) => {
            return {
                // 发送行为
            }
        };
        
        // 进行第二层包装,生成的新组件拥有 接收和发送 数据的能力
        export default connect(mapStateToProps, mapDispatchToProps)(Main);
    
  • 到这里,我们的 新组件 就能够收发数据了,那怎么接收和发送呢,别急,我们接着就来完成 mapStateToProps(更新回调) 和 mapDispatchToProps(发送行为) 两个方法。首先,我们需要通过 mapDispatchToProps 来发送行为,然后通过 mapStateToProps 来监听 state 的变化,这边我们需要发送的行为 typeCHANGE_TEXT,当发送行为之后,reducer 就会去匹配 行为的类型,进行相应操作:

        // 发送行为
        const mapDispatchToProps = (dispatch) => {
            return {
                onChangeText: () => dispatch(changeText('外部传值')),
            }
        };
    
  • reducer 接收到我们触发的 行为 并进行一系列处理后,最终会返回一个新的 state,那么 就会自动调用 mapStateToProps 来告诉系统,state 被操作了,那么我们就可以通过 mapStateToProps 来获取 state 状态:

        // 获取 state 变化
        const mapStateToProps = (state) => {
            return {
                value: state.text,
            }
        };
    
  • 那么接下来我们 怎么改变文字 呢?前面提到,connect 作用就是生成一个新的组件,新的组件的 props 中包含了数据获取和操作数据的函数,所以我们需要让 子组件拿到容器组件中的 props,然后在 子组件 中通过 props 就可以拿到上面 定义的 value 和 onChangeText:

        export default class TestText extends Component {
            render() {
        
                // 获取 props 中的 value
                const { value } = this.props;
        
                return (
                    // 根据 value 改变内部文字
                    <Text>{value}</Text>
                );
            }
        }
    
  • 到这里,我们就能成功改变文字了。

效果演示.gif

小结论

  • 其实从上面的 demo 就可以看出,使用了 redux 的项目变得比原本要复杂得多,原本几句代码就能搞定的事情现在要来个 山路十八弯 ,这是因为 redux 是为了解决复杂工程而孕育的,所以不要为了使用 redux 而去使用它,使用之前需要权衡一下利弊,其中的好与坏只能自己慢慢体会。

  • redux 对于刚入门的朋友来说确实比较绕,帮助理解的办法就是多练,如果只看的话可能会越看越乱,所以还是建议多练,熟练之后就感觉没什么了。

中间件


  • 我个人认为 中间件 只需要注意 “顺序” 就可以了。使用方法什么的在 中间件的说明文档 中都讲得很清楚。

  • 关于 中间件 的使用,这边就不多讲了,因为可用的 中间件 很多,不可能一个一个讲,等后面文章涉及哪些 中间件 再讲。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容