小程序实现全局状态管理,告别一层层的props传递

303 阅读7分钟

写在前面

篇幅较长,不想了解其原理,直接获取使用方法可以直接前往github查看,请留下你的star。

为啥要做全局状态管理机制

小程序开发有一个弊端没有全局状态管理的机制,也就提供了一个getApp().globalData的一个全局的变量,虽然全局可以访问到但是他并没有状态,不会引起页面刷新,遇到相关兄弟组件通信,爷孙组件通信时不是用ref就是用props一层一层传递,写出的代码不优雅,也不易读,所以我想办法参考react-redux方案实现了一个小程序的全局管理状态的插件

react-redux实现的基本原理

react-redux在class-component下的基本原理就是通过HOC(也就是高阶组件)实现的props注入,然后在redux的dispatch时,在注册的高阶组件内进行setState,实现了状态的改变,从而引起组件的渲染。这里不再表述源码,下面看一段我理解的class-component下react-redux的实现原理,关于现在的主流hook实现的function-component我们就不再关注了,毕竟小程序的页面或者组件的注册方式类型与class-component相似。

let somePorps = {
    test:1
}
const componentsMap = new Map()

export function updateSomeProps(params) {
    // 相当于react-redux的dispatch,修改全局状态值,并且在每个注册的高阶组件里执行setState,引发子组件渲染
  somePorps = { ...somePorps, ...params }
  for (let compnent of componentsMap.values()) {
    compnent.setState({
      somePorps: _.cloneDeep(somePorps),
    })
  }
}

export function mappingSomePropsToWrapperComponent(WrapperComponent) {
  return class extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        somePorps: _.cloneDeep(somePorps),
      }
      // 将组件示例注册到全局的一个map里
      componentsMap.set(this, this)
    }
    componentWillUnmount() {
      componentsMap.delete(this)
    }
    componentDidMount() {
       // ref 传递
      if (typeof this.props.getInstance === 'function') {
        this.props.getInstance(this.wrapperComponentRef)
      }
    }
    updateSomeProps(someProps) {
       // 相当于react-redux dispatch
      updateSomeProps(someProps)
    }
    render() {
      const { getInstance, ...reset } = this.props
      return (
        <WrapperComponent
          ref={(ref) => {
            this.wrapperComponentRef = ref
          }}
          updateSomeProps={this.updateSomeProps}
          someProps={this.state.someProps}
          {...reset}
        />
      )
    }
  }
}

上面代买基本上就能看懂react-redux的实现方式了,下面简单分析一下

  1. mappingSomePropsToWrapperComponent是一个高阶组件,参数接受了一个想要使用全局状态的组件
  2. 在这个高阶组件的constructor内,将全局变量someProps转为自己state,并将this实例注册到一个全局的map内
  3. render方法里将高阶组件接收到的组件WrapperComponent注入相关属性someProps和updateSomeProps,这样在WrapperComponent内通过this.props.someProps就能取到这个全局的状态,通过this.props.updateSomeProps更新全局的状态,引起其他组件的render
  4. updateSomeProps就相当于redux内的dispatch,更新已经注册的各个高阶组件的state,从而引起WrapperComponent的props变化,从而引起子组件的render
  5. componentWillUnmount内删除掉已经注册的高阶组件

以上基本就是简单的react-redux实现原理了,我们接下来就参考这个原理实现了一个简单小程序的全局状态管理器

小程序实现全局状态管理

小程序内没有高阶组件一说,实际上也就没有办法做到通过属性的注入实现全局状态管理。引起渲染的机制就只有两个一个是data的变化,另外一个是props的变化,既然props方式走不通我们就只能想办法通过注入data的方式来实现这个功能。我们观察一下一般小程序的组件和页面的注册的代码结构

// 页面注册
Page({
    data:{},
    onLoad(){},
    onUnload(){}
})
// 组件注册
Component({
    data:{},
    onInit(){},
    didUnmount(){},
    methods:{},
})

市面上的小程序开发基本上都是这种结构,通过调用Page和Component方法来进行页面和组件的注册,这样就给了我们一些操作空间,那就是重写Page和Component方法,然后通过拦截注入相关生命周期函数来实现data的注入,以及所有实例的注册。下面看一下具体的实现过程

  1. 首先明确我们的使用规范参考的是react-redux,至于为什么按照这个规范来,这个就不在这里细说了,因为我们一般人肯定没有写redux和react-redux的人聪明。
  2. 实现Component的注入,Page的注入相同,就不再多写了
