Redux 核心概念

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。

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

推荐阅读更多精彩内容