# mstore

`mstore`是内存型管理组件状态和组件通信的工具

## 简介

小程序官方并没有提供组件（页面）间的状态管理工具，组件通信方式也比较古老（可类比JQuery时代的模式），三中情况下的通信方式分别是

+ 父 -> 子
  + 通过properties传递属性值
  + 通过this.selectComponent读取组件实例，直接操纵实例（高耦合低内聚，大雾！！）

+ 子 -> 父
  + triggerEvent事件传递

+ 兄弟之间，隔代兄弟
  + 链式传递，子1 -> 父 -> 子2（接触过vue或react SPA开发后怎能接受这种方式）
  + 定义事件中心，通过自定义事件传递(事件管理比较耗费心智)
  + 使用全局变量getApp()实例（大雾！）

`mstore` 主要解决的是兄弟节点状态传递的问题，其核心思想是在组件外创建一个全局内存区域，
组件与`mstore`直接通信，简化通信方式，让通信有迹可循。

`mstore`提供了以下几种能力

+ state: 定义组件共享的数据状态，可读可写
+ getter: 定义组件共享的数据状态getter拦截器，可以配合setter使用，也可以独立使用
+ setter: 定义组件共享的数据状态setter拦截器，可以配置getter使用，也可以独立使用
+ watch：定义响应式action，通过观测mstore中的state变化，执行注册的action
+ method：定义mstore的内聚的功能，可以将通用的状态逻辑封装再mstore内
+ event：定义事件中心事件，管理自定义事件

## 与Vuex，Redux对比

Vuex 的状态存储是响应式的，因为Vue从原生上就支持了响应式，通过编译手段收集依赖，
注册响应关系，因此，Vuex只有`state`、`getter`并且是单项数据流，不允许直接改变state的值，
需要通过`mutation`或`action`修改`state`状态，完成单项数据流的闭环，并且因为响应式，
所以`state`的变化可以自动更新到view中。

Redux的状态管理更纯粹，只示范了state的定义，修改与订阅方式，结合React时需要借助React-Redux，
但用起来也很繁琐，需要通过层层的函数过程来完成state的改变。
state的变化订阅则是通过在顶级App组件包裹context的方式，并利用connect生成的HOC中订阅变化，
触发被connect的组件的更新，实现state变化自动更新view的功能。

mstore没有实现Vuex的响应式更新是因为小程序本身不是响应式的，虽然可以通过一定的手段实现响应式，
但setData本身的性能限制可能会导致在一定的场景中出现不必要的更新而引发性能问题，
因此，mstore还是放弃了这种实现。但mstore借鉴了Vuex简洁的接口格式和使用方式，所以其使用上和Vuex很像。

mstore没有采用Redux的模式是因为使用起来太繁琐，而且其状态变化的订阅太粗放，
随着状态数量的增加，函数调用过程的开销也越来越大，这对于嵌在微信中的小程序来说稍显吃力，
小程序需要更快速，更直接的订阅变化的方式，因此，mstore提供了watch接口，直接订阅关心的state，
变化后执行回调函数，从而达到直接、快读的细粒度订阅。小程序没有HOC的概念，
因此通过HOC实现自动view更新也不太可能。

mstore提供了event事件中心的功能，是因为确实存在一定的更适合event的场景，
比如点击提交按钮，每次点击都要做出响应，但没有任何相关的state发生变化，
它就是一个事件，需要对事件做出响应，这种情况就是适合event的场景，
虽然可以通过一个随机变量或时间戳的state来强行记录状态的变化，
利用state变化驱动点击动作的响应，但这未免显得有些别扭，
所以，mstore在接口中提供了event管理。

## 使用说明

1. 安装依赖

  ```shell
  tnpm i -S @woa/mstore
  ```
