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);// 3addTen(2);// 12
组合(compose)
将函数的嵌套执行,组合为一个从右到左的函数执行流
var toUpperCase = function(x) { return x.toUpperCase(); };var exclaim = function(x) { return x + '!'; };//不使用组合var shout = function(x){return exclaim(toUpperCase(x));};//使用composevar 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,因为提到了数据:wordvar snakeCase = function (word) {return word.toLowerCase().replace(/\s+/ig, '_');};// pointfreevar 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 = nextReducerdispatch({ 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/6895error => console.log('An error occurred.', error)).then(json =>// 可以多次 dispatch!// 这里,使用 API 请求结果来更新应用的 state。dispatch({type:'receive',payload}))}}
后记
到此,redux的所有知识都介绍完了,核心原理都梳理了一遍,应该会有所收获。其中有一些代码可能不是很理解,可以强行记忆下来,毕竟这也是学习的一个环节,然后在反复巩固。
关注一下↑↑↑ 每天进步一点点
