What does mobx react to?

Mobx 是一个通过战火洗礼的库,它经过透明的 函数响应式编程(transparently applying functional reactive programming - TFRP)使得 状态管理变得简单和可扩展.

Mobx

上面这段话引自 Mobx 的官方文档,说明了 Mobx 是一个应用了函数响应式的状态管理库。所谓的响应式就是事件监听,也是 Mobx 背后的哲学:javascript

任何源自应用状态的东西都应该自动地得到

        这里说的 “应用状态” 就是 state,在 Mobx 的世界里叫 observable;源自应用状态的 “东西” 叫作 derivations,derivations 能够分为两大类:computedreaction
        computed 表示从应用状态派生出来的新状态,也就是派生值。好比你定义了两个 state 分别叫作 a 和 b,它们的和叫作 total,而 total 能够经过 a + b 获得,你不必定义一个新的 state,这个 total 就叫作 computed。
        reaction 表示从应用状态派生出来的反作用,也就是派生行为。好比有一个分页选择器:你用一个叫作 index 的 state 表示当前页码,初始值是 1,当你改变这个 index 值为 2 的时候,就须要触发一个跳转到第2页的行为,这个行为是由 index 派生出来的,就叫作 reaction。
        Mobx 的核心概念其实就是这三个:observable、computed 和 reaction。html

依赖收集

任何源自应用状态的东西都应该自动地得到

        上面这句话还有很重要的一点没有讲到,就是 Mobx 哲学所声明的 “自动”,用高大上一点的术语讲就是依赖收集。咱们能够举个栗子:java

let message = observable({
    title: "Foo"
})

autorun(() => {
    console.log(message.title)
})
// 输出:
// "Foo"

message.title = "Bar"
// 输出:
// "Bar"

        咱们声明了一个 observable 的 message 对象,并调用了一个 autorun 函数用来输出 message 的 title 属性,这时候控制台立刻输出 "Foo"。嗯,一切都在掌控之中。
        接下来咱们尝试修改了 message 的 title 属性为 "Bar"。这时候神奇的事情发生了,autorun 里面传入的函数又自动执行了一遍,控制台输出了新的 title 值 "Foo",到底发生了什么?
        咱们先看下官方给咱们的解释react

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

        嗯,看不懂。es6

        接下来官方又对上面这句话作了解释:编程

  • “读取”是对象属性的间接引用,能够用过.(例如user.name) 或者[](例如user['name']) 的形式完成。
  • “追踪函数”computed表达式、observer 组件的render()方法和whenreactionautorun的第一个入参函数。
  • “过程(during)”意味着只追踪那些在函数执行时被读取的 observable 。这些值是否由追踪函数直接或间接使用并不重要。

        嗯,好像有点懂了,让咱们从新分析下上面的代码:app

// 这是可观察对象
let message = observable({
    title: "Foo"
})

// autorun 是“追踪函数”
autorun(() => {
    // message.title 是“读取”操做
    // 此次读取操做在函数执行“过程”中
    console.log(message.title)
})
// 输出:
// "Foo"

message.title = "Bar"
// 输出:
// "Bar"

        咱们声明的 message 是一个可观察对象,咱们注册了一个 autorun 做为追踪函数,在这个追踪函数中咱们传入一个函数参数,这个函数进行了一次 message.title 的读取操做,且此次操做在函数执行过程中。知足全部条件,bingo!!!异步

        可是你真的懂了吗?async

        我再举几个例子,你们能够根据上面的规则本身再判断一下:
例1.ide

let message = observable({
    title: "Foo"
})

autorun(() => {
    console.log(message.title)
})
// 输出:
// "Foo"

message = { title: "Bar" }

        上面我把 message.title = "Bar" 的赋值操做改成了直接修改 message 对象:message = { title: "Bar" },这时候 autorun 会执行吗?

例2.

let message = observable({
    title: "Foo"
})

let title = message.title
autorun(() => {
    console.log(title)
})
// 输出:
// "Foo"

message.title = "Bar"

        例2咱们新定义了一个 title = message.title 的变量,而后在 autorun 中输出这个变量。

例3.

let message = observable({
    title: "Foo"
})

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

message.title = "Bar"

        例3咱们在 autorun 中直接输出了 message 对象。

        上面3个例子都是不能在 message 的 title 变动的时候正常响应的:

  1. 例1由于 autorun 追踪的是 message 对 title 属性的读取操做,可是咱们变动的是 message 引用,原 message 对象的 title 属性并无发生变动,因此 autorun 不会自动执行;
  2. 例2由于 autorun 里面并无 “读取” 操做,因此不会追踪 message.title 的变动;
  3. 例3可能难理解一些,由于 console.log(message) 实际上是 “读取” 了 message 对象的全部属性并输出到控制台上的,因此这里知足了 “追踪函数”“读取” 两个条件,既然还有问题,那确定是没有知足 “过程” 这个条件。缘由就是 console.log 函数是异步的,它并无在函数的执行过程当中当即调用。

        是否是发现事情开始并得复杂了起来?

        如今让咱们放慢一下脚步,中止对 Mobx 官方解释的过分理解,这些只是 Mobx 实现者的文字游戏,他们并无告诉咱们事情的本质。

