Redux是一个通用的前端状态管理库,它不只普遍应用于 React App,在 Wepy、Flutter 等框架中也随处可见它的身影,可谓是一招鲜吃遍天,它同时深受喜欢函数式编程(Functional Programming)人们的追捧,今天我就来和你们聊一聊Redux的基本思想。html
Flux是Facebook用于构建客户端Web应用程序的基本架构,咱们能够将Flux看作一种应用程序中的数据流的设计模式,而Redux正是基于Flux的核心思想实现的一套解决方案,它也获得了原做者的确定。前端
首先,在Flux中会有如下几个角色的出现:react
从通信的角度还可将其视为Action请求层 -> Dispatcher传输层 -> Store处理层 -> View视图层
。git
Flux应用中的数据以单一方向流动:github
单一方向数据流还具备如下特色:npm
从上面的章节中咱们大概知道了Flux中各个角色的职责,那如今咱们再结合着简单的代码示例讲解一下他们是如何构成一整个工做流的: 编程
上图中有一个Action Creator
的概念,其实他们就是用于辅助建立Action对象,并传递给Dispatcher:redux
function addTodo(desc) {
const action = {
type: 'ADD_TODO',
payload: {
id: Date.now(),
done: false,
desciption: desc
}
}
dispatcher(action)
}
复制代码
在这里我仍是但愿经过代码的形式进行简单的描述,会更直观一点,首先初始化一个项目:设计模式
mkdir flux-demo && cd flux-demo
npm init -y && npm i react flux
touch index.js
复制代码
而后,咱们建立一个Dispatcher对象,它的本质是Flux系统中的事件系统,用于触发事件与响应回调,并且在Flux中仅会有一个全局的Dispatcher对象:数组
import { Dispatcher } from 'flux';
const TodoDispatcher = new Dispatcher();
复制代码
接着,注册一个Store,响应Action方法:
import { ReduceStore } from 'flux/utils';
class TodoStore extends ReduceStore {
constructor() {
super(TodoDispatcher);
}
getInitialState() {
return [];
}
reduce(state, action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat(action.payload);
default:
return state;
}
}
}
const TodoStore = new TodoStore();
复制代码
在Store的构造器中将TodoDispatcher
传递给了父级构造器调用,实际上是在Dispatcher上调用register
方法注册了Store,将其做为dispatch
的回调方法,用于响应每个Action对象。
到了这里几乎已经完成了一个Flux示例,就剩下链接视图了。当 Store 改变时,会触发一个 Change 事件,通知视图层进行更新操做,如下为完整代码:
const { Dispatcher } = require('flux');
const { ReduceStore } = require('flux/utils');
// Dispatcher
const TodoDispatcher = new Dispatcher();
// Action Types
const ADD_TODO = 'ADD_TODO';
// Action Creator
function addTodo(desc) {
const action = {
type: 'ADD_TODO',
payload: {
id: Date.now(),
done: false,
desciption: desc
}
};
TodoDispatcher.dispatch(action);
}
// Store
class TodoStore extends ReduceStore {
constructor() {
super(TodoDispatcher);
}
getInitialState() {
return [];
}
reduce(state, action) {
switch (action.type) {
case ADD_TODO:
return state.concat(action.payload);
default:
return state;
}
}
}
const todoStore = new TodoStore();
console.log(todoStore.getState()); // []
addTodo('早晨起来,拥抱太阳');
console.log(todoStore.getState()); // [ { id: 1553392929453, done: false, desciption: '早晨起来,拥抱太阳' } ]
复制代码
Flux 这样的架构设计其实在很早以前就出现了,可是为何近几年才盛行呢?我认为很大一部分因素取决于 React 框架的出现,正是由于 React 的 Virtual DOM 让数据驱动成为了主流,再加上高效率的React diff
,使得这样的架构存在更加合理:
在靠近视图的顶层结构中,有一个特殊的视图层,在这里咱们称为视图控制器( View Controller ),它用于从Store中获取数据并将数据传递给视图层及其后代,并负责监听Store中的数据改变事件。
当接受到事件时,首先视图控制器会从Store获取最新的数据,并调用自身的setState
或forceUpdate
函数,这些函数会触发View的render与全部后代的re-render方法。
一般咱们会将整个Store对象传递到View链的顶层,再由View的父节点依次传递给后代所须要的Store数据,这样能保证后代的组件更加的函数化,减小了Controller-View的个数也意味着使更好的性能。
Redux是JavaScript应用可预测的状态管理容器,它具备如下特性:
它还有三大原则:
Redux受到了Flux架构的启发,但在实现上有一些不一样:
(state, action) => state
,而纯函数也是实现了这一思想。在Redux中,Action 是一个纯粹的 JavaScript 对象,用于描述Store 的数据变动信息,它们也是 Store 的信息来源,简单来讲,全部数据变化都来源于 Actions 。
在 Action 对象中,必须有一个字段type
用于描述操做类型,他们的值为字符串类型,一般我会将全部 Action 的 type 类型存放于同一个文件中,便于维护(小项目能够没必要这样作):
// store/mutation-types.js
export const ADD_TODO = 'ADD_TODO'
export const REMOVE_TODO = 'REMOVE_TODO'
// store/actions.js
import * as types from './mutation-types.js'
export function addItem(item) {
return {
type: types.ADD_TODO,
// .. pass item
}
}
复制代码
Action对象除了type之外,理论上其余的数据结构均可由本身自定义,在这里推荐flux-standard-action这个Flux Action标准,简单来讲它规范了基本的Action对象结构信息:
{
type: 'ADD_TODO',
payload: {
text: 'Do something.'
}
}
复制代码
还有用于表示错误的Action:
{
type: 'ADD_TODO',
payload: new Error(),
error: true
}
复制代码
在构造 Action 时,咱们须要使 Action 对象尽量携带更少的数据信息,好比能够经过传递 id 的方式取代整个对象。
咱们将Action Creator与Action进行区分,避免混为一谈。在Redux中,Action Creator是用于建立动做的函数,它会返回一个Action对象:
function addTodo(text) {
return {
type: 'ADD_TODO',
payload: {
text,
}
}
}
复制代码
与Flux
所不一样的是,在Flux 中Action Creator 同时会负责触发 dispatch 操做,而Redux只负责建立Action,实际的派发操做由store.dispatch
方法执行:store.dispatch(addTodo('something'))
,这使得Action Creator的行为更简单也便于测试。
一般咱们不会直接使用store.dispatch
方法派发 Action,而是使用connect方法获取dispatch
派发器,并使用bindActionCreators
将Action Creators自动绑定到dispatch函数中:
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{ addTodo },
dispatch
);
}
const Todo = ({ addTodo }) => {}
export default connect(null, mapDispatchToProps)(Todo);
复制代码
经过bindActionCreators
以后,咱们能够将这些Action Creators传递给子组件,子组件不须要去获取dispatch
方法,而是直接调用该方法便可触发Action。
对于Action来说,它们只是描述了发生了什么事情,而应用程序状态的变化,全由Reducers进行操做更改。
在实现Reducer函数以前,首先须要定义应用程序中State的数据结构,它被储存为一个单独的对象中,所以在设计它的时候,尽可能从全局思惟去考虑,并将其从逻辑上划分为不一样的模块,采用最小化、避免嵌套,并将数据与UI状态分别存储。
Reducer是一个纯函数,它会结合先前的state状态与Action对象来生成的新的应用程序状态树:
(previousState, action) => newState
复制代码
内部通常经过switch...case
语句来处理不一样的Action。
保持Reducer的纯函数特性很是重要,Reducer须要作到如下几点:
Date.now()
或Math.random()
这样的非纯函数。Redux应用程序最多见的State形状是一个普通的Javascript对象,其中包含每一个顶级键的特定于域的数据的“切片”,每一个“切片”都具备一个相同结构的reducer函数处理该域的数据更新,多个reducer也可同时响应同一个action,在须要的状况独立更新他们的state。
正是由于这种模式很常见,Redux就提供了一个工具方法去实现这样的行为:combineReducers
。它只是用于简化编写Redux reducers最多见的示例,并规避一些常见的问题。它还有一个特性,当一个Action产生时,它会执行每个切片的reducer,为切片提供更新状态的机会。而传统的单一Reducer没法作到这一点,所以在根Reducer下只可能执行一次该函数。
Reducer函数会做为createStore
的第一个参数,而且在第一次调用reducer时,state
参数为undefined
,所以咱们也须要有初始化State的方法。举一个示例:
const initialState = { count: 0 }
functino reducer(state = initialState, action) {
switch (action.type) {
case: 'INCREMENT':
return { count: state.count + 1 }
case: 'DECREMENT':
return { count: state.count - 1 }
default:
return state;
}
}
复制代码
对于常规应用来说,State中会储存各类各样的状态,从而会形成单一Reducer函数很快变得难以维护:
...
case: 'LOADING':
...
case: 'UI_DISPLAY':
...
...
复制代码
所以咱们的核心目标是将函数拆分得尽量短并知足单一职责原则,这样不只易于维护,还方便进行扩展,接下来咱们来看一个简单的TODO示例:
const initialState = {
visibilityFilter: 'SHOW_ALL',
todos: []
}
function appReducer(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.concat({
id: action.id,
text: action.text,
completed: false
})
})
}
default:
return state
}
}
复制代码
这个函数内包含了两个独立的逻辑:过滤字段的设置与TODO对象操做逻辑,若是继续扩展下去会使得Reducer函数愈来愈庞大,所以咱们须要将这两个逻辑拆分开进行单独维护:
function appReducer(state = initialState, action) {
return {
todos: todosReducer(state.todos, action),
visibilityFilter: visibilityReducer(state.visibilityFilter, action)
}
}
function todosReducer(todosState = [], action) {
switch (action.type) {
case 'ADD_TODO': {
return Object.assign({}, state, {
todos: state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
})
}
default:
return todosState
}
}
function visibilityReducer(visibilityState = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return setVisibilityFilter(visibilityState, action)
default:
return visibilityState
}
}
复制代码
咱们将整个Reducer对象拆为两部分,而且他们独自维护本身部分的状态,这样的设计模式使得整个Reducer分散为独立的切片。Redux内置了一个combineReducers
工具函数,鼓励咱们这样去切分顶层Reducer,它会将全部切片组织成为一个新的Reducer函数:
const rootReducer = combineReducers({
todos: todosReducer,
visibilityFilter: visibilityReducer
})
复制代码
在 combineReducers 返回的state对象中,每一个键名都表明着传入时子Reducer的键名,他们做为子Reducer中 State 的命名空间。
在Redux应用中只有一个单一的store,经过createStore
进行建立。Store对象用于将Actions与Reducers结合在一块儿,它具备有如下职责:
getState()
方法访问State。dispatch(action)
方法将Action派发到Reducer函数,以此来更新State。subscribe(listener)
监听状态更改。对于subscribe
来说,每次调用dispatch
方法后都会被触发,此时状态树的某一部分可能发生了改变,咱们能够在订阅方法的回调函数里使用getState
或dispatch
方法,但须要谨慎使用。subscribe
在调用后还会返回一个函数unsubscribe
函数用于取消订阅。
对于中间件的概念相信你们经过其余应用有必定的概念了解,对于Redux来说,当咱们在谈论中间件时,每每指的是从一个Action发起直到它到达Reducer以前的这一段时间里所作的事情,Redux经过Middleware机制提供给三方程序扩展的能力。
为了更好的说明中间件,我先用Redux初始化一个最简实例:
const { createStore } = require('redux');
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
function reducer(state = 0, action) {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
throw new Error('decrement error');
default:
return state;
}
}
void function main() {
const store = createStore(reducer);
store.dispatch({ type: INCREMENT });
console.log(store.getState()); // 打印 1
}()
复制代码
为了深入的理解Redux中间件,咱们一步步去实现具备中间件功能的函数。为了追踪程序的状态变化,可能咱们须要实现一个日志打印中间件机制,用于打印Action与执行后的State变化。咱们首先经过store
对象建立一个logger
对象,在dispatch
的先后进行日志打印:
void (function main() {
const store = createStore(reducer);
const logger = loggerMiddleware(store);
logger({ type: INCREMENT });
function loggerMiddleware(store) {
return action => {
console.log('dispatching', action);
let result = store.dispatch(action);
console.log('next state', store.getState());
return result;
};
}
})();
// 程序运行结果
dispatching { type: 'INCREMENT' }
next state 1
复制代码
为了监控应用程序的状态,咱们还须要实现一个中间件,当在应用程序dispatch
过程当中发生错误时,中间件能及时捕获错误并上报(一般可上报至Sentry,但在这里就简单打印错误了):
void (function main() {
const store = createStore(reducer);
const crasher = crashMiddleware(store);
crasher({ type: DECREMENT });
function crashMiddleware(store) {
return action => {
try {
return dispatch(action);
} catch (err) {
console.error('Caught an exception!', err);
}
};
}
})();
复制代码
执行程序后,可在命令行内看到函数正确的捕获DECREMENT中的错误
:
Caught an exception! ReferenceError: dispatch is not defined
复制代码
在应用程序中通常都会有多个中间件,而将不一样的中间件串联在一块儿是十分关键的一步操做,若你读过Koa2
的源码,你大概了解一种被称之为compose
的函数,它将负责处理中间件的级联工做。
在这里,为了理解其原理,咱们仍是一步一步进行分析。前面两个中间件的核心目标在于将Dispatch方法进行了一层包装,这样来讲,咱们只须要将dispatch一层层进行包裹,并传入最深层的中间件进行调用,便可知足咱们程序的要求:
dispatch = store.dispatch
↓↓↓
// 没有中间件的状况
dispatch(action)
↓↓↓
// 当添加上LoggerMiddleware
LoggerDispatch = action => {
// LoggerMiddleware TODO
dispatch(action)
// LoggerMiddleware TODO
}
dispatch(action)
↓↓↓
// 当添加上CrashMiddleware
CrashDispatch = action => {
// CrashMiddleware TODO
LoggerDispatch(action)
// CrashMiddleware TODO
}
复制代码
若是你熟悉使用高阶函数,相信上述思路并不难以理解,那让咱们经过修改源代码,尝试一下经过这样的方式,是否能使两个中间件正常工做:
void function main() {
const store = createStore(reducer);
let dispatch = store.dispatch
dispatch = loggerMiddleware(store)(dispatch)
dispatch = crashMiddleware(store)(dispatch)
dispatch({ type: INCREMENT });
dispatch({ type: DECREMENT });
function loggerMiddleware(store) {
return dispatch => {
return action => {
console.log('dispatching', action);
let result = dispatch(action);
console.log('next state', store.getState());
return result;
};
};
}
function crashMiddleware(store) {
return dispatch => {
return action => {
try {
return dispatch(action);
} catch (err) {
console.error('Caught an exception!', err);
}
};
};
}
}();
复制代码
此时打印结果为(符合预期):
dispatching { type: 'INCREMENT' }
next state 1
dispatching { type: 'DECREMENT' }
Caught an exception! Error: decrement error
复制代码
固然,咱们但愿以更优雅的方式生成与调用dispatch,我会指望在建立时,经过传递一个中间件数组,以此来生成Store
对象:
// 简单实现
function createStoreWithMiddleware(reducer, middlewares) {
const store = createStore(reducer);
let dispatch = store.dispatch;
middlewares.forEach(middleware => {
dispatch = middleware(store)(dispatch);
});
return Object.assign({}, store, { dispatch });
}
void function main() {
const middlewares = [loggerMiddleware, crashMiddleware];
const store = createStoreWithMiddleware(reducer, middlewares);
store.dispatch({ type: INCREMENT });
store.dispatch({ type: DECREMENT });
// ...
}()
复制代码
经过Step 1 ~ 3 的探索,咱们大概是照瓢画葫实现了Redux的中间件机制,如今让咱们来看看Redux自己提供的中间件接口。
在createStore
方法中,支持一个enhancer
参数,意味着三方扩展,目前支持的扩展仅为经过applyMiddleware
方法建立的中间件。
applyMiddleware支持传入多个符合Redux middleware API
的Middleware,每一个Middleware的形式为:({ dispatch, getState }) => next => action
。让咱们稍做修改,经过applyMiddleware与createStore接口实现(只须要修改建立store的步骤):
// ...
const middlewares = [loggerMiddleware, crashMiddleware];
const store = createStore(reducer, applyMiddleware(...middlewares));
// ...
复制代码
经过applyMiddleware方法,将多个 middleware 组合到一块儿使用,造成 middleware 链。其中,每一个 middleware 都不须要关心链中它先后的 middleware 的任何信息。 Middleware最多见的场景是实现异步actions方法,如redux-thunk
与redux-saga
。
对于一个标准的Redux应用程序来讲,咱们只能简单的经过派发Action执行同步更新,为了达到异步派发的能力,官方的标准作法是使用 redux-thunk 中间件。
为了明白什么是 redux-thunk ,先回想一下上文介绍的Middleware API:({ dispatch, getState }) => next => action
,借由灵活的中间件机制,它提供给 redux-thunk 延迟派发Action的能力,容许了人们在编写Action Creator时,能够不用立刻返回一个Action对象,而是返回一个函数进行异步调度,因而称之为Async Action Creator
:
// synchronous, Action Creator
function increment() {
return {
type: 'INCREMENT'
}
}
// asynchronous, Async Action Creator
function incrementAsync() {
return dispatch => {
setTimeout(() => dispatch({ type: 'INCREMENT' }), 1000)
}
}
复制代码
而 redux-thunk 源码也不过10行左右:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
复制代码
经过dispatch(ActionCreator())
进行调用时,函数会判断参数的类型:
至于为何称其为"thunk",它是来源于"think",i变为了u,意味着将绝对权从我转交给你,这是我认为较好的解释。若是要溯源的话,其实这是一种“求值策略”的模式,即函数参数到底应该什么时候求值,好比一个函数:
function test(y) { return y + 1 }
const x = 1;
test(x + 1);
复制代码
这时人们有两种争论点:
x + 1 = 2
,再将值传入函数;x + 1
传入函数,须要用到时再计算表达式的值。而一般编译器的“传名调用”的实现,每每是将参数放到一个临时函数中,再将临时函数传入函数体内,而这个函数就被称之为 Thunk ,若采起传名调用,上面的函数调用会转化为 Thunk 传参形式:
const thunk = () => (x + 1)
function test(thunk) {
return thunk() + 1;
}
复制代码