我的GitHub
0%

无星的RN学习之旅(八)-Mobx入门到放弃

写的都是自己的一些感受=。=可能都是野路子=。=大牛就别看了=。=
代码:https://github.com/XingXiaoWu/mobx-/tree/master
先声明一句话:

#MobX 会对在追踪函数执行过程中读取现存的可观察属性做出反应。
以下所有内容基于rn0.56版本以前(即低于0.56),语法是ES6语法(可能有ES7,反正我是分不清=。=)
#一、引入Mobx
首先,安装yarn(别问我为什么不用npm)

##1.在项目根目录下输入
yarn add mobx mobx-react
此命令作用:引入mobx和mobx-react
##2.在项目根目录下继续输入(看清楚你的版本选择命令)

1
2
3
4
5
6
7
//这是0.57以下的
yarn add babel-plugin-transform-decorators-legacy babel-preset-react-native-stage-0
//这是0.57及以上的
yarn add @babel/core --dev
yarn add @babel/plugin-proposal-decorators --dev
yarn add @babel/plugin-transform-runtime --dev
yarn add @babel/runtime --dev

此命令作用:能够使用@标签

##3.用IDE打开项目
打开.babelrc文件
修改为(看清楚版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//0.57以下
{
"presets": ["react-native"],
"plugins": ["transform-decorators-legacy"]
}

//0.57及以上
{
"presets": ["module:metro-react-native-babel-preset"],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/transform-runtime", {
"helpers": true,
"regenerator": false
}]
]
}

至此,如果没报错,环境配置完毕,可以正常使用mobx了
#二、mobx的常用标签
##1.定义observable
observable,顾名思义,可以被观察的,即被观察者
一般用于数据,几乎所有数据都可以被观察,无论是对象、数组、类。 循环数据结构、引用,都可以被观察,只要给他们打上@ observable的标签,他们都可以被观察。

我写了三种导出模式,相互的优劣和使用方式还需要琢磨
#ThingModel=>static定义的,导出的是ThingModel这个class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {observable} from "mobx"

export class ThingModel {
@observable
static thing = "我是数据1"

@action
static setThing(value) {
ThingModel.thing = value
}
}

//使用
ThingModel.thing
ThingModel. setThing(xxx)

导出的ThingModel可以理解为全局的,所有的属性和方法都挂载在ThingModel这个类上,因此不会被垃圾回收

#InitModel = > 导出的是InitModel这个Class

1
2
3
4
5
6
7
8
9
10
11
12
13
export class InitModel {
@observable
firstValue = "我是初始数据"

@action
setFirstValue(value) {
this.firstValue = value
}
}
//使用
let initmodel = new InitModel()
initmodel.firstValue
initmodel.setFirstValue(xxx)

导出的是InitModel,但是其内部所有的方法都是由实例调用的,所以在页面使用前需要对其做实例化,let initmodel = new InitModel(),然后通过initmodel去调用,由于不是单例,因此每个页面new得到的实例化也不一样,我们理解其为每个页面单独的model,一旦页面被销毁,其实例化会被垃圾回收,因此销毁页面后重新进来页面的时候,model是全新的。(
xx食堂使用的全是ThingModel类型,全都写了销毁时调用的初始化方法,可以对此做优化)。当然,如果对此类型的model做单例的初始化,则是另一回事(例如xx食堂中的urlModel)。

#SecondModel=>导出的是SecondModel的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

class SecondModel {
@observable
seconedValue = "我是初始数据seconedValue"

@action
setSeconedValue(value) {
this.seconedValue = value
}
}

const secondSelf = new SecondModel()

export default secondSelf

//使用
secondSelf.seconedValue
secondSelf.setSeconedValue(xxxx)

采用这种方式会发现,导出来的全是secondSelf这个已经被实例化的类,也是全局的。其数据可以在各个页面通用。

###1)observable修饰的东西其实都是对象
你会发现observable可以修饰的东西很多,基本类型,对象,数组等等都可以用observable修饰。但是注意了,大多数的时候,都会被转变成对象。
例如:

1
2
@ observable
array = []