依赖收集的实现

        让咱们换一个角度,思考一下 Mobx 的依赖收集究竟是如何实现的?
        仍是上文的例子,这一次让咱们剖析一下这段代码的实现原理:

1  let message = observable({
2     title: "Foo"
3  })
4
5  autorun(() => {
6     console.log(message.title)
7  })
8  // 输出:
9  // "Foo"
10
11 message.title = "Bar"
12 // 输出:
13 // "Bar"

        上面的 1 到 3 行代码咱们声明了一个 message 对象,而且用 Mobx 的 observable 进行了封装。这里 observable 的意思就是让 message 对象变成可观察对象,observable 作的事情就是用 ES6 Proxy 代理了 { title: "Foo" } 这个普通对象并返回代理对象给 message。这样 Mobx 就有能力去监听 message 的变动了,咱们能够本身实现一个 observable:

function observable(origin) {
  return new Proxy(origin, {
    // 监听取值操做
    get: function (target, propKey, receiver) {
        // ...
        return Reflect.get(target, propKey, receiver);
    },
    // 监听赋值操做
    set: function (target, propKey, value, receiver) {
        // ...
        return Reflect.set(target, propKey, value, receiver);
    }
  })
}

        第 5 到 7 行咱们传入了一个函数参数调用了 autorun,函数参数只是简单输出 message 的 title 属性到控制台。通过这一步之后咱们在 11 行修改了 message 的 title 属性,autorun 的注册函数就会自动执行,在控制台输出最新的 message.title 信息。
        再从新看一下上面的代码,思考一个问题:autorun 为何会知道它须要去关心 message 对象的 title 属性?咱们没有传相似 ["message", "title"] 这样明确的参数给他,它接受的惟一参数只是一个执行函数,看起来就好像它自动去解析了执行函数的函数体内容,这就像个魔术同样。
        Mobx 的执行确实像魔术同样神奇,可是就像不少魔术的原理都很简单,Mobx 的依赖收集原理也很简单。解开这个魔术的钥匙就是 “全局变量”
        联系一下上面提供的几个线索:

  1. message 对象是一个 Proxy 对象;
  2. autorun 注册了一个执行函数,执行函数内部有 message.title 的 get 操做;
  3. 对 message.title 进行 set,autorun 的注册函数自动运行;

        让咱们解开 autorun 的秘密:

function autorun(trigger) {
  window.globalState = trigger
  trigger()
  window.globalState = null
}

        autorun 函数先将接收的执行函数挂载到 globalState 的全局变量上,接下来当即触发一次执行函数,最后将 globalState 重置为 null。
        咱们再改写一下咱们的 observable 函数:

function observable(origin) {
  let listeners = {}
  return new Proxy(origin, {
    // 监听取值操做
    get: function (target, propKey, receiver) {
        if(window.globalState) {
          listeners[propKey] = listeners[propKey] || []
          listeners[propKey] = [...listeners, window.globalState]
        }
        return Reflect.get(target, propKey, receiver);
    },
    // 监听赋值操做
    set: function (target, propKey, value, receiver) {
        listeners[propKey].forEach((fn) => fn())
        return Reflect.set(target, propKey, value, receiver);
    }
  })
}

        新的 observable 函数维护了一个事件队列,在每次对象属性的取值操做时去检查全局的 globalState 属性,若是发现当前取值操做是在一个追踪函数内执行的,就将 globalState 的值放入事件队列中;在每次对象的赋值操做发生时执行一遍事件队列。
        上面的 observable 和 autorun 只用于解释基本原理,不表明 Mobx 的真实实现。

        如今咱们对 Mobx 的依赖收集有了更深入的理解,再让咱们回过头去看一下比较难理解的例3:

let message = observable({
    title: "Foo"
})

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

message.title = "Bar"

        这里的关键在于 console.log 是一个异步的函数,将它代入 autorun:

function autorun(trigger) {
  window.globalState = trigger
  trigger()
  window.globalState = null
}

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

        让咱们解构一下函数执行:

window.globalState = () => console.log(message)
// async
console.log(message)
window.globalState = null

        假设有一个 print 函数能够在控制台同步输出信息,由于 console.log 是异步的,上面的代码执行会变成:

window.globalState = () => console.log(message)
window.globalState = null
print(`{ message: ${ message.title } }`)

        虽然 message.title 作了一次 get 操做,但这时候的 globalState 已经变成 null 了,message 对象的事件队列固然不能注册到这个执行函数。下次遇到相似的问题,你均可以试着把执行函数代入到 autorun 中分析一下,结果就能一目了然了。

        Mobx 对于 autorun 的说明也从侧面验证了咱们上面的实现:

当使用 autorun时,所提供的函数老是当即被触发一次,而后每次它的依赖关系改变时会再次被触发。

What does mobx react to?

        那么 Mobx 对于什么会作出响应,你如今比之前更清楚一些了吗?

相关文章
相关标签/搜索