Redux应用架构基础——Redux 是什么?如何实现?

485 阅读6分钟

这是我参与更文挑战的第17天,活动详情查看: 更文挑战

前言

Redux 自2015年横空出世后,很快席卷了 React 社区,备受推崇。那么它究竟是什么,怎么使用,又能解决 React 应用开发过程中的哪些痛点?

本文,我们将一起学习 Redux 并简单实现一个 Redux, 同时也会捎带讲一下如何使用中间件来增强redux的能力。

Redux 是什么

React 应用离不开组件,而组件的状态非常重要。React 官方提倡单向数据流,这在一定程度上将组件间的数据通信简化了。但是往往一个团队内的不同成员的开发习惯不同,应用的状态难免会随着迭代的进行变得复杂且混乱。

此时,Redux 作为一个应用数据流架构,可以和 React 完美配合,一定程度上解决上面的问题。

Redux is a predictable state container from JavaScript app.

对于 Javascript 应用而言, Redux 是一个可预测状态的“容器”。

相比于 Jquery 等,Redux 是库,又不仅仅是库。 Redux 约定了开发者在使用时的条条框框,更像是一个一种设计模式。

Redux 借鉴了函数式编程的思想,采用了单向数据流理念。它只专注于全局状态的管理,并且为数据状态的管理封装提供了很多方法。通过一系列的“魔法”和“约定模式”,使的数据状态变得可预测可追溯

Redux 和 React 的关系

两者并没关系,Redux 只关心数据状态的管理,它可以和任何视图库配合使用。react-redux 作为桥梁,将两者结合起来,构建出一个“有序”的应用架构。

Redux 设计哲学

Single source of truth

这里指的是数据来源的单一。

在使用 React 时,应用就是一个个状态机。无论是简单的应用,还是一个复杂的应用,我们都是使用一个 Javascript 对象来描述整个状态机的状态,并存储在 store 中。

这样设计的好处在于,我们在处理状态时只需要把注意力全部集中到这个对象上即可,而不用关心各个组件内的状态数据。

State is read-only

状态是“只读”的。这里的“只读”不是指对象不能修改,而是当页面需要新的数据状态时重新生成一个全新的状态数据树,使得 store.getState() 返回的是一个全新的对象。

那么,我们该如何生成一个新的状态数据树呢?

Redux 规定了,当页面需要展现新的数据树时,我们需要 dispatch(派发)一个 action(动作)。这个 action 也是一个普通对象,它描述了一个动作单元变化的所有信息。

Changes are made with pure functions called reducer

action 经过 reducer 函数处理后,会返回一个新的对象(新的状态数据树)。

(preState, action) => newState

如上所示,reducer 接收两个参数:

  • 当前页面数据状态
  • 派发的 action

reducer 和 Array.prototype.reduce 有什么关系

Javascript 数组的 reduce 方法是一种运算合成,通过遍历,将数组的所有成员“累积”为一个值。

与此类似,reducer 在具备初始状态的情况下,每一次运算其实都是根据之前的状态和现有的 action 来更新 state 的,这个 state 可以理解为上面所说的累积的结果。这也是 Redux 中函数式编程的体现。

reducer 函数必须是纯函数

纯函数代表这样一类函数:

  • 对于指定输出,返回指定结果
  • 不存在副作用

reducer 使用纯函数,为开发和调试带来了便利性。

Store

store 是 Redux 的核心概念。之前我们说 Redux 是一个可预测状态的“容器”,这个容器指的就是 store。

store 是一个普通对象,包含了 dispatch 以及获取页面状态数据树的方法等。

store = {
    dipatch,
    getState,
    subscribe,
    replaceReducer
}

Action

action 描述了状态变更的信息。通常我们规定 action 有个 type 属性,用来确定 action 的唯一性。另外我们还定义 payload 属性,用来携带一些数据信息。

action = {
    type: 'UPDATE_MESSAGE',
    payload: {
        msg: "hello world"
    }
}

Reducer

action 描述状态变更的信息,真正落实变化的是 reducer 函数。

function reducer(state = initState, action){
    case 'UPDATE_MESSAGE':
        return {
            ...state,
            msg: action.payload.msg
        };
        break;
    default:
       return state;
    ....
}

实现一个简单的 Redux

Redux 基础实现就是一个发布订阅系统,只是在这个基础上还应用了函数式编程等其他编程范式。

Store

我们简单实现一个 Store:

// store.js
class Store {
  constructor(reducer) {
    this.reducer = reducer; // 单个 reducer 函数
    this.state = reducer(undefined, { type: undefined }); // 运行一次 reducer,拿到初始的数据状态
    this.listeners = []; // 订阅者列表
  }

  dispatch(action) {
    this.state = this.reducer(this.state, action);
    this.listeners.forEach((listener) => {
      listener(this.state);
    });
  }

  subscribe(fn) {
    if (typeof fn === "function") {
      this.listeners.push(fn);
    }
  }

  getState() {
    return this.state;
  }
}

module.exports = Store;

