主流小程序状态管理器的不足
我们在小程序日常开发中,常常需要用到全局维护的公共状态,这个时候就需要用到小程序状态管理器了。目前诸如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地址。转载请注明出处手把手教你写一个小程序状态管理器。谢谢~