教你如何实现一个简易的 redux.js 和 redux 中间件

首先咱们要弄清楚 reduxjs 的思想、做用是什么,这样咱们才能开始下一步的构思。在我看来 reduxjs 核心就是一种单一数据源的概念,数据存储在一个函数的 state 变量中,只能经过固定的方法去修改和获取 dispatch()、getState()react

在 SPA 应用中,reduxjs 被普遍使用。对数据进行统一管理、实现数据共享,一般组件和组件之间、页面和页面之间能够数据共享。在 react 开发中,我常常将共用的数据和异步请求数据存放在 state 中。经过 props 的形式存在,只要在一个组件中对数据源进行了修改,其余共享的组件都会及时获得更新和渲染UI界面。redux

如今咱们知道了关于 redux 的关键思想和用途,接下来咱们一步一步实现它。我会按照下面这个列表的顺序给你们详细说明:数组

  • createStore
  • reduce、combineReducers
  • applyMiddleware
  • 中间件原理
  • 改造后的 dispatch
  • redux 应用 demo

createStore()

function createStore(reducer, initState) {
    // 声明一个初始化用的 action
    const INIT_ACTION = undefined;
    // 绑定监听事件的集合
    const listeners = [];
    // 这就是咱们一直说的那个【数据源】
    // 参数 initState 能够有,也能够没有。通常状况下不须要传递
    let state = initState ? initState : {};
    
    function dispatch(action) {
      // action 必须是一个纯对象,不能是其余的类型
      if (Object.prototype.toString.call(action) === '[object Object]') {
        throw new Error('Actions must be plain objects');
      }

      // 注意:这里是最终仍是经过调用 reducer 方法
      state = reducer(state, action);
      // 遍历 listeners
      for (let i = 0; i < listeners.length; i++) {
        listeners[i]();
      }
    }

    // 获取 state 数据
    function getState() {
      return state;
    }
    
    // 绑定监听事件
    function subscription(listener) {
      listeners.push(listener);
      // 取消监听,将事件从 listeners 中移除
      return function() {
        const idx = listeners.indexOf(listener);
        if (idx >= 0) {
          listeners.splice(idx, 1);
        }
      }
    }

    // 这是啥意思了,其实这是在调用 createStore() 时,就初始化了一个 state
    dispatch(INIT_ACTION);

    // 经过对象,将这些内部函数传递到外部。不要怀疑,这就是一个典型的闭包
    return {
      dispatch,
      getState,
      subscription,
    };
  }
复制代码

createStore 方法中咱们能够看出来,其实他就是 js模块。利用了局部变量和闭包的特性,将 state 隐藏起来,只能经过闭包的形式进行访问和修改。闭包

reduce、combineReducers

首先 reduce 它是一个函数,咱们能够本身定义。咱们能够把咱们的项目想像成以下的一个场景,修改用户的信息:app

function userName(state = {}, action = {}) {
    switch (action.type) {
      case 'name':
        return { ...state, name: action.data };
      case 'age':
        return { ...state, age: action.data };
      case 'sex':
        return { ...state, sex: action.data };
      // 必须设置 default,直接返回 state
      default:
        return state;
    }
  }
复制代码

若是咱们的项目中只须要这一种交互场景,那么定义 userName() 就够了。这个时候 咱们把 userName 传递给 createStore异步

const { getState } = createStore(userName);
  // 返回的是一个 {}
  console.log(getState());
复制代码

上面的代码在执行 createStore(userName) 时,内部执行一次 dispatch(INIT_ACTION) ,从而在 dispatch 方法内部调用了 userName({}, undefined)。因此打印的结果是一个空对象。函数

若是交互场景比较多的时候呢,一个 reducer 确定不够用啊,那么这个时候咱们可能会定义多个相似 userName 这个的 reducer 函数,因此咱们还须要定义一个工具函数 combineReducers,将多个 reducer 函数组合成一个 reducer 函数。工具

