专栏首页多云转晴Redux 原理与实现

Redux 原理与实现

redux 工作原理

ReduxReact 之间并没有什么关系,脱离了 ReactRedux 也可以与其它的 js 库(甚至是原生 js)搭配使用,Redux 只是一个状态管理库,但它与 React 搭配时却很好用,使开发 React 应用更加简介。而使用 Redux 库时,需要先做“配置”,因为这些代码的书写是必不可少的。下面的图是 redux 的工作流:

redux 工作流

首先,react 组件从 store 中获取原始的数据,然后渲染。当 react 中的数据发生改变时,react 就需要使用 action,让 action 携带新的数据值派发给 store,store 把 action 发给 reducer 函数,reducer 函数处理新的数据然后返回给 store,最后 react 组件拿到更新后的数据渲染页面,达到页面更新的目的。

需要注意的是,在使用 Redux 时,最好不要监视最外层的容器,这样会把整个页面重新渲染,这是很浪费的,你应该绑定到像 App 这样的容器组件中。然后在容器组件中通过 props 向展示组件传递数据。

有关容器组件和展示组件的定义,可以参看这篇文档:

Redux 的 React 绑定库是基于 容器组件和展示组件相分离 的开发思想[1]

实现 Redux

首先捋一下思路,Redux 库中都有哪些函数?这些函数的参数都有哪些?参数类型是什么?执行函数后会返回什么?下面就一一介绍一下 redux 中的函数,当然在实际的 redux 源码中要复杂一些,不过在这篇文章中核心概念是一样的。

1. createStore

该函数会创建一个 store,专门用于存储数据。他返回四个函数:

  • dispatch:用于派发 action;
  • getState:用于获得 store 中的数据;
  • subscribe:订阅函数,当 state 数据改变后,就会触发监听函数;
  • replaceReducer:reducer 增强器;

createStore 可以接收三个参数:

  • reducer - 我们自己写的 reducer 函数;
  • preloadedState - 可选参数,表示默认的 state 值,没有这个参数时,enhancer 可以是 createStore 的第二个参数;
  • enhancer - 可选参数,增强器,比如 applyMiddleware 就是一个 enhancer;

该函数的模样:

function createStore(reducer,preloadedState,enhancer){
  let state;

  //  用于存放被 subscribe 订阅的函数(监听函数)
  let listeners = [];

  // getState 是一个很简单的函数
  const getState = () => state;

  return {
    dispatch,
    getState,
    subscribe,
    replaceReducer
  }
}

那就来一一实现各个“子函数”。

dispatch

该函数是派发 action 的,因此会接受一个 action 对象作为参数:

function dispatch(action) {
  // 通过 reducer 返回新的 state
  // 这个 reducer 就是 createStore 函数的第一个参数
  state = reducer(state, action);
  // 每一次状态更新后,都需要调用 listeners 数组中的每一个监听函数
  listeners.forEach(listener => listener());
  return action;    // 返回 action
}

subscribe

这个函数主要是往 listeners 数组中放入监听函数,参数就是一个监听函数。它还会返回一个 unsubscribe 函数,用于取消当前的订阅。

function subscribe(listener){
  listeners.push(listener);
  // 函数取消订阅函数
  return () => {
    listeners = listeners.filter(fn => fn !== listener);
  }
}

replaceReducer

顾名思义,这个函数可以替换 reducer,它传入一个 reducer 用以替代当前执行的 reducer 函数。

function replaceReducer(reducer){
  if (typeof nextReducer !== 'function') {
    throw new Error('Expected the nextReducer to be a function.')
  }

  currentReducer = nextReducer;
}

需要注意的是,在源码中完成负值后还会再派发一个类型为 @@redux/INIT 的 action。

2. combineReducers

该函数接收一个对象参数,对象的值是小的 reducer 函数。combineReducers 函数会返回总的 reducer 函数。combineReducers 函数样子:

function combineReducers(reducers){
  // 返回总的 reducer 函数,
  // 与小的 reducer 函数功能一样,返回更新后的 state
  return (state = {},action) => {
    // ...
  }
}

调用 combineReducers 函数:

import { combineReducers, createStore } from "redux";
import reducer1 from "./reducer1";
import reducer2 from "./reducer2";
// rootReducer 是一个新的 reducer 函数
const rootReducer = combineReducers({
  reducer1,
  reducer2
});
var store = createStore(rootReducer);

具体实现:

function combineReducers(reducers){
    return (state = {},action) => {
        // 返回的是一个对象,reducer 就是返回的对象
        return Object.keys(reducers).reduce(
            (accum,currentKey) => {
                accum[currentKey] = reducers[currentKey](state[currentKey],action);
                return accum;
            },{}        // accum 初始值是空对象
        );
    }
}

