微信小程序官方在 2019 年针对小程序发布了 MobX 辅助绑定库,可以让我们在微信小程序中使用 Mobx 进行状态管理:
- mobx-miniprogram:相当于 MobX;
- mobx-miniprogram-bindings:针对小程序的 MobX 辅助绑定库,类似 mobx-react;
这篇文章和大家分享为什么要使用 MobX,我对 Mobx 核心概念的理解,以及如何在微信小程序中使用 Mobx。
为什么要用 Mobx
关于为什么要在小程序使用 MobX,可以看小程序 MobX 绑定库官方开发者的一个回复:
问:打包成 json 页面传值不是也很香吗?跨的页面比较多使用全局变量应该也可以吧?
答:那样的话每次数据变动都得传,是"过程式"的方法。状态管理框架在处理数据项需要在多个页面间同步的情况中会更加方便。
再看另一个问答:
问:在小程序中,可使用 mobx-miniprogram 配合 mobx-miniprogram-bindings 实现全局数据共享,也可以用 globalData 是实现数据共享。请问登录之后的用户信息应该放在哪里?
答:globalData 一般适用于放置一些极少改变的全局状态。事实上,也可以通过 require 一个独立的 js 文件来代替 globalData 。MobX (和类似的状态管理库)适用于维护需要跟踪更新的界面状态数据。所以用户信息还是建议放在 globalData 里管理。
Mobx 的核心概念
要在微信小程序中使用 MobX,首先要了解 MobX,要了解 MobX,首先要知道 MobX 的三个核心概念:
- observable:顾名思义,这是一个可以被监测的对象,对象的各个属性通常被用来存放应用的状态(state),所以我们可以通过监测一个 observable 达到监测应用状态的目的,创建一个 observable 的方法是给 MobX 的 observable 函数 传入一个对象(包括数组),例如:
javascript
const { observable } = require("mobx-miniprogram");
const myObservable = observable({ name: "kejiweixun" });
- action:一段改变 state 的代码,比如上面的例子,如果我的网页中有一个按钮,点击该按钮会触发一个事件函数,该事件函数会改变 name 这个 state,那这个函数就是一个 action,myObservable.name = "keke" 也是一个 action,因为它也会改变 name,但最佳做法是使用 action 函数显式告诉 MobX 这段代码是一个 action,action 函数需要接收一个函数作为参数,即它只能把一个函数(可以是 async 函数)显式声明为 action,我们可以把 observable 对象中的 method 声明为 action(如果它改变了 state 的话),也可以把一个事件函数声明为对象;
javascript
const { observable, action } = require("mobx-miniprogram");
const state = observable({ value: 0 });
const increment = (state) => {
state.value++;
};
//即使没有使用 action 函数,increment 也是一个 action,
//因为它修改了 observable,但建议总是使用 action 函数进行显式声明
const incrementAction = action(increment);
increment(state);
- derivations:从 state 衍生出来的东西,分为:
- Computed values:在 observable 对象中增加一个 getter,observable 函数会自动地把它处理成一个 computed value,computed values 需要是 pure function,它不应该改变任何 state,只是从 state 中生成新的值;
- Reactions:类似 Computed values,但 Computed values 是产生一个新的值,而 reactions 是产生一个 side effect,例如把最新的 state 显示在屏幕上就是一个 side effect,对于 react,是通过 mobx-react 这个 MobX binding 实现 reaction 的,而对于微信小程序,则是开头提及的 mobx-miniprogram-bindings。我们还可以使用 autorun、reaction 执行自定义 reactions,autorun 常用来打印日志。
以上三个核心概念的关系可以通过下面这张图片理解:
在微信小程序中使用 Mobx
在微信小程序中使用 MobX 需要同时安装以下两个依赖,这在开头就说过了。第一个依赖其实是 MobX 官方项目的 fork,把它理解成 mobx 即可;第二个依赖是针对小程序的 binding,MobX 为多个 web 框架分别开发了对应的 binding,例如 React 的是 mobx-react,Vue 的是 mobx-vue。
- mobx-miniprogram
- mobx-miniprogram-bindings
构造一个小程序页面有两种方法,一种是使用 Page 构造器,另一种是使用 Component 构造器,这里只分享使用 Component 构造器构造页面时 mobx-miniprogram-bindings 的使用。以下是一个例子:
javascript
// store.js,在这个文件中创建一个 observable 对象
import { observable, action } from "mobx-miniprogram";
export const store = observable({
// 数据字段
numA: 1,
numB: 2,
// 计算属性,前面说过了,getter 会被 observable() 处理成一个 computed value
get sum() {
return this.numA + this.numB;
},
// actions,前面说过了,修改了数据字段的代码就是 action,需要用 action() 显式声明
update: action(function () {
const sum = this.sum;
this.numA = this.numB;
this.numB = sum;
}),
});
javascript
//使用 Component 构造小程序页面,引入 storeBindingsBehavior,并增加 storeBindings 属性,该属性规定有 store、fields、actions 三个字段
import { storeBindingsBehavior } from "mobx-miniprogram-bindings";
import { store as myStore } from "./store.js";
Component({
behaviors: [storeBindingsBehavior],
storeBindings: {
store: myStore, //使用 observable 函数创建的 observable 对象,常被叫做 store
fields: ["numA", "numB", "sum"], //observable 对象中的数据属性、计算属性(getter)
actions: ["update"], //observable 对象中的 action
},
});
mobx-miniprogram-bindings 的核心是它 export 的 storeBindingsBehavior,它的作用主要有三个:
- 处理 storeBindings.fields:当小程序页面进入页面节点树时,createDataFieldsReactions 函数会被调用,该函数会使用 MobX 的 reaction 函数创建一些自定义 reaction,这些自定义 reaction 的具体任务是看看 storeBindings.fields 中各字段的值是否发生了变化,如果变化了,就调用微信小程序的 this.setData() 把更新后的值渲染到页面;
- 处理 storeBindings.actions:当页面引入该 storeBindingsBehavior 时,storeBindingsBehavior 中的 definitionFilter 函数会被执行,该函数会进一步调用 createActions 函数,从而把 observable 对象中相应的 action(例如代码示例中的 update)挂载到页面的 method 对象中,这样页面就可以这样触发该 action:this.update(),其实等同于 myStore.update();
- storeBindingsBehavior 还定义了一个 detached 生命周期函数,其工作内容是:当页面从页面节点树移除时,给前面定义的所有 reaction 函数传入一个空参数,即执行 reaction(),从而避免内存泄漏。
总结一下,mobx-miniprogram-bindings 的核心是 storeBindingsBehavior,而 storeBindingsBehavior 的核心是 createDataFieldsReactions、createActions。顾名思义,createDataFieldsReactions 是为 storeBindings.fields 中的各字段创建自定义 reaction,createActions 是把 storeBindings.actions 中的各 action 函数挂载到页面下方便调用。
需要注意的是,storeBindings.actions 的各 action 会被挂载到页面的 this.methods 下,所以我们可以像 this.update() 这样调用它,而 storeBindings.fields 并没有被挂载到 this.data 下,但我们依然可以像 this.data.numA 这样访问它的值,为什么?这是因为 createDataFieldsReactions 函数所创建的 reaction 在调用 setData 时,会把值 set 到页面中同名的 data 字段中,比如 numA 这个 field 所对应的 reaction 会这样 setData:this.setData({numA: 1})。于是,页面本来是没有 this.data.numA 这个字段的,被这个 reaction 一番操作后,就多了这么一个字段。
createDataFieldsReactions 在创建一个 reaction 时,把 fireImmediately 设置为了 true,如果把它改为 false,在页面的 onload 函数的开头位置访问 this.data.numA 会得到 undefined,这充分证明了这个页面本来确实没有 this.data.numA,后来之所以有了,是被 reaction setData 上去的。
所以要注意了,我们不应该在页面的 js 文件中使用 setData 赋值给与 storeBindings.fields 同名的字段,正确的做法应该是使用 storeBindings.actions 修改 storeBindings.fields 字段的值,剩下的就交给 MobX,你可以相信 MobX 会把这个值渲染到界面中,如果这个值发生了变化的话。
MobX 的原则是:
Make sure that everything that can be derived from the application state, will be derived. Automatically.
这也应该是我们使用 MobX 的原则:定义最基本的 state,所有可以从这些 state 衍生出来的 computed value 或 reaction,都应该让 MobX 自动衍生,避免人为干预。所有修改 state 的函数或方法都应该显式声明为 action,我们只负责触发 action,剩下的都交给 MobX:action 触发会导致 state 发生改变,这进一步会导致 computed value 发生改变,以及 reaction 做出反应,这些都是由 MobX 自动进行的,交给 MobX 吧,我们只负责触发 action。
使用 MobX 容易让人产生困惑的一点是,如果一个 observable 对象的某个属性的值是一个对象(下方示例的 name),并且某个 action 被触发后只修改了该对象的某个属性(下方示例的 name.first),那 mobx-miniprogram-bindings 是不会把修改后的值渲染到界面中的。
示例代码:
javascript
!-- index.html -->
<view>First Name: {{name.first}}</view>
<view>Last Name: {{name.last}}</view>
<button bindtap="handleTap">点击更换 First Name</button>
javascript
//index.js
import { storeBindingsBehavior } from "mobx-miniprogram-bindings";
import { store } from "./store";
Component({
behaviors: [storeBindingsBehavior],
storeBindings: {
store,
fields: ["name"],
actions: ["changeFirstName"],
},
methods: {
handleTap: function () {
this.changeFirstName("Williams");
},
},
});
javascript
//store.js
import { observable, action } from "mobx-miniprogram";
export const store = observable({
name: {
first: "Elon",
last: "Musk",
},
changeFirstName: action(function (firstName) {
//1、需要替换整个对象:
this.name = Object.assign({}, this.name, { first: firstName });
//2、只是更新 name 对象的某个属性,是不会引起界面变化的,如:
//this.name.first = firstName;
}),
});
这个例子表明,更改某个值为 object 的 state 时,不能只更改这个 state 的某个属性,而应该替换整个 state,否则不会引起界面变化。
总结
在微信小程序中使用 MobX,需要安装 mobx-miniprogram 和 mobx-miniprogram-bindings,接着在一个单独的 js 文件(通常命名为 store.js)中使用 MobX 下的 observable 函数创建一个 observable 对象,以用作存放应用状态的 store。
我们通常会在 observable 对象中定义一些数据属性、computed value、action。在小程序页面中,建议使用 Component 构造器构造页面,在页面增加 storeBindings 字段,并从 mobx-miniprogram-bindings 导出 storeBindingsBehavior,该 behavior 会依据 storeBindings.fields 创建相应的 MobX reaction,同时把 storeBindings.actions 挂载到页面的 this.methods 对象下。
当一个页面需要更新 store 中的某个 state 时,我们只需要触发相应的 action,剩下的都交给 MobX:action 会修改 state,state 的改变会引起 computed value 的自动更新,state 和 computed value 的改变都会引发 reaction,在小程序中,就是把更新后的值渲染到页面中展示。
需要特别注意的是,如果更新的 state 是一个对象,需要替换整个对象,如果只更新该对象的某个属性,是不会引起界面变化的。
摘录
https://kejiweixun.com/blog/how-to-use-mobx-in-wechat-miniprogram