手把手教你写一个小程序状态管理器

1,033 阅读6分钟

主流小程序状态管理器的不足

我们在小程序日常开发中,常常需要用到全局维护的公共状态,这个时候就需要用到小程序状态管理器了。目前诸如omix等状态管理器在使用的时候,随着公共状态越来越大,操作越来越频繁,就会对性能造成越来越大的负担。具体就体现在小程序audits检查时,常常会报“存在setData调用过于频繁---1秒内执行了 xxx 次setData”。你可能会觉得奇怪,我没有这么频繁的操作setData呀?是不是这个检测工具搞错了呀?
事实上,这就是状态管理器造成的,我们一起来看看,小程序的状态管理器是怎么实现的,以及它是如何造成setData频繁调用状况的。

小程序的状态管理器实现原理

小程序每个页面都是一个page实例,内部并没有一个比较好的全局对象来维护全局状态,因此omix是这样实现的:

export default create
create.Page = _Page
function create(store, option) {
  if (arguments.length === 2) {
    if (!store.instances) {
      store.instances = {}
    }
    getApp().globalData && (getApp().globalData.store = store)
    option.data = option.data || {}
    option.data.store = store.data
    observeStore(store)
    const onLoad = option.onLoad

    option.onLoad = function (e) {
      this.store = store
	  // ...
      onLoad && onLoad.call(this, e)
    }
    Page(option)
  } else {
    // ...
  }
}

omix导出一个方法create,将store数据进行了响应式的处理,并劫持了小程序Page方法,注册页面实例后将实例在一个对象里维护。

function _update(kv, store) {
  // store.instances存储的是以页面路由为key 页面实例为值的数组
  console.log(store.instances)
  for (let key in store.instances) {
    store.instances[key].forEach(ins => {
      // 实例修改挂载在实例data里的store
      ins.setData.call(ins, kv)
      updateStoreByFnProp(ins, store.data)
    })
  }
  // 如果有store的onChange方法 执行
  store.onChange && store.onChange(kv)
  // 在控制台打印store更改的log
  config.logger.isOpen && storeChangeLogger(store)
}

程序一旦对store的数据进行更改,就会触发属性的set方法,set方法调用_update函数,对所有页面实例遍历,并将实例中对应的属性进行setData处理。
这样一来,就发生了我们一开始的时候所说的,频繁操作setData的情况了。
频繁setData,会在视图层和数据层之间频繁通讯,消耗内存,降低应用的性能。因此aHu小程序脚手架工具就应运而生了。

aHu小程序脚手架工具

ahu是我为了解决小程序的更合适模板的问题而写的一个项目,其中就实现了一个轻量化的状态管理器,这是项目地址及截图:github:aHu小程序脚手架 请拼命帮我star哈哈

频繁setData解决思路

既然之前的问题是在同一时间遍历所有页面实例并setData,我们可不可以把这个工作放在每个实例的生命周期onShow里面呢?这样做有两个好处:

  • 可以在页面真正使用并加载时,才调用setData,降低瞬时调用造成内存膨胀
  • 可以将各个属性值变化合并,进一步减少setData调用量

同时,我们注意到store里面有很多数据是用于逻辑层判断的,并不需要渲染到页面上,因此我还对store数据进行分拆,拆成storeView和storeModel,只有storeView里的数据变化,才会触发setData。

实现的核心代码

第一步我们还是和omix一样,导出方法,让每个页面调用来注册Page,同时对store数据做响应式处理。重写了onLoad方法和onShow方法,其中onShow方法判断该实例的_remainChangeView变量是否有值,若有,则执行一次合并的setData,若无,则啥也不做。

const aHu = (options) => {
  _init(options)
}

