从0到1实现一个简易版的redux

Posted by franki on November 13, 2019

redux flow

背景:工作中经常使用redux,但是一直有个想法,想摸透里面的来龙去脉,虽然知道里面运用了观察者模式,但是苦于没有看源码,一直不知道里面的代码组合,今天终于动手,到redux源码中去徜徉,揣摩优秀的设计,尝试学着造一个简易redux

一般情况下,我们可以通过发布订阅实现一个状态管理器

最简单的状态管理器

let state = {
  count: 1
};

let listeners = [];

// substribe
function substribe(listener) {
  listeners.push(listener);
}

function changeCount(count) {
  state.count = count;
  for (let i=0; i<listeners.length; i++) {
    const listener = listeners[i];
    listener();
  }
}

// 订阅一下,count改变时实时输出
substribe(function() {
  console.log(state.count);
});

// 修改state 通过changeCount来改变
changeCount(2); // 输出2
changeCount(3); // 输出3
changeCount(4); // 输出4

通过以上代码,就完成了一个极简的状态管理器,主要原理是:发布订阅

这就可以了吗,看看有何问题没?

发现没有:所有的变量都暴露在全局中,可以随意修改,并且只做到控制count


如何解决呢?

想办法把变量都放到函数里面形成局部作用域,只有通过特定的方法(接口)才可以访问里面的变量

怎么做?

变量封装,暴露方法

