Redux原理与函数式编程有着怎样的不解之缘?
前言
工作之初接触的就是react框架,一直觉得react是一个很优秀的框架,里面的思想和设计原理可以给人很多启发,所以工作后一直保持着对react相关知识的学习,于是想做一个学习笔记,梳理下相关知识点,也算是一种刻意练习中成果反馈(《刻意练习》这本书不错,推荐大家看看)。
本文主要介绍redux相关的原理,话不多说,直接进入正文部分:
redux的相关概念
本节介绍下redux的相关概念,可以强行记忆,学习是一个循序渐进的过程,强行记忆也是其中的一个重要环节
函数式编程
整个react框架,以及redux这个状态管理库都是函数式编程思想的产物,从中可以看到很多函数式思想的影子
函数式编程的核心思想就是【纯】。函数是里面的一等公民,每一个函数都要尽可能的纯,就像现在和谐社会要求我们每个人都要是一个守法的好公民。
纯函数的定义
1.相同的输入,永远会得到相同的输出
2.没有产生任何可观察的副作用
一个函数必须满足以上两点,才能成为一个纯函数。第一个很好理解,至于第二点,没有产生副作用,可以用一个例子来展示:
/*不是纯函数,因为外部的 arr 被修改了*/
function b( arr ){
return arr.push(1);
}
let arr = [1, 2, 3];
b(arr);
console.log(arr); //[1, 2, 3, 1]
/*不是纯函数,因为依赖了外部的 x*/
let x = 1;
function c( count ){
return count + x;
}
以上两个例子,都产生了外界可观察的副作用,所以都不是纯函数
常见的产生副作用的行为有:
•更改文件系统•往数据库插入记录•发送一个 http 请求•可变数据•打印/log•获取用户输入•DOM 查询•访问系统状态
函数式编程里为了保证函数的【纯】,制定了一系列的工具来完成这个目标,让你更容易地写出一个个纯函数。其中最重要的两个就是柯里化(curry)和组合(compose)
柯里化(curry)
只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
组合(compose)
将函数的嵌套执行,组合为一个从右到左的函数执行流
var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
//不使用组合
var shout = function(x){
return exclaim(toUpperCase(x));
};
//使用compose
var compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
var shout = compose(exclaim, toUpperCase);
shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"
组合中的函数序列满足结合律,可任意结合,但顺序很重要,不能乱,因为是从右到左的函数执行流
compose(toUpperCase, compose(head, reverse));
// 等价于
compose(compose(toUpperCase, head), reverse);
讲到这就会引出函数式编程的一个重要概念:pointfree
(有人认为这是函数式编程的终极目的)
pointfree
函数只需关注内部逻辑,无须提及将要操作的数据是什么样的
借助柯里化和组合这两个工具,我们可以方便地实现这个目的
// 非 pointfree,因为提到了数据:word
var snakeCase = function (word) {
return word.toLowerCase().replace(/\s+/ig, '_');
};
// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
以上就是函数式编程的基本概念,初次学习,掌握这些就够了,剩下的就是内功,需要在实际中慢慢领悟。前置知识介绍完了,接下来可以一起看看redux中是怎么运用函数式思想来进行状态管理的
redux的核心原理
本小节默认你已经对redux有基本了解,并在工作中使用过,不熟悉的推荐看下阮一峰的入门教程。
有一点必须强调的是redux和react没有任意关系,虽然Dan都是它们的核心开发人员,开发思想很类似。redux在react中使用的实际状态管理工具是react-redux,针对react框架封装了一些核心api,后面会介绍
这是redux状态管理的全景架构图,以下围绕这张图中涉及的核心概念进行展开:
•createStore
创建 store 对象,包含state,listeners等属性, getState, dispatch, subscribe, replaceReducer等方法
•reducer
reducer 是一个纯函数,接收旧的 state 和 action,每次生成新的 state并返回
•action
action 是一个对象,必须包含 type 字段,其他字段任意,约定为payload
例:let action = {type:'add',payload:1}
•dispatch
dispatch( action ) 触发 action,执行subscribe订阅的监听回调函数,生成新的 state
•subscribe
实现订阅功能,每次触发 dispatch 的时候,会执行订阅函数;返回值为一个函数,用于取消订阅
•combineReducers
多 reducer 合并成一个 reducer
•replaceReducer
替换 reducer 函数
•middleware
扩展 dispatch 函数,在触发action之后,更新state之前执行!
接下来对上面列出的核心api进行代码层面的解析,看看redux是怎么实现这些功能的
store
redux中最核心的部分,通过createStore方法生成,store就是一个plain object
createStore(reducer, initState) {
let state = initState;
let listeners = [];
function subscribe(listener) {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
function dispatch(action) {
state = reducer(state, action);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
function getState() {
return state;
}
function replaceReducer(nextReducer) {
reducer = nextReducer
dispatch({ type: Symbol() });
}
dispatch({ type: Symbol() });
return {
subscribe,
dispatch,
getState,
replaceReducer
}
}
其中state的获取通过getState得到,更新通过dispatch触发reducer完成,reducer就是一个函数,执行它返回一个新的state
reducer
reducer(state, action) {
if (!state) {
state = initState;
}
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.name
}
case 'SET_DESCRIPTION':
return {
...state,
description: action.description
}
default:
return state;
}
}
reducer就是一个函数,接受参数返回新的state
状态较多时,会进行reducer的拆分和合并,即combineReducers逻辑。这部分不用过多讨论,因为我们知道reducer的合并,其实就是把所有reducer放在一个数组中,有action产生时,依次执行每个reducer,合并为一个大的state对象返回即可
上述知识点就是redux单向数据流的基本组成部分,接下来是redux另一个核心概念:中间件,借助中间件,我们可以在数据流中添加自定义的处理逻辑
middleware(中间件)
中间件就是扩展了dispatch函数的功能,在触发action之后,更新state之前,增加一些额外的处理逻辑
那么要实现这些功能,我们应该怎么做呢
•拦截dispatch方法,增加某个中间件的指定处理逻辑•多个中间件从右到左依次执行,
拦截
重写一下dispatch方法就可以了,这个技巧也叫monkeypatch(将任意的方法替换成你想要的)
const store = createStore(reducer);
const next = store.dispatch;
/*重写了store.dispatch*/
store.dispatch = (action) => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
多个中间件
实质就是上一个中间件需要当作参数传递给下面的中间件执行
//原始版本,写死中间件
const store = createStore(reducer);
const next = store.dispatch;
const middleware1 = (action) => {
console.log('middleware1');
next(action);
}
const middleware2 = (action) => {
middleware1();
console.log('middleware2');
next(action);
}
store.dispatch = middleware2;
不可能将所有中间件的逻辑都罗列在一起,所以需要把中间件当作参数,动态去执行
const store = createStore(reducer);
const next = store.dispatch;
const middleware1 = (next) => (action) => {
console.log('middleware1',store.getState());
next(action);
}
const middleware2 = (next) => (action) => {
console.log('middleware2',store.getState());
next(action);
}
store.dispatch = middleware2(middleware1(next));
以上就满足了中间件执行的两条基本原则,我们还需要在根据实际情况优化一下。中间件通常需要外部引入,上述例子中都是依赖本地的store,所以我们需要把store也当作参数传给中间件,再给它包一层,接受store参数
const store = createStore(reducer);
const next = store.dispatch;
const middleware1 = (store) => (next) => (action) => {
console.log('middleware1',store.getState());
next(action);
}
const middleware2 = (store) => (next) => (action) => {
console.log('middleware2',store.getState());
next(action);
}
const middlewareInstance1 = middleware1(store);
const middlewareInstance2 = middleware2(store);
store.dispatch = middlewareInstance2(middlewareInstance1(next));
到此,中间件的功能就全部完成了。看到上面,是不是很容易想到之前的函数式编程的思想,我们可以用curry+compose的方式来完善它:使用applyMiddleware将中间件串行执行,applyMiddleware返回一个函数,会作为createStore的第三个参数
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
const applyMiddleware = function (...middlewares) {
return (oldCreateStore) => (reducer, initState) =>{
const store = oldCreateStore(reducer, initState);
/*给每个 middleware 传下store,相当于 const middlewareInstance1 = middleware1(store);*/
/* const chain = [middleware1, middleware2, middleware3]*/
const simpleStore = { getState: store.getState };
const chain = middlewares.map(middleware => middleware(simpleStore));
const dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch
}
}
}
使用例子:
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import rootReducer from './reducers'
const loggerMiddleware = createLogger()
export default function configureStore(preloadedState) {
return createStore(
rootReducer,
preloadedState,
applyMiddleware(
thunkMiddleware,
loggerMiddleware
)
)
}
//createStore的源码,解析第三个参数
function createStore(reducer, initState, rewriteCreateStoreFunc) {
if (typeof initState === 'function' && typeof rewriteCreateStoreFunc === 'undefined') {
rewriteCreateStoreFunc = initState;
initState = undefined;
}
if (rewriteCreateStoreFunc) {
//在这里执行,产生一个新的store,其中的dispatch已经被中间件增强了
const newCreateStore = rewriteCreateStoreFunc(createStore);
return newCreateStore(reducer, initState);
}
//...
}
到此,中间件的逻辑全部梳理完成,接下来又引出一个重要的概念:异步数据流
异步数据流
redux的数据流为store->dispatch->action->reducer->state->view
之前介绍的redux的整个流程都是同步执行的,如果action中需要去调后端接口拿数据,异步更新state,redux应该怎么处理?
借助中间件,我们可以实现这点,让异步操作返回结果后,自动通知我们去更新state,只需要增强下dispatch就可以了。这类中间件以redux-thunk和redux-promise为代表,它们增强dispatch,使之可以接受函数或者 Promise的action
以redux-thunk为例,介绍下它是怎么增强dispatch的。源码很简单,就十几行代码:
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;
原理很简单,就是判断了下action类型,如果为函数,就去执行它,否则就正常执行dispatch(next)
下面以一个实际例子介绍下,dispatch是如何处理函数类型的action的
import fetch from 'cross-fetch'
// 来看一下我们写的第一个 thunk action 创建函数!
// 虽然内部操作不同,你可以像其它 action 创建函数 一样使用它:
// store.dispatch(fetchPosts('reactjs'))
export function fetchPosts(subreddit) {
// Thunk middleware 知道如何处理函数。
// 这里把 dispatch 方法通过参数的形式传给函数,
// 以此来让它自己也能 dispatch action。
return function (dispatch) {
// 首次 dispatch:更新应用的 state 来通知
// API 请求发起了。
dispatch({type:'request',payload})
// thunk middleware 调用的函数可以有返回值,
// 它会被当作 dispatch 方法的返回值传递。
// 这个案例中,我们返回一个等待处理的 promise。
// 这并不是 redux middleware 所必须的,但这对于我们而言很方便。
return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
// 不要使用 catch,因为会捕获
// 在 dispatch 和渲染中出现的任何错误,
// 导致 'Unexpected batch number' 错误。
// https://github.com/facebook/react/issues/6895
error => console.log('An error occurred.', error)
)
.then(json =>
// 可以多次 dispatch!
// 这里,使用 API 请求结果来更新应用的 state。
dispatch({type:'receive',payload})
)
}
}
后记
到此,redux的所有知识都介绍完了,核心原理都梳理了一遍,应该会有所收获。其中有一些代码可能不是很理解,可以强行记忆下来,毕竟这也是学习的一个环节,然后在反复巩固。
关注一下↑↑↑ 每天进步一点点