有 Store 类,这个类已经拥有了 dispatch 派发动作、添加订阅以及获取状态数据树的能力。然后我们要创建实例。

createStore

// createStore.js
const Store = require("./store.js");

function createStore(reducer) {
  let store = new Store(reducer);
  return store;
}

module.exports = createStore;

测试一下,这个可不可以正常工作。

// test.js
const createStore = require("./createStore.js");

let initState = {
  msg: "hello",
};

function reducer(state = initState, action) {
  switch (action.type) {
    case "update_msg":
      return {
        ...state,
        msg: action.payload,
      };
  }
}

const store = createStore(reducer);
store.subscribe((data) => {
  console.log("触发了");
  console.log(data);
  console.log(store.getState())
});

store.dispatch({
  type: "update_msg",
  payload: "hello world",
});

输出:

触发了
{ msg: 'hello world' }
{ msg: 'hello world' }

combineReducers

上面的 store 只支持单个reducer。当应用复杂的时候,我们要拆分reducer,Redux提供了一个 combineReducers 方法。我们也来实现一个:

function combineReducers(reducers) {
  Object.defineProperty(reducers, "@@isReducers", {
    configutable: true,
    enumerable: false, //  避免被枚举到
    value: true,
  });
  return reducers;
}

module.exports = combineReducers;

修改一下 Store

class Store {
  constructor(reducer) {
    this.reducer = reducer;
    this.combined = "@@isReducers" in reducer // 存在,表示使用了combineReducers
    // 新增
    this.state = this.combined
        ? Object.keys(reducer).reduce((rootState, key) => {
            rootState[key] = reducer[key](undefined, { type: undefined });
            return rootState;
          }, {})
        : reducer(undefined, { type: undefined });
    this.listeners = [];
  }

  dispatch(action) {
    // 新增
    this.state = this.combined
        ? Object.keys(this.reducer).reduce((rootState, key) => {
            rootState[key] = this.reducer[key](rootState[key], action);
            return rootState;
          }, this.state)
        : this.reducer(this.state, action);

    this.listeners.forEach((listener) => {
      listener(this.state);
    });
  }
  ...
}

module.exports = Store;

这样 combineReducers 也就实现了。

createStore 既支持 createStore(reducer), 也支持 createStore(combineReducers(reducers))

实现一个 redux-thunk,支持异步action

redux-thunk 是通过 Redux 的中间件特性扩展出来的。要想支持异步,action 就得是函数。

applyMiddleware

Redux 的中间件的执行时机:dispatch 之后, reducer 执行之前。

我们来模拟下添加中间件的方法 applyMiddleware

const compose = require("lodash/fp/compose");

function applyMiddleware(...middlewares) {
  return (store) => {
    const chain = middlewares.map((m) =>
      m({
        dispatch: (action) => store.dispatch(action),
        getState: store.getState,
      })
    );
    const dispatch = compose(chain)(store.dispatch);
    store.dispatch = dispatch;
    return store;
  };
}

module.exports = applyMiddleware;

redux-thunk

接入了 redux-thunk 后,如果 action 是个函数,要把 store.dispatch 传进去:

// redux-thunk.js
function thunk({ dispatch, getState }) {
  return function (next) {
    return function (action) {
      if (typeof action === "function") {
        action(dispatch, getState);
      } else {
        next(action);
      }
    };
  };
}

module.exports = thunk;

注意,Store 中的 dispatch 需要改成箭头函数。

测试一下:

const createStore = require("./createStore.js");
const combineReducers = require("./combineReducers.js");
const applyMiddleware = require("./applyMiddleware.js");
const thunk = require("./redux-thunk.js");

function reducer(
  state = {
    msg: "hello",
  },
  action
) {
  switch (action.type) {
    case "update_msg":
      return {
        ...state,
        msg: action.payload,
      };
    default:
      return state;
  }
}

const store = createStore(reducer);

applyMiddleware(thunk)(store);

let date = +new Date();

store.subscribe((data) => {
  console.log("触发了", +new Date() - date);
  console.log(data);
  console.log(store.getState());
});

store.dispatch((dispatch) => {
  setTimeout(() => {
    dispatch({
      type: "update_msg",
      payload: "hello xxx",
    });
  }, 1000);
});

store.dispatch({
  type: "update_msg",
  payload: "hello world",
});

输出:

触发了 0
{ msg: 'hello world' }
{ msg: 'hello world' }
触发了 1004
{ msg: 'hello xxx'}
{ msg: 'hello xxx'}

实现一个日志中间件 redux-logger

和 redux-thunk类似,也是很简单

// redux-logger
function logger({ dispatch, getState }) {
  return function (next) {
    return function (action) {
      console.log(action);
      next(action);
    };
  };
}

module.exports = logger;

总结

本文我们简单了解了什么 Redux,以及 Redux 的设计哲学,并且我们还实现了一个简单的 reduxredux-thunk

最后的简易实现demo,我也贴出来,感兴趣的可以去看看—— mini-redux

课后作业:applyMiddleware 传入多个中间件时,日志中间件redux-logger 应该放在哪里?开头还是结尾?为什么?