探究redux源码-衍生-中间件思想

311 阅读30分钟

本文主要是阅读redux实现方式的时候,思路的一些拓展。大概是在三四个月前就看过redux源码,一直想写一些东西。但是迫于项目的紧急性,以及个人能力精力有限,就搁浅了。现在又重新看,而且很多时候,看懂一些东西可能不难,但是真正深入进去研究,会发现很多东西并不是很清楚,这就需要多思考一些,再写下来能有清晰的思路就更难了。这次的文章需要你对redux,react-redux都有一定的了解,很多地方我没有做过多的解释,还有本文不完美的地方,还请指出。

redux基础

  • 我们先大概过一下redux暴露的几个方法。
// index.js
export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
}
  • createStore
    一个工厂函数传入reducer,创建store,返回几个函数,主要是dispatch,getState,subscribe,replaceReducer,以及结合rx这种发布订阅库的symbol(?observable)

  • combineReducers
    把单个的reducer组合成一个大的reducer

  • bindActionCreators
    把我们写的一个js中的好多ActionCreator 通过遍历搞的一个对象里,并返回。

  • applyMiddleware
    一个三阶函数,是用来改写store的dispatch方法,并把所有的中间件都compose串联起来,通过改写dispatch,来实现redux功能的拓展。

  • compose
    一个组合多个middleware的方法,通过reduceRight方法(同理也可以是reduce),把传进来的middleware串成一条链,也可以看成回调接回调,一个预处理的方法。

redux-middleware

接触过后端的同学,对中间件这个概念一定不陌生。像node中的express,koa框架,middleware都起到了重要作用。redux中的实现方式不太一样,不过原理思想都是差不多的,都是链式组合,可以应用多个中间件。它提供了action发起之后,到达reducer之前的拓展功能。可以利用Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。

我们从redux中applyMiddleware使用入口开始研究。

中间件

  //日志中间件1
  const logger1 = store => next => action => {
    console.log('logger1 start', action);
    next(action);
    console.log('logger1 end', action);
  }
  
  //日志中间件2
  const logger2 = store => next => action => {
    console.log('logger2 start', action);
    next(action);
    console.log('logger2 end', action);
  }

为什么中间件要定义成这种三阶的样子呢,当然是中间件的消费者(applyMiddleware)规定的。

先通过一个小栗子看一下middleware的使用。

  //定义一个reducer
  const todoList = [];
  function addTodo(state = todoList, action) {
    switch (action.type) {
      case 'ADD_TODO':
        return [...state, action.text];
        break;
      default:
        return state;
    }
  }

 //创建store
 //为了先减轻其他方法带来的阅读困难,我选用直接使用applyMiddleware的方法创建store
  
  import { createStore, applyMiddleware } from 'redux';
  
  const store = applyMiddleware(logger1, logger2)(createStore)(reducer);
  

 // store注入Provider    
  ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));

通过applyMiddleware执行可以得到一个store,store再通过react-redux中的provider注入。此时得到的store就是被改造了dispatch的。通过图来形象的解释一下:

  • 默认的redux流程

  • applyMiddleware封装之后

可以看出redux在事件或者某个函数调用后,执行action(可能是bindActionCreators处理后的),由于bindActionCreator会去调用dispatch,

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

dispatch内部会把currenReducer执行,并把监听者执行。实现view更新。
但是经过applyMiddleware的包装,store里面的被封装,在调动action之后,执行封装后的dispatch就会经过一系列的中间件处理,再去触发reducer。

然后我们再通过研究源码,看他是怎么实现的封装dispatch。

思路可以从通过applyMiddleware创建store一点一点的看。

//applyMiddleware 源码

middlewares => createStore => (reducer, preloadedState) => {

 // 第一步先创建一个store
  var store = createStore(reducer, preloadedState, enhancer)
  
 // 缓存dispatch,原store的dispatch要改写。
  var dispatch = store.dispatch
  
 // 定义chain来存放 执行后的二阶中间件
  var chain = []

 // middleware 柯理化的第一个参数。参照logger1的store,这里只保留getState,和改造后的dispatch两个方法。
  var middlewareAPI = {
    getState: store.getState,
    dispatch: (action) => dispatch(action)
  }
  
  // 把中间件处理一层,把getState,dispatch方法传进去,也就是中间件柯理化第一次的store参数。
  // 这样能保证每个中间件的store都是同一个,柯理化的用途就是预置参数嘛。
  chain = middlewares.map(middleware => middleware(middlewareAPI))
  
  // 串联起所有的中间件,dispatch重新赋值,这样调用dispatch的时候,就会穿过所有的中间件。
  dispatch = compose(...chain)(store.dispatch)

  return {
    ...store,
    dispatch
  }
}

