redux 原理解析

186 阅读6分钟

redux 是前端的状态管理方案之一,经常在 React 项目中使用。其自身的实现用到了函数式编程和观察者模式,值得深入研究一下。

三大特性

  1. 全局唯一 store
  2. state 只读,即数据的不可变性
  3. reducer 必须是一个纯函数

单向数据流

  • 视图层调用 store.dispatch(action) 派发一个 action
  • reducer 以当前 state 和 action 为参数,匹配到对应的 action.type 计算生成一份新的 state 并返回
  • 执行 store.subscribe() 注册的监听函数,触发视图更新

createStore

  • 这个函数用来生成 store,接收三个参数,第一个 reducer 必传,后面的 initialState 和 enhancr 可选,返回的 store 上面有 getState,dispatch 和 subscribe 方法,由于 state 只能通过派发 action 修改,不允许直接访问和修改,因此放到函数内部,基本 API 实现如下:
// redux.js

export function createStore(reducer, initialState, enhancr) {
    let currentState = initialState;       // 应用的共享状态
    let isDispatching = false;
    let currentListeners = [];            // 注册的监听函数
    
    function getState() {
        if (isDispatching) throw New Error('err');
        return currentState;             // 获取当前的应用状态
    }
    function dispatch(action) {          // 修改当前的应用状态
        // 计算生成新的 state
        currentState = reducer(currentState, action);
        // state 发生改变,调用监听函数触发视图更新,观察者模式
        currentListeners.forEach(fn => fn());
    }
    function subscribe (listener) {        // 注册监听函数
        if (isDispatching) throw New Error('err');
        currentListeners.push(listener);
        return function unsubscribe() {};   // subscribe 的返回值是一个取消监听的函数
    }
    
    reutrn {
        getState,
        dispatch,
        subscribe,
    }
}
  • 这样我们在自己的项目中就可以通过 createStore 来创建 store,编写对应的 reducer 来修改状态,例如官方文档中的计数器:
reducer.js

export default function counter(state, action) {
switch(action.type) {
    case 'add':
        return {
            ...state,
            count: state.count + 1,
        }
    case '':
        return {
            ...state,
            count: state.count - 1,
        }
   default:
       return state;
    }
}
// app.js

import reducer from './reducer';
impoer { createStore } from './redux';
const initialState = { count: 0 };
const store = createStore(reducer, initialState);
store.subscribe(() => { console.log('需要更新') });   // 注册监听函数,state 发生变化就自动执行
console.log(store.getState());                     // 获取应用当前状态
store.dispatch({ type: 'add' });                   // 计数器加一
console.log(store.getState());                     // 获取应用当前状态
store.dispatch({ type: 'add' });                   // 计数器减一
console.log(store.getState());                     // 获取应用当前状态

代码链接

中间件

  • 可以看到我们已经实现了 redux 的基本功能,但此时它只能处理同步的 action;此外如果我们想添加一些额外功能比如日志记录等该怎么办呢,最简单的方法就是像上面一样手动记录,但这样肯定不行,反正 state 的改变是在 dispatch 的时候发生的,那我们可以尝试改写 dispatch 方法,让它自带日志打印的功能:
const next = store.dispatch;               // 原本只能派发 action 的旧 dispatch
store.dispatch = function(action) {        // 新改写的具有额外功能的方法
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}
  • 假如还需要有一个额外的记录错误信息的功能,那该怎么办呢,try catch 包一下:
const next = store.dispatch;               
store.dispatch = function(action) {
  try {
    console.log('this state', store.getState());
    console.log('action', action);
    next(action);
    console.log('next state', store.getState());
  } catch(err) {
    // 记录 err
  }
  
}
  • 但是这样我们相当于把不同的功能耦合到了一起,是无法良好的扩展的,比如再想增加一个派发 action 的时候输出时间戳的功能,就得继续修改 dispatch 函数,所以我们需要考虑实现具有良好扩展的多中间件模式,可以把多个功能单独分离出来:
