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

Mobx

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

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

        这里说的 “应用状态” 就是 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。

依赖收集

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

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

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",到底发生了什么?
        我们先看下官方给我们的解释

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

        嗯,看不懂。

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

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

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

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

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

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

        我们声明的 message 是一个可观察对象,我们注册了一个 autorun 作为追踪函数,在这个追踪函数中我们传入一个函数参数,这个函数进行了一次 message.title 的读取操作,且这次操作在函数执行过程中。满足所有条件,bingo!!!

        但是你真的懂了吗?

        我再举几个例子,大家可以根据上面的规则自己再判断一下:
例1.

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 对于什么会做出响应,你现在比以前更清楚一些了吗?


xh4722
240 声望11 粉丝