function createStore(initState) {
  let state = initState;
  let listeners = [];
  
  // substribe
  function substribe(listener) {
    listeners.push(listener);
  }
  
  // change state
  function changeState(newState) {
    state = newState;
    // trigger
    for (let i=0; i<listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }
  
  function getState() {
    return state;
  }
  
  return {
    substribe,
    changeState,
    getState
  };
}

let initState = {
  counter: {count: 0},
  info: {name: '', desc: ''}
};

let store = createStore(initState);

store.substribe(() => {
  let state = store.getState();
  console.log('count: ', state.counter.count);
});

store.substribe(() => {
  let state = store.getState();
  console.log(`info.name: ${state.info.name}`, `info.desc: ${state.info.desc}`);
});

store.changeState({counter: {count: 1}, info: {name: '', desc: ''}});

store.changeState({counter: {count: 2}, info: {name: 'franki', desc: 'handsome boy'}});

这样就满足了把变量藏匿于函数,不再直接暴露于全局,通过返回的对象,提供对应的接口进行数据修改,并且支持多个状态值(不单单是count)

以上就完了吗?

答案是好戏才刚刚开场

想想还有什么问题呢?


有计划的改变

什么叫做有计划的改变?

function createStore(plan, initState) {
  let state = initState;
  let listeners = [];
  
  function substribe(listener) {
    listeners.push(listener);
  }
  
  function changeState(action) {
    state = plan(state, action);
    for (let i=0; i<listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }
  
  function getState() {
    return state;
  }
  
  return {
    substribe,
    changeState,
    getState
  };
}

let initState = {
  count: 0
};

function plan(state, action) {
  switch(action.type) {
    case 'INCREMENT':
      return {...state, count: state.count+1};
    case 'DECREMENT':
      return {...state, count: state.count-1};
    default:
      return state;
  }
}

let store = createStore(plan, initState);

store.substribe(() => {
  let state = store.getState();
  console.log('count: ', state.count);
});

store.changeState({type: 'INCREMENT'});

store.changeState({type: 'DECREMENT'});

到现在,已经可以做到有计划的去修改值,而不是直接通过传入一个具体值,直接赋值,这样的好处是:规定改变数据的方式只有通过特定的方法去触发,并且规定触发的需要传入特定的参数。

做到这样,一个初步的redux已经完工,为了提高逼格,可以把上面代码的plan换成reducer,changeState改为dispatch,这样就跟redux的api一致了

还是那句话,这样就完了吗?

还是那句话,革命尚未成功,同志仍需努力!

想想还有什么方面需要需改?


拆分reducer

上面的代码有个很严重的问题,就是当store树很大的时候,需要维护一个非常臃肿的plan函数,所以拆分plan函数成了首要任务

function createStore(reducer, initState) {
  let state = initState;
  let listeners = [];
  
  function substribe(listener) {
    listeners.push(listener);
  }
  
  function dispatch(action) {
    state = reducer(state, action);

    for (let i=0; i<listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }
  
  function getState() {
    return state;
  }
  
  return {
    substribe,
    dispatch,
    getState
  };
}

function counterReducer(state, action) {
  switch(action.type) {
    case 'INCREMENT':
      return {...state, count: state.count+1};
    case 'DECREMENT':
      return  {...state, count: state.count-1};
    default:
      return state;
  }
}

function infoReducer(state, action) {
  switch(action.type) {
    case 'CHANGE_NAME':
      return {...state, name: action.name};
    case 'CHANGE_DESC':
      return  {...state, desc: action.desc};
    default:
      return state;
  }
}

function combineReducer(reducers) {
  let reducersKeys = Object.keys(reducers);
  
  return function combination(state={}, action) {
    // 生成新的state
    let nextState = {};

    for (let i=0; i<reducersKeys.length; i++) {
      const key = reducersKeys[i];
      const reducer = reducers[key];
      // 获取旧state
      const prevStateForKey = state[key];
      const nextStateForKey = reducer(prevStateForKey, action);
      nextState[key] = nextStateForKey;
    }
    return nextState;
  }
}

const reducers = combineReducer({
  counter: counterReducer,
  info: infoReducer
});

let initState = {
  counter: {count: 0},
  info: {name: '', desc: ''}
};

let store = createStore(reducers, initState);

store.substribe(() => {
  let state = store.getState();
  console.log('count: ', state.counter.count)
});

store.substribe(() => {
  let state = store.getState();
  console.log(`info.name: ${state.info.name}`, `info.desc: ${state.info.desc}`);
});

store.dispatch({type: 'INCREMENT'});

store.dispatch({type: 'DECREMENT'});

store.dispatch({type: 'CHANGE_NAME', name: 'franki'});

store.dispatch({type: 'CHANGE_DESC', desc: 'handsome boy'});

现在已经拆分完了reducer,已经可以把reducer慢慢细化,划分为一个个小的状态树,符合单一职责的要求

目前为止,redux较为完善

但是还有些瑕疵,下面继续讲解


中间件

function createStore(reducers, initState) {
  let state = initState;
  let listeners = [];
  
  function substribe(listener) {
    listeners.push(listener);
    return function unsubstribe() {
      const index = listeners.indexOf(listener);
      listeners.splice(index, 1);
    }
  }
  
  function dispatch(action) {
    state = reducers(state, action);

    for (let i=0, l=listeners.length; i<l; i++) {
      const listener = listeners[i];
      listener();
    }
  }
  
  function getState() {
    return state;
  }
  
  return {
    substribe,
    dispatch,
    getState
  }
}

function counterReducer(state={count: 0}, action) {
  switch(action.type) {
    case 'INCREMENT':
      return {...state, count: state.count+1};
    case 'DECREMENT':
      return {...state, count: state.count-1};
    default:
      return state;
  }
}

function infoReducer(state={name: '', desc: ''}, action) {
  switch(action.type) {
    case 'CHANGE_NAME':
      return {...state, name: action.name};
    case 'CHANGE_DESC':
      return {...state, desc: action.desc};
    default:
      return state;
  }
}

function combineReducers(reducers) {
  let reducersKeys = Object.keys(reducers);
  
  // 处理后返回一个新的reducer
  return function(state = {}, action) {
    // 新的state
    let nextState = {};

    for (let i=0; i<reducersKeys.length; i++) {
      // 当前key
      const key = reducersKeys[i];
      // key对应的reducer => counterReducer, infoReducer
      const reducer = reducers[key];

      // 旧state
      const prevStateForKey = state[key];
      // 新state
      const nextStateForKey = reducer(prevStateForKey, action);

      nextState[key] = nextStateForKey;
    }

    return nextState;
  }
}

let initState = {
  counter: {count: 0},
  info: {name: '', desc: ''}
};

let reducers = combineReducers({
  counter: counterReducer,
  info: infoReducer
});

let store = createStore(reducers, initState);

let dispatch = store.dispatch;

const loggerMiddleware = action => {
  console.log('cur state: ', store.getState());
  dispatch(action);
  console.log('next state: ', store.getState());
};

store.dispatch = loggerMiddleware;

store.substribe(() => {
  let state = store.getState();
  console.log('count: ', state.counter.count);
});

store.substribe(() => {
  let state = store.getState();
  console.log('info.name: ', state.info.name);
  console.log('info.desc: ', state.info.desc);
});

store.dispatch({type: 'INCREMENT'});

store.dispatch({type: 'DECREMENT'});

store.dispatch({type: 'CHANGE_NAME', name: 'franki'});

store.dispatch({type: 'CHANGE_DESC', desc: 'handsome boy'});

这样一来,就可以通过中间件去做一些其他事了,上面的例子的中间件是用来记录数据变化,使得数据变化更加有迹可循。

有个问题,上面的中间件也只做了一件事,如果我不但需要记录数据的变化,还需要监控异常怎么办?

你可能想到这样?

const exceptionMiddleware = action => {
    try {
        loggerMiddleware(action);
    } catch (error) {
        console.log('something wrong! ', error);
    }
};

store.dispatch = exceptionMiddleware;

再来一个统计时间的中间件,你也可能这样?

const timeMiddleware = action => {
  const time =
        new Date().getFullYear().toString() + '/' +
        (new Date().getMonth() + 1).toString() + '/' +
        new Date().getDate().toString() + ' ' +
        new Date().getHours() + ':' +
        new Date().getMinutes() + ':' +
        new Date().getSeconds();
  
  console.log('time: ', time);
  exceptionMiddleware(action);
};

store.dispatch = timeMiddleware;

确实可以这样实现,但是如此实现,确实不太优雅

如何做到优雅呢?

想办法把各个中间件写成独立的模块,通过参数传递的方式传入想要的值,而不是在代码上硬编码固定

const loggerMiddleware = store => next => action => {
  console.log('cur state: ', store.getState());
  next(action);
  console.log('next state: ', store.getState());
};

const exceptionMiddleware = store => next => action => {
  try {
    next(action);
  } catch (error) {
    console.log('something wrong! ', error);
  }
};

const timeMiddleware = store => next => action => {
  const time =
        new Date().getFullYear().toString() + '/' +
        (new Date().getMonth() + 1).toString() + '/' +
        new Date().getDate().toString() + ' ' +
        new Date().getHours() + ':' +
        new Date().getMinutes() + ':' +
        new Date().getSeconds();
  console.log('time: ', time);
  next(action);
};

const chain = [exceptionMiddleware, timeMiddleware, loggerMiddleware].map(middleware => middleware(store));

const next = store.dispatch;
chain.map(middleware => {
  dispatch = middleware(next);
});

store.dispatch = dispatch;

看懂了没,稍微解释下,每个中间件都是独立的模块,需要的变量都是通过参数的形式传递过来(像是store dispatch action)

chain的作用是返回一个新的数组

[
  next => action => {
    try {
      next(action);
    } catch (error) {
      console.log('something wrong! ', error);
    }
  },
  next => action => {
    const time =
          new Date().getFullYear().toString() + '/' +
          (new Date().getMonth() + 1).toString() + '/' +
          new Date().getDate().toString() + ' ' +
          new Date().getHours() + ':' + new Date().getMinutes() + ':' +
          new Date().getSeconds();
    console.log('time: ', time);
    next(action);
  },
  next => action => {
    console.log('cur state: ', store.getState());
    next(action);
    console.log('next state: ', store.getState());
  }
]

方便好记
[
  exception,
  time,
  logger
]

接着再次map目的把dispatch重新赋值,就是利用这行代码

chain.map(middleware => {
  dispatch = middleware(next);
});

相当于实现了dispatch = exception(time(logger(dispatch)))

最后把store.dispatch = dispatch

这样一来,只要执行dispatch操作,首先通过exceptionMiddleware => timeMiddleware => loggerMiddleware => dispatch => reducer => newState

以上的代码够完美了吗?

答案是。。还有些不舒服的地方

还可以怎么继续优化?

往下看


上面的例子有个问题是,中间件的聚合都是通过手动拼接,能不能通过一个函数来完成这一系列的操作呢?

// 返回一个dispatch被重写了新的store
function applyMiddleware(...middlewares) {
  // 返回一个重写了的createStore
  return function reWriteCreateStoreFunc(oldCreateStore) {
    // 返回一个新的createStore
    return function newCreateStore(reducer, initState) {
      // 生成store
      const store = oldCreateStore(reducer, initState);
      // 按照最小开放原则,中间件,只能拿到getState方法,不容许进行其他操作
      const newStore = {getState: store.getState};
      let dispatch = store.dispatch;

      const chain = middlewares.map(middleware => middleware(newStore));

      chain.map(middleware => {
        dispatch = middleware(dispatch);
      });

      store.dispatch = dispatch;
      return store;
    };
  };
}

上面的函数做了这样的一件事,首先把中间件全部收集起来,接着传入旧store函数(createStore),最后返回的是一个新的newCreateStore,利用的新的newCreateStore,传入reducers和initState参数,返回newStore,继续新的substribe和dispatch

眼尖的同学,可能发现

const chain = middlewares.map(middleware => middleware(store));

chain.map(middleware => {
  dispatch = middleware(dispatch);
});

store.dispatch = dispatch;

如此实现包裹的dispatch并不优雅,redux官方版本用了compose(组合)实现

// 组合方法 返回a(b(c...))这样形式的结构
function compose(...funcs) {
  if (funcs.length === 0) {
    return funcs[0];
  }
  
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

利用compose方法可以这样做

const chain = middlewares.map(middleware => middleware(store));

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

store.dispatch = dispatch;

可能还有些同学发现

let newStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore)(reducers, initState);

这样会产生一个新的变量,而且与旧store在一起在全局中容易混淆

你还可以这样做

从createStore函数入手,通过参数来判断是直接生成store,还是需要加入中间件之后生成的store

function createStore(reducers, initState, reWriteCreateStoreFunc) {
  if (typeof initState === 'function') {
    reWriteCreateStoreFunc = initState;
    initState = undefined;
  }

  if (reWriteCreateStoreFunc) {
    const newCreateStore = reWriteCreateStoreFunc(createStore);
    return newCreateStore(reducers, initState);
  }

  let state = initState;
  let listeners = [];

  function substribe(listener) {
    listeners.push(listener);
    return function unsubstribe() {
      const index = listeners.indexOf(listener);
      listeners.splice(index, 1);
    };
  }

  function dispatch(action) {
    state = reducers(state, action);

    for (let i = 0, l = listeners.length; i < l; i++) {
      const listener = listeners[i];
      listener();
    }
  }

  function getState() {
    return state;
  }

  return {
    substribe,
    dispatch,
    getState
  };
}

// 调用的时候这样写
let store = createStore(reducers, initState, applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware));

// 还可以不传initState
let store = createStore(reducers, applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware));

// 或者initState和中间件相关都不传
let store = createStore(reducers);

这样的话,就不用管理多个store了

redux官方还提供replaceReducer方法,这个是如何做的呢?

// 在createStore方法加入
function replaceReducer(newReducer) {
  // 把新reducer赋予旧的reducer达到替换的效果
  reducer = newReducer;
  // 重新dispatch达到更换效果
  dispatch({type: Symbol()});
}

// 接着返回
return {
  ...
  replaceReducer
}

// 这样使用
// 生成新的reducer并替换掉旧的reducer
const newReducers = combineReducers({
    counter: counterReducer,
});

store.replaceReducer(newReducers);

store.dispatch({ type: 'INCREMENT' });

做完以上的所有事情一个完整的redux浮出水面

下面放上完整的redux代码,如果嫌长,可以自行划分模块处理

function createStore(reducers, initState, reWriteCreateStoreFunc) {
  if (typeof initState === 'function') {
    reWriteCreateStoreFunc = initState;
    initState = undefined;
  }

  if (reWriteCreateStoreFunc) {
    const newCreateStore = reWriteCreateStoreFunc(createStore);
    return newCreateStore(reducers, initState);
  }

  let state = initState;
  let listeners = [];

  function substribe(listener) {
    listeners.push(listener);
    return function unsubstribe() {
      const index = listeners.indexOf(listener);
      listeners.splice(index, 1);
    };
  }

  function dispatch(action) {
    state = reducers(state, action);

    for (let i = 0, l = listeners.length; i < l; i++) {
      const listener = listeners[i];
      listener();
    }
  }

  function getState() {
    return state;
  }

  function replaceReducer(newReducer) {
    // 把新reducer赋予旧的reducer达到替换的效果
    reducer = newReducer;
    // 重新dispatch达到更换效果
    dispatch({ type: Symbol() });
  }

  dispatch({ type: Symbol() });

  return {
    substribe,
    dispatch,
    getState,
    replaceReducer
  };
}

function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}