function combineReducers(reducers) {
    const keys = Object.keys(reducers);
    const finallyKeys = [];
    for (let i = 0; i < keys.length; i++) {
      if (typeof reducers[keys[i]] !== 'function') throw Error('reducer must be a function');
      finallyKeys.push(keys[i]);
    }

    // 看,最后返回的仍是一个 function
    return function(state = {}, action) {
      let hasChange = false;
      const newState = {};
      // 遍历全部的 reducer 函数
      finallyKeys.forEach(key => {
        // 获取这个 reducer 函数对应的 state。注意它多是一个 undefined
        // 没错,在 createStore() 中执行 dispatch(INIT_ACTION),这个时候 prevState_key 可能就是一个 unudefined
        const prevState_key = state[key];
        const reducer = reducers[key];
        // 调用该 reducer,返回一个新的 state
        const nextState_key = reducer(prevState_key, action);

        // 注意这里,若是 reducer 函数返回的是一个 undefined。那么这里就会报错了
        // 因此咱们在定义 reducer 函数时,应该有一个限制:若是没有匹配到 action 的 type 。应该默认返回 previous state。
        if (typeOf nextState_key === 'undefined') {
          throw Error('to ignore an action, you must explicitly return the previous state');
        }

        // 当 reducer 执行完成时,会在 newState 上添加一个新属性,属性值就是 nextState_key
        // 其实,从这个地方咱们就应该能够猜想到,最终获得的 state【数据源】,它的结果应该和咱们传入的 reducers 结构是同样的
        newState[key] = nextState_key;
        hasChange = hasChange || nextState_key !== prevState_key;
      });
      return hasChange ? newState : state;
    }
  }
复制代码

结合以前的 createStore,咱们看看下面的 demo:ui

function menu(state = {}, action = {}) {
    switch (action.type) {
      case 'home':
        return { ...state, home: action.data };
      case 'list':
        return { ...state, list: action.data };
      case 'detail':
        return { ...state, detail: action.data };
      default:
        return state;
    }
  }

  const reducer = combineReducers({ userName, menu });
  const { getState } = createStore(userName);
  // 返回的是一个 { userName: {}, menu: {} }
  // 这里和咱们传递给 combineReducers() 中的参数的结构是一致的。
  console.log(getState());
复制代码

上面的 reduceruserName, menu 的一个组合体,因此每次调用 dispatch(action) 时,都会遍历全部的 reducers。还有一个很重要的地方就是,每一个 reducer 函数在没有匹配到 action.type 时,必须把 reducer() 的参数 state 做为返回值,不然就报错。spa

applyMiddleware

reduxjs 还有一个很是厉害的功能,就是能够利用中间件,作不少事情。好比说,咱们比较经常使用的 redux-thunk、redux-logger 等。

// 这里先不考虑参数为空的状况
  function compose() {
    const middleware = [...arguments];
    // 这里利用了redux 高阶函数 
    // 第一次执行时,将 middleware 中的第一个和第二个元素赋值给 a、b。而后将返回的结果函数 fn 赋值给 a。
    // 第二次执行时,a 就是上一次的执行结果,这个时候将 middleware 中的第三个元素赋值给 b。而后将返回的结果函数 fn 赋值给 a。
    // 第三次,第四次。依次类推。。。
    return middleware.reduce(function(a, b) {
      return function fn () {
        return a(b.apply(null, arguments));
      }
    });
  } 

  function applyMiddlyWare(createStore) {
    return function(reducer) {
      // 接收中间件做为参数
      return function(...middlewares) {
        const { dispatch, getState, subscription } = createStore(reducer);
        // 将 dispatch 赋值给变量 _dispatch
        let _dispatch = dispatch;

        const disp = (...args) => {
          _dispatch(...args);
        }

        // 将上面定义 disp 内部函数,传递给每个中间件函数
        // 因此上面的 disp 就构成了一个闭包
        const chain = middlewares.map(middleware => middleware({ dispatch: disp, getState }));

        // 这里又对变量 _dispatch 进行了赋值。这里理解可能有点绕,后面再详细介绍
        // 注意这里是一个科里化函数的调用, 参数 dispatch 是原始,没有进过改造的
        _dispatch = compose(...chain)(dispatch);

        return {
          dispatch: _dispatch,
          getState,
          subscription,
        }
      }
    }
  }
