MobX 绑定过程和其中的一些坑的总结

开发过程中使用Mobx极大的方便了我们,但是在使用过程中还是会或多或少地遇到一些问题导致绑定失败,下面我们来一起探讨下Mobx的绑定过程,以方便我们来更好的使用它。

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

MobX的官方文档将MobX的绑定及相应过程总结为这么一句话,并标出了“读取”、“追踪函数”和“过程”三个关键字。

Mobx会收集哪些地方的绑定

“追踪函数” 是 computed
表达式、 observer
组件的 render()
方法和 when
reaction
autorun
的第一个入参函数。

文档说明的也比较清楚,会对文件当中的 @computed
修饰的方法、 render()
方法、 when
方法的第一个入参函数、 reaction
方法的第一个入参函数、 autorun
方法的第一个入参函数这些地方收集,MobX会在页面加载执行前扫描所有的文件,收集这些地方的绑定。

以下调用和赋值 this.store.listA
的地方,MobX都会去收集绑定,其它的地方则不会去收集。

//数据绑定文件
import { computed, observable } from 'mobx';

class IndexStore {
    @observable listA = ["1","2","3"];
    @observable listB = ["a","b","c"];

    @computed get dataA() {
        return this.listA;
    }
}

//Index视图文件
import { observer } from 'mobx-react/native';
import IndexStore from 'indexStore';

@observer
class Index extends Component {
    constructor() {
        super();
        this.store = new IndexStore();
        when(
            () => this.store.listA.length == 0,
            () => console.log("listA none")
        );
    }

    const autorun1 = autorun(() => {
        console.log(this.store.listA);
    })

    const reaction2 = reaction(
        () => this.store.listA,
        listA => console.log(listA.join(", "))
    )

    render() {
        return (
             
            } />
        )
    }
}

MobX会收集哪些绑定

“过程(during)” 意味着只追踪那些在函数执行时被读取的 observable
。这些值是否由追踪函数直接或间接使用并不重要。

这句话解释了MobX收集哪些绑定,是那些在函数执行时被读取的 observable
,例如上面实例代码
中的 this.store.listA
,它在 render()
函数执行时被调用,用于作为 MainItem
的数据源,所以这个绑定就被收集到了, render()
函数和 listA
之间就建立了联系,当 listA
发生变化时, render()
函数就会被调用,界面也就从新渲染刷新了。同理,上面的一些log也会调用,会在控制台输出相应信息。

需要注意的是上面实例代码中的 renderHeader={() => }
当中的 this.store.listB
并不会被收集到,为什么呢?因为 renderHeader
作为一个属性传入 MainItem
renderHeader
当中的 this.store.listB
并没有在 Index
render()
去使用,而是在 Item
当中使用了它,所以 Index
render()
并没有与 listB
之间建立连接,当 listB
发生变化就不会调用 Index
render()
,界面也就不会重新渲染刷新。

我们想在修改 listB
时让界面刷新作出相应,我们该怎么办呢?我们需要在 Item
当中添加 @observer
去捕获这个绑定,因为 listB
Item
render()
当中进行了调用,所以正确的写法如下:

//Index视图文件
render() {
    return (
         
        } />
    )
}

//Item视图文件
import { observer } from 'mobx-react/native';

@observer
class Item extends Component {
    constructor() {
        super();
    }

    render() {
        return (
            {this.props.data.listB[0]}
        )
    }
}

Tips:当我们修改 listB
时, Item
就会重新调用其 render()
函数重新渲染,我们重新渲染 Item
比重新渲染整个 Index
所消耗的资源会更少,虽然MobX已经有机制帮我们减少不必要的渲染,但是这样还是会消耗更少的资源,所以官方推荐我们绑定粒度越细越好。

细心的同学可能会注意到为什么 Item
data
当中是 this.store
而不是我们认为的 this.store.listB
,是写错了吗,并不是,这恰恰是正确的写法,下面我们就来讨论下这个问题。