function infoReducer(state = { name: '', desc: '' }, action) {
  switch (action.type) {
    case 'CHANGE_NAME':
      return { ...state, name: action.name };
    case 'CHANGE_DESC':
      return { ...state, desc: action.desc };
    default:
      return state;
  }
}

function combineReducers(reducers) {
  let reducersKeys = Object.keys(reducers);

  // 处理后返回一个新的reducer
  return function(state = {}, action) {
    // 新的state
    let nextState = {};

    for (let i = 0; i < reducersKeys.length; i++) {
      // 当前key
      const key = reducersKeys[i];
      // key对应的reducer => counterReducer, infoReducer
      const reducer = reducers[key];

      // 旧state
      const prevStateForKey = state[key];
      // 新state
      const nextStateForKey = reducer(prevStateForKey, action);

      nextState[key] = nextStateForKey;
    }

    return nextState;
  };
}

let initState = {
  counter: { count: 0 },
  info: { name: '', desc: '' }
};

let reducers = combineReducers({
  counter: counterReducer,
  info: infoReducer
});

const loggerMiddleware = store => next => action => {
  console.log('cur state: ', store.getState());
  next(action);
  console.log('next state: ', store.getState());
};

const exceptionMiddleware = store => next => action => {
  try {
    next(action);
  } catch (error) {
    console.log('something wrong! ', error);
  }
};