compose还是比较重要的

//compose
其实compose是函数式编程中比较重要的一个方法。上面调用compose的时候可见是一个二阶函数。
 
const compose = (...funcs) => {
  
  //没有参数,那就返回一个function
  if (!funcs.length) {
    return arg => arg
  }
  //一个中间件,返回它
  if (funcs.length === 1) {
    return funcs[0];
  }
  // 最后一个
  var last = funcs[funcs.length -1];
  
  // 复制一份,除去last
  var rest = funcs.slice(0, -1);
  
  // 返回函数,可以接收store.dispatch。
  // reduceRight 反向迭代。
  
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}

compose执行

  • chain中都是已经预置middlewareAPI参数后的二阶函数。执行传入的参数都是 形参next。

  • 通过执行compose(...chain)(store.dispatch),last是最后一个中间件,执行并传入 store.dispatch, 返回一个只剩一阶的(action) => {}, 不过已经预置了next参数,也就是store.dispatch

  • 然后last(...args)返回的结果传入reduceRight的回调, 对应形参是composed。

  • f是rest的最后一项, 执行并把 composed 传入,等同于f形参中的next... 得到的结果也是一阶函数,预置的next是last(...args) ...

  • 以此类推。这样,就形成了一个嵌套多层的语句。
    类似于logger1(logger2(store.dispatch),当然这只是一个比喻。
    只不过到第一个middleware的时候,是二阶函数传入next执行,得到一阶函数返回赋值给dispatch,这时的一阶函数已经变成了形似这样:

    function (action) {
      console.log('logger1 start', action);
      next(action);
      console.log('logger1 end', action);
    }
    

经过compose之后的dispatch执行

  • 返回的store中dispatch被修改,执行store.dispatch的时候,也就是这个函数执行.

  • 当执行到next(action)的时候,会调用已经预置的next函数,也就是第二个中间件的(action) => {},依次类推。直到最后一个中间件,他的next函数是store.dispatch函数,执行并把action传入。

  • 执行完最后一个中间件的next(action),也就是初始的dispatch。next后面的代码再执行,再逆向把中间件走一遍,直到第一个中间件执行完毕。
    就会出现这种效果

    start logger1 Object {type: "ADD_TODO", text: "defaultText"}
    start logger2 Object {type: "ADD_TODO", text: "defaultText"}
    dispatch()
    end logger2 Object {type: "ADD_TODO", text: "defaultText"}
    end logger1 Object {type: "ADD_TODO", text: "defaultText"}
    

用图形象点就是

这样redux middleware的执行流程就搞清楚了。

应用applyMiddleware的方式

import { createStore, applyMiddleware } from 'redux';

1. compose(applyMiddleware(logger1, logger2))(createStore)(reducer);

2. applyMiddleware(logger1, logger2)createStore)(reducer);

3. createStore(reducer, [], applyMiddleware(logger1, logger2));

createStore源码中有一个判断,

createStore(reducer, preloadedState, enhancer) => {
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // 所以第三种直接传入applyMiddleware(logger1, logger2),效果是一样的。
    return enhancer(createStore)(reducer, preloadedState)
  }
}

第一种先compose同理。一个参数的时候会返回applyMiddleware,变形之后也是一样的。

enhancer的用法很多种,不仅仅是applyMiddleware,比如Redux-Devtools, 都是利用了compose函数。自定义开发一些拓展功能还是很强大的...
redux里的compose是处理三阶函数的,恰巧createStore, applyMiddleware都是三阶函数,都可以通过compose串联起来。不禁感叹函数式编程思维的强大啊。

应用异步action

  • redux-thunk

简单来说,就是dispatch(action), action 可以是function. 当然这种写法需要配合bindActionCreator处理。

actionCreator之前都是返回一个{type: 'UPDATE', text: 'aaa'}这样的简单对象。通过thunk中间件,可以处理返回function的情况。

const reduxThunk = store => next => action => {
  if (typeof action === 'function') {
    console.log('thunk');
    return action(store.dispatch);
  }
  return next(action);
}

//action 可能是这样。
const addAsync = function() {
  return (dispatch) => {
    setTimeout(() => {
      dispatch({ type: 'ADD_TODO', text: 'AsyncText' })
    }, 1000)
  }
}

  • redux-promise

用来处理actions返回的是promise对象的情况。其实道理很简单,thunk去判断传进中间件的action是不是function,这里就判断是不是promise就行了。

//判断promise
function isPromise(val) {
  return val && typeof val.then === 'function';
}