####正常情况下,这是一个数组,但是当你使用Array.isArray(array)做判断的时候,会发现的返回值都是false。
所以在使用的时候,如果你真的想得到一个数组,不妨使用slice方法创建一个真正的数组。
###2)如何创建动态对象并让属性变成可观察的?
实际开发过程中肯定经常碰到一些数据是要求动态创建的并且要求可观察。这时候建议使用 [Observable Map]对象。
因为普通的对象只有初始化时便存在的属性会转换成可观察的。虽然新添加的属性可以通过使用 extendObservable 转换成可观察的。但毕竟是还是麻烦。
###3)@ observable修饰的数组有哪些api

  • intercept(interceptor) - 可以用来在任何变化作用于数组前将其拦截。参见 observe & intercept
  • observe(listener, fireImmediately? = false) - 监听数组的变化。回调函数将接收表示数组拼接或数组更改的参数,它符合 ES7 提议。它返回一个清理函数以用来停止监听器。
  • clear() - 从数组中删除所有项。
  • replace(newItems) - 用新项替换数组中所有已存在的项。
  • find(predicate: (item, index, array) => boolean, thisArg?) - 基本上等同于 ES7 的 Array.find 提议。
  • findIndex(predicate: (item, index, array) => boolean, thisArg?) - 基本上等同于 ES7 的 Array.findIndex 提议。
  • remove(value) - 通过值从数组中移除一个单个的项。如果项被找到并移除的话,返回 true 。(如果数组里面放了对象,是没法通过这种方式删掉的,亲测)
  • peek() - 和 slice() 类似, 返回一个有所有值的数组并且数组可以放心的传递给其它库。

slice 相反,peek 不创建保护性拷贝。如果你确定是以只读方式使用数组,请在性能关键的应用中使用此方法。 在性能关键的部分,还建议使用一个扁平的 observable 数组。
###4)@ observable修饰的map有哪些api(es提供的就没写,下面是mobx提供的)

  • toJS() - 将 observable 映射转换成普通映射。
  • toJSON(). 返回此映射的浅式普通对象表示。(想要深拷贝,请使用 mobx.toJS(map))。
  • intercept(interceptor) - 可以用来在任何变化作用于映射前将其拦截。参见 observe & intercept
  • observe(listener, fireImmediately?) - 注册侦听器,在映射中的每个更改时触发,类似于为 Object.observe 发出的事件。想了解更多详情,请参见 observe & intercept
  • merge(values) - 把提供对象的所有项拷贝到映射中。values 可以是普通对象、entries 数组或者 ES6 字符串键的映射。
  • replace(values) - 用提供值替换映射全部内容。是 .clear().merge(values) 的简写形式。

##2.定义observer
observer,观察者。观察者观察被观察对象。
在mobx中,定义好观察者和被观察者以后,一旦被观察者发生变化,mobx会做如下操作:
图5

1.可观察值向其所有观察者发送过时通知,表明它已变得陈旧。任何受影响的计算值将以递归方式将通知传递给其观察者。因此,依赖关系树的一部分将被标记为陈旧。在图5的示例依赖关系树中,当值“1”改变时将变为陈旧的观察者用橙色虚线边框标记。这些都是可能受变化值影响的推导。
2.发送过时通知并存储新值后,将发送就绪通知。此消息还指示值是否确实更改。
3.一旦派生收到步骤1中收到的每个陈旧通知的就绪通知,它就会知道所有观察到的值都是稳定的并且它将开始重新计算。计算就绪/陈旧消息的数量将确保,例如,计算值’4’将仅在计算值’3’变得稳定之后重新评估。
4.如果没有任何就绪消息指示值已更改,则派生将简单地告诉其自己的观察者它已再次准备好,但不更改其值。否则,计算将重新计算并向其自己的观察者发送就绪消息。这导致执行的顺序如图5所示。注意(例如)如果计算值’4’重新评估但没有产生新值,那么最后一个反应(用’ - ‘标记)将永远不会执行。

关于使用:
一般用于视图的UI更新
例如在示例demo中
将视图定义为观察者对象

在视图中,使用被观察的数据,当该数据发生改变的时候,UI视图将会“自动”刷新(原理上面已经解释了)

##3.action
这个action就比较多了,先说效果
效果就是,使用action包裹的方法修改被观察者的值,可以触发观察者的刷新。(注意,只有当前状态,异步中的状态等是无法被action捕获的,可以在runInAction的例子中感受一下)

