Redux核心概念

移动开发
http://gaearon.github.io/redux/index.html ,文档在http://rackt.github.io/redux/index.html 。本文不是官方文档的翻译。你可以在阅读官方文档之前和之后阅读本文,以加深其中的重点概念。根据该项目源码的习惯,示例都是基于 ES2015 的语法来写的。

[[145606]]

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

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

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

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

  1. const initState = ''
  2. const actions = ['a''b''c']; 
  3. const newState = actions.reduce( 
  4.     ( (prevState, action) => prevState + action ), 
  5.     initState 
  6. ); 

从 Redux 的角度来看,应用程序的状态类似于上面函数中的 initState 和 newState 。给定 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 的消费者在判断数据是否变化时,只要简单地进行引用比较即可,例如:

  1. newState.todos === prevState.todos 

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

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

  1. `let myStuff = [ 
  2.     {name: 'henrik'
  3.  
  4. myStuff = [...mystuff, {name: 'js lovin fool']` 

myStuff 是一个全新的对象。

如果更新的是 Object ,则:

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

而不是:

  1. counters.faves = counters.faves + 1

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

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

而不是:

  1. let todos = [ 
  2.     { id: 1, text: 'have lunch'
  3. 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 来引用。

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

  1. [{ 
  2.   id: 1
  3.   title: 'Some Article'
  4.   author: { 
  5.     id: 1
  6.     name: 'Dan' 
  7.   } 
  8. }, { 
  9.   id: 2
  10.   title: 'Other Article'
  11.   author: { 
  12.     id: 1
  13.     name: 'Dan' 
  14.   } 
  15. }] 

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

  1.   result: [12], 
  2.   entities: { 
  3.     articles: { 
  4.       1: { 
  5.         id: 1
  6.         title: 'Some Article'
  7.         author: 1 
  8.       }, 
  9.       2: { 
  10.         id: 2
  11.         title: 'Other Article'
  12.         author: 1 
  13.       } 
  14.     }, 
  15.     users: { 
  16.       1: { 
  17.         id: 1
  18.         name: 'Dan' 
  19.       } 
  20.     } 
  21.   } 

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

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

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

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

Reducer

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

  1. function todoAppReducer(state = initialState, action) { 
  2.   switch (action.type) { 
  3.   case SET_VISIBILITY_FILTER: 
  4.     return Object.assign({}, state, { 
  5.       visibilityFilter: action.filter 
  6.     }); 
  7.   case ADD_TODO: 
  8.     return Object.assign({}, state, { 
  9.       todos: [...state.todos, { 
  10.         text: action.text, 
  11.         completed: false 
  12.       }] 
  13.     });  
  14.   default
  15.     return state; 
  16.   } 

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

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

  1. import { combineReducers } from 'redux'
  2.  
  3. const todoAppReducer = combineReducers({ 
  4.   visibilityFilter: visibilityFilterReducer 
  5.   todos: todosReducer 
  6. }); 

visibilityFilterReducer 和 todosReducer 是两个小 Reducers,其中一个如下:

  1. function visibilityFilterReducer(state = SHOW_ALL, action) { 
  2.   switch (action.type) { 
  3.   case SET_VISIBILITY_FILTER: 
  4.     return action.filter; 
  5.   default
  6.     return state; 
  7.   } 

visibilityFilterReducer 仅仅负责处理 State.visibilityFilter 字段的状态,这是通过向combineReducers 传递如下形式的参数实现的:

  1.   field1: reducerForField1, 
  2.   field2: reducerForField2 

filed1 和 filed2 表示 State 中的字段,reducerForField1 和 reducerForField2 是对应的 Reducers,每个 Reducers 将仅仅获得 State.field1 或者 state.field2 的值,而看不到 State 下的其他字段的内容。响应的返回结果也会被合并到对应的 State 字段中。

使用 combineReducers 的前提是,每一个被组合的 Reducer 仅仅和 State 的一部分数据相关,例如:todos Reducer 只消费 state.todos 数据,也只产生 state.todos 数据。如果需要消费其他 State 字段,那么还是需要在大 switch 中为特定处理函数传入整个 State,例如:

  1. function todoAppReducer(state = initialState, action) { 
  2.   switch (action.type) { 
  3.   case FAVE_ALL_ITEMS: 
  4.     return Object.assign({}, state, faveThemAll(state)); 
  5.   default
  6.     return state; 
  7.   } 

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

  1. function createReducer(initialState, handlers) { 
  2.   return function reducer(state = initialState, action) { 
  3.     if (handlers.hasOwnProperty(action.type)) { 
  4.       return handlers[action.type](state, action); 
  5.     } else { 
  6.       return state; 
  7.     } 
  8.   } 
  9.  
  10. export const todosReducer = createReducer([], { 
  11.   [ActionTypes.ADD_TODO](state, action) { 
  12.     let text = action.text.trim(); 
  13.     return [...state, text]; 
  14.   } 

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 是如何建立的:

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

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

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

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

Action

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

  1.   type: 'ADD_TODO'
  2.   text: 'Build Redux app' 

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

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

  1.   type: 'ADD_TODO'
  2.   payload: { 
  3.     text: 'Do something.'   
  4.   }, 
  5.   `meta: {}` 

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

  1.   type: 'ADD_TODO'
  2.   payload: new Error(), 
  3.   error: true 

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

Action Creator

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

  1. `function addTodo(text) { 
  2.   return { 
  3.     type: ADD_TODO, 
  4.     text 
  5.   }; 
  6. }` 

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。先来看一个例子:

  1. import { createStore, combineReducers, applyMiddleware } from 'redux'
  2.  
  3. // applyMiddleware takes createStore() and returns// a function with a compatible API. 
  4. let createStoreWithMiddleware = applyMiddleware( 
  5.   logger, 
  6.   crashReporter 
  7. )(createStore); 
  8.  
  9. // Use it like you would use createStore()let todoApp = combineReducers(reducers); 
  10. let store = createStoreWithMiddleware(todoApp); 

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

logger 的代码如下:

  1. `// Logs all actions and states after they are dispatched. 
  2. const logger = store => next => action => { 
  3.   console.log('dispatching', action); 
  4.   let result = next(action); 
  5.   console.log('next state', store.getState()); 
  6.   return result; 
  7. };` 

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

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

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

vanilla promise

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

  1. `/** 
  2.  * Lets you dispatch promises in addition to actions. 
  3.  * If the promise is resolved, its result will be dispatched as an action. 
  4.  * The promise is returned from `dispatch` so the caller may handle rejection. 
  5.  */ 
  6. const vanillaPromise = store => next => action => { 
  7.   if (typeof action.then !== 'function') { 
  8.     return next(action); 
  9.   } 
  10. `  // the action is a promise` 
  11.   return Promise.resolve(action).then(store.dispatch); 
  12. };` 

这个例子中,如果传入的 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

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

  1. `/** 
  2.  * Schedules actions with { meta: { delay: N } } to be delayed by N milliseconds. 
  3.  * Makes `dispatch` return a function to cancel the interval in this case. 
  4.  */ 
  5. const timeoutScheduler = store => next => action => { 
  6.   if (!action.meta || !action.meta.delay) { 
  7.     return next(action); 
  8.   } 
  9.  
  10.   let intervalId = setTimeout( 
  11.     () => next(action), 
  12.     action.meta.delay 
  13.   ); 
  14.  
  15.   return function cancel() { 
  16.     clearInterval(intervalId); 
  17.   }; 
  18. };` 

这个例子中,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 的实现非常简单:

  1. `const thunk = store => next => action => 
  2.   typeof action === 'function' ? 
  3.     action(store.dispatch, store.getState) : 
  4.     next(action);` 

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

  1. const createStoreWithMiddleware = applyMiddleware( 
  2.   logger, 
  3.   thunk 
  4.   timeoutScheduler 
  5. )(createStore); 
  6. const store = createStoreWithMiddleware(combineReducers(reducers)); 
  7.  
  8. function addFave(tweetId) { 
  9.   return (dispatch, getState) => { 
  10.     if (getState.tweets[tweetId] && getState.tweets[tweetId].faved) 
  11.         return
  12.  
  13.     dispatch({type: IS_LOADING}); 
  14.     // Yay, that could be sync or async dispatching 
  15.     remote.addFave(tweetId).then( 
  16.       (res) => { dispatch({type: ADD_FAVE_SUCCEED}) }, 
  17.       (err) => { dispatch({type: ADD_FAVE_FAILED, err: err}) }, 
  18.   }; 
  19.  
  20. store.dispatch(addFave()); 

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

当 thunk Middleware 发现传入的 action 是这样的 Thunk 函数时,就会为该函数配齐 dispatch 和 getState 参数,让 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})。无论如何,reducer 最后收到的 action 对象一定是这种 Javascript Plain Object。

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

例如:我们的 Middlewares 配置为 applyMiddleware(logger, thunk, timeoutScheduler),当 action 是 Thunk 函数时,这个 action 将没有机会被 timeoutSchedulerMiddleware 执行,而 logger Middleware 则有机会在 thunk Middleware 之前执行。

applyMiddleware

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

  1. function applyMiddleware(store, middlewares) { 
  2.   middlewares = middlewares.slice(); 
  3.   middlewares.reverse(); 
  4.  
  5.   let next = store.dispatch; 
  6.   middlewares.forEach(middleware => 
  7.     next = middleware(store)(next) 
  8.   ); 
  9.  
  10.   return Object.assign({}, store, { dispatch: next }); 

结合 Middleware 的写法:

  1. const logger = store => next => action => { 
  2.   console.log('dispatching', action); 
  3.   let result = next(action); 
  4.   console.log('next state', store.getState()); 
  5.   return result; 
  6. }; 

我们可以看到,给 Middleware 传入 store 和 next 之后,返回的是一个新的 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 方法。最后新生成的 newStore 的 dispatch 方法则是 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 对象作为返回值。

  1. 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。下面我们来分析一个具体的例子。

  1. import { Component } from 'react'
  2.  
  3. export default class Counter extends Component { 
  4.   render() { 
  5.     return ( 
  6.       <button onClick={this.props.onIncrement}> 
  7.         {this.props.value} 
  8.       </button> 
  9.     ); 
  10.   } 

这是一个 React Component,显示了一个按钮。按下这个按钮,就会调用 this.props.onIncrement。onIncrement的具体内容在下面的例子中, 起作用为每次调用 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 函数帮助你完成这个功能:

  1. import { Component } from 'react'
  2. import { connect } from 'react-redux'
  3.  
  4. import Counter from '../components/Counter'
  5. import { increment } from '../actionsCreators'
  6.  
  7. // Which part of the Redux global state does our component want to receive as props? 
  8. function mapStateToProps(state) { 
  9.   return { 
  10.     value: state.counter 
  11.   }; 
  12.  
  13. // Which action creators does it want to receive by props? 
  14. function mapDispatchToProps(dispatch) { 
  15.   return { 
  16.     onIncrement: () => dispatch(increment()) 
  17.   }; 
  18.  
  19. export default connect(   // Line 20 
  20.   mapStateToProps, 
  21.   mapDispatchToProps 
  22. )(Counter); 

第 20 行的 connect将 state 的某个(些)属性映射到了 Counter Component 的 this.props 属性中,同时也把针对特定的Action Creator 的 dispatch 方法传递给了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 提供的工具组件。

  1. React.render( 
  2.   <Provider store={store}> 
  3.     {() => <MyRootComponent />} 
  4.   </Provider>, 
  5.   rootEl 
  6. ); 

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,这个方法被称为 selector。selector 的作用就是为 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 对象,并且以此为集中点实现很多控制逻辑,例如 Log,Undo, ErrorHandler 等。
  6. Redux 仅仅专注于应用状态的维护,reducer、dispatch/middleware 是两个常用扩展点、Higher-order Store 则仅针对需要扩展全部 Store 功能时使用。
  7. react-redux 是 Redux 针对 React/React-Native 的 Binding,connect/selector是扩展点。
  8. Redux 是典型的函数式编程的产物,了解函数式编程会利于理解其实现原理,虽然使用它不需要了解很多函数式编程的概念。和 Flux 相比,Redux 的概念更精简、约定更严格、状态更确定、而是扩展却更灵活。
  9. 通过 https://github.com/xgrommx/awesome-redux 可以获得大量参考。

其他参考

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

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

责任编辑:倪明 来源: 简书
相关推荐

2021-02-19 08:38:36

Kubernetes容器化分布式

2020-10-23 09:26:57

React-Redux

2023-08-24 10:33:19

serviceexportsinfo类

2009-12-18 17:20:00

Ruby核心类

2022-03-10 13:11:11

DDD领域驱动设计

2022-02-09 09:53:43

Spring框架开源

2023-01-03 08:31:54

Spring读取器配置

2022-01-27 13:47:10

Kubernete命令Linux

2019-05-28 12:03:59

vuejavascript前端

2020-05-08 13:44:26

Spark架构RDD

2021-11-14 23:06:49

Python代码开发

2023-10-22 23:28:34

2020-04-29 21:54:46

操作系统核心概念

2011-07-14 15:23:34

java

2018-11-08 15:12:16

数据分析算法决策树

2017-07-14 15:40:28

2017-04-25 09:50:16

SparkRDD核心

2018-11-26 09:00:14

2020-12-08 12:24:55

接口测试Interface

2014-07-16 10:32:46

点赞
收藏

51CTO技术栈公众号