const reduxPromise = store => next => action => {
  return isPromise(action)
    ? action.then(store.dispatch)
    : next(action);
}

// 源码还多了一个判断,判断action是不是标准的flux action对象(简单对象,包含type属性...)

express中的middleware

当一个客户端的http请求过来的时候,匹配路由处理前后,会经过中间件的处理,比如一些CORS处理,session处理...

  • 用法

    var app = express();
    
    app.use(function (req, res, next) {
      console.log('Time:', Date.now());
      next();
    });
    
    app.use(middleware1);
    app.use(middleware2);
    app.use(middleware3);
    
    app.listen(3000);

    每次访问这个app应用的时候,都会执行

  • 模拟

看了源码,自己模拟一下,当然是很简单的用法了。这是应用层的中间件,要实现路由器层的话,只需要根据路由 保存不同的数组就好了,然后匹配。

const http = require('http');
function express () {
  const app = function(req, res) {
    let index = 0;
    //重点在于next函数的实现,express是用一个数组维护的。
    function next() {
      const routes = app.route;
      routes[index++](req, res, next);
    }
    next();
  };
  
  app.route = [];
  
  // 很明显use 是往数组里push。
  app.use = function (callback) {
    this.route.push(callback);
  };
  
  // listen函数是一个语法糖,利用http模块
  app.listen = function(...args) {
    http.createServer(app).listen(...args);
  }

  return app;
}

const app = express();

app.use((req, res, next) => {
  setTimeout(() => {
    console.log('async');
    next();
  }, 1000);
});

app.use((req, res, next) => {
  console.log( 'logger request url:', req.url);
  next();
});


app.listen(3333);

假总结

现在web的中间件概念,都区别于最早严格意义上的中间件,其实我们现在的很多编程思想都是借鉴的先驱提出的一些东西。JAVA中类似的是AOP,即面向切面编程,以补充OOP(面向对象)多个对象公用某些方法时造成的耦合。

目前js中见到的中间件思想用法都是差不多的,只有调用next,程序才会继续往下执行,没有next,可以抛出异常等。只不过redux使用的函数式编程思想,用法偏函数式一些。

demo代码我会放到middleware-demo目录里,可以clone下来操作一番。链接

先到这,下次衍生就是函数式编程了。







redux入门之三大原则

三个概念

  • Store

  • Action

  • Reducer

三大准则

  • 单一数据源

    整个应用状态,都应该被存储在单一store的对象树中(object tree)。

  • State 是只读的

    唯一改变state的方法,就是发送(dispatch)一个动作(Action),action 是一个用于描述已发生事件的普通对象

  • 使用纯函数去修改状态

    为了描述action 如何改变 state tree,需要写reducers

    reducer 只是一些纯函数。。(pure function)可被看成是一个状态机,在任何时候,只要有相同的输入,就会得到相同的输出

    function add1(a,b) {
        return a + b
    }
    var a = 0;
    function add2(b) {
        return a = a + b
    }
    add1(1,2)   add1(1,2) 
    
    add2(1)     add2(1)    
    

    This is an source code.



reducer

  • 为什么叫reducer
    大概是由于reducer函数都能作为数组的reduce方法的参数,所以叫reducer的吧。
  • Array中的reduce
    reduce需要两个参数,一个是回调函数,一个是初始值,没有初始值,会默认把数组第一个当初始值,并从第二个开始

模拟数组的reduce方法

Array.prototype.reduce = function reduce (callback, init) {
  var i = 0;
  if(typeof init === 'undefined') {
    init = this[0];
    i = 1;
  }
  if(typeof callback !== 'function') {
    throw new Error(callback + ' is not function')
  }
  for( ;i< this.length; i++ ) {
    init = callback(init, this[i])
  }
  return init ;
}

reduce的使用

var ary = [1,2,3];
console.log(ary.reduce((initialValue, next) => {
  console.log(initialValue, next);
  return next;
},0))
// 01  12  23  3

写一个简单的reducer

function reducer (initialValue, next) {
  console.log(initialValue, next)
  switch (next) {
    case 1:
      return next;
      break;
    default:
      return initialValue
  }
}
// 这个reducer 判断传入的值next。是1 的话 返回结果是 next 也就是1 ,所以最后结果都是1
console.log(ary.reduce(reducer))
// 12  13  1

reducer在redux中的作用

reducer的作用就是设计state结构,它可以给定state 的初始值,更重要的是告诉store,根据对应的action如何更新state。 通常我们的store需要多个reducer组合,成为我们最后的state tree

注意点

  • 保持reducer 的纯净

