本文主要是阅读redux实现方式的时候,思路的一些拓展。大概是在三四个月前就看过redux源码,一直想写一些东西。可是迫于项目的紧急性,以及我的能力精力有限,就搁浅了。如今又从新看,并且不少时候,看懂一些东西可能不难,可是真正深刻进去研究,会发现不少东西并非很清楚,这就须要多思考一些,再写下来能有清晰的思路就更难了。此次的文章须要你对redux,react-redux都有必定的了解,不少地方我没有作过多的解释,还有本文不完美的地方,还请指出。javascript
// index.js
export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose,
}复制代码
createStore
一个工厂函数传入reducer,建立store,返回几个函数,主要是dispatch,getState,subscribe,replaceReducer,以及结合rx这种发布订阅库的symbol($$observable)html
combineReducers
把单个的reducer组合成一个大的reducerjava
bindActionCreators
把咱们写的一个js中的好多ActionCreator 经过遍历搞的一个对象里,并返回。node
applyMiddleware
一个三阶函数,是用来改写store的dispatch方法,并把全部的中间件都compose串联起来,经过改写dispatch,来实现redux功能的拓展。react
compose
一个组合多个middleware的方法,经过reduceRight方法(同理也能够是reduce),把传进来的middleware串成一条链,也能够当作回调接回调,一个预处理的方法。git
接触事后端的同窗,对中间件这个概念必定不陌生。像node中的express,koa框架,middleware都起到了重要做用。redux中的实现方式不太同样,不过原理思想都是差很少的,都是链式组合,能够应用多个中间件。它提供了action发起以后,到达reducer以前的拓展功能。能够利用Redux middleware 来进行日志记录、建立崩溃报告、调用异步接口或者路由等等。github
咱们从redux中applyMiddleware使用入口开始研究。web
中间件express
//日志中间件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在事件或者某个函数调用后,执行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
简单来讲,就是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)
}
}
复制代码
用来处理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下来操做一番。连接
先到这,下次衍生就是函数式编程了。
三个概念
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.
模拟数组的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是纯函数(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了。
为何要从新返回一个对象。
首先 咱们默认的初始state是不能直接改变的,咱们的reducer函数 在数据failure的时候 return了默认的state,这个initialState 是不该该被修改的。
另外,咱们的react组件 会屡次接受store传入props,每一次都应该是一个全新的对象引用,而不是同一个引用。好比咱们须要比较两次传入的props,利用componentWillReciveProps(nextProps) 比较this.props 跟nextProps,确定是须要两个对象空间的,否则是同一个对象引用也就无法比较了。
多个reducer组合成咱们的state tree
一般咱们会引入redux提供的一个函数
import { combineReducers } from 'redux'
复制代码
//首先咱们组合获得的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是一个管理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至关于一个载体,它携带数据(多是用户操做产生,或者接口数据),而且会根据事先定义好的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 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。
三大原则
整个应用的 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
}
]
}
*/复制代码
改变 state 的惟一方式是触发 (emit) action,action 是描述发生了什么的对象。
这确保了视图和网络请求等都不能直接修改 state,相反它们只能表达想要修改的意图。由于全部的修改都被集中化处理,且严格按照一个接一个的顺序执行,所以不用担忧 race condition 的出现。
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})复制代码
为描述 action 怎么改变 state tree,你要编写 reducers。
Reducer 只是一些纯函数,它接收以前的 state 和 action,并返回新的 state。刚开始你可能只须要一个 reducer ,但随着应用变大,你会须要拆分 reducer 。
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 里作这些操做:
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
}
}复制代码
注意:
Object.assign({}, ...)
新建了一个副本。default
状况下返回旧的 state。 遇到未知的 action 时,必定要返回旧的 state。咱们看到,多个 action 下,reducer 开始变得复杂。是否能够更通俗易懂?这里的 todos
和 visibilityFilter
的更新看起来是相互独立的,咱们能够尝试拆分到单独的函数里。
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 有如下职责:
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 个步骤:
调用 store.dispatch(action)
Redux store 调用传入的 reducer 函数。
根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
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 开发思想在这方面解释的很是棒。
展现组件: 纯粹的UI组件,定义外观而不关心数据怎么来,怎么变。传入什么就渲染什么。
容器组件: 把展现组件链接到 Redux。监听 Redux store 变化并处理如何过滤出要显示的数据。
其它组件 有时很难分清到底该使用容器组件仍是展现组件,而且组件并不复杂,这时能够混合使用。
实现组件
省略其它部分,主要讲讲容器组件通常怎么写。
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 ,给展现组件使用。
1. createStore(reducer, [preloadedState], enhancer)
建立一个 Redux store 来以存放应用中全部的 state。详情可见 Redux API,这里主要强调两点:
preloadedState
:初始时的 state。在同构中会用到,好比从一个session恢复数据。当 store 建立后,Redux 会 dispatch action({ type: ActionTypes.INIT })
) 到 reducer 上,获得初始的 state 来填充 store。因此你的初始 state 是 preloadedState
在 reducers 处理 ActionTypes.INIT
action 后的结果。 github.com/reactjs/red…
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)
):
dispatch
。dispatch
本质上是同步的,但咱们能够经过 thunk 等延迟执行 dispatch
。chain[index](dispatch) --> (action) => action
,即咱们获得的 dispatch
是一个层层嵌套的 (action) => action
函数。next
是本来的 dispatch
,剩下的都是被层层嵌套的 (action) => action
函数,而且越右侧越嵌套在里面,因此当 dispatch(action)
调用时,将会如下面顺序执行:A -> B -> C -> B -> A
。C
以前 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。
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 中。