vlambda博客
学习文章列表

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 = 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的所有知识都介绍完了,核心原理都梳理了一遍,应该会有所收获。其中有一些代码可能不是很理解,可以强行记忆下来,毕竟这也是学习的一个环节,然后在反复巩固。

关注一下↑↑↑ 每天进步一点点