通常我们的reducer是纯函数(pure function) 即固定的输入返回固定的输出,没有副作用,没有API请求... 等等,之后我们说为什么这么做。
通常我们在处理业务中,比如请求一个列表的数据并渲染。
举个栗子

const initialState = {
  code: -1,
  data: [],
  isFetching: false
};
//初始化我们的state,也就是没有请求之前,我们根据接口的数据格式做一个模拟

function List(state = initialState, action) {
  switch (action.type) {
// 这里的types 通常是我们保存这种常量的一个对象

    case types.FETCH_LIST_SUCCESS:
      return {...state, data:action.data,isFetching:false};
    case types.FETCHING_LIST:
      return {...state, isFetching: true}
    case types.FETCH_LIST_FAILURE:
      return {...state, isFetching:false};
    default:
      return state
    }
}

{...X,...X} 是浅复制,和Object.assign 类似都是浅复制,实现代码:

		<script type="text/javascript">
			var _extends = Object.assign || function(target) {
				for(var i = 1; i < arguments.length; i++) {
					var source = arguments[i];
					for(var key in source) {
						if(Object.prototype.hasOwnProperty.call(source, key)) {
							target[key] = source[key];
						}
					}
				}
				return target;
			};

			var a = {
				"a": {
					"b": {
						"c": 100,
						"d": 200,
						"e": {
							"f": 300
						}
					}
				}
			};
			var b = {
				"a": {
					"b": {
						"g": 400
					}
				}
			};

			console.log(_extends({}, a, b));
		</script>


我们的reducer函数就是根据请求的状态返回不同的数据,但是数据格式是一定的。Fetching就是 请求过程中,比如我们做一个loading效果可能需要这个。然后type是success就是成功我们返回数据。这些请求都放到actions 中了,actions去处理逻辑,数据API,重组数据。只需要传给reducer函数数据结果就ok了。

为什么要重新返回一个对象。

我们可以看到reducer函数在拿到数据后通过Object.assign 重新返回一个对象,直接state.data 修改,返回state不行吗?

首先 我们默认的初始state是不能直接改变的,我们的reducer函数 在数据failure的时候 return了默认的state,这个initialState 是不应该被修改的。

另外,我们的react组件 会多次接受store传入props,每一次都应该是一个全新的对象引用,而不是同一个引用。比如我们需要比较两次传入的props,利用componentWillReciveProps(nextProps) 比较this.props 跟nextProps,肯定是需要两个对象空间的,不然是同一个对象引用也就没法比较了。

所以redux 中的reducer 函数要求我们必须返回新的对象state

redux文档-reducer

多个reducer组合成我们的state tree

通常我们会引入redux提供的一个函数

import { combineReducers } from 'redux'

其实combineReducers做的事情很简单,顾名思义就是合并多个reducer

比如我们一个项目有多个reducer但是最后需要合并成一个,然后告诉store生成state tree,再注入Provider组件,先不关注Provider的问题。我们看一下combineReducers的简单实现

//首先我们组合得到的reducer仍旧是一个函数
//这个reducer会整合所有的reducer
//然后根据我们定义的状态树的格式返回一个大的state tree
// 根据reducers这个对象的key,取到reducer函数,并传入对应的 state

const combineReducers = function combineReducers (reducers) {
  return (state = {}, action) {
    Object.keys(reducers).reduce((initialState, key) => {
      initialState[key] = reducers[key](state[key], action)
      return initialState
    },{})

  }
}

这个函数返回一个rootReducer,然后createStore接收rootReducer,在createStore内部会调用一次dispatch(init),rootReducer 会执行,所有我们制定的reducrs对象中的key 都会被添加到 一个初始化initialState中,遍历将每个子级state添加到initialState 。init的时候,state[key]是undefined,每个reducer函数有初始值 返回。以后的dispatch ,因为有了state tree,state[key]都可以取到值了。




store

store 是什么

store是一个管理state的大对象,并且提供了一系列的方法

getState(),  //返回state
dispatch(action),  // 派发一个action
subscribe()  //订阅监听

通过redux 提供的 createStore,传入reducer函数,我们可以得到一个store对象

import { createStore } from 'redux'
const store = createStore(reducer)

简单实现一个createstore函数

//这是一个工厂函数,可以创建store