复制代码

到这里为止,reduxjs 就基本实现了。可是咱们的探讨尚未结束,继续往下看

从上面的代码咱们能够看出来,applyMiddlyWare 函数其实就是对 createStore 的一层封装,最终输出的 dispatch 是通过中间件改造过的。如今咱们来看看这个 dispatch 究竟是什么,它和咱们传入的中间件有什么关系???

中间件原理

const chain = middlewares.map(middleware => middleware({ dispatch: disp, getState }));
  _dispatch = compose(...chain)(dispatch);
复制代码

上面的两行代码,先遍历执行中间件,再将变量 chain 传递给 compose 函数。因此咱们应该能够猜想到,表达式 middleware({ dispatch: disp, getState }) 应该返回一个函数,否则 compose 中的 reduce 就没有办法执行了。

这里还要考虑到中间件执行的策略,全部的中间件必须串联起来,挨个往下执行。因此中间件应该还应该接收另外一个中间件做为参数。因此如今咱们能够大体的猜想到一个中间件应该是这样的:

function middleware({ dispatch, getState }) {
    return function (nextMiddleware) {
      return function () {
        // 这里应该先执行一些任务,而后再去执行下一个中间件
        ...
        nextMiddleware();
      }
    }
  }
复制代码

这个时候其实中间件的模型还不够完整,少了一些东西。少了什么了,就是 action 呀!applyMiddlyWare 函数经过中间件对 dispatch 进行改造。因此仍是要接收 action 才能对 state 进行修改。因此这下咱们清楚了

function middleware({ dispatch, getState }) {
    return function (nextMiddleware) {
      return function (action) {
        // 在调用 nextMiddleware 以前能够进行一些操做
        console.log(1111);
        // 必须将 action 传递给下一个中间件
        const result = nextMiddleware(action);
        // 在调用 nextMiddleware 以后能够进行一些操做
        console.log(222);
        return result;
      }
    }
  }
复制代码

改造后的 dispatch 具体是个啥

如今咱们清楚了中间件的模型了,能够来专门研究一下 applyMiddlyWare 函数返回的 dispatch 是啥玩意了

function compose() {
    const middleware = [...arguments];
    return middleware.reduce(function(a, b) {
      return function fn () {
        return a(b.apply(null, arguments));
      }
    });
  }
  function one(next) {
    console.log('one');
    return function one_(action) {
      console.log('这是中间件one,你能够在这里作不少事情', action);
      return next(action)
    }
  }
  function two(next) {
    console.log('two');
    return function two_(action) {
      console.log('这是中间件two,你能够在next调用以前作一些事情', action);
      const result = next(action);
      console.log('这是中间件two,也能够在next调用以后作一些事情', action);
      return result;
    }
  }
  function three(next) {
    console.log('three');
    return function three_(action) {
      console.log('这是中间件three,你能够在这里作不少事情', action);
      return next(action)
    }
  }
  // 能够把它看成 createStore 函数返回的 dispatch 方法
  function dispatch(action) {
    console.log(action);
  }

  // 我这么写,你们应该能够理解哈。由于 compose 函数接收到的实际上是 middleware({ dispatch, getState }) 返回的结果
  // 因此这里的 one, two, three 能够理解为是 middleware({ dispatch, getState }) 返回的结果
  // 这里只是作一个简单的 demo,用不到 dispatch, getState。
  var disp = compose(one, two, three)(dispatch);
复制代码

咱们把 compose(one, two, three)(dispatch) 这段代码用咱们本身的代码实现一下,大体就是下面这样的效果:

var fn = (function(one, two, three) {
    var first = function() {
      return one(two.apply(null, arguments));
    };
    
    var next = function() {
      return first(three.apply(null, arguments));
    };
    return next
  })(one, two, three);

  var disp = fn(dispatch);