这里使用了 ES6 数组当中的 reduce 函数。首先拿出来对象的键进行遍历,accum 的初始值是一个空对象,currentKey 表示当前遍历的键。state[currentKey] 可能是没有的,默认值我们可能并没有指定,但并不影响。原因是这样的,state 对象中没有 currentKey 属性时,返回 undefined,这时如果小的 reducer 指定了默认值,或者 createStore 指定了默认值,就会使用默认值。就像下面的代码:

function fn(a = 123){
  console.log(a);
}

fn(undefined);    // 123,当参数是 undefined 时会使用默认值
fn(456);          // 456

combineReducers 函数返回一个 reducer 函数,当调用这个 reducer 函数时就会返回如下形式的对象:

{
  reducer1: { count: 1 },
  reducer2: { bool: true },
  reducer3: { ... },
  // ....
}

在写 React 时,可以通过 connect 中的 mapStateToProps 函数获取到 state,如果使用了 combineReducers,那么获取特定容器组建的 reducer 的 state 是这样获取的:

mapStateToProps(state){
  return {
    // reducer1 就是 combineReducers 对象参数中的一个键(每个键对应一个 reducer 函数)
    count: state.reducer1.count
  }
}

需要注意的是,如果你使用了 combineReducers,并且想把 state 初始值指定在 createStore 中,那么就要把默认值写成这种形式,不然小的 reducer 中的 state 参数就无法获取到默认值。

const defaultState = {
  // 对象的键应与 combineReducers 函数传入的对象参数中的键相同
  reducer1: {},
  reducer2: {},
  // ...
}

比如下面两个 reducer 没有指定 state 默认值,而是在 createStore 中指定的,当然这里直接给 rootReducer 指定的默认值,原理都是一样的,因为在 createStore 函数的 dispatch 函数中会调用 rootReducer 函数,把 createStore 中接收的默认 state 传入 rootReducer 函数中。

function reducer1(state,action){
    switch (action.type) {
        case "ADD": return { count: action.payload + 1 };
        case 'MINUS': return { count: action.payload - 1 };
        default: return state;
    }
}

function reducer2(state,action){
    switch(action.type){
        case 'SWITCH': return { bool: !state };
        default: return state;
    }
}

const rootReducer = combineReducers({reducer1,reducer2});

var state = rootReducer({
  // 设置默认值
  reducer1: { count: 1 },
  reducer2: true
},{type: 'ADD', payload: 3});

3. applyMiddleware

实现之前先说一下这个函数,在使用时是把它传递给 createStore 的:

import { createStore,applyMiddleware } from "redux";
import reduxThunk from "redux-thunk";
var store = createStore(reducer,applyMiddleware(reduxThunk));

前面已经说了,createStore 的 enhancer 是一个函数,因此 applyMiddleware 执行后应该返回一个函数。enhancer 函数被称为增强器。enhancer 函数接收 createStore 函数作为参数,并又返回一个函数,这个函数有两个参数:reducerpreloadedState,就是 createStore 的前两个参数。即:

function applyMiddleware(...middlewares){
  return function(createStore){
    return function(reducer,preloadedState){
      // ...
      // 最后把增强后的 store 返回
      return {
        ...store,
        // 因为改进了 dispatch,因此要把原来的 dispatch 覆盖掉
        dispatch
      }
    }
  }
}

可以发现,applyMiddleware 函数是一个三级柯里化函数:

applyMiddleware(...middlewares)(createStore)(reducer,preloadedState);

因此我们需要改造 createStore 函数,当有 enhancer 函数时就要调用 enhancer 函数:

function createStore(reducer,preloadedState,enhancer){
  // ...
  if(typeof preloadedState === "function" && typeof enhancer === 'undefined'){
    enhancer = preloadedState;
    preloadedState = undefined;
  }

  if(typeof enhancer !== "undefined"){
    if(typeof enhancer !== "function"){
      throw new Error("Expected the enhancer to be a function.");
    }
    // 如果有 enhance 函数,就执行 enhancer 函数,返回增强后的那四个 store 中的函数
    return enhancer(createStore)(reducer,preloadedState);
  }
  // ...
}

具体实现

function applyMiddleware(...middlewares){
  return function(createStore){
    return function(reducer,initialState){
      var store = createStore(reducer,initialState);
      var dispatch = store.dispatch;
      var chain = [];

      var middlewareAPI = {
        getState: store.getState,
        dispatch: (action) => dispatch(action)
      };

      chain = middlewares.map(middleware => middleware(middlewareAPI));

      dispatch = compose(...chain)(store.dispatch);
      return { ...store, dispatch };
    }
  }
}