const  createStore = (reducer) => {
   let state; // 定义存储的state
   let listeners = [];
   
  //  getState的作用很简单就是返回当前是state
  const  getState = ()=> state;
  
    //定义一个派发函数
    //当在外界调用此函数的时候,会修改状态
  const dispatch = (action)=>{
      //调用reducer函数修改状态,返回一新的状态并赋值给这个局部状态变量
      state = reducer(state,action);
      //依次调用监听函数,通知所有的监听函数
      listeners.forEach(listener => listener());
  }
   //订阅此状态的函数,当状态发生变化的时候记得调用此监听函数
  const subscribe = function(listener){
      //先把此监听 加到数组中
      listeners.push(listener);
      
      //返回一个函数,当调用它的时候将此监听函数从监听数组移除
      return function(){
          listeners = listeners.filter(l => l != listener);
      }
  }
    //默认调用一次dispatch给state赋一个初始值
   dispatch({types: 'INIT'});
  return {
      getState,
      dispatch,
      subscribe
  }
}

使用

function numReducer (state = 0, action) {
  switch (action.types) {
    case 'increase':
      return state + 1
      break;
    case 'deincrease':
      return state - 1
    default:
      return state
  }
}
let store = createStore(numReducer);
store.subscribe(() => {
  console.log('触发了')
})
store.dispatch({types: 'increase'})
console.log(store.getState())
// 触发了 1

注意的点

根据官方的说法,一个应用应该只有一个store,即单一数据源,我们通过合并reducer 来壮大state tree
未完





action

  • action相当于一个载体,它携带数据(可能是用户操作产生,或者接口数据),并且会根据事先定义好的type,传给store.dispatch(action),触发reducer方法,更新state。

    通常一个action是一个对象,类似于这样

    {
        type: 'UPDATE_TEXT',
        text: 'update'
    }
    

    需要dispatch更新的时候dispatch(action),就会传给reducer(action),reducer函数根据具体的type返回state,store会更新state。

  • actionCreator
    顾名思义,action创建函数。

    function updateText(text) {
       return {
         type: 'UPDATE_TEXT',
         text, 
       }
    }

    这样看起来更独立一些,当操作的时候我们只需要把text传给creator,就可以得到一个符合要求的action。所有相关的actionCreator都放到一个文件里,放到一个对象里actionCreators, 调用action的时候,只需要dispatch(actionCreators.updateText('abc'))。

bindActionCreators

这个函数接受两个参数, (actionCreators, dispatch)。 第一个参数是actionCreator的集合,是一个对象。第二个参数dispatch由store提供。
这个函数通常是配合react-redux使用,通过connect把改造后的actions对象传给Component。这样,我们就可以在一个专门的actions.js文件里定义他们,而我们的组件内部,不需要写dispatch(action)这样的操作,组件内部感知不到redux的存在。这样的好处是降低耦合性,组件内部只是调用一个方法,而这些方法通过connect传给父组件,子组件仍旧是独立的,或者说是木偶组件。

//actions.js
function updateText(text) {
return {
  type: 'UPDATE_TEXT',
  text
}
}
function addText(text) {
return {
   type: 'ADD_TEXT',
   text
}
}
const actions = { updateText, addText  }

import { bindActionCreators, createStore } from 'redux';
const store = createStore(reducer);
const bindActions = bindActionCreators(actions, store.dispatch)
store.actions = bindActions;

ReactDOM.render(
<App  store />
)
//伪代码




Redux 入门介绍 :https://github.com/creeperyang/blog/issues/32

Redux is a predictable state container for JavaScript apps.

Redux 是一个给JavaScript app使用的可预测的状态容器。

为什么需要Redux?(动机)

JavaScript单页应用越来越复杂,代码必须管理远比以前多的状态(state)。这个状态包括服务端返回数据,缓存数据,本地创建的数据(未同步到服务器);也包括UI状态,如需要管理激活的路由,选中的标签,是否显示加载动效或者分页器等等。

管理不断变化的状态是很难的。如果一个 model 可以更新另一个 model ,那么一个 view 也可以更新一个 model 并导致另一个 model 更新,然后相应地,可能导致另一个 view 更新 —— 你理不清你的 app 发生了什么,失去了对 state 什么时候,为什么,怎么变化的控制 。当系统变得 不透明和不确定,就很难去重现 bug 和增加 feature 了。

通过 限制何时以及怎么更新,Redux 试图让 state 的变化可以预测 。

这里可以配合阅读 You Might Not Need Redux : Redux 的引入并不一定改善开发体验,必须权衡它的限制与好处。

Redux本身很简单,我们下面首先阐述它的核心概念和三大原则。

核心概念

想象一下用普通 JavaScript 对象 来描述 app 的 state:

// 一个 todo app 的 state 可能是这样的:
{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}

这个对象就像没有 setter 的 model,所以其它部分的代码不能随意修改它而造成难以复现的 bug 。

