Redux 核心概念

作者: jacobbubu | 来源:发表于2015-08-17 15:19 被阅读66912次

    http://gaearon.github.io/redux/index.html ,文档在 http://rackt.github.io/redux/index.html 。本文不是官方文档的翻译。你可以在阅读官方文档之前和之后阅读本文,以加深其中的重点概念。

    根据该项目源码的习惯,示例都是基于 ES2015 的语法来写的。

    Redux 是应用状态管理服务。虽然本身受到了 Flux 很深的影响,但是其核心概念却非常简单,就是 Map/Reduce 中的 Reduce。

    我们看一下 Javascript 中 Array.prototype.reduce 的用法:

    const initState = '';
    const actions = ['a', 'b', 'c'];
    const newState = actions.reduce(
        ( (prevState, action) => prevState + action ),
        initState
    );
    

    从 Redux 的角度来看,应用程序的状态类似于上面函数中的 initStatenewState 。给定 initState 之后,随着 action 的值不断传入给计算函数,得到新的 newState

    这个计算函数被称之为 Reducer,就是上例中的 (prevState, action) => prevState + action

    Immutable State

    Redux 认为,一个应用程序中,所有应用模块之间需要共享访问的数据,都应该放在 State 对象中。这个应用模块可能是指 React Components,也可能是你自己访问 AJAX API 的代理模块,具体是什么并没有一定的限制。State 以 “树形” 的方式保存应用程序的不同部分的数据。这些数据可能来自于网络调用、本地数据库查询、甚至包括当前某个 UI 组件的临时执行状态(只要是需要被不同模块访问)、甚至当前窗口大小等。

    Redux 没有规定用什么方式来保存 State,可能是 Javascript 对象,或者是 Immutable.js 的数据结构。但是有一点,你最好确保 State 中每个节点都是 Immutable 的,这样将确保 State 的消费者在判断数据是否变化时,只要简单地进行引用比较即可,例如:

    newState.todos === prevState.todos
    

    从而避免 Deep Equal 的遍历过程。

    为了确保这一点,在你的 Reducer 中更新 State 成员需要这样做:

    `let myStuff = [
        {name: 'henrik'}
    ]
    
    myStuff = [...mystuff, {name: 'js lovin fool']`
    

    myStuff 是一个全新的对象。

    如果更新的是 Object ,则:

    let counters = {
        faves: 0,
        forward: 20,
    }
    // this creates a brand new copy overwriting just that key
    counters = {...counters, faves: counters.faves + 1}
    

    而不是:

    counters.faves = counters.faves + 1}
    

    要避免对 Object 的 in-place editing。数组也是一样:

    let todos = [
        { id: 1, text: 'have lunch'}
    ]
    todos = [...todos, { id: 2, text: 'buy a cup of coffee'} ]
    

    而不是:

    let todos = [
        { id: 1, text: 'have lunch'}
    ]
    todos.push({ id: 2, text: 'buy a cup of coffee'});
    

    遵循这样的方式,无需 Immutable.js 你也可以让自己的应用程序状态是 Immutable 的。

    在 Redux 中,State 只能通过 action 来变更。Reducer 就是根据 action 的语义来完成 State 变更的函数。Reducer 的执行是同步的。在给定 initState 以及一系列的 actions,无论在什么时间,重复执行多少次 Reducer,都应该得到相同的 newState。这使得你的应用程序的状态是可以被 Log 以及 Replay 的。这种确定性,降低了前端开发所面临的复杂状态的乱入问题。确定的状态、再加上 Hot-Reloaidng 和相应的 Dev-Tool,使得前端应用的可控性大大增强了。

    State 结构设计

    Redux (Flux) 都建议在保存 State 数据的时候,应该尽可能地遵循范式,避免嵌套数据结构。如果出现了嵌套的对象,那么尽量通过 ID 来引用。

    假设远程服务返回的数据是这样的:

    [{
      id: 1,
      title: 'Some Article',
      author: {
        id: 1,
        name: 'Dan'
      }
    }, {
      id: 2,
      title: 'Other Article',
      author: {
        id: 1,
        name: 'Dan'
      }
    }]
    

    那么,转换成以下形式会更有效率:

    {
      result: [1, 2],
      entities: {
        articles: {
          1: {
            id: 1,
            title: 'Some Article',
            author: 1
          },
          2: {
            id: 2,
            title: 'Other Article',
            author: 1
          }
        },
        users: {
          1: {
            id: 1,
            name: 'Dan'
          }
        }
      }
    }
    

    范式化的存储让你的数据的一致性更好,上例中,如果更新了users[1].name,那么在显示 articles 的 component 中,作者姓名也被更新了。

    其实传统关系数据库的设计原则就是如此,只不过随着对数据分布能力和水平扩展性的要求(放弃了一定程度的数据一致性),服务端数据的冗余越来越多。但是回到客户端,由于需要保存的数据总量不大(往往就是用户最近访问数据的缓存),也没有分布式的要求,因此范式化的数据存储就更有优势了。除了可以收获一致性,还可以减少存储空间(存储空间在客户端更加宝贵)。

    除此之外,范式化的存储也利于后面讲到的 Reducer 局部化,便于将大的 Reducer 分割为一系列小的 Reducers

    由于服务器端返回的 JSON 数据(现在常见的方式)往往是冗余而非范式的,因此,可能需要一些工具来帮助你转换,例如:https://github.com/gaearon/normalizr , 虽然很多时候自己控制会更有效一些。

    Reducer

    下面我们以熟悉 todoApp 来看一下 Reducer 的工作方式:

    function todoAppReducer(state = initialState, action) {
      switch (action.type) {
      case SET_VISIBILITY_FILTER:
        return Object.assign({}, state, {
          visibilityFilter: action.filter
        });
      case ADD_TODO:
        return Object.assign({}, state, {
          todos: [...state.todos, {
            text: action.text,
            completed: false
          }]
        }); 
      default:
        return state;
      }
    }
    

    这个例子演示了 Reducers 是如何根据传入的 action.type 分别更新不同的 State 字段。

    如果当应用程序中存在很多 action.type 的时候,通过一个 Reducer 和巨型 switch 显然会产生难以维护的代码。此时,比较好的方法就是通过组合小的 Reducer 来产生大的 Reducer,而每个小 Reducer 只负责处理 State 的一部分字段。如下例:

    import { combineReducers } from 'redux';
    
    const todoAppReducer = combineReducers({
      visibilityFilter: visibilityFilterReducer
      todos: todosReducer
    });
    

    visibilityFilterReducertodosReducer 是两个小 Reducers,其中一个如下:

    function visibilityFilterReducer(state = SHOW_ALL, action) {
      switch (action.type) {
      case SET_VISIBILITY_FILTER:
        return action.filter;
      default:
        return state;
      }
    }
    

    visibilityFilterReducer 仅仅负责处理 State.visibilityFilter 字段的状态(通过 action.typeSET_VISIBILITY_FILTER 的 action 来改变)。Reducers 划分是通过向 combineReducers 传递如下形式的参数实现的:

    {
      field1: reducerForField1,
      field2: reducerForField2
    }
    

    filed1filed2 表示 State 中的字段,reducerForField1reducerForField2 是对应的 Reducers,每个 Reducers 将仅仅获得 State.field1 或者 state.field2 的值,而看不到 State 下的其他字段的内容。响应的返回结果也会被合并到对应的 State 字段中。每个 Reducer 如果遇到自己不能处理的 action,那么必须原样返回传入的 state,或者该 Reducer 设定的初始状态(如果传入的 stateundefined)。

    使用 combineReducers 的前提是,每一个被组合的 Reducer 仅仅和 State 的一部分数据相关,例如:todos Reducer 只消费 State.todos 数据,也只产生 State.todos 数据。这个基本的原则和上面提到的“State 结构设计”范式相结合,可以满足我们大部分需求。

    不过,有时我们就是需要在一个 Reducer 之中访问另外一个 Reducer 负责的 state,这需要我们创建更上一层的 Reducer(Root Reducer) 来控制这个过程,例如:

    function a(state, action) { }
    function b(state, action, a) { } // depends on a's state
    
    function something(state = {}, action) {
      let a = a(state.a, action);
      let b = b(state.b, action, a); // note: b depends on a for computation
      return { a, b };
    }
    

    在这个例子中,我们有两个 Reducers, ab,其中,b 在计算自己的 state 的还需要依赖 a 的计算结果。因此,我们就不能依靠 combineReducers 来完成这种需求,而是需要自己写 Root Reducer 了。reduce-reducers 也可以帮我们完成类似的任务:

    var reducers =  reduceReducers(
      combineReducers({
        router: routerReducer,
        customers,
        stats,
        dates,
        filters,
        ui
      }),
      // cross-cutting concerns because here `state` is the whole state tree
      (state, action) => {
        switch (action.type) {
          case 'SOME_ACTION':
            const customers = state.customers;
            const filters = state.filters;
            // ... do stuff
        }
      }
    );
    

    上面的例子里,在 combineReducers 的基础上,如果某些 action 需要触发跨 Reducers 的状态改变,则可以用上面的写法。reduce-reducers 组合(每个参数就是一个 Reducer)的每一个 Reducer 都可以获取整个 State,所以请不要滥用(请参见相关讨论:https://github.com/reactjs/redux/issues/749 ),在大部分情况下,如果严格遵循数据范式,通过计算的方法获得跨越 Reducers 的状态是推荐的方法(http://redux.js.org/docs/recipes/ComputingDerivedData.html )。


    一个 Reducer 可以处理多种 action.type,而 一种 action.type 也可能被多个 Reducers 处理,这是多对多的关系。以下 Helper 函数可以简化 Reducer 的创建过程:

    function createReducer(initialState, handlers) {
      return function reducer(state = initialState, action) {
        if (handlers.hasOwnProperty(action.type)) {
          return handlers[action.type](state, action);
        } else {
          return state;
        }
      }
    }
    
    export const todosReducer = createReducer([], {
      [ActionTypes.ADD_TODO](state, action) {
        let text = action.text.trim();
        return [...state, text];
      }
    }
    

    Store

    在 Redux 中,Store 对象就是用来维护应用程序状态的对象。构造 Store 对象,仅需要提供一个 Reducer 函数即可。如前所述,这个 Reducer 函数是负责解释 Action 对象的语义,从而改变其内部状态(也就是应用程序的状态)。

    因此 Store 对象有两个主要方法,一个次要方法:

    1. store.getState(): 获取最近的内部状态对象。
    2. store.dispatch(action): 将一个 action 对象发送给 reducer

    一个次要方法为:const unsure = store.subscribe(listener),用来订阅状态的变化。在 React + Redux 的程序中,并不推荐使用 store.subscribe 。但是如果你的应用程序是基于 Observable 模式的,则可以用这个方法来进行适配;例如,你可以通过这个方法将 Redux 和你的 FRP (Functional Reactive Programming) 应用结合。

    下面这个例子演示了 Store 是如何建立的:

    import { combineReducers, createStore } from 'redux';
    import * as reducers from './reducers';
    
    const todoAppReducer = combineReducers(reducers);
    const store = createStore(todoAppReducer);  // Line 5
    
    store.dispatch({type: 'ADD_TODO', text: 'Build Redux app'});
    

    我们也可以在 createStore 的时候为 Store 指定一个初始状态,例如替换第 5 行为:

    const store = createStore(reducers, window.STATE_FROM_SERVER);
    

    这个例子中,初始状态来自于保存在浏览器 window 对象的 STATE_FROM_SERVER 属性。这个属性可不是浏览器内置属性,是我们的 Web Server 在返回的页面文件中以内联 JavaScript 方式嵌入的。这是一种 Universal(Isomorphic) Application 的实现方式。Client 无需发起第一个 AJAX API 请求,就可以直接从当前页面中直接获得初始状态。

    Action

    在 Redux 中,改变 State 只能通过 action。并且,每一个 action 都必须是 Javascript Plain Object,例如:

    {
      type: 'ADD_TODO',
      text: 'Build Redux app'
    }
    

    Redux 要求 action 是可以被序列化的,使这得应用程序的状态保存、回放、Undo 之类的功能可以被实现。因此,action 中不能包含诸如函数调用这样的不可序列化字段。

    action 的格式是有建议规范的,可以包含以下字段:

    {
      type: 'ADD_TODO',
      payload: {
        text: 'Do something.'  
      },
      `meta: {}`
    }
    

    如果 action 用来表示出错的情况,则可能为:

    {
      type: 'ADD_TODO',
      payload: new Error(),
      error: true
    }
    

    type 是必须要有的属性,其他都是可选的。完整建议请参考 Flux Standard Action(FSA) 定义。已经有不少第三方模块是基于 FSA 的约定来开发了。

    Action Creator

    事实上,创建 action 对象很少用这种每次直接声明对象的方式,更多地是通过一个创建函数。这个函数被称为Action Creator,例如:

    function addTodo(text) {
      return {
        type: ADD_TODO,
        text
      };
    }
    

    Action Creator 看起来很简单,但是如果结合上 Middleware 就可以变得非常灵活。

    Middleware

    如果你用过 Express,那么就会熟悉它的 Middleware 系统。在 HTTP Request 到 Response 处理过程中,一系列的 Express Middlewares 起着不同的作用,有的 Middleware 负责记录 Log,有的负责转换内部异常为特定的 HTTP Status 返回值,有的负责将 Query String 转变到 request 对象的特定属性。

    Redux Middleware 的设计动机确实是来自于 Express 。其主要机制为,建立一个 store.dispatch 的链条,每个 middleware 是链条中的一个环节,传入的 action 对象逐步处理,直到最后吐出来是 Javascript Plain Object。先来看一个例子:

    import { createStore, combineReducers, applyMiddleware } from 'redux';
    
    // applyMiddleware takes createStore() and returns// a function with a compatible API.
    let createStoreWithMiddleware = applyMiddleware(
      logger,
      crashReporter
    )(createStore);
    
    // Use it like you would use createStore()let todoApp = combineReducers(reducers);
    let store = createStoreWithMiddleware(todoApp);
    

    这个例子中,loggercrashReporter 这两个 Middlewares 分别完成记录 action 日志和记录 action 处理异常的功能。

    logger 的代码如下:

    // Logs all actions and states after they are dispatched.
    const logger = { getState } => next => action => {
      console.log('dispatching', action);
      let result = next(action);
      console.log('next state', getState());
      return result;
    };
    

    logger 是一个 currying (这是函数式编程的一个基本概念,相比 Flux,Redux 大量使用了函数式编程的范式)之后的函数。next 则是下一个 Middleware 返回的 dispatch 函数(后面会有分析)。对于一个 Middleware 来说,有了 store对象,就可以通过 store.getState() 来获取最近的应用状态以供决策,有了 next ,则可以控制传递的流程。

    ES6 的 Fat Arrow Function 语法(logger = store => next => action =>)让原本 function 返回 function 的语法变得更简洁(I love ☕️script!)。

    工业化的 logger 实现可以参见:https://github.com/fcomb/redux-loggerhttps://github.com/fcomb/redux-diff-logger 。同一个作者写了两个,后面这个支持 State 的差异显示。

    vanilla promise

    Middleware 还可以用来对传入的 action 进行转换,下面这个例子里,传入的 action 是一个 Promise(显然不符合 action 必须是 Javascript Plain Object 的要求),因此需要进行转换:

    /**
     * Lets you dispatch promises in addition to actions.
     * If the promise is resolved, its result will be dispatched as an action.
     * The promise is returned from `dispatch` so the caller may handle rejection.
     */
    const vanillaPromise = { getState, dispatch } => next => action => {
      if (typeof action.then !== 'function') {
        return next(action);
      }
      // the action is a promise, we should resolve it first
      return Promise.resolve(action).then(dispatch);
    };
    

    这个例子中,如果传入的 action 是一个 Promise(即包含 .then 函数,这只是一个粗略的判断),那么就执行这个 Promise,当 Promise 执行成功后,将结果直接传递给 store.dispatch(这个例子中我们短路了 Middlewares 链中的后续环节)。当然,我们要确保 Promise 的执行结果返回的是 Javascript Plain Object。

    这种用法可能并非常用,但是从这个例子我们可以体会到,我们可以定义自己 action 的语义,然后通过相应的 middleware 进行解析,产生特定的执行逻辑以生成最终的 action 对象。这个执行过程可能是同步的,也可能是异步的。

    从这个例子你可能也会发现,如果们也装载了 logger Middleware,那么 logger 可以知道 Promise action 进入了 dispatch 函数链条,但是却没有机会知道最终 Promise 执行成功/失败后发生的事情,因为无论 Promise 执行成功与否,都会直接调用最原始的 store.dispatch,没有走 Middlewares 创建的 dispatch 函数链条。

    对 Promise 的完整支持请参见:https://github.com/acdlite/redux-promise

    Scheduled Dispatch

    下面这个例子略微复杂一些,演示了如何延迟执行一个 actiondispatch

    /**
     * Schedules actions with { meta: { delay: N } } to be delayed by N milliseconds.
     * Makes `dispatch` return a function to cancel the interval in this case.
     */
    const timeoutScheduler = store => next => action => {
      if (!action.meta || !action.meta.delay) {
        return next(action);
      }
    
      let intervalId = setTimeout(
        () => next(action),
        action.meta.delay
      );
    
      return function cancel() {
        clearInterval(intervalId);
      };
    };
    

    这个例子中,timeoutScheduler Middleware 如果发现传入的 action 参数带有 meta.delay 字段,那么就认为这个 action 需要延时发送。当声明的延迟时间(meta.delay)到了,action 对象才会被送往下一个 Middleware 的 dispatch 方法。

    下面这个 Middleware 非常简单,但是却提供了非常灵活的用法。

    Thunk

    如果不了解 Thunk 的概念,可以先阅读 http://www.ruanyifeng.com/blog/2015/05/thunk.html

    thunk Middleware 的实现非常简单:

    const thunk = store => next => action =>
      typeof action === 'function' ?
        action(store.dispatch, store.getState) :
        next(action);
    

    下面的例子装载了 thunk,且 dispatch 了一个 Thunk 函数作为 action

    const createStoreWithMiddleware = applyMiddleware(
      logger,
      thunk
      timeoutScheduler
    )(createStore);
    const store = createStoreWithMiddleware(combineReducers(reducers));
    
    function addFave(tweetId) {
      return (dispatch, getState) => {
        if (getState.tweets[tweetId] && getState.tweets[tweetId].faved)
            return;
    
        dispatch({type: IS_LOADING});
        // Yay, that could be sync or async dispatching
        remote.addFave(tweetId).then(
          (res) => { dispatch({type: ADD_FAVE_SUCCEED}) },
          (err) => { dispatch({type: ADD_FAVE_FAILED, err: err}) },
      };
    }
    
    store.dispatch(addFave());
    

    这个例子演示了 “收藏” 一条微博的相关的 action 对象的产生过程。addFave 作为 Action Creator,返回的不是 Javascript Plain Object,而是一个接收 dispatchgetState 作为参数的 Thunk 函数。

    thunk Middleware 发现传入的 action 是这样的 Thunk 函数时,就会为该函数配齐 dispatchgetState 参数,让 Thunk 函数得以执行,否则,就调用 next(action) 让后续 Middleware 获得 dispatch 的机会。

    在 Thunk 函数中,首先会判断当前应用的 state 中的微博是否已经被 fave 过了,如果没有,才会调用远程方法。

    如果需要调用远程方法的话,那么首先发出 IS_LOADING action,告诉 关心这个状态的reducer 一个远程调用启动了。从而让 reducer 可以更新对应的 state 属性。这样关心此状态的 UI Component 则可以据此更新界面提示信息。

    远程方法如果调用成功,就会 dispatch 代表成功的 action 对象({type: ADD_FAVE_SUCCEED}),否则,产生的就是代表失败的 action 对象({type: ADD_FAVE_FAILED, err: err}),自然会有关心这两个 actionreducer 来据此更新状态。无论如何,reducer 最后收到的 action 对象一定是这种 Javascript Plain Object。

    当 Thunk Middleware 处理了 Thunk 函数类型的 action 之后,如果有配置了其他后续 Middlewares, 则将被跳过去而没有机会执行。

    例如:我们的 Middlewares 配置为 applyMiddleware(logger, thunk, timeoutScheduler),当 action 是 Thunk 函数时,这个 action 将没有机会被 timeoutScheduler Middleware 执行,而 logger Middleware 则有机会在 thunk Middleware 之前执行。每个 Middleware 自己决定给不给后续 Middleware 处理的机会。

    applyMiddleware

    拼装 Middlewares 的工具函数是 applyMiddleware,该函数的模拟实现如下:

    function applyMiddleware(store, middlewares) {
      middlewares = middlewares.slice();
      middlewares.reverse();
    
      let next = store.dispatch;
      middlewares.forEach(middleware =>
        next = middleware(store)(next)
      );
    
      return Object.assign({}, store, { dispatch: next });
    }
    

    结合 Middleware 的写法:

    const logger = store => next => action => {
      console.log('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      return result;
    };
    

    我们可以看到,给 Middleware 传入 storenext 之后,返回的是一个新的 dispatch 方法。而传入的 next 参数则是之前 Middleware 返回的 dispatch 函数。这样,在真正传入 action 之前,我们得到了一个串联在一起的 dispatch 函数,该函数用来替代原本的store.dispatch 方法(通过 Object.assign(...))。Redux Middleware 机制的目的,就是以插件形式改变 store.dispatch 的行为方式,从而能够处理不同类型的 action 输入,得到最终的 Javascript Plain Object 形式的 action 对象。

    每一个 Middleware 可以得到:

    1. 最初的 store 对象 (dispatch 属性还是原来的),因此,可以通过 store.getState 获得最近的状态,以及通过原本的 dispatch 对象直接发布 action 对象,跳过其他 Middleware dispatch 方法(next)。上面 vanillaPromise 演示了这样的用法。
    2. next 方法: 前一个Middleware 返回的 dispatch 方法。当前 Middleware 可以根据自己对 action 的判断和处理结果,决定是否调用 next 方法,以及传入什么样的参数。

    newStore = applyMiddleware(logger,thunk,timeoutScheduler)(store)) 这样的声明为例,timeoutScheduler 得到的next 参数就是原始的 store.dispatch 方法;thunk 拥有 timeoutScheduler 返回的 dispatch 方法,而 logger 又拥有 thunk 返回的 dispatch 方法。最后新生成的 newStoredispatch 方法则是 logger 返回的。因此实际的 action 流动的顺序先到 logger 返回的 dispatch 方法,再到 thunk 返回的 dispatch 方法,最后到 timeoutScheduler 返回的 dispatch 方法。

    需要注意一点, logger 因为排在 dispatch 链条的第一个,因此可以获得进入的每一个 action 对象。但是由于其他 Middleware 有可能异步调用 dispatch (异步调用前一个 Middleware 返回的 dispatch 方法或者原始的 store.dispatch ),因此,logger 并一定有机会知道 action 最终是怎么传递的。

    Middleware 可以有很多玩法的,下面文档列出了 Middleware 的原理和七种Middlewares:http://rackt.github.io/redux/docs/advanced/Middleware.html

    store/reducer 是 Redux 的最核心逻辑,而 Middleware 是其外围的一种扩展方式,仅负责 action 对象的产生。但是由于 Redux 对于核心部分的限定非常严格(保持核心概念的简单):例如,reducer 必须是同步的,实际工程需求所带来的需求都被推到了 Dispatch/Middleware 这部分,官方文档提到的使用方式则起到了”最佳实践”的指导作用。

    Higher-Order Store

    Middleware 是对 store.dispatch 方法的扩展机制。但有些时候则需要对整个 store 对象都进行扩充,这就引入了 Higher-Order Store 的概念。

    这个概念和 React 的 Higher-Order Component 概念是类似的。https://github.com/gaearon/redux/blob/cdaa3e81ffdf49e25ce39eeed37affc8f0c590f7/docs/higher-order-stores.md ,既提供一个函数,接受 store 对象作为输入参数,产生一个新的 store 对象作为返回值。

    createStore => createStore'
    

    Redux 建议大家在 Middleware 不能满足扩展要求的前提下再使用 Higher-Order Store,与 Redux 配套的 redux-devtools 就是一个例子。

    Binding To React (React-Native)

    上面的章节介绍了 Redux 的核心组组件和数据流程,可以通过下图回味一下:

                                                                                          ┌──────────────┐
                            ┌─────────────┐                                           ┌──▶│ subReducer 1 │
                       ┌───▶│Middleware 1 │                                           │   └──────────────┘
                       │    └─────────────┘                                           │           │       
                       │           │                                                  │           ▼       
    ┌─────────────┐    │           │              ┌───────────────┐    ┌──────────┐   │   ┌──────────────┐
    │   action'   │────┘           ▼          ┌──▶│store.dispatch │───▶│ reducer  │───┘   │ subReducer m │
    └─────────────┘         ┌─────────────┐   │   └───────────────┘    └──────────┘       └──────────────┘
                            │Middleware n │   │                                                   │       
                            └─────────────┘   │                                                   │       
                                   │          │                                                   ▼       
                                   │          │                                           ┌──────────────┐
                                   └──────────┘                                           │    state     │
                                   plain action                                           └──────────────┘                                                            
                                                                                                                   
    

    Redux 解决的是应用程序状态存储以及如何变更的问题,至于怎么用,则依赖于其他模块。关于如何在 React 或者 React-Native 中使用 Redux ,则需要参考 react-redux

    react-redux 是 React Components 如何使用 Redux 的 Binding。下面我们来分析一个具体的例子。

    import { Component } from 'react';
    
    export default class Counter extends Component {
      render() {
        return (
          <button onClick={this.props.onIncrement}>
            {this.props.value}
          </button>
        );
      }
    }
    

    这是一个 React Component,显示了一个按钮。按下这个按钮,就会调用 this.props.onIncrementonIncrement的具体内容在下面的例子中, 起作用为每次调用 onIncrement 就会 dispatch {type: INCREMENT} Action 对象来更新 Store/State

    react-redux 中,这样的 Component 被称为 “Dumb” Component,既其本身对 Redux 完全无知,它只知道从 this.props 获取需要的 Action Creator 并且了解其语义,适当的时候调用该方法。而 “Dumb” Component 需要展现的外部数据也来自于 this.props

    如何为 “Dumb” Component 准备 this.props 呢?react-redux 提供的 connect 函数帮助你完成这个功能:

    import { Component } from 'react';
    import { connect } from 'react-redux';
    
    import Counter from '../components/Counter';
    import { increment } from '../actionsCreators';
    
    // Which part of the Redux global state does our component want to receive as props?
    function mapStateToProps(state) {
      return {
        value: state.counter
      };
    }
    
    // Which action creators does it want to receive by props?
    function mapDispatchToProps(dispatch) {
      return {
        onIncrement: () => dispatch(increment())
      };
    }
    
    export default connect(   // Line 20
      mapStateToProps,
      mapDispatchToProps
    )(Counter);
    

    第 20 行的 connectstate 的某个(些)属性映射到了 Counter Component 的 this.props 属性中,同时也把针对特定的Action Creatordispatch 方法传递给了 this.props。这样在 Counter Component 中仅仅通过 this.props 就可以完成 action dispatching 和 应用程序状态获取的动作。

    如果 connect 函数省掉第二个参数,connect(mapStateToProps)(Counter),那么 dispatch 方法会被直接传递给 this.props。这不是推荐的方式,因为这意味着 Counter 需要了解 dispatch 的功能和语义了。

    Components 的嵌套

    你可以在你的组件树的任何一个层次调用 connect 来为下层组件绑定状态和 dispatch 方法。但是仅在你的顶层组件调用 connect 进行绑定是首选的方法。

    Provider Component

    上面的例子实际上是不可执行的,因为 connect 函数其实并没有 Redux store 对象在哪里。所以我们需要有一个机制让 connect 知道从你那里获得 store 对象,这是通过 Provider Component 来设定的,Provider Component 也是 react-redux 提供的工具组件。

    React.render(
      <Provider store={store}>
        {() => <MyRootComponent />}
      </Provider>,
      rootEl
    );
    

    Provider Component 应该是你的 React Components 树的根组件。由于 React 0.13 版本的问题,Provider Component 的子组件必须是一个函数,这个问题将在 React 0.14 中修复。

    Provider Component 和 connect 函数的配合,使得 React Component 在对 Redux 完全无感的情况下,仅通过 React 自身的机制来获取和维护应用程序的状态。

    selector

    在上面的例子中,connect(mapStateToProps,mapDispatchToProps)(Counter) 中的 mapStateToProps 函数通过返回一个映射对象,指定了哪些 Store/State 属性被映射到 React Component 的 this.props,这个方法被称为 selectorselector 的作用就是为 React Components 构造适合自己需要的状态视图。selector 的引入,降低了 React Component 对 Store/State 数据结构的依赖,利于代码解耦;同时由于 selector 的实现完全是自定义函数,因此也有足够的灵活性(例如对原始状态数据进行过滤、汇总等)。

    reselect 这个项目提供了带 cache 功能的 selector。如果 Store/State 和构造 view 的参数没有变化,那么每次 Component 获取的数据都将来自于上次调用/计算的结果。得益于 Store/State Immutable 的本质,状态变化的检测是非常高效的。

    总结

    1. Redux 和 React 没有直接关系,它瞄准的目标是应用状态管理。
    2. 核心概念是 Map/Reduce 中的 Reduce。且 Reducer 的执行是同步,产生的 State 是 Immutable 的。
    3. 改变 State 只能通过向 Reducer dispatch actions 来完成。
    4. State 的不同字段,可以通过不同的 Reducers 来分别维护。combineReducers 负责组合这些 Reducers,前提是每个 Reducer 只能维护自己关心的字段。
    5. Action 对象只能是 Javascript Plain Object,但是通过在 store 上装载 middleware,则可以任意定义 action 对象的形式,反正会有特定的 middleware 负责将此 action 对象变为 Javascript Plain Object。可以以middleware 链条为集中点实现很多控制逻辑,例如 Log,Undo, ErrorHandler 等。
    6. Redux 仅仅专注于应用状态的维护,reducerdispatch/middleware 是两个常用扩展点、Higher-order Store 则仅针对需要扩展全部 Store 功能时使用。
    7. react-redux 是 Redux 针对 React/React-Native 的 Binding,connect/selector 是扩展点,负责将 store 中的状态添加到 React componentprops 中。
    8. Redux 借用了很多函数式编程的思想,了解函数式编程会利于理解其实现原理,虽然使用它不需要了解很多函数式编程的概念。和 Flux 相比,Redux 的概念更精简、约定更严格、状态更确定、而是扩展却更灵活。
    9. 通过 https://github.com/xgrommx/awesome-redux 可以获得大量参考。

    其他参考

    大而全的所有 Redux 参考资料。

    https://github.com/xgrommx/awesome-redux

    Slack 讨论组

    加入 https://reactiflux.slack.com Team,然后选择 redux channel。

    相关文章

      网友评论

      • 祖大胖:作者您好,关于中间件(store)=>(next)=>(action)第一个参数store那里有一些错误吧,根据applyMiddleware源码:
        const middlewareAPI = {
        getState: store.getState,
        dispatch: (action) => dispatch(action)
        }
        chain = middlewares.map(middleware => middleware(middlewareAPI))
        这时的store中的dispatch方法是生成的最新的dispatch,应该不是文章中所说的最原始的dispatch吧
      • 2017pan:学习了
      • 17bc405b121c:您好,
        文章解决了一直困惑我的问题,谢谢。
        顺便请教一下数据流程这个怎么做出来的,好COOL!!!
      • 60abc742d122:为什么react+redux中不推荐使用subscribe,那么在更改redux时怎么去重新渲染呢?
      • 7978fe6d03aa:写的太棒了:+1:
      • JamesSawyer:最近看这个感觉思绪很乱,首先感到困难的是state tree的设计问题,有哪些数据要添加到state tree中,哪些不需要,感到很困惑,请教一下您,有什么好的方式来理解这个问题吗?
        JamesSawyer:@jacobbubu 谢谢指点,以后动手前先按您这个分析一下,react全家桶学起来有点累:frowning:
        jacobbubu:@JamesSawyer 不是某个组件自用的状态;不能在父子组件之间直接传递的状态;需要在不同组件树分支下共享的状态;react 组件树和外部系统共享的状态;以上这些都可以被 state tree管理
      • 杨斌1994:如果rootstate是Immutable数据结构,其中一个字段是buttons:immutable.list.of(),那么如果本次action是给buttons,push一个对象,所以返回的结果是state.set(“buttons”,state.get("buttons").push({……})),我试过好像报错了,我现在用的是展开运算符,但是我不知道为什么错,可以帮我吗?另外webpack的热加载,指的是react的热加载,有没有文章推荐,我个人弄了半天也没弄好,而且好像react-hot-loader各种更新……



      • 福斯基:写的很不错,一下就理解redux的机制了
      • 7b01b20d0280:最近用redux开发 感觉非常不爽 原来答案在这。哪些场景数据适合哪些不适合?
        jacobbubu:@zhongjiechen 性能。毕竟为了维护 Immutable 和可维护性,组件间数据传递的路径是很长的,也经过了很多环节。因此如果要维护的状态和动画执行过程之中有关,通过 Redux Store 就不划算了。
      • 就叫肤浅的大西瓜:写得真棒!
      • GG93YN: :snowman: 非常不错
      • 9d48ff9726ab:我现在理解的是整个应用的state树和action creater从应用顶层往底层传,这样在顶层到底层之间就会存在很多无用state和action creater?我这样理解对吗?
        丿阿晨丶:这样应该是不对的。
        jacobbubu:@mycall 不好意思,没有看懂你的问题,能详细解释一下吗?
      • 1df8e6a2ce2a:写得真好!
      • jProvim:TLDR;
        Thunk: lazy evaluation.
        Higher-Order Store: Another Store wrapper, takes a store and return a store.
      • jacobbubu:首先,组件自身是可以分为环境感知和环境无知两种的,如下文的 Smart Component 和 Dumb Component,这样可以最大限度的重用环境无知的组件。这种方式不一定用于 Redux,可能用于任何和 React 相关的状态管理框架。
        https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.d4e0j9fdh

        然后就是具体工程中如何和 Redux 结合的方法:
        https://medium.com/lexical-labs-engineering/redux-best-practices-64d59775802e#.s5qgitqxv
      • e0d633a87102:最近也在看redux有个问题,组件如果嵌套级别比较多,那么最里边的组件,如果需要用到dispatch或者是包装过的actions,那岂不是需要每一层一层的传递到最里边的组件?比如我看例子里的写法,Todo需要一个completeTodo的action,那么,App要把completeTodo传递给TodoList,TodoList再传递给Todo。感觉非常不自然。而如果不传递action,改成注入dispatch的话,似乎简单一些(原来是n个函数,现在只有1个函数),但是又意味着组件需要知道redux的存在,这个问题应该怎么理解呢??? :worried:
      • 2e88cecde7a8:请问用我想用 Redux 进行一个富交互项目的搭建,有很多类型的 UI 组件。但是很疑惑是否应该将每个 UI 组件的 state 都使用 Redux 管理,比如一个 Accordion 组件的每个 panel 的开关状态,都用 Redux 的 store 管理会不会这个 Store 会变得无比的庞大,但是拆开 Store 的话,和Redux 一个 Store 管理的理念是不是相违背了
        jacobbubu:@爱斯基摩兔子 跨越component树的状态交互才用Redux,一棵树下props和state足以,性能也好
      • 7af317b92199:想问下in-place editing是什么意思,用会用,但是无法理解
        cace3686d9b2:@jacobbubu 谢谢,理解了,我的情况是更改array里的object的,
        但是用的是
        for(var v of state),将v指向新的object,犯了低级错误
        应该用for in 的,将state[k]指向新的object
        jacobbubu:@wangd933
        ```
        var obj1 = {a : 1}, obj2 = obj1;
        obj1.a = 2;
        obj1 === obj2 // true
        ```
        上面的写法是对 obj 进行了 in-place editing。

        ```
        var obj1 = {a: 1}, obj2 = obj1;
        obj1 = {a: 2};
        obj1 === obj2; // false
        ```
        这种写法就是 Immutable 的写法, 通过引用判等就知道对象的值是否变化了。
      • 王大狗:您好,最近在学习react开发,想请教下,由于之前有angular开发经验,angualr是broswer渲染,react宣称的是server和broswer都能渲染,对于首屏渲染,server端渲染broswer马上显示,这个好理解,但是对于后续的在broswer端用户不停的交互,每次跳转到subpage都需要server端渲染么?如果真是这样,对于访问量很大的页面,每个用户有自己的数据来填充页面这样的话,服务器压力会不会太大? 还有如果我ajax获取了页面的其他填充数据导致页面改变,broswer和server端的state又如何同步等
        王大狗:@jacobbubu 明白了!谢谢您的答复!
        jacobbubu:@王大狗 当然不是。所谓需要首屏渲染的原因是,应用本身是SPA(Single Page Application)的。Client 通过 AJAX 或者 ws 在一个页面中向 Server 发起请求,获取数据,改变 DOM。这才需要首屏渲染来让用户能够在第一个 AJAX/WS 请求获得结果前能让用户看到东西,也利于 SEO。
      • a680db56abaa:您好,想向您请教,normalized state 跟 immutable state 是可以共存的吗?谢谢。
        jacobbubu:@amowu 没发现有什么不能共存的。你发现哪些场景不能共存了吗?
      • a680db56abaa:總結得真棒!
      • ec2c5e1918d9:非常好的文章,3q
      • mdemo:写的正好。

      本文标题:Redux 核心概念

      本文链接:https://www.haomeiwen.com/subject/oxowqttx.html