2. 构建npm

  步骤请参考[官方文档](https://developers.weixin.qq.com/miniprogram/dev/devtools/npm.html)

3. 项目中使用

  ```javascript
  // store/bar.js
  const bar = {
    name: 'bar',     // 必填，模块名称
    state: {         // 选填，模块的状态，任意类型变量，非函数
      str: 'str',
      obj: {},
      arr: [],
      bool: true,
      _foo: null,
    },
    getter: {        // 选填，模块的getter属性，可与setter配合，也可以独立使用，使用函数定义，不能接收参数
      foo() {
        return this._foo   // return是必须的
      }
    },
    setter: {        // 选填，模块的setter属性，可与getter配合，也可以独立使用，使用函数定义，只接收一个value参数，即被赋值的值
      foo(value) {
        this._foo = value
        // return this.bar   // 不需要return，return是无效的
      }
    },
    method: {        // 选填，模块的方法，函数，可带任意数量参数
      baz(...args) {
        // 使用this访问定义在本模块中的state，getter/setter, method，event, 等
        this.str = ''             // 修改state
        const obj = this.obj      // 使用state
        this.foo = 123            // 修改setter
        this.arr.push(this.foo)   // 使用getter
        this.emit(this.event.update, this.boo, obj) // 派发event
        const tmp = this.asyncFn() // 调用method
        const otherModule = this._$store.global // 通过this._$store可以访问到其他module
        return this.bool          // 返回值可以只同步，也可以是异步
      },
      syncFn() {  // 同步函数
        return this.asyncFn()
      },
      asyncFn() { // 异步函数
        return Pormise.resolve()
      },
    }
    event: [         // 选填，模块的支持的事件名，需要先声明（字符串）才能使用
      'click',
      'update',
      'delete',
    ]
  }
  export default global   // 对外暴露整个模块对象

  // store/global.js
  const global = {
    name: 'global',
    state: {
      dev: true,                  // 环境参数，控制API请求等
    },
    getter: {
    },
    method: {
    },
  }
  export default global

  // store/index.js
  import initStore from '@tencent/mstore'   // 引入mstore
  import bar from './bar'                   // 引入各个module
  import global from './global'

  const store = initStore({                 // 初始化store
    global,
    bar,
  })

  // 为了能使用 import { module } from 'store/index' 进行声明
  // 需要使用commonjs规范对外暴露接口
  module.exports = store

  // app.js
  import { global } from './store/index'  // 在需要使用store的地方按模块引入

  App({
    onLaunch() {
      console.log(global.dev === true)
    }
  })

  // pages/pageA.js
  import { global, bar } from '../store/index'

  Page({
    data: {
      some: ''
    },
    onLoad() {
      console.log(bar.str === 'str') // 读取state
      // 事件监听，第一个参数是事件名，需要在module的event中先声明，
      // 第二个参数是事件的回调，尽量不要写匿名函数(箭头函数)，有内存泄露的风险`
      // 回调函数中如果需要使用到this，需要使用bind方法重定向this
      this.clickCallbak = this.clickCallbak.bind(this)
      bar.on(bar.event.click, this.clickCallbak)
    },
    onShow() {
      if (global.dev) {
        console.log('这是开发环境')
        bar.obj.attr1 = ''    // 修改state, pageB的watchCallback1会执行
        if (bar.foo === 1) {  // 读取getter
          bar.foo = 2         // 修改setter
        }
        bar.asyncFn().then(() => {  // 调用异步method
          bar.syncFn()    // 调用同步method
        })
      }
      ajaxSth().then(data => {
        bar.obj.attr2 = data // 网络请求后修改state，pageB的watchCallback2会执行
        this.someFn1()
      })
    },
    onUnload() {
      // 取消事件监听，防止内存泄露
      // 第一个参数是事件名
      // 第二个参数是事件的回调，如果不填写会销毁这个事件的所有监听回调
      // 包括在其他页面注册的监听函数
      bar.off(bar.event.click, this.clickCallbak)
    },
    someFn1() {

    },
    clickCallbak(...args) {
      console.log('click事件被触发，携带参数有', ...args)
    },
  })

  // pages/pageB.js
  import { global, bar } from '../store/index'

  Page({
    data: {
      some: ''
    },
    onLoad() {
      // 观测state变化，发生变化后执行回调，mstore会进行浅diff，发生diff才会执行回调
      // 如果回调函数需要使用this，需要使用bind方法重定向this
      // 第一个参数是被观测的state，字符串类型，支持路径格式'a.b[0]c',
      // 被观测的state要先声明才能被观测，比如'a.b.c' state要先声明state: { a: b: {} }， 否则观测不到变化
      // 动态的objec不能被观测到
      this.watchCallback1 = this.watchCallback1.bind(this)
      this.watchCallback2 = this.watchCallback2.bind(this)
      bar.watch('obj.attr1', this.watchCallback1)
      bar.watch('obj.attr2', this.watchCallback2)
    },
    onShow() {
      if (global.dev) {
        console.log('这是开发环境')
        bar.emit(bar.event.click, 1, 2, 3)    // 派发事件，pageA的clickCallbak回调会执行
      }
    },
    onUnload() {
      // 注销观测回调，防止内存泄露
      bar.unWatch('obj.attr1', this.watchCallback1)
      bar.unWatch('obj.attr2', this.watchCallback2)
    },
    watchCallback1(newVal, oldVal) {

    },
    watchCallback2(newVal, oldVal) {
      console.log('obj.attr2发生变化新值是', newVal)
    },
  })

  ```

## 限制

`mstore`是内存型状态管理工具，因此其生命周期在小程序启动那一刻开始，在小程序被清退内存而结束。

小程序切换到后态，并没用终止进程的情况下，`mstore`依旧是存活状态，即使是打开一个新的分享连接，或识别小程序码进入新的页面，`mstore`依旧是缓存在内存中，只有真正的冷启动才会重新初始化`mstore`。

由于是内存型，在使用`mstore`时不仅要关注其初始化和销毁时机，更要注意发生内存泄露情况的发生！