action: 创建一个动作
action.bound: 创建有范围的动作,不需要name函数,自动绑定上下文对象,不可与箭头函数同用,因为箭头函数已经绑定了(暂时找不到一个好例子,这有个例子,但我无法复现,自行体会一下吧https://segmentfault.com/q/1010000011520041)
下面是官方的例子,应该更容易理解。主要是异步会产生的问题,建议使用bound修饰
官网的例子:https://cn.mobx.js.org/best/actions.html

一般情况下使用@action修饰被观察值的变化方法,注意,是:方法
下面是从中文网抄来的用法:

1
2
3
4
5
6
7
1.  action(fn)
2. action(name, fn)
3. @action classMethod
4. @action(name) classMethod
5. @action boundClassMethod = (args) => { body }
6. @action.bound boundClassMethod(args) { body }
7. runInAction ps:action(name, fn)()的语法糖,常用于异步

我们一个个说
1)action(fn)
这其实没啥说的,就是声明接下来会有一个动作,这个动作会触发被观察的值的改变,会触发观察者捕获到相应的改变并作出应该的动作(比如视图会刷新)

1
2
3
action((value)=>{
ThingModel. thing = value
})

2)action(name, fn)
和上面那个一样,只是多了个name,name可以做调试别名,只是用过相关调试,换而言之对我来说其实是一个没什么用的值,可以说明你接下来这个函数是干嘛用的,使用方法同上
3)@action classMethod

1
2
3
4
@action
setThing(value){
ThingModel. thing = value
}

4.@action(name) classMethod

1
2
3
4
@action('name')
setThing(value){
ThingModel. thing = value
}

5.@action boundClassMethod = (args) => { body }

1
2
3
@action setFirstValueBoundName = (value)=>{
this.seconedValue = value
}
  1. @action.bound
    1
    2
    3
    4
    @action.bound
    setFirstValueBound(value) {
    this.seconedValue = value
    }
    7.runInAction
    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
    //大家可以测试一下下面这三种写法
    1、正常
    @action
    async setFirstValuePromise() {
    try {
    let value = await this.getPromise()
    console.log('value = ' + value)
    runInAction(() => {
    this.seconedValue = value
    })
    } catch (e) {
    console.log(e)
    }
    }

    2.报错
    @action
    async setFirstValuePromise() {
    try {
    let value = await this.getPromise()
    console.log('value = ' + value)
    this.seconedValue = value
    } catch (e) {
    console.log(e)
    }
    }


    3.正常
    @action
    async setFirstValuePromise() {
    try {
    let value = await this.getPromise()
    console.log('value = ' + value)
    this.setFirstValue(value)
    } catch (e) {
    console.log(e)
    }
    }
    @action
    setFirstValue(value){
    this.seconedValue = value
    }
    从这个例子其实我们就能知道,action其实只会对“当前状态”的被观察对象的改变有触动。对一些已执行(比如:异步)的方法内的被观察值的改变无法知晓。
    就如上述例子中,await后面的操作,action其实就无法捕捉了,这时候就需要对后面的执行方法用action包裹(例如第3种写法),或者使用runInAction语法糖(例如第1种写法)

#4.computed
这个比较有意思,一般用于修饰 与已经被监听的值有关,但又不想产生新值的东西。
举个简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@observable
number = 1

@action
setNumber(value) {
this.number = value
}

@observable
price = 1

@action
setPrice(value) {
this.price = value
}

@computed
get total() {
return this.number * this.price
}

我定义了number和price,但我想要知道总价,这时候就可以使用computed修饰total方法了。注意,我说的是方法。
你看,total返回的东西 与被监听值有关,但是又不想产生新值。
使用的时候如下:

1
<Text>{secondSelf.total}</Text>

和iOS类似,.语法指get,所以可以看见total方法中存在get 修饰符
至于set,是自动的,不用管。

善于动手的同学会发现,用computed修饰的get方法和自己手动写一个没什么区别。
这里摘录一下区别:https://github.com/mobxjs/mobx/issues/161

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Both solutions will exhibit the same behavior, but introducing @computed is better in the sense that it allows MobX to optimized it away more smartly.

If getFullName is not computed, and you change firstName, everyone using getFullName will recompute as well.

