mengtuifrontend / Blog

芦叶满汀洲,寒沙带浅流。二十年重过南楼。柳下系船犹未稳,能几日,又中秋。 黄鹤断矶头,故人今在否?旧江山浑是新愁。欲买桂花同载酒,终不似,少年游。
18 stars 5 forks source link

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

Open shenxuxiang opened 4 years ago

shenxuxiang commented 4 years ago

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

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

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

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 它是一个函数,我们可以自己定义。我们可以把我们的项目想像成如下的一个场景,修改用户的信息:

  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:

  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 作为返回值,否则就报错。

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

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

最后,要过年了,祝大家新年快乐。