MobX绑定了什么

“读取” 是对象属性的间接引用,可以用过 .
(例如 user.name
) 或者 []
(例如 user['name']
) 的形式完成。

这句话解释了MobX绑定了什么,它绑定的是可观察对象的引用,例如上面提到的 listA
,MobX并不是将 listA
的内容 ["1","2","3"]
render()
函数绑定,而是将 listA
变量对 ["1","2","3"]
的引用与 render()
函数绑定绑定,所以只有当 listA
对值的引用发生变化时, render()
函数才会调用。(也可以理解为c/c++当中的指针的概念, render()
函数是与 listA
变量的指针值绑定)

所以针对上面的 listA
,你只修改数组的内容,render()函数是不会调用的,界面也是不会刷新的。下面这种写法,界面并不会刷新:

//数据绑定文件
modifyListA1() {
    this.listA[0] = "4";
}

//此处只是数组["1","2","3"]的内容发生了变化,listA的引用却没有发生变化

正确的写法

//数据绑定文件
modifyListA2() {
    this.listA = ["4","2","3"];
}

//此处listA被赋值了一个新数组["4","2","3"],listA的引用发生了变化

我们再回到 listB
的问题上,如果我们是
这种写法,我们相当于把 ["a","b","c"]
这个值传递给了 Item
当中的 data
,通过 data
对数组的操作,并未出现 listB
的引用,也就无法建立绑定。而我们将整个 store
传递过去, data
就被赋值了 store
的内容,当 data
引用 listB
时,也就出现了 listB
的引用,这样绑定才建立了起来。

所以针对子组件,需要将被观察属性的父级传递过去,这样才能在子组件中出现被观察属性的引用,才能建立绑定。

MobX绑定过程

了解了以上三个概念,MobX的绑定过程就比较清晰了。MobX在代码编译时/代码执行之前扫描代码中的 computed
表达式、 observer
组件的 render()
方法等地方,将这些方法中出现的直接调用的观察属性的引用和这些方法绑定起来,这就是MobX的绑定过程。绑定完成之后,当这些引用发生变化时,相应的绑定方法就调用,界面就会刷新重新渲染或者相应的逻辑就会执行。

observable和@observable

上面我们 modifyListA1
提到的修改并不会触发绑定的界面刷新操作,需要我们使用 modifyListA2
当中的修改方式,给 listA
提供一个新数组,改变它的引用值。那有没有方法可以让 listA
只修改一个值(如 modifyListA1
当中的操作)就触发界面的刷新呢,答案是肯定的,我们只需要将 listA
初始的赋值方式改成如下方式即可:

//数据绑定文件
import { computed, observable } from 'mobx';

class IndexStore {
    listA = observable(["1","2","3"]);
    @observable listB = ["a","b","c"];

    @computed get dataA() {
        return this.listA;
    }
}

为什么这种方式就可以呢?因为 observable
默认情况下会递归应用,相比较 @observable
细粒度的观察,只监测 listA
的引用, observable
则会递归这些观察,将 listA[0]
listA[1]
listA[2]
的引用都作为监测对象,这样 listA[0]
listA[1]
listA[2]
的引用就和 render()
函数建立起了绑定,当 listA[0]
被赋值,引用发生变化时, render()
也就被调用了,界面也就刷新了。

如果需要一个数据源的内部数据发生变化引起相应操作,我们可以使用 observable
,然而 observable
也有它的弊端,它会建立起来比较多的冗余绑定,也会使后续的维护变得复杂,总体上我们推荐使用 @observable
这样细粒度的控制,它会使我们的项目更加清晰便于维护,同时也会大大的降低那些莫名其妙的bug的概率。

了解更多

详情参考官方文档

MobX 会对什么作出反应?

常见陷阱与最佳实践

稿源:大搜车产品技术设计团队 (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 前端开发 » MobX 绑定过程和其中的一些坑的总结

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录