当前小程序项目全局状态及逻辑判断太复杂,尝试引入状态管理优化逻辑,同时将重复的逻辑判断放到getters中。这里我们使用mobx4,因为mobx使用了小程序不兼容的proxy。

mobx4介绍

主要是这几个api:

  • observable
  • get
  • autorun
  • action

我们引入observable创建一个store,创建一些观察对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {observable, autorun} from 'mobx'

const store = observable({
// 响应状态
value: 1,
list: [],
// computed
get count () {
return this.list.count
}
// actions
setValue (val) {
this.value = val
}
addItem (item) {
this.list.push(item)
}
})

autorun(() => {
console.log(store)
})

其中get可以创建计算属性,其他函数为action,修改状态值。当值发生修改时,可触发autorun函数。

observable状态对象需要通过toJS转换为js对象。

引入mobx状态管理

小程序引入mobx,可以用observable创建一个store对象,然后类似react中使用,需要创建一个connect函数,来把需要的状态值转换为js对象,再注入页面或组件对象。

这里我找了一个第三方库mobx-wxapp,下面是它的connect方法和使用。

connect方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 映射所需的数据到data
* @param {Object} context
* @param {Function} mapDataToStore
* @param {Object} options
*/
function connect (context, mapDataToStore, options = {}) {
if (!isTypeFunction(mapDataToStore)) {
throw new TypeError('mapDataToStore 必须是一个function')
}

// const delay = options.delay || 30 // setData执行的最小间隔
const callback = options.setDataCallback || (() => {}) // setData的回调

let tempdata = {}
// let last = 0
const update = nextdata => {
Object.assign(tempdata, nextdata)
// clearTimeout(last)
// last = setTimeout(() => {
const newValue = diff(context.data, tempdata)
console.log('new data:', newValue)
context.setData(newValue, () => {
callback(newValue)
})
tempdata = {}
// }, delay)
}
const func = mapDataToStore
mapDataToStore = function () {
const data = func()
for (let k in data) {
const item = data[k]
if (
isObservableObject(item) ||
isObservableArray(item) ||
isObservableObject(item) ||
isBoxedObservable(item) ||
isObservableMap(item)
) {
data[k] = toJS(item)
}
}
update(data)
}
const disposer = autorun(mapDataToStore)
const onUnload = context.onUnload
if (onUnload) {
context.onUnload = function () {
disposer()
onUnload.apply(context, arguments)
}
}
return disposer
}

其中的防抖函数被我注释了,虽然对性能有优化,但会造成我之前代码的回调触发顺序错误。

可以看到核心代码就是在connect函数执行时,将原先的mapDataToStore转换为js对象去合并旧的data对象,然后执行authrun函数去监听变化,当发生变化时,执行重写的mapDataToStore方法,将每个状态值转换为js对象,再setData到当前context。

在页面使用:

1
2
3
4
5
6
7
8
9
Page({
onLoad() {
connect(this, () => ({
value: store.value,
list: store.list,
})
)
},
})

在组件使用:

1
2
3
4
5
6
7
8
9
10
11
12
Component({
//..
ready(){
this.disposer = connect(this,mapDataToStore,options)
},

//请务必在组件生命周期结束前执行销毁器!
detached(){
this.disposer();
}
//...
})

存在的分包问题及解决方式

在实际使用中,发现跨分包时,引入的store对象的值都是初始值,而不是主包store的当前状态。其实,我们去看一下文档就能发现,es模块的import多次引入同一个文件,只会执行一次,后面再次引入会直接拿到这个对象,所以我们可以在各个js文件import同一个模块,而不用担心两个引入的对象不一样。

但是小程序如果开启了分包,情况就变了,每个分包的js环境都是相对独立的,所有不同分包的store对象并不相同。

因此,我们不能直接在各个分包import这个store模块,而是应该在app.js引入一次,然后把它挂到公用的app对象上。之前还遇到了消息队列在分包不等待主包阻塞的问题,应该也是同一个原因,这次一起把api挂到app上。

1
2
3
4
5
6
7
8
9
10
import * as apis from '/apis/api'
import globalStore from '/stores/index'

App({
stores: {
globalStore,
},
apis,
...
})

拆分store及依赖关系

随着业务的复杂,我们需要将store按照不同业务进行拆分,以便更清晰的管理状态和业务。这多个store是怎样的结构,相互间的依赖关系呢?我们可能存在一些公用状态,然后其他store的计算属性依赖了这个状态。

按照官网的例子,如果store是用class创建的,可以在new其他store时,在构建函数传入rootStore,再把rootStore挂在子store上以便访问,也可以把子store挂载rootStore上。

这里我们之前是用observable直接创建的一个对象,就不改成class了。在store目录新建多个store文件,分别导出各个store的对象。在index.js中引入所有store,一起export。

1
2
3
4
5
6
7
import globalStore from './globalStore.js'
import subscribeStore from './subscribeStore.js'

export default {
globalStore,
subscribeStore,
}

在app.js也是引入整个stores,挂在app上。

1
2
3
4
5
6
import stores from '/stores/index'

App({
stores,
...
})

重点是,在subscribeStore中新建一个计算属性,依赖globalStore中的属性进行计算。当我们调用这个属性是,当然会触发,但是我们修改globalStore中的属性,这个计算属性是否会自动执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { observable } from '/utils/mobx/mobx'
// import config from '../config'
// import { MPROVINCECODE } from '/common/constance'
import globalStore from './globalStore'

const subscribeStore = observable({
// observable
selectMemberLevel: '2',

// computed

// 授权
get testFlag () {
consoel.log('globalStore.userInfo has changed')
return globalStore.userInfo && globalStore.userInfo.mciGyUd != null && globalStore.userInfo.mciGyId !== ''
},

// actions
setData (data) {
for (const key in data) {
this[key] = data[key]
}
},
})

export default subscribeStore

如上,我们在subscribeStore中创建了一个计算属性,依赖globalStore的userInfo。再在页面js中connect注入这个属性,在页面axml绑定渲染,然后我们在控制台中获取app.globalStore,手动修改userInfo,可以看到控制台触发了这个console,并且页面渲染改变了。说明在一个store中可以监测到另一个store的依赖变化。

注意:不要循环依赖。公用的状态都放到globalStore中。