// 负责日志打印的
const loggerMiddleware = function(action) {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

// 负责错误收集的
const exceptionMiddleware = function(action) {
  try {
    loggerMiddleware(action)
  } catch(err) {
    // err 记录
  }
}
  • 这样在使用的时候就可以先调用错误收集的中间件,传入 action,再在内部调用日志收集的中间件:
const next = store.dispatch;
store.dispatch = exceptionMiddleware;
  • 但这样的问题是中间件代码都是写死的,我们需要让中间件的调用是动态的,想调用哪个都可以,所以需要把中间件函数写成一个高阶函数:
// 负责错误收集的
const exceptionMiddleware = function(middleWare) {
    return function(action) {
      try {
        middleWare(action)
      } catch(err) {
        // err 记录
      }
    }
}

// 负责日志打印的
const loggerMiddleware = function(middleWare) {
  return function(action) {
    console.log('this state', store.getState());
    middleWare(action);
    console.log('this state', store.getState());
  }
}
  • 由于两层函数是直接 return,所以我们可以使用箭头函数改写,就是喜闻乐见的双箭头函数了:
const exceptionMiddleware = middleWare => action => {
    try {
        middleWare(action)
      } catch(err) {
        // err 记录
      }
}
const loggerMiddleware = middleWare => action => {
    console.log('this state', store.getState());
    middleWare(action);
    console.log('this state', store.getState());
}
  • 此时我们已经完成了一个扩展性良好的中间件模式,这样在调用的时候就应该是:
const next = store.dispatch;
store.dispatch = exceptionMiddleware(loggerMiddleware(next));
  • 但中间件肯定是要独立出去的,而 store 是我们自己生成的,也就是说中间件内部不能直接包含 store,这个变量也应该是执行时传入的,所以应该再加一层:
const exceptionMiddleware = store => middleWare => action => {
    try {
        middleWare(action)
      } catch(err) {
        // err 记录
      }
}
const loggerMiddleware = store => middleWare => action => {
    console.log('this state', store.getState());
    middleWare(action);
    console.log('this state', store.getState());
}

const store = createStore(reducer);
const next  = store.dispatch;

const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
store.dispatch = exception(logger(next));
  • 现在,真正独立可扩展的中间件就完成了,比如现在需要加一个输出时间戳的功能,就可以:
...
const timeMiddleware = store => middleWare => action => {
  console.log('time', Date.now());
  middleWare(action);
}
const time = timeMiddleware(store);
store.dispatch = exception(time(logger(next)));

applyMiddleWare

  • 在 redux 中我们已经有了 store,其实只需要知道准备使用哪些中间件就可以,其他细节都可以封装到 redux 自身的实现中,并且这种依次嵌套的调用方式不那么友好,反正实现的功能就是若干个函数(中间件)依次调用且一个函数的输出结果是下一个函数的输入,因此可以采用 compose 的方式去组合函数:
// 之前的调用方式
const store = createStore(reducer);
const next  = store.dispatch;
...
store.dispatch = exception(time(logger(next)));

// 期待的调用方式
const newCreateStore = applyMiddleWare(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore)

// 生成一个 dispatch 方法被重写(增强)后的,有了额外功能 store
const store = newCreateStore(reducer);

第一版 applyMiddleWare

/**
 * applyMiddleWare 函数接收若干个中间件作为参数
 * 执行的返回结果是一个函数,对应 createStore 中的第三个参数 enhancer
 * 用来接收旧的 createStore,返回一个新的 createStore,其内部的 dispatch 是经过增强的
 */

export function applyMiddleWare(...middleWares) {
  return function enhancer(createStore) {
    return function(reducer, initialState) {
      const oldStore = createStore(reducer, initialState);
      let dispatch = oldStore.dispatch;
      // 传入 store,进行第一次调用
      const middleWareChain = middleWares.map(middleWare => middleWare(oldStore));
      middleWareChain.reverse().forEach(middle => {
        dispatch = middle(dispatch)
      })
      return {
        ...oldStore,
        dispatch,
      }
    }
  }
}

代码链接

使用 compose 聚合中间件,同时保证中间件执行顺序

compose 是函数式编程里常用的方法,作用是把类似于 var a = fn1(fn2(fn3(fn4(x)))) 这种嵌套的调用方式改成 var a = compose(fn1,fn2,fn3,fn4)(x)。compose 的运行结果是一个函数,调用这个函数传递的参数将会成为 compose 最后一个参数成员的参数,从而实现类似洋葱圈的从内向外,逐步调用方式:

function compose(...funcs) {
  if (funcs.length === 0) return () => {};
  if (funcs.length === 1) return funcs[0];
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

...
dispatch = compose(...middleWareChain)(dispatch)
return {
  ...oldStore,
  dispatch,
}
  • 写一个简单的 thunk 中间件,测试异步 action:
// thunk.js

export default function thunk(store) {
  return function (middleWare) {
    return function(action) {
      if (typeof action === 'function') return action(middleWare, store);
      return middleWare(action)
    }
  }
}

总结

  • 这样 redux 的核心功能就实现了,其实主要就是其中间件的实现。此外还有 combineReducers 和用于注销订阅的 unsubscribe 函数,比较简单,可以直接去看源码了。

本文使用 mdnice 排版