Mobx 是一个通过战火洗礼的库,它经过透明的 函数响应式编程(transparently applying functional reactive programming - TFRP)使得 状态管理变得简单和可扩展.
上面这段话引自 Mobx 的官方文档,说明了 Mobx 是一个应用了函数响应式的状态管理库。所谓的响应式就是事件监听,也是 Mobx 背后的哲学:javascript
任何源自应用状态的东西都应该自动地得到
这里说的 “应用状态” 就是 state,在 Mobx 的世界里叫 observable;源自应用状态的 “东西” 叫作 derivations,derivations 能够分为两大类:computed 和 reaction 。
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()
方法和when
、reaction
和autorun
的第一个入参函数。- “过程(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 变动的时候正常响应的:
是否是发现事情开始并得复杂了起来?
如今让咱们放慢一下脚步,中止对 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 的依赖收集原理也很简单。解开这个魔术的钥匙就是 “全局变量”。
联系一下上面提供的几个线索:
让咱们解开 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
时,所提供的函数老是当即被触发一次,而后每次它的依赖关系改变时会再次被触发。
那么 Mobx 对于什么会作出响应,你如今比之前更清楚一些了吗?