手写一个Redux,深入理解其原理

2020-10-15 10:05:17 浏览数 (1)

Redux可是一个大名鼎鼎的库,很多地方都在用,我也用了几年了,今天这篇文章就是自己来实现一个Redux,以便于深入理解他的原理。我们还是老套路,从基本的用法入手,然后自己实现一个Redux来替代源码的NPM包,但是功能保持不变。本文只会实现Redux的核心库,跟其他库的配合使用,比如React-Redux准备后面单独写一篇文章来讲。有时候我们过于关注使用,只记住了各种使用方式,反而忽略了他们的核心原理,但是如果我们想真正的提高技术,最好还是一个一个搞清楚,比如Redux和React-Redux看起来很像,但是他们的核心理念和关注点是不同的,Redux其实只是一个单纯状态管理库,没有任何界面相关的东西,React-Redux关注的是怎么将Redux跟React结合起来,用到了一些React的API。

本文全部代码已经上传到GitHub,大家可以拿下来玩下:github.com/dennis-jian…

基本概念

Redux的概念有很多文章都讲过,想必大家都看过很多了,我这里不再展开,只是简单提一下。Redux基本概念主要有以下几个:

Store

人如其名,Store就是一个仓库,它存储了所有的状态(State),还提供了一些操作他的API,我们后续的操作其实都是在操作这个仓库。假如我们的仓库是用来放牛奶的,初始情况下,我们的仓库里面一箱牛奶都没有,那Store的状态(State)就是:

代码语言:javascript复制
{
	milk: 0
}

Actions

一个Action就是一个动作,这个动作的目的是更改Store中的某个状态,Store还是上面的那个仓库,现在我想往仓库放一箱牛奶,那"我想往仓库放一箱牛奶"就是一个Action,代码就是这样:

代码语言:javascript复制
{
  type: "PUT_MILK",
  count: 1
}

Reducers

前面"我想往仓库放一箱牛奶"只是想了,还没操作,具体操作要靠Reducer,Reducer就是根据接收的Action来改变Store中的状态,比如我接收了一个PUT_MILK,同时数量count是1,那放进去的结果就是milk增加了1,从0变成了1,代码就是这样:

代码语言:javascript复制
const initState = {
  milk: 0
}

function reducer(state = initState, action) {
  switch (action.type) {
    case 'PUT_MILK':
      return {...state, milk: state.milk   action.count}
    default:
      return state
  }
}
复制代码

可以看到Redux本身就是一个单纯的状态机,Store存放了所有的状态,Action是一个改变状态的通知,Reducer接收到通知就更改Store中对应的状态。

简单例子

下面我们来看一个简单的例子,包含了前面提到的Store,Action和Reducer这几个概念:

代码语言:javascript复制
import { createStore } from 'redux';

const initState = {
  milk: 0
};

function reducer(state = initState, action) {
  switch (action.type) {
    case 'PUT_MILK':
      return {...state, milk: state.milk   action.count};
    case 'TAKE_MILK':
      return {...state, milk: state.milk - action.count};
    default:
      return state;
  }
}

let store = createStore(reducer);

// subscribe其实就是订阅store的变化,一旦store发生了变化,传入的回调函数就会被调用
// 如果是结合页面更新,更新的操作就是在这里执行
store.subscribe(() => console.log(store.getState()));

// 将action发出去要用dispatch
store.dispatch({ type: 'PUT_MILK' });    // milk: 1
store.dispatch({ type: 'PUT_MILK' });    // milk: 2
store.dispatch({ type: 'TAKE_MILK' });   // milk: 1
复制代码

自己实现

前面我们那个例子虽然短小,但是已经包含了Redux的核心功能了,所以我们手写的第一个目标就是替换这个例子中的Redux。要替换这个Redux,我们得先知道他里面都有什么东西,仔细一看,我们好像只用到了他的一个API:

createStore:这个API接受reducer方法作为参数,返回一个store,主要功能都在这个store上。

看看store上我们都用到了啥:

store.subscribe: 订阅state的变化,当state变化的时候执行回调,可以有多个subscribe,里面的回调会依次执行。 store.dispatch: 发出action的方法,每次dispatch action都会执行reducer生成新的state,然后执行subscribe注册的回调。 store.getState:一个简单的方法,返回当前的state

看到subscribe注册回调,dispatch触发回调,想到了什么,这不就是发布订阅模式吗?我之前有一篇文章详细讲过发布订阅模式了,这里直接仿写一个。

代码语言:javascript复制
function createStore() {
  let state;              // state记录所有状态
  let listeners = [];     // 保存所有注册的回调

  function subscribe(callback) {
    listeners.push(callback);       // subscribe就是将回调保存下来
  }

  // dispatch就是将所有的回调拿出来依次执行就行
  function dispatch() {
    for (let i = 0; i < listeners.length; i  ) {
      const listener = listeners[i];
      listener();
    }
  }

  // getState直接返回state
  function getState() {
    return state;
  }

  // store包装一下前面的方法直接返回
  const store = {
    subscribe,
    dispatch,
    getState
  }

  return store;
}
复制代码

上述代码是不是很简单嘛,Redux核心也是一个发布订阅模式,就是这么简单!等等,好像漏了啥,reducer呢?reducer的作用是在发布事件的时候改变state,所以我们的dispatch在执行回调前应该先执行reducer,用reducer的返回值重新给state赋值,dispatch改写如下:

代码语言:javascript复制
function dispatch(action) {
  state = reducer(state, action);

  for (let i = 0; i < listeners.length; i  ) {
    const listener = listeners[i];
    listener();
  }
}
复制代码

到这里,前面例子用到的所有API我们都自己实现了,我们用自己的Redux来替换下官方的Redux试试:

代码语言:javascript复制
// import { createStore } from 'redux';
import { createStore } from './myRedux';

可以看到输出结果是一样的,说明我们自己写的Redux没有问题:

了解了Redux的核心原理,我们再去看他的源码应该就没有问题了,createStore的源码传送门。

最后我们再来梳理下Redux的核心流程,注意单纯的Redux只是个状态机,是没有View层的哦。

除了这个核心逻辑外,Redux里面还有些API也很有意思,我们也来手写下。

手写combineReducers

combineReducers也是使用非常广泛的API,当我们应用越来越复杂,如果将所有逻辑都写在一个reducer里面,最终这个文件可能会有成千上万行,所以Redux提供了combineReducers,可以让我们为不同的模块写自己的reducer,最终将他们组合起来。比如我们最开始那个牛奶仓库,由于我们的业务发展很好,我们又增加了一个放大米的仓库,我们可以为这两个仓库创建自己的reducer,然后将他们组合起来,使用方法如下:

代码语言:javascript复制
import { createStore, combineReducers } from 'redux';

const initMilkState = {
  milk: 0
};
function milkReducer(state = initMilkState, action) {
  switch (action.type) {
    case 'PUT_MILK':
      return {...state, milk: state.milk   action.count};
    case 'TAKE_MILK':
      return {...state, milk: state.milk - action.count};
    default:
      return state;
  }
}

const initRiceState = {
  rice: 0
};
function riceReducer(state = initRiceState, action) {
  switch (action.type) {
    case 'PUT_RICE':
      return {...state, rice: state.rice   action.count};
    case 'TAKE_RICE':
      return {...state, rice: state.rice - action.count};
    default:
      return state;
  }
}

// 使用combineReducers组合两个reducer
const reducer = combineReducers({milkState: milkReducer, riceState: riceReducer});

let store = createStore(reducer);

store.subscribe(() => console.log(store.getState()));

// 操作

0 人点赞