首先调用 createStore,创建出 store,拿到 store 当中的 dispatch 方法。middlewareAPI 是传递给中间件函数的参数,每个中间件在书写时都应该有一个参数,里面有 getState 方法和 dispatch 包装函数。而 chain 数组里面就是中间件函数。这时 dispatch 函数就可能有多个,但实际的 dispatch 只有一个,因此需要使用 compose 函数将多个 dispatch 函数变成一个。要想变成一个也很简单,compose 函数返回一个 dispatch 函数,该函数内部是所有 dispatch 函数的执行。在 redux 源码中 compose 函数大致是这样的:

function compose(...funcs) {
  if (funcs.length === 0) {
    // 当没有 dispatch 增强函数时,就返回一个函数
    return arg => arg;
  }
  if (funcs.length === 1) {
    // 当只有一个 dispatch 函数时,就直接返回
    return funcs[0];
  }
  return funcs.reduce((accum, currentFn) => {
    return (...args) => {
      return accum(currentFn(...args));
    }
  });
}

下面解释一下这个函数的实现逻辑。首先需要了解到在 applyMiddleware 函数中,调用 compose 函数是这样调用的:

dispatch = compose(...chain)(store.dispatch);

compose 接收一个数组,会返回一个函数,这个函数将原始的 dispatch 作为参数传入,并返回一个全新的 dispatch 函数。在说实现逻辑之前,我们需要先了解一下中间件的概念,如果使用过 express 或者 koa 框架的话对中间件应该不会陌生。每个中间件都会有一个 next 函数,它指代下一个中间件,当调用 next 函数时就相当于调用了下一个中间件。在 redux 中也是如此,并且中间件是有顺序的,chain 数组最左侧的中间件会先调用,然后在内部调用 next 方法,表示执行下一个中间件。因为我们是改进 dispatch 函数,毫无疑问 next 其实就是每个中间件改进后的 dispatch 函数。当我们看 redux 中间件源码时就会发现每个中间件都是下面这个样子:

function middleware({ getState, dispatch }){
  return next => action => {
    // ....
    next(action);
  }
}

中间件拦截到 dispatch,做一些操作后,把 action 传给 next,自动执行下一个中间件函数。

现在再来看一下 compose 函数,如果数组 chain 中有值,那么它们都应该长这个样子(调用了 middleware 后会返回一个带有 next 参数的函数):

function fn(next){
  return action => {
    // ...
    // 一些操作后调用 next,执行下一个中间件
    next(action);
  }
}

在 compose 函数中又使用了 reduce 函数,这里再说一下 reduce 函数,上面使用该函数实现 combineReducers 函数时有个初始值,而这里没有,当 reduce 函数不指定初始值时,会将数组的第一项作为初始值,currentFn 的第一次调用就变成了数组的第二项。看到 reduce 函数是估计有些晕,这里解释一下,reduce 每次都返回一个函数(accum),在这个函数内部,一个函数的执行结果(返回 dispatch 函数)会作为另一个函数的参数传入(next 参数),假如 chain 数组有两个函数:[a,b],当调用 compose 函数时,b 的执行结果会是一个 dispatch 函数,把这个函数传给 a,此时 a 的 next 就是 b 函数的返回值,当在 a 中执行 next 方法时,就会调用 b 中返回的那个函数。

b 也是一个中间件,因此 b 中返回的 dispatch 函数内部也应调用 next 方法,让下一个中间件去执行别的操作,但是如果 b 后面没有中间件了呢?没有中间件时就执行原始的 dispatch 函数,即:将 next 可以指向原始的 dispatch 函数,于是在 applyMiddleware 函数中就有了这种写法:

dispatch = compose(...chain)(store.dispatch);

调用 compose 函数时,返回的是一个大的中间件函数,store.dispatch 函数是中间件的 next,因此调用中间件函数后会返回一个 dispatch。这个 dispatch 是数组最左侧的那个函数返回的 dispatch。当派发 action 时,就会执行 dispatch,dispatch 中的 next 函数也自然就会执行。

这也就是为什么 redux-logger 中间件为什么放在数组最右边,最左边的中间件会先执行,不那样做可能就无法打印出准确的 action 信息。

compose 函数代码虽然很少,但是里面使用了很多函数式编程的概念,比如柯里化函数、高阶函数等,让人看起来比较费解。

4. 写一个中间件

通过上面 applyMiddleware 函数内部可以看出,中间件的参数是接收一个对象,该对象中有两个函数:getStatedispatch。我们使用这两个函数就可以做一些事情。以 redux-logger 中间件为例,该函数可以在 dispatch 派发时打印日志。它的结构大致是这样的:

function logger(middlewareAPI){
  const { getState, dispatch } = middlewareAPI;
  return next => {
    // 返回一个全新的 dispatch 函数
    return (action) => {
      console.log(action.type);
      console.log('action',action);
      console.log('previous state', store.getState());

      // 调用原始的 dispatch 函数并记录日志
      const returnAction = next(action);
      console.log('next state', getState());
      console.log(action.type);

      return returnAction;
    }
  }
}