const _init = (options) => {
  // 判断路由队列
  if (!store.routeToVm) {
    store.routeToVm = {}
  }
  let user_onLoad = options.onLoad
  // reset option onload
  options.onLoad = function(e) {
    // 同一个page的this也会不同
    let vm = this
    // 先判断是否已被响应式处理
    if(!_isObr(store)) {
      // 如果没有响应式 将未响应处理的数据挂在_noOb上
      store._noObView = JSON.parse(JSON.stringify(store.storeView))
      store._noObModel = JSON.parse(JSON.stringify(store.storeModel))
      // 挂载 storeView会在页面展示 修改需调用setdata
      options.data.storeView = JSON.parse(JSON.stringify(store._noObView))
      options.data.storeModel = JSON.parse(JSON.stringify(store._noObModel))
      // 响应式处理
      observer(store.storeView, 'storeView')
      observer(store.storeModel, 'storeModel')
    } else {
      options.data.storeView = Object.assign({}, store._noObView)
      options.data.storeModel = Object.assign({}, store._noObModel)
    }
    // 用来记录待更改state
    options.data._remainChangeView = Object.create(null)
    vm.store = store
    // onload时将实例插入队列
    store.routeToVm[vm.route] || (store.routeToVm[vm.route] = [])
    // 栈里两个同样页面不同vm时 __wxExparserNodeId__不同
    store.routeToVm[vm.route].push(vm)
    // setdata
    vm.setData(options.data)
    // 调用传入的onload
    user_onLoad && user_onLoad.call(this, e)
  }
  let user_onShow = options.onShow
  options.onShow = function(e) {
    // 将未修改的数据修改
    let vm = this
    // console.log(vm.data._remainChangeView, Object.keys(vm.data._remainChangeView))
    if (Object.keys(vm.data._remainChangeView).length) {
      //  console.log(vm.data._remainChangeView)
      let remainChangeView = JSON.parse(JSON.stringify(vm.data._remainChangeView))
      vm.setData(remainChangeView)
      vm.data._remainChangeView = Object.create(null)
    }
    user_onShow && user_onShowcall(this, e)
  }
  // 调用Page
  Page(options)
}

响应式处理,我们主要在set方法针对storeView和storeModel进行分别处理,同时对当前实例马上执行setData即时渲染在视图上,对路由栈里的实例进行维护,并修改他们的_remainChangeView,使得他们在onShow的时候就可以执行渲染。(注意,路由返回后会失效,要同时对失效的路由实例推出store.routeToVm

// 设置响应式 最后一个val防止死循环栈溢出
const _defineReactive = (data, key, path, val) => {
  Object.defineProperty(data, key, {
    get: function() {
      return val
    },
    set: function (value) {
      let pages = getCurrentPages()
      let currentVm = pages[pages.length-1]
      // 更改并遍历store.routeToVm
      let oldValue = JSON.parse(JSON.stringify(val))
      if (JSON.stringify(val) === JSON.stringify(value)) return
      // 设置store.storeView或store.storeModel
      val = value

      // 设置store类型
      let type = path.startsWith('storeView')

      // 设置noOb
      let noObView = store._noObView
      let noObModel = store._noObModel

      path.split('.').forEach((item, index) => {
        if (index > 0) {
          type ? noObView = noObView[item] : noObModel = noObModel[item]
        }
      })
      type ? noObView[key] = JSON.parse(JSON.stringify(value)) : noObModel[key] = JSON.parse(JSON.stringify(value))

      // 重新设置的对象需要响应式处理 是数组则重设原型方法
      _walkChild(val, path, data, key)

      Object.keys(store.routeToVm).forEach((item, index) => {
        // 除了当前路由 其他页面的setdata分别插入他们的onshow里执行 防止性能开销过大
        // 重复的页面不同的vm单实例设置是可以影响同页面不同实例的data 但不会渲染到其他页面视图上
        // 所以要遍历同路由页面的不同实例
        let dataSign = `${path}.${key}`
        store.routeToVm[item].forEach((vm, rIndex) => {
          if (pages.some(page => page === vm)) {
            // 在路由列表中才改变
            if (type) {
              if (vm === currentVm) {
                // 只有当前实例改变 奇怪的是会改变当前实例的渲染和其他实例的data 但不改变其他实例的渲染
                vm.setData({
                  [dataSign]: JSON.parse(JSON.stringify(value))
                })
              } else {
                vm.data._remainChangeView[dataSign] = JSON.parse(JSON.stringify(value))
              }
            } else {
              // 逻辑层数据因为不涉及渲染 直接改动所有vm的值
              let modelPath = vm.data
              path.split('.').forEach(item => {
                  modelPath = modelPath[item]
              })
              modelPath[key] = JSON.parse(JSON.stringify(value))
            }
          } else {
            // 在store.routeToVm中删除其无效vm
            store.routeToVm[item].splice(rIndex, 1)
            // console.log(item, store.routeToVm[item])
          }
        })
      })
      _log(`${path}.${key}`, oldValue, value)
    }
  })
}

以上就是改写小程序状态管理器的核心思想和代码,这里还实现了响应式处理的递归遍历walk方法,新设置属性的响应式处理以及数组的原型方法的重写等,有兴趣的小伙伴可以直接点store源码

结语

总的来说,小程序状态管理器就是使用Object.defineProperty()进行响应式处理,在拦截的set方法里面进行每个页面实例的处理,我的处理是对当前实例执行setData立即渲染,对其他实例不立即执行,而是在每个实例的属性上维护,并在他们的onShow里执行setData渲染。
码文字不易,请大家多多star,github地址。转载请注明出处手把手教你写一个小程序状态管理器。谢谢~