如果要改变 state ,我们必须 dispatch 一个 action。action 是描述发生了什么的普通 JavaScript 对象。

// 下面都是action:
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

强制 每个 change 都必须用 action 来描述,可以让我们清楚 app 里正在发生什么, state 是为什么改变的。最后,把 state 和 actions 联结起来,我们需要 reducer 。

reducer 就是函数,以之前的 state 和 action 为参数,返回新的 state :

// 关注 visibilityFilter 的 reducer
function visibilityFilter(state = 'SHOW_ALL', action) {
  if (action.type === 'SET_VISIBILITY_FILTER') {
    return action.filter;
  } else {
    return state;
  }
}

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action)
  };
}

以上就是 Redux 的核心概念,注意到我们并没有用任何 Redux 的 API,没加入任何 魔法。 Redux 里有一些工具来简化这种模式,但是主要的想法是描述如何根据这些 action 对象来更新 state。

三大原则

1. 单一数据源

整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

console.log(store.getState())

/* Prints
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}
*/

2. State 是只读的

改变 state 的唯一方式是触发 (emit) action,action 是描述发生了什么的对象。
这确保了视图和网络请求等都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心 race condition 的出现。

store.dispatch({
  type: 'COMPLETE_TODO',
  index: 1
})

store.dispatch({
  type: 'SET_VISIBILITY_FILTER',
  filter: 'SHOW_COMPLETED'
})

3. 使用纯函数来执行修改

为描述 action 怎么改变 state tree,你要编写 reducers。

Reducer 只是一些纯函数,它接收之前的 state 和 action,并返回新的 state。刚开始你可能只需要一个 reducer ,但随着应用变大,你会需要拆分 reducer 。

以 todo app 为例迅速上手 Redux

1. 定义 actions

Action 就是把数据从应用(这些数据有可能是服务器响应,用户输入或其它非 view 的数据)发送到 store 的有效载荷。 它是 store 数据的唯一来源,你通过 store.dispatch(action) 来发送它到 store。

添加新 todo 任务的 action 是这样的:

const ADD_TODO = 'ADD_TODO'

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

Action 本质上是 JavaScript 普通对象。Action 必须有一个字符串类型的 type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块/文件来存放 action。

除了 type 字段外,action 对象的结构完全由你自己决定。但通常,我们希望减少 action 中传递的数据。

Action 创建函数 (action creator)

Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。

// 生成一个 ADD_TODO 类型的 action
function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

2. Reducers

Action 只是描述了有事情发生了这一事实,并没有指明应用如何更新 state。而这正是 reducer 要做的事情。

设计 State 结构

在 Redux 应用中,所有的 state 都被保存在一个单一对象中。最好可以在写代码之前想好 state tree 应该是什么形状的。

通常,这个 state tree 需要存放一些数据,以及一些 UI 相关的 state。这样做没问题,但尽量把数据与 UI 相关的 state 分开。

// todo app 的 state
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}

处理 Action

有了 state 结构后,我们可以来写 reducer 了。 reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

(previousState, action) => newState

保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:

  • 修改传入参数;
  • 执行有副作用的操作,如 API 请求和路由跳转;
  • 调用非纯函数,如 Date.now()Math.random()

在高级篇里会介绍如何执行有副作用的操作。现在只需要记住 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

import { VisibilityFilters } from './actions'

function todoApp(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
          }
        ]
      })
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: state.todos.map((todo, index) => {
          if(index === action.index) {
            return Object.assign({}, todo, {
              completed: !todo.completed
            })
          }
          return todo
        })
      })
    default:
      return state
  }
}

注意:

  • 不要修改 state。 使用 Object.assign({}, ...) 新建了一个副本。
  • default 情况下返回旧的 state。 遇到未知的 action 时,一定要返回旧的 state。

我们看到,多个 action 下,reducer 开始变得复杂。是否可以更通俗易懂?这里的 todosvisibilityFilter 的更新看起来是相互独立的,我们可以尝试拆分到单独的函数里。

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: todos(state.todos, action)
      })
    default:
      return state
  }
}

注意 todos 依旧接收 state,但它变成了一个数组!现在 todoApp 只把需要更新的一部分 state 传给 todos 函数,todos 函数自己确定如何更新这部分数据。这就是所谓的 reducer 合成,它是开发 Redux 应用最基础的模式。

现在更进一步,把 visibilityFilter 独立出去。那么我们可以有个主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 不再需要知道完整的 initial state。初始时,如果传入 undefined, 子 reducer 将负责返回它们(负责部分)的默认值。

// 彻底地拆分:
function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

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

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。

