zoukankan      html  css  js  c++  java
  • 深入学习Redux

    redux工作流程

    首先,用户发出Action。

    store.dispatch(action);
    

    然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。

    let nextState = todoApp(previousState, action);
    

    State 一旦有变化,Store 就会调用监听函数。

    // 设置监听函数
    store.subscribe(listener);
    

    listener可以通过store.getState()得到当前状态。如果使用的是 React,这时可以触发重新渲染 View。

    function listerner() {
      let newState = store.getState();
      component.setState(newState);   
    }
    

    中间件与异步操作

    默认情况下,action是同步的,这个时候异步操作怎么办?这个时候就要用到新的中间件reducer,,这就是异步操作。

    「图片来自ruanyf,学前端怎能不看ruanyf呢?」

    哈哈哈 看完下面就懂了为啥要在dispatch action的时候运用中间件了

    站在框架作者的角度思考问题:如果要添加功能,你会在哪个环节添加?

    1. Reducer:纯函数,只承担计算 State 的功能,不合适承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。
    2. View:与 State 一一对应,可以看作 State 的视觉层,也不合适承担其他功能。
    3. Action: 存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。
    let next = store.dispatch;
    store.dispatch = function dispatchAndLog(action) {
      console.log('dispatching', action);
      next(action);
      console.log('next state', store.getState());
    }
    

    上面代码中,对store.dispatch进行了重定义,在发送 Action 前后添加了打印功能。这就是中间件的雏形。

    中间件就是一个函数,对store.dispatch方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。

    中间件的用法

    import { applyMiddleware, createStore } from 'redux';
    import createLogger from 'redux-logger';
    const logger = createLogger();
    
    const store = createStore(
      reducer,
      applyMiddleware(logger)
    );
    

    redux-logger提供一个生成器createLogger,可以生成日志中间件logger。然后,将它放在applyMiddleware方法之中,传入createStore方法,就完成了store.dispatch()的功能增强。

    注意:

    1. createStore方法可以接受整个应用的初始状态作为参数,那样的话,applyMiddleware就是第三个参数了。

      const store = createStore(
        reducer,
        initial_state,
        applyMiddleware(logger)
      );
      
    2. 这里之前自己就看了 中间件的次序是有讲究的 比如thunk log

      const store = createStore(
        reducer,
        applyMiddleware(thunk, promise, logger)
      );
      

      上面代码中,applyMiddleware方法的三个参数,就是三个中间件。有的中间件有次序要求,使用前要查一下文档。比如,logger就一定要放在最后,否则输出结果会不正确。

    applyMiddlewares()

    源码

    export default function applyMiddleware(...middlewares) {
      return (createStore) => (reducer, preloadedState, enhancer) => {
        var store = createStore(reducer, preloadedState, enhancer);
        var dispatch = store.dispatch;
        var chain = [];
    
        var middlewareAPI = {
          getState: store.getState,
          dispatch: (action) => dispatch(action)
        };
        chain = middlewares.map(middleware => middleware(middlewareAPI));
        dispatch = compose(...chain)(store.dispatch);
    
        return {...store, dispatch}
      }
    }
    

    上面代码中,所有中间件被放进了一个数组chain,然后嵌套执行,最后执行store.dispatch。可以看到,中间件内部(middlewareAPI)可以拿到getStatedispatch这两个方法。

    异步操作的基本思路

    同步操作只要发出一种 Action 即可,异步操作的差别是它要发出三种 Action。

    操作发起时的 Action

    操作发起时的 Action

    操作发起时的 Action

    以向服务器取出数据为例,三种 Action 可以有两种不同的写法。

    // 写法一:名称相同,参数不同
    { type: 'FETCH_POSTS' }
    { type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
    { type: 'FETCH_POSTS', status: 'success', response: { ... } }
    // 写法二:名称不同
    { type: 'FETCH_POSTS_REQUEST' }
    { type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
    { type: 'FETCH_POSTS_SUCCESS', response: { ... } }
    

    除了 Action 种类不同,异步操作的 State 也要进行改造,反映不同的操作状态。下面是 State 的一个例子。

    let state = {
    // ... 
    isFetching: true,
    didInvalidate: true,
    lastUpdated: 'xxxxxxx'
    };
    

    上面代码中,State 的属性isFetching表示是否在抓取数据。didInvalidate表示数据是否过时,lastUpdated表示上一次更新时间。

    现在,整个异步操作的思路就很清楚了。

    • 操作开始时,送出一个 Action,触发 State 更新为"正在操作"状态,View 重新渲染
    • 操作结束后,再送出一个 Action,触发 State 更新为"操作结束"状态,View 再一次重新渲染

    redux-thunk 中间件

    异步操作至少要送出两个 Action:用户触发第一个 Action,这个跟同步操作一样,没有问题;如何才能在操作结束时,系统自动送出第二个 Action 呢?

    奥妙就在 Action Creator 之中。

    class AsyncApp extends Component {
    componentDidMount() {
     const { dispatch, selectedPost } = this.props
     dispatch(fetchPosts(selectedPost))
    }
    
    // ...
    

    上面代码是一个异步组件的例子。加载成功后(componentDidMount方法),它送出了(dispatch方法)一个 Action,向服务器要求数据 fetchPosts(selectedSubreddit)。这里的fetchPosts就是 Action Creator。

    下面就是fetchPosts的代码,关键之处就在里面。

    const fetchPosts = postTitle => (dispatch, getState) => {
      dispatch(requestPosts(postTitle));
      return fetch(`/some/API/${postTitle}.json`)
        .then(response => response.json())
        .then(json => dispatch(receivePosts(postTitle, json)));
      };
    };
    
    // 使用方法一
    store.dispatch(fetchPosts('reactjs'));
    // 使用方法二
    store.dispatch(fetchPosts('reactjs')).then(() =>
      console.log(store.getState())
    

    上面代码中,有几个地方需要注意。

    (1)fetchPosts返回了一个函数,而普通的 Action Creator 默认返回一个对象。

    (2)返回的函数的参数是dispatchgetState这两个 Redux 方法,普通的 Action Creator 的参数是 Action 的内容。

    (3)在返回的函数之中,先发出一个 Action(requestPosts(postTitle)),表示操作开始。

    (4)异步操作结束之后,再发出一个 Action(receivePosts(postTitle, json)),表示操作结束。

    这样的处理,就解决了自动发送第二个 Action 的问题。但是,又带来了一个新的问题,Action 是由store.dispatch方法发送的。而store.dispatch方法正常情况下,参数只能是对象,不能是函数。

    这时,就要使用中间件redux-thunk

    import { createStore, applyMiddleware } from 'redux';
    import thunk from 'redux-thunk';
    import reducer from './reducers';
    
    // Note: this API requires redux@>=3.1.0
    const store = createStore(
    reducer,
    applyMiddleware(thunk)
    );
    

    上面代码使用redux-thunk中间件,改造store.dispatch,使得后者可以接受函数作为参数。

    因此,异步操作的第一种解决方案就是,写出一个返回函数的 Action Creator,然后使用redux-thunk中间件改造store.dispatch

    redux-promise 中间件

    既然 Action Creator 可以返回函数,当然也可以返回其他值。另一种异步操作的解决方案,就是让 Action Creator 返回一个 Promise 对象。

    这就需要使用redux-promise中间件。

    import { createStore, applyMiddleware } from 'redux';
    import promiseMiddleware from 'redux-promise';
    import reducer from './reducers';
    
    const store = createStore(
    reducer,
    applyMiddleware(promiseMiddleware)
    ); 
    

    这个中间件使得store.dispatch方法可以接受 Promise 对象作为参数。这时,Action Creator 有两种写法。写法一,返回值是一个 Promise 对象。

    const fetchPosts = 
    (dispatch, postTitle) => new Promise(function (resolve, reject) {
      dispatch(requestPosts(postTitle));
      return fetch(`/some/API/${postTitle}.json`)
        .then(response => {
          type: 'FETCH_POSTS',
          payload: response.json()
        });
    });
    

    写法二,Action 对象的payload属性是一个 Promise 对象。这需要从redux-actions模块引入createAction方法,并且写法也要变成下面这样。

    import { createAction } from 'redux-actions';
    
    class AsyncApp extends Component {
    componentDidMount() {
     const { dispatch, selectedPost } = this.props
     // 发出同步 Action
     dispatch(requestPosts(selectedPost));
     // 发出异步 Action
     dispatch(createAction(
       'FETCH_POSTS', 
       fetch(`/some/API/${postTitle}.json`)
         .then(response => response.json())
     ));
    }
    

    上面代码中,第二个dispatch方法发出的是异步 Action,只有等到操作结束,这个 Action 才会实际发出。注意,createAction的第二个参数必须是一个 Promise 对象。

    看一下redux-promise源码,就会明白它内部是怎么操作的。

    export default function promiseMiddleware({ dispatch }) {
    return next => action => {
     if (!isFSA(action)) {
       return isPromise(action)
         ? action.then(dispatch)
         : next(action);
     }
    
     return isPromise(action.payload)
       ? action.payload.then(
           result => dispatch({ ...action, payload: result }),
           error => {
             dispatch({ ...action, payload: error, error: true });
             return Promise.reject(error);
           }
         )
       : next(action);
    };
    }
    

    从上面代码可以看出,如果 Action 本身是一个 Promise,它 resolve 以后的值应该是一个 Action 对象,会被dispatch方法送出(action.then(dispatch)),但 reject 以后不会有任何动作;如果 Action 对象的payload属性是一个 Promise 对象,那么无论 resolve 和 reject,dispatch方法都会发出 Action。

    React-Redux 的用法

    redux是单独的库,你要是可以,vue也可以用,传说中的vue-redux???

    为了方便使用,Redux 的作者封装了一个 React 专用的库 React-Redux,本文主要介绍它。

    这个库是可以选用的。实际项目中,你应该权衡一下,是直接使用 Redux,还是使用 React-Redux。后者虽然提供了便利,但是需要掌握额外的 API,并且要遵守它的组件拆分规范。

    这个图片有点好看

    react有两种组件,这个是自己熟知的,一个是UI组件,只负责显示的,props由另外一个容器组件传来。

    UI 组件有以下几个特征。

    • 只负责 UI 的呈现,不带有任何业务逻辑
    • 没有状态(即不使用this.state这个变量)
    • 所有数据都由参数(this.props)提供
    • 不使用任何 Redux 的 API

    容器组件的特征恰恰相反。

    • 负责管理数据和业务逻辑,不负责 UI 的呈现
    • 带有内部状态
    • 使用 Redux 的 API

    总之,只要记住一句话就可以了:UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。

    connect()

    React-Redux 提供connect方法,用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来。

    import { connect } from 'react-redux'
    const VisibleTodoList = connect()(TodoList);
    

    上面代码中,TodoList是 UI 组件,VisibleTodoList就是由 React-Redux 通过connect方法自动生成的容器组件。

    但是,因为没有定义业务逻辑,上面这个容器组件毫无意义,只是 UI 组件的一个单纯的包装层。为了定义业务逻辑,需要给出下面两方面的信息。

    (1)输入逻辑:外部的数据(即state对象)如何转换为 UI 组件的参数

    (2)输出逻辑:用户发出的动作如何变为 Action 对象,从 UI 组件传出去。

    因此,connect方法的完整 API 如下。

    import { connect } from 'react-redux'
    
    const VisibleTodoList = connect(
    mapStateToProps,
    mapDispatchToProps
    )(TodoList)
    

    上面代码中,connect方法接受两个参数:mapStateToPropsmapDispatchToProps。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state映射到 UI 组件的参数(props),后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。

    mapStateToProps()

    我的理解是 把store里面存储的state映射到props里面,这样我们就可以通过this.props.state拿到对应的state

    ruanyf老师是这么说的

    mapStateToProps是一个函数。它的作用就是像它的名字那样,建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系。

    作为函数,mapStateToProps执行后应该返回一个对象,里面的每一个键值对就是一个映射。请看下面的例子。

    const mapStateToProps = (state) => {
      return {
        todos: getVisibleTodos(state.todos, state.visibilityFilter)
      }
    }
    

    上面代码中,mapStateToProps是一个函数,它接受state作为参数,返回一个对象。这个对象有一个todos属性,代表 UI 组件的同名参数,后面的getVisibleTodos也是一个函数,可以从state算出 todos 的值。

    下面就是getVisibleTodos的一个例子,用来算出todos

    const getVisibleTodos = (todos, filter) => {
    switch (filter) {
     case 'SHOW_ALL':
       return todos
     case 'SHOW_COMPLETED':
       return todos.filter(t => t.completed)
     case 'SHOW_ACTIVE':
       return todos.filter(t => !t.completed)
     default:
       throw new Error('Unknown filter: ' + filter)
    }
    }
    

    mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。

    mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象。

    // 容器组件的代码
    //    <FilterLink filter="SHOW_ALL">
    //      All
    //    </FilterLink>
    
    const mapStateToProps = (state, ownProps) => {
    return {
     active: ownProps.filter === state.visibilityFilter
    }
    }
    

    使用ownProps作为参数后,如果容器组件的参数发生变化,也会引发 UI 组件重新渲染。

    connect方法可以省略mapStateToProps参数,那样的话,UI 组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。

    mapDispatchToProps()

    同样,我的理解是把store里面存储的state映射到props里面,这样我们就可以通过this.props.dispatch什么的拿到对应的dispatch

    ruanyf老师这么说

    mapDispatchToPropsconnect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。

    如果mapDispatchToProps是一个函数,会得到dispatchownProps(容器组件的props对象)两个参数。

    const mapDispatchToProps = (
      dispatch,
      ownProps
    ) => {
      return {
        onClick: () => {
          dispatch({
            type: 'SET_VISIBILITY_FILTER',
            filter: ownProps.filter
          });
        }
      };
    }
    

    从上面代码可以看到,mapDispatchToProps作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。

    如果mapDispatchToProps是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。举例来说,上面的mapDispatchToProps写成对象就是下面这样。

    const mapDispatchToProps = {
      onClick: (filter) => {
        type: 'SET_VISIBILITY_FILTER',
        filter: filter
      };
    }
    

    <Provider> 组件

    connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。

    一种解决方法是将state对象作为参数,传入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级级将state传下去就很麻烦。

    React-Redux 提供Provider组件,可以让容器组件拿到state

    import { Provider } from 'react-redux'
    import { createStore } from 'redux'
    import todoApp from './reducers'
    import App from './components/App'
    
    let store = createStore(todoApp);
    
    render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    )
    

    上面代码中,Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。

    它的原理是React组件的context属性,请看源码。

    class Provider extends Component {
      getChildContext() {
        return {
          store: this.props.store
        };
      }
      render() {
        return this.props.children;
      }
    }
    
    Provider.childContextTypes = {
      store: React.PropTypes.object
    }
    

    上面代码中,store放在了上下文对象context上面。然后,子组件就可以从context拿到store,代码大致如下。

    class VisibleTodoList extends Component {
      componentDidMount() {
        const { store } = this.context;
        this.unsubscribe = store.subscribe(() =>
          this.forceUpdate()
        );
      }
    
      render() {
        const props = this.props;
        const { store } = this.context;
        const state = store.getState();
        // ...
      }
    }
    
    VisibleTodoList.contextTypes = {
      store: React.PropTypes.object
    }
    

    React-Redux自动生成的容器组件的代码,就类似上面这样,从而拿到store

    本文出自ruanyf,学前端,还是看看ruanyf吧 [Doge][Doge][Doge]

  • 相关阅读:
    YCSB测试Mysql,MongoDB,TokuMX,Couchbase性能
    使用FIO对SATA、SSD和PCIe Flash进行测试
    使用jenkins实现持续集成
    linux服务器性能(网卡流量、CPU、内存、磁盘使用率)监控
    Linux下Hadoop2.7.1集群环境的搭建(超详细版)
    10分钟学会js处理json常用方法
    springboot:Java模板引擎Thymeleaf介绍
    springboot使用遇到问题:Class “model.Address” is listed in the persistence.xml file but not mapped
    eclipse4.6.1安装SpringSource Tool Suite(sts-eclipse)插件
    由一条sql语句想到的子查询优化
  • 原文地址:https://www.cnblogs.com/ssaylo/p/12666133.html
Copyright © 2011-2022 走看看