复制代码
  • 当调用 fn(dispatch) 时,three.apply(null, dispatch) 开始执行,返回一个 three_ 函数。继续往下执行。

  • first(three_) 开始执行,而后执行 two.apply(null, three_)two 执行完成,返回一个 two_ 函数。继续往下执行。

  • one(two_) 开始执行,并返回一个 one_ 函数,这个函数最终做为 fn(dispatch) 执行的最终结果,并赋值给变量 disp

disp(action) 执行时,先调用 one_(action) 而后是 two_(action) 最后是 three_(action)注意最后一个中间件接收的参数不是中间件参数了,而是原始的 dispatch 方法。因此会在最后一个中间件中执行 dispatch(action),从而调用 rducer 函数修改数据源【state】。

执行 disp({data: 1200, type: 'username'})这段代码,看下打印的结果是啥

这下咱们就很是清楚了,原来通过 applyMiddlyWare 改造后输出的 dispatch 方法,在调用时,会挨个执行每个传入 applyMiddlyWare 函数的中间件,并在最后一个中间件中调用原始的 dispatch() 方法。

最后本身实现一个 reduxjs 的应用

中间件定义

// 中间件1
  function thunk ({dispatch, getState}) {
    return function (next) {
      return function(action) {
        if (typeof action === 'function') {
          action({dispatch, getState});
        } else {
          return next(action);
        }
      } 
    }
  }
  // 中间件2
  function dialog ({dispatch, getState}) {
    return function (next) {
      return function(action) {
        console.log('prevstate:', getState());
        const result = next(action);
        console.log('nextstate:', getState());
        return result;
      }
    }
  }
复制代码

effects 方法定义

// 模拟用户http请求
  function getUserName(name) {
    return ({dispatch}) => {
      setTimeout(() => {
        dispatch({type: 'name', data: name})
      }, 0);
    }
  }
  function getUserAge(age) {
    return ({dispatch}) => {
      setTimeout(() => {
        dispatch({type: 'age', data: age})
      }, 0);
    }
  }
  function getUserSex(sex) {
    return ({dispatch}) => {
      setTimeout(() => {
        dispatch({type: 'sex', data: sex})
      }, 0);
    }
  }
  function getHome(value) {
    return ({dispatch}) => {
      setTimeout(() => {
        dispatch({type: 'home', data: value})
      }, 0);
    }
  }
  function getList(value) {
    return ({dispatch}) => {
      setTimeout(() => {
        dispatch({type: 'list', data: value})
      }, 0);
    }
  }
  function getDetail(value) {
    return ({dispatch}) => {
      setTimeout(() => {
        dispatch({type: 'detail', data: value})
      }, 0);
    }
  }
复制代码

初始化 state, 绑定到 DOM

// userName, menu 直接复制前面的代码
  var reducer = combineReducers({ userName, menu });

  var { dispatch, getState, subscription } = applyMiddlyWare(store)(reducer)(thunk, dialog);
  console.log(getState(), 'initState');

  const name_button = document.querySelector('.name');
  const age_button = document.querySelector('.age');
  const sex_button = document.querySelector('.sex');
  const home_button = document.querySelector('.home');
  const list_button = document.querySelector('.list');
  const detail_button = document.querySelector('.detail');
  const addListener = document.querySelector('.addListener');
  const removeListener = document.querySelector('.removeListener');

  name_button.onclick = function() {
    dispatch(getUserName('shenxuxiang'))
  };

  age_button.onclick = function() {
    dispatch(getUserAge('29'))
  };

  sex_button.onclick = function() {
    dispatch(getUserSex('man'))
  };

  home_button.onclick = function() {
    dispatch(getHome('home_page'))
  };

  list_button.onclick = function() {
    dispatch(getList('list_page'))
  };

  detail_button.onclick = function() {
    dispatch(getDetail('detail_page'))
  };

  let removeListen;
  addListener.onclick = function() {
    removeListen = subscription(function() {
      console.log('咱们添加了一个事件监听器', getState())
    })
  };

  removeListener.onclick = function() {
    removeListen && removeListen();
  };
复制代码

最后,要过年了,祝你们新年快乐。

相关文章
相关标签/搜索