react,redux和dva的源码解析系列

427 阅读10分钟

前言

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

下一篇,会介绍react-redux的知识,主要是redux针对react框架封装的几个核心api,还有基于react-redux,并整合了redux-saga,react-router等功能的优秀状态管理框架Dva,这是阿里大佬开源的基于redux的最佳实践,在实际项目中使用很广泛,从中也可以学习到很多优秀的设计思想