If, in contrast, @computed get fullName is used, the fullname is cached, so if the firstname is changed, the fullname is recomputed as well, but if the output of fullname doesn't change, nobody would be notified.

Also, if you read test.fullName somwhere, you will (usually) get a cached result back if decorated, while without decorator it would always be recomputed (which is the normal function behavior after all)

So, when in doubt, use it (if your function is pure in terms of depending only on observable values, which it should be)
简单翻译一下:
首先这两者解决方法都会得到一个相同的结果,但使用@computed的意义在于它能够由MobX进行更智能的优化。
如果我不使用computed属性,直接使用自定义的getTheValue函数的话,那么只要value改变,所有用到getTheValue函数的地方都将重新计算。
如果使用了@computed get getValue,那么getValue将会被缓存,如果value没有改变,那么getValue也不会改变,其它组件也不会收到通知。
此外如果你读取getValue的值,你通常会得到一个缓存的值,而不带@computed装饰器,则会重新计算……

所以应该是有一个缓存的优化。

#5.autorun
可以理解为自动执行的方法,不过要搭配observable使用
使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SecondModel {

constructor(){
autorun(()=>{
console.log(this.zhangsan.money)
})
}

@observable
zhangsan = {
name: 'zhangsan',
age: 18,
money: 20
}

@action
setZhangsan(value) {
this.zhangsan = value
}
}

接下来你会发现,改变zhangsan的name,age,都不会有打印,唯独改变money会触发autorun的方法打印。
autorun中使用的被观察者属性改变会触发autorun的方法。
官方文档的经验法则:如果你有一个函数应该自动运行,但不会产生一个新的值,请使用autorun。 其余情况都应该使用 computed。 Autoruns 是关于 启动效果 (initiating effects) 的 ,而不是产生新的值。
不过确实不知道为什么会有这么一个经验法则。可能还需要经验积累
#6.when
和autorun差不多,他有两个参数,第一个参数的返回值如果是true则执行第二个参数
例如:

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

class SecondModel {

constructor() {
when(
() => {
return this.zhangsan.money > 30
},
() => {
console.log('张三有钱了')
}
)
autorun(() => {
console.log(this.zhangsan.money)
})
}

@observable
zhangsan = {
name: 'zhangsan',
age: 18,
money: 20
}

@action
setZhangsan(value) {
this.zhangsan = value
}
}

配合测试demo中修改张三的金钱,可以发现,当修改金钱大于30时,会打印出“张三有钱了”

#7.reaction
我找不到一个好的case去表现

#另外的一些东西:
##1).MobX 会对什么作出反应?

MobX 通常会对你期望的东西做出反应。 这意味着在90%的场景下,mobx “都可以工作”。 然而,在某些时候,你会遇到一个情况,它可能不会像你所期望的那样工作。 在这个时候理解 MobX 如何确定对什么有反应就显得尤为重要。

MobX 会对在追踪函数执行过程中读取现存的可观察属性做出反应。

“读取” 是对象属性的间接引用,可以用过 . (例如 user.name) 或者 [] (例如 user[‘name’]) 的形式完成。
“追踪函数” 是 computed 表达式、observer 组件的 render() 方法和 when、reaction 和 autorun 的第一个入参函数。
“过程(during)” 意味着只追踪那些在函数执行时被读取的 observable 。这些值是否由追踪函数直接或间接使用并不重要。
换句话说,MobX 不会对其作出反应:

从 observable 获取的值,但是在追踪函数之外
在异步调用的代码块中读取的 observable
更详细:https://cn.mobx.js.org/best/react.html

##2).mobx和state是否能同时使用?
答案是:可以
而且你可以通过componentWillReact这个生命函数来看当前的刷新到底是由setstate引起的刷新还是由mobx变化引起的刷新。(没有引入mobx的组件是不会有这个生命周期的)
##3). observer用多了是不是不好?
@observer 只会增强你正在装饰的组件,而不是内部使用了的组件。 所以通常你的所有组件都应该是装饰了的。但别担心,这样不会降低效率,相反 observer 组件越多,渲染效率越高。

#一些注意事项
https://cn.mobx.js.org/best/pitfalls.html

我是阿星,阿星的阿,阿星的星!