当应用越来越复杂,我们还可以将拆分后的 reducer 放到不同的文件中, 以保持其独立性并用于专门处理不同的数据域。

最后,Redux 提供了 combineReducers() 工具来做上面 todoApp 做的事情。可以用它这样重构 todoApp:

import { combineReducers } from 'redux';

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

// 完全等价于
function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

3. 创建 store

前面两小节中,我们学会了使用 action 来描述“发生了什么”,和使用 reducers 来根据 action 更新 state 的用法。

Store 就是把它们联系到一起的对象。Store 有以下职责:

  • 维持应用的 state;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。

再次强调一下 Redux 应用只有一个 单一 的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。

根据已有的 reducer 来创建 store 是非常容易的。在前面我们使用 combineReducers() 将多个 reducer 合并成为一个。现在我们将其导入,并传给 createStore()

import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)

你可以把初始状态 intialState 作为第二个参数传给 createStore()。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。

let store = createStore(todoApp, window.STATE_FROM_SERVER)

发起 actions

现在我们已经创建好了 store ,可以验证一下:

import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions'

// 打印初始状态
console.log(store.getState())

// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

// 发起一系列 action
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

// 停止监听 state 更新
unsubscribe();

4. 数据流

严格的单向数据流 是 Redux 架构的设计核心。

这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个且独立的无法相互引用的重复数据。

Redux 应用中数据的生命周期遵循下面 4 个步骤:

  1. 调用 store.dispatch(action)

  2. Redux store 调用传入的 reducer 函数。

  3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。

  4. Redux store 保存了根 reducer 返回的完整 state 树。

这个新的树就是应用的下一个 state!所有订阅 store.subscribe(listener) 的监听器都将被调用;监听器里可以调用 store.getState() 获得当前 state。

搭配 React 一起使用

首先强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。

尽管如此,Redux 还是和 React 和 Deku 这类框架搭配起来用最好,因为这类框架允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。

安装 react-redux

Redux 自身并不包含对 React 的绑定库,我们需要单独安装 react-redux

Presentational and Container Components

绑定库是基于 容器组件和展示组件相分离 的开发思想。建议先读完这篇文章。

技术上讲,我们可以手动用 store.subscribe() 来编写容器组件,但这就无法使用 React Redux 做的大量性能优化了。一般使用 React Redux 的 connect() 方法来生成容器组件。(不必为了性能而手动实现 shouldComponentUpdate 方法)

设计组件层次结构

还记得前面 设计 state 根对象的结构 吗?现在就要定义与它匹配的界面的层次结构。这不是 Redux 相关的工作,React 开发思想在这方面解释的非常棒。

  1. 展示组件: 纯粹的UI组件,定义外观而不关心数据怎么来,怎么变。传入什么就渲染什么。

  2. 容器组件: 把展示组件连接到 Redux。监听 Redux store 变化并处理如何过滤出要显示的数据。

  3. 其它组件 有时很难分清到底该使用容器组件还是展示组件,并且组件并不复杂,这时可以混合使用。

实现组件

省略其它部分,主要讲讲容器组件一般怎么写。

import { connect } from 'react-redux'

// 3. connect 生成 容器组件
const ContainerComponent = connect(
  mapStateToProps,
  mapDispatchToProps
)(PresentationalComponent)

// 2. mapStateToProps 指定如何把当前 Redux store state 映射到展示组件的 props 中
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)
  }
}

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

// 1. mapDispatchToProps() 方法接收 dispatch() 方法并返回期望注入到展示组件的 props 中的回调方法。
const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}
// 可以使用 Redux 的 bindActionCreators 把所有的暴露出来的 actionCreators 转成方法注入 props

export default ContainerComponent

connect 本身还是很明确的,指定我们注入哪些 data 和 function 到展示组件的 props ,给展示组件使用。



API 探索

Redux API

1. createStore(reducer, [preloadedState], enhancer)

创建一个 Redux store 来以存放应用中所有的 state。详情可见 Redux API,这里主要强调两点:

  1. preloadedState:初始时的 state。在同构中会用到,比如从一个session恢复数据。

当 store 创建后,Redux 会 dispatch action({ type: ActionTypes.INIT })) 到 reducer 上,得到初始的 state 来填充 store。所以你的初始 state 是 preloadedState 在 reducers 处理 ActionTypes.INIT action 后的结果。 github.com/reactjs/red…

  1. enhancer:如果有 enhancer,那么会首先得到增强的 createStore,然后再createStore(reducer, [preloadedState])

github.com/reactjs/red… 可结合下面 applyMiddleware一起看。

2. middleware 与 applyMiddleware(...middlewares)

