关于react.js:手写一个Redux深入理解其原理面试进阶

28次阅读

共计 10946 个字符,预计需要花费 28 分钟才能阅读完成。

Redux 可是一个赫赫有名的库,很多中央都在用,我也用了几年了,明天这篇文章就是本人来实现一个 Redux,以便于深刻了解他的原理。咱们还是老套路,从根本的用法动手,而后本人实现一个 Redux 来代替源码的 NPM 包,然而性能放弃不变。本文只会实现 Redux 的外围库,跟其余库的配合应用,比方 React-Redux 筹备前面独自写一篇文章来讲。有时候咱们过于关注应用,只记住了各种应用形式,反而疏忽了他们的外围原理,然而如果咱们想真正的进步技术,最好还是一个一个搞清楚,比方 Redux 和 React-Redux 看起来很像,然而他们的核心理念和关注点是不同的,Redux 其实只是一个单纯状态治理库,没有任何界面相干的货色,React-Redux 关注的是怎么将 Redux 跟 React 联合起来,用到了一些 React 的 API。

基本概念

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

Store

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

{milk: 0}

Actions

一个 Action 就是一个动作,这个动作的目标是更改 Store 中的某个状态,Store 还是下面的那个仓库,当初我想往仓库放一箱牛奶,那 ” 我想往仓库放一箱牛奶 ” 就是一个 Action,代码就是这样:

{
  type: "PUT_MILK",
  count: 1
}

Reducers

后面 ” 我想往仓库放一箱牛奶 ” 只是想了,还没操作,具体操作要靠 Reducer,Reducer 就是依据接管的 Action 来扭转 Store 中的状态,比方我接管了一个 PUT_MILK,同时数量count 是 1,那放进去的后果就是 milk 减少了 1,从 0 变成了 1,代码就是这样:

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 这几个概念:

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: 参考 前端 react 面试题具体解答

createStore:这个 API 承受 reducer 办法作为参数,返回一个 store,次要性能都在这个store 上。

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

store.subscribe: 订阅 state 的变动,当 state 变动的时候执行回调,能够有多个subscribe,外面的回调会顺次执行。

store.dispatch: 收回 action 的办法,每次 dispatch action 都会执行 reducer 生成新的 state,而后执行subscribe 注册的回调。

store.getState: 一个简略的办法,返回以后的state

看到 subscribe 注册回调,dispatch触发回调,想到了什么,这不就是公布订阅模式吗?

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改写如下:

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

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

到这里,后面例子用到的所有 API 咱们都本人实现了,咱们用本人的 Redux 来替换下官网的 Redux 试试:

// import {createStore} from 'redux';
import {createStore} from './myRedux';

能够看到输入后果是一样的,阐明咱们本人写的 Redux 没有问题:

理解了 Redux 的外围原理,咱们再去看他的源码应该就没有问题了

最初咱们再来梳理下 Redux 的外围流程,留神单纯的 Redux 只是个状态机,是没有 View 层的哦。

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

手写combineReducers

combineReducers也是应用十分宽泛的 API,当咱们利用越来越简单,如果将所有逻辑都写在一个 reducer 外面,最终这个文件可能会有成千上万行,所以 Redux 提供了combineReducers,能够让咱们为不同的模块写本人的reducer,最终将他们组合起来。比方咱们最开始那个牛奶仓库,因为咱们的业务倒退很好,咱们又减少了一个放大米的仓库,咱们能够为这两个仓库创立本人的reducer,而后将他们组合起来,应用办法如下:

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()));

// 操作🥛的 action
store.dispatch({type: 'PUT_MILK', count: 1});    // milk: 1
store.dispatch({type: 'PUT_MILK', count: 1});    // milk: 2
store.dispatch({type: 'TAKE_MILK', count: 1});   // milk: 1

// 操作大米的 action
store.dispatch({type: 'PUT_RICE', count: 1});    // rice: 1
store.dispatch({type: 'PUT_RICE', count: 1});    // rice: 2
store.dispatch({type: 'TAKE_RICE', count: 1});   // rice: 1

下面代码咱们将大的 state 分成了两个小的 milkStatericeState,最终运行后果如下:

晓得了用法,咱们尝试本人来写下呢!要手写 combineReducers,咱们先来剖析下他干了啥,首先它的返回值是一个reducer,这个reducer 同样会作为 createStore 的参数传进去,阐明这个返回值是一个跟咱们之前一般 reducer 构造一样的函数。这个函数同样接管 stateaction而后返回新的 state,只是这个新的state 要合乎 combineReducers 参数的数据结构。咱们尝试来写下:

function combineReducers(reducerMap) {const reducerKeys = Object.keys(reducerMap);    // 先把参数外面所有的键值拿进去

  // 返回值是一个一般构造的 reducer 函数
  const reducer = (state = {}, action) => {const newState = {};

    for(let i = 0; i < reducerKeys.length; i++) {
      // reducerMap 外面每个键的值都是一个 reducer,咱们把它拿进去运行下就能够失去对应键新的 state 值
      // 而后将所有 reducer 返回的 state 依照参数外面的 key 组装好
      // 最初再返回组装好的 newState 就行
      const key = reducerKeys[i];
      const currentReducer = reducerMap[key];
      const prevState = state[key];
      newState[key] = currentReducer(prevState, action);
    }

    return newState;
  };

  return reducer;
}