而在 redux-logger 库中,使用 createLogger 函数时可以传递参数这是怎么做到的呢?其实也很简单,在上面 redux applyMiddleware 函数是一个柯里化函数,createLogger 也是如此:

function createLogger(options = {}){
  // ... createLogger 的一些实现

  return ({dispatch, getState}) =>  next =>  action => {
    // ... 改进 dispatch 函数
  }
}

// 用于挂载中间件的函数
function logger({ getState, dispatch } = {}){
  if (typeof dispatch === 'function' || typeof getState === 'function') {
    return createLogger()({ dispatch, getState });
  }
}

当我们想要自定义配置时需要调用 createLogger 并传入配置参数。这时就会返回一个带有 dispatchgetState 的对象参数的函数,而这个函数与 logger 函数形式相同,于是直接使用这个函数作为中间件即可。也就是说,在不做配置时,我们可以直接使用 logger 函数,在配置参数时,我们需要使用 createLogger 的返回值作为中间件函数:

import { createLogger } from "redux-logger";

const logger = createLogger({
  // options...
});

const store = createStore(reducer,applyMiddleware(logger));

redux-thunk

redux-thunk 实现起来就更简单了,先回顾一下 redux-thunk 的使用方式,要想用 dispatch 派发异步请求来的数据需要在定义一个函数,该函数返回一个函数,参数是 dispatch:

// actions.js

const ajaxDataAction = (data) => ({ type: "ajax_data", payload: data });

export function ajaxAction(){
  // 这个 action 会返回一个函数
  return (dispatch) => {
    fetch("/info").then(json => json())
    .then(data => {
      // 得到数据后派发 action
      dispatch(ajaxDataAction(data));
    });
  }
}

因此,redux-thunk 函数内部需要先拦截 dispatch 函数,判断 action 参数的数据类型是不是函数,如果是函数就执行函数:

function thunk({ getState, dispatch }){

  return  next =>  action => {
    if(typeof action === "function"){
      return action(dispatch, getState);
    }
    // 传递给下一个中间件
    return next(action);
  }
}

redux-thunk 源码大概也就那么多,但是 GitHub 上却有将近 15K 的 star。除了使用 redux-thunk 作为异步处理中间件之外,还可以使用 redux-saga,只是后者的学习成本会高一些。

通过分析可以了解,redux 库代码量虽然很少,只有六七百行,但是 redux 可以说是函数式编程的典范,对于一些代码的逻辑并不太好理解。

参考资料

[1]

Redux 的 React 绑定库是基于 容器组件和展示组件相分离 的开发思想: https://www.redux.org.cn/docs/basics/UsageWithReact.html

本文分享自微信公众号 - Neptune丶(Neptune_mh_0110),作者:多云转晴丶

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-02-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 实现一个 EventEmitter 类

    在前端开发中,经常会使用到发布订阅模式,发布订阅模式也被称为观察者模式。最常见的发布订阅模式莫过于给 DOM 绑定事件,当点击一个按钮或者鼠标移动到某个元素上就...

    多云转晴
  • 算法之递归

    也就是递归一般会有一个判断,这是递归算法的出口(1 处);还有一个返回这个函数的执行结果(2 处);这两点是实现递归的关键。如果没有出口,递归就会变成死循环,而...

    多云转晴
  • 函数柯里化

    在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的...

    多云转晴
  • Azure上基于HTTP trigger的Lambda Function

    Azure上通过HTTP方式触发的Lambda Function,函数体直接在浏览器里编写:

    Jerry Wang
  • python 函数

    def functionname( parameters ): "函数_文档字符串" function_suite return [expression]

    py3study
  • SQL优化大神玩转MySQL函数系列(一)

    比如: 20001元 需要 变成 20000元 ,20015 变成 20010 这样

    [3306 Pai ] 社区
  • c++ 深入理解虚函数

    拾点阳光
  • Office 远程溢出漏洞测试与分析

    在 2017 年 11 月,微软发布的 11 月更新布丁中,微软将隐藏许久的 office 远程代码执行漏洞 (CVE-2017-11882)给修复了,由于该漏...

    信安之路
  • R语言自定义函数

    R语言在使用过程中有些时候代码过长或者很多重复性的功能需要对代码的结构进行管理以及梳理。R语言具有自定义函数的功能,如果有些代码实现的功能具有重复性,那么就可以...

    一粒沙
  • 护网杯pwn——huwang超详细wp

    比赛结束快一个星期了,复现了一下这道题,借鉴了一下网上的wp发现大佬们写的都很简略,所以这里写一个详细的wp供小白们学习。

    安恒网络空间安全讲武堂

扫码关注云+社区

领取腾讯云代金券