const timeMiddleware = store => next => action => {
  const time =
        new Date().getFullYear().toString() + '/' +
        (new Date().getMonth() + 1).toString() + '/' +
        new Date().getDate().toString() + ' ' +
        new Date().getHours() + ':' +
        new Date().getMinutes() + ':' +
        new Date().getSeconds();
  console.log('time: ', time);
  next(action);
};

function compose(...funcs) {
  if (funcs.length === 0) {
    return funcs[0];
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

// 返回一个dispatch被重写了新的store
function applyMiddleware(...middlewares) {
  // 返回一个重写了的createStore
  return function reWriteCreateStoreFunc(oldCreateStore) {
    // 返回一个新的createStore
    return function newCreateStore(reducer, initState) {
      // 生成store
      const store = oldCreateStore(reducer, initState);
      const newStore = { getState: store.getState };
      let dispatch = store.dispatch;

      const chain = middlewares.map(middleware => middleware(newStore));

      // chain.map(middleware => {
      //     dispatch = middleware(dispatch);
      // });
      dispatch = compose(...chain)(dispatch);

      store.dispatch = dispatch;
      return store;
    };
  };
}

let store = createStore(reducers, initState, applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware));

store.substribe(() => {
  let state = store.getState();
  console.log('count: ', state.counter.count);
});

store.substribe(() => {
  let state = store.getState();
  console.log('info.name: ', state.info.name);
  console.log('info.desc: ', state.info.desc);
});

store.dispatch({ type: 'INCREMENT' });

store.dispatch({ type: 'DECREMENT' });

store.dispatch({ type: 'CHANGE_NAME', name: 'franki' });

store.dispatch({ type: 'CHANGE_DESC', desc: 'handsome boy' });

// 生成新的reducer并替换掉旧的reducer
const newReducers = combineReducers({
  counter: counterReducer
});

store.replaceReducer(newReducers);

store.dispatch({ type: 'INCREMENT' });

码字不易

小小总结下,redux的完整流程

views > store.dispatch > store (reducer state) > views

接着详细解释下,redux里面用到的一些重要名词

  1. createStore 创建store对象,包含substribe dispatch getState replaceReducer
  2. reducer 是一个计划函数,接收旧state 和 action,用于改变state树
  3. dispatch 用于触发action,生成新state
  4. substribe 订阅功能,每次触发dispatch的时候,会执行订阅函数
  5. combineReducers 将多个reducer合并成一个reducer
  6. replaceReducer 替换 reducer 函数
  7. middleware 用于扩展dispatch方法