手写applyMiddleware

middleware是 Redux 外面很重要的一个概念,Redux 的生态次要靠这个 API 接入,比方咱们想写一个 logger 的中间件能够这样写(这个中间件来自于官网文档):

// logger 是一个中间件,留神返回值嵌了好几层函数
// 咱们前面来看看为什么这么设计
function logger(store) {return function(next) {return function(action) {console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result
    }
  }
}

// 在 createStore 的时候将 applyMiddleware 作为第二个参数传进去
const store = createStore(
  reducer,
  applyMiddleware(logger)
)

能够看到上述代码为了反对中间件,createStore反对了第二个参数,这个参数官网称为 enhancer,顾名思义他是一个增强器,用来加强store 的能力的。官网对于 enhancer 的定义如下:

type StoreEnhancer = (next: StoreCreator) => StoreCreator

下面的构造的意思是说 enhancer 作为一个函数,他接管 StoreCreator 函数作为参数,同时返回的也必须是一个 StoreCreator 函数。留神他的返回值也是一个 StoreCreator 函数,也就是咱们把他的返回值拿进去继续执行应该失去跟之前的 createStore 一样的返回构造,也就是说咱们之前的 createStore 返回啥构造,他也必须返回构造,也就是这个store

{
  subscribe,
  dispatch,
  getState
}

createStore反对enhancer

依据他对于 enhancer 的定义,咱们来改写下本人的createStore,让他反对enhancer

function createStore(reducer, enhancer) {   // 接管第二个参数 enhancer
  // 先解决 enhancer
  // 如果 enhancer 存在并且是函数
  // 咱们将 createStore 作为参数传给他
  // 他应该返回一个新的 createStore 给我
  // 我再拿这个新的 createStore 执行,应该失去一个 store
  // 间接返回这个 store 就行
  if(enhancer && typeof enhancer === 'function'){const newCreateStore = enhancer(createStore);
    const newStore = newCreateStore(reducer);
    return newStore;
  }

  // 如果没有 enhancer 或者 enhancer 不是函数,间接执行之前的逻辑
  // 上面这些代码都是之前那版
  // 省略 n 行代码
    // .......
  const store = {
    subscribe,
    dispatch,
    getState
  }

  return store;
}

applyMiddleware返回值是一个enhancer

后面咱们曾经有了 enhancer 的根本构造,applyMiddleware是作为第二个参数传给 createStore 的,也就是说他是一个 enhancer,精确的说是applyMiddleware 的返回值是一个 enhancer,因为咱们传给createStore 的是他的执行后果applyMiddleware()

function applyMiddleware(middleware) {
  // applyMiddleware 的返回值应该是一个 enhancer
  // 依照咱们后面说的 enhancer 的参数是 createStore
  function enhancer(createStore) {
    // enhancer 应该返回一个新的 createStore
    function newCreateStore(reducer) {
      // 咱们先写个空的 newCreateStore,间接返回 createStore 的后果
      const store = createStore(reducer);
      return store
    }

    return newCreateStore;
  }

  return enhancer;
}

实现applyMiddleware

下面咱们曾经有了 applyMiddleware 的根本构造了,然而性能还没实现,要实现他的性能,咱们必须先搞清楚一个中间件到底有什么性能,还是以后面的 logger 中间件为例:

function logger(store) {return function(next) {return function(action) {console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result
    }
  }
}

这个中间件运行成果如下:

能够看到咱们 let result = next(action); 这行执行之后 state 扭转了,后面咱们说了要扭转 state 只能 dispatch(action),所以这里的next(action) 就是 dispatch(action),只是换了一个名字而已。而且留神最初一层返回值return function(action) 的构造,他的参数是 action,是不是很像dispatch(action),其实他就是一个新的dispatch(action),这个新的dispatch(action) 会调用原始的dispatch,并且在调用的前后加上本人的逻辑。所以到这里一个中间件的构造也分明了:

  1. 一个中间件接管 store 作为参数,会返回一个函数
  2. 返回的这个函数接管老的 dispatch 函数作为参数,会返回一个新的函数
  3. 返回的新函数就是新的 dispatch 函数,这个函数外面能够拿到里面两层传进来的 store 和老 dispatch 函数

所以说白了,中间件就是增强 dispatch 的性能,用新的 dispatch 替换老的 dispatch,这不就是个装璜者模式吗?其实后面enhancer 也是一个装璜者模式,传入一个 createStore,在createStore 执行前后加上些代码,最初又返回一个增强版的createStore

遵循这个思路,咱们的 applyMiddleware 就能够写进去了:

// 间接把后面的构造拿过去
function applyMiddleware(middleware) {function enhancer(createStore) {function newCreateStore(reducer) {const store = createStore(reducer);

      // 将 middleware 拿过去执行下,传入 store
      // 失去第一层函数
      const func = middleware(store);

      // 解构出原始的 dispatch
      const {dispatch} = store;

      // 将原始的 dispatch 函数传给 func 执行
      // 失去增强版的 dispatch
      const newDispatch = func(dispatch);

      // 返回的时候用增强版的 newDispatch 替换原始的 dispatch
      return {...store, dispatch: newDispatch}
    }

    return newCreateStore;
  }

  return enhancer;
}

照例用咱们本人的 applyMiddleware 替换老的,跑起来是一样的成果,阐明咱们写的没问题,哈哈~

反对多个middleware

咱们的 applyMiddleware 还差一个性能,就是反对多个middleware,比方像这样:

applyMiddleware(
  rafScheduler,
  timeoutScheduler,
  thunk,
  vanillaPromise,
  readyStatePromise,
  logger,
  crashReporter
)

其实要反对这个也简略,咱们返回的 newDispatch 外面顺次的将传入的 middleware 拿进去执行就行,多个函数的串行执行能够应用辅助函数 compose,这个函数定义如下。只是须要留神的是咱们这里的compose 不能把办法拿来执行就完了,应该返回一个包裹了所有办法的办法。

function compose(...func){return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

这个 compose 可能比拟让人困惑,我这里还是解说下,比方咱们有三个函数,这三个函数都是咱们后面接管 dispatch 返回新 dispatch 的办法:

const fun1 = dispatch => newDispatch1;
const fun2 = dispatch => newDispatch2;
const fun3 = dispatch => newDispatch3;

当咱们应用了 compose(fun1, fun2, fun3) 后执行程序是什么样的呢?

// 第一次其实执行的是
(func1, func2) => (...args) => func1(fun2(...args))
// 这次执行完的返回值是上面这个,用个变量存起来吧
const temp = (...args) => func1(fun2(...args))

// 咱们下次再循环的时候其实执行的是
(temp, func3) => (...args) => temp(func3(...args));
// 这个返回值是上面这个,也就是最终的返回值,其实就是从 func3 开始从右往左执行完了所有函数
// 后面的返回值会作为前面参数
(...args) => temp(func3(...args));

// 再看看下面这个办法,如果把 dispatch 作为参数传进去会是什么成果
(dispatch) => temp(func3(dispatch));

// 而后 func3(dispatch)返回的是 newDispatch3,这个又传给了 temp(newDispatch3),也就是上面这个会执行
(newDispatch3) => func1(fun2(newDispatch3))

// 下面这个外面用 newDispatch3 执行 fun2(newDispatch3)会失去 newDispatch2
// 而后 func1(newDispatch2)会失去 newDispatch1
// 留神这时候的 newDispatch1 其实曾经蕴含了 newDispatch3 和 newDispatch2 的逻辑了,将它拿进去执行这三个办法就都执行了

所以咱们反对多个 middleware 的代码就是这样:

// 参数反对多个中间件
function applyMiddleware(...middlewares) {function enhancer(createStore) {function newCreateStore(reducer) {const store = createStore(reducer);

      // 多个 middleware,先解构出 dispatch => newDispatch 的构造
      const chain = middlewares.map(middleware => middleware(store));
      const {dispatch} = store;

      // 用 compose 失去一个组合了所有 newDispatch 的函数
      const newDispatchGen = compose(...chain);
      // 执行这个函数失去 newDispatch
      const newDispatch = newDispatchGen(dispatch);

      return {...store, dispatch: newDispatch}
    }

    return newCreateStore;
  }

  return enhancer;
}

最初咱们再加一个 logger2 中间件实现成果:

function logger2(store) {return function(next) {return function(action) {let result = next(action);
      console.log('logger2');
      return result
    }
  }
}

let store = createStore(reducer, applyMiddleware(logger, logger2));

能够看到 logger2 也曾经打印进去了,功败垂成。

当初咱们也能够晓得他的中间件为什么要包裹几层函数了:

第一层:目标是传入 store 参数

第二层:第二层的构造是 dispatch => newDispatch,多个中间件的这层函数能够compose 起来,造成一个大的dispatch => newDispatch

第三层:这层就是最终的返回值了,其实就是newDispatch,是加强过的dispatch,是中间件的真正逻辑所在。

到这里咱们的 applyMiddleware 就写完了

总结

  1. 单纯的 Redux 只是一个状态机,store外面存了所有的状态state,要扭转外面的状态state,只能dispatch action
  2. 对于收回来的 action 须要用 reducer 来解决,reducer会计算新的 state 来代替老的state
  3. subscribe办法能够注册回调办法,当 dispatch action 的时候会执行外面的回调。
  4. Redux 其实就是一个公布订阅模式!
  5. Redux 还反对 enhancerenhancer 其实就是一个装璜者模式,传入以后的createStore,返回一个加强的createStore
  6. Redux 应用 applyMiddleware 反对中间件,applyMiddleware的返回值其实就是一个enhancer
  7. Redux 的中间件也是一个装璜者模式,传入以后的dispatch,返回一个加强了的dispatch
  8. 单纯的 Redux 是没有 View 层的,所以他能够跟各种 UI 库联合应用,比方react-redux,打算下一篇文章就是手写react-redux

正文完
 0