我们可以用 middleware 来扩展 Redux。Middleware 可以让你包装 store 的 dispatch 方法来达到你想要的目的。同时, middleware 还拥有“可组合”这一关键特性。多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。

middleware 的函数签名是 ({ getState, dispatch }) => next => action

如下是两个 middleware:

// logger middleware
function logger({ getState }) {
  return (next) => (action) => {
    console.log('will dispatch', action)

    // 调用 middleware 链中下一个 middleware 的 dispatch。
    let returnValue = next(action)

    console.log('state after dispatch', getState())

    // 一般会是 action 本身,除非
    // 后面的 middleware 修改了它。
    return returnValue
  }
}

// thunk middleware
function thunk({ dispatch, getState }) {
  return (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState);
    }

    return next(action);
  }
}

applyMiddleware 返回一个应用了 middleware 后的 store enhancer。这个 store enhancer 的签名是 createStore => createStore,但是最简单的使用方法就是直接作为最后一个 enhancer 参数传递给 createStore() 函数。

再来看下 applyMiddleware

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 是 [(next) => (action) => action, ...]
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    // compose(...chain) 返回这样一个函数:
    // 对 chain 进行 reduce,从右向左执行,每次的结果作为下次执行的输入
    dispatch = compose(...chain)(store.dispatch)
    // 最终的 dispatch 是这样的:(action) => action

    return {
      ...store,
      dispatch
    }
  }
}

可以看到(假设 enhancerTest = applyMiddleware(A, B, C)):

  1. middleware 其实只是劫持/包装了 dispatch
  2. dispatch 本质上是同步的,但我们可以通过 thunk 等延迟执行 dispatch
  3. chain[index](dispatch) --> (action) => action,即我们得到的 dispatch 是一个层层嵌套的 (action) => action 函数。
  4. 除了最右侧的 C 得到的 next 是原本的 dispatch,剩下的都是被层层嵌套的 (action) => action 函数,并且越右侧越嵌套在里面,所以当 dispatch(action) 调用时,将会以下面顺序执行:A -> B -> C -> B -> AC 之前 A/B 都只执行了 next 之前的逻辑,之后各自完全执行。

3. combineReducers(reducers)

把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数。

真的很简单,从逻辑上来讲,就是:

combineReducers({
  keyA: reducerA,
  keyB: reducerB
})

// --->

function recuderAll (prevState, action) {
  return {
    keyA: reducerA(prevState.keyA, action),
    keyB: reducerB(prevState.keyB, action)
  }
}

核心就是干了上面的事,只是多了一些判断和检查。

4. bindActionCreators(actionCreators, dispatch)

把 action creators 转成拥有同名 keys 的对象,但使用 dispatch 把每个 action creator 包围起来,这样可以直接调用它们。

const actionCreators = {
  updateOrAddFilter(filter) {
    type: UPDATE_OR_ADD_FILTER,
    filter
  },
  removeFilter(type) {
    type: REMOVE_FILTER,
    filterType: type
  }
}

bindActionCreators(actionCreators, dispatch)

// -->

{
  updateOrAddFilter: (...args) => dispatch(original_updateOrAddFilter(...args)),
  removeFilter: (...args) => dispatch(original_removeFilter(...args)),
}

核心就是自动 dispatch ,这样我们可以在 react 组件里直接调用,Redux store 就能收到 action。

React Redux API

1. Provider

用法:

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

源码:

// storeKey 默认是 'store'
class Provider extends Component {
    getChildContext() {
      return { [storeKey]: this[storeKey], [subscriptionKey]: null }
    }

    constructor(props, context) {
      super(props, context)
      this[storeKey] = props.store;
    }

    render() {
      return Children.only(this.props.children)
    }
}
Provider.propTypes = {
    store: storeShape.isRequired,
    children: PropTypes.element.isRequired,
}
Provider.childContextTypes = {
    [storeKey]: storeShape.isRequired,
    [subscriptionKey]: subscriptionShape,
}

注意到, Provider 应用了 React Context,子组件都可以去访问 store

2. connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

connect 的函数签名是 ([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) => (WrappedComponent) => ConnectComponent,最后返回的 onnectComponent 可以通过 context 去访问 store

connect API 比较复杂,这里主要讲下前两个参数。

  • [mapStateToProps(state, [ownProps]): stateProps] (Function): 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。如果你省略了这个参数,你的组件将不会监听 Redux store。
  • [mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function): 如果传递的是一个对象,那么每个定义在该对象的函数都将被当作 Redux action creator,而且这个对象会与 Redux store 绑定在一起,其中所定义的方法名将作为属性名,合并到组件的 props 中。