function componentInject() {
  // 将component方法暂存起来
  const old_component = Component;
  //重写component方法
  Component = function (prototype) {
     // 以下的两步注入实际上就是修改prototype
     
     // 将store的全局状态注入到data中
    injectStore(prototype, true);
    // 将dispatch注入到menthods中
    injectDispatch(prototype, true);
    
    // 使用修改后的prototype,注册component
    old_component(prototype);
  };
}

function injectStore(prototype) {
  //如果prototype 没有mapStateToData就不执行注入,这个方法就是告知想把全局的那个状态注入到这个组件的data里。
  if (!prototype.mapStateToData) {
    return;
  }
  
  // 将store的值注入到data中,重写onInit生命周期
    const originInit = prototype.onInit;
    prototype.onInit = function (...args) {
       // 调用mapStateToData将store的属性添加到data中
      const data = prototype.mapStateToData(store.state);
      this.setData({
        ...data,
      });
     // 注册这个组件的实例和mapStateToData到一个全局的map中,用来在dispatch时修改这个组件的data,来引发render
      register(this, prototype.mapStateToData, cloneDeep(data));
      // 执行原有的onInit生命周期
      originInit.apply(this, args);
    };
  
    // 重写didUnmount, 取消这个组件的注册
    const didUnmount = prototype.didUnmount;
    prototype.didUnmount = function (...args) {
      unregister(this.$id);
      didUnmount.apply(this, args);
    };
  
}

function injectDispatch(prototype) {
  if (!prototype.mapDispatchToMethods) {
    return;
  }
   // 将dispatch调用方法,注入到this上,方便在组件内通过this调用dispatch
    if (!prototype.methods) {
      prototype.methods = {};
    }
    prototype.methods = { ...prototype.methods, ...prototype.mapDispatchToMethods(store.dispatch) };
    return;
  
  Object.assign(prototype, prototype.mapDispatchToMethods(store.dispatch));
}

至此Component注入就完成了,在onInit时将全局store内的值注入到了data中,将dispatch方法注入到methods中,将组件实例注册到了全局map中,这个样子我们基本的全局状态的管理的架构就完成了,下面完善store和reducer的实现

  1. 实现combineReducer,首先我们得知道啥是reducer,reducer其实就是一个纯函数,我们通过执行一次reducer(state,action)能获取到一个object,执行多个不同reducer方法获取到的多个object组合起来,就得到了全局状态store内存储的state,然后组件或者页面内通过mapStateToData方法获取到这个组件需要关注的全局的状态值,
import { AddCountAction } from '../actions/testAction'

//这就是一个简单的reducer方法,通过传入state和action获取一个全局的状态值,state默认为initObj

//初始state
const initObj = {
  count: 1,
}

const testReducer = (state = initObj, action) => {
  switch (action.type) {
    case AddCountAction:
      return { ...state, count: state.count + action.payload }
    default:
      return state
  }
}

export default testReducer

不同的业务逻辑可能需要不同的reducer,我们不能在初始化或者dispatch时挨个reducer写上去执行,于是就需要一个combineReducer方法,将多个reducer方法结合起来

export function combineReducer(reducers) {
  return (state = {}, action) => {
    let newState = {};
    for (let key in reducers) {
      newState[key] = reducers[key](state[key], action);
    }
    return newState;
  };
}

combineReducer方法将多个reducer结合起来生成一个rootReducer 4. createStore实现,createStore方法就是初始化全局的状态,并且实现dispatch方法,实现组件和页面的注入

const createStore = (reducer) => {
  inject();
  // reducer就是combineReducer后生成的,获取到的object就是一个总的state
  store.state = reducer(store.state, { type: '@init' });
  store.getState = () => {
    return cloneDeep(store.state);
  };
  // dispatch实现
  store.dispatch = (action) => {
     // dispatch传入action,引起reducer返回值的改变,重新赋值store.state
    store.state = reducer(store.state, action);
    // 获取到新的state,将注册的组件或者页面挨个进行setData,触发render
    const instanceMap = getInstanceMap();
    for (const value of instanceMap.values()) {
      const { instance, mapStateToData, originData } = value;
      const data = mapStateToData(store.state);
      // 深度比较一下,上个data和当前data是否有变化,如果没变化则跳过
      if (isEqual(data, originData)) {
        continue;
      }
      value.originData = data;
      instance.setData({ ...data });
    }
  };
};
  1. 至此小程序的全局状态管理就已经实现了。只需要在应用初始化前调用createStore和注册页面和组件时添加mapStateToState方法就能实现全局的状态管理了。

上面的实现原理和使用规范参考了react-redux,使用过react-redux的一看便知,没用用过的可能会有一些学习成本。不过用上这个实现组件间的状态共享是真的便利。

上面代码的只是节选了一部分,完整代码可以去github查看,里面有具体的使用方法,对你有用的话,留下你的star。