Mobx|Mobx autorun 原理解析

本次分享主题为 "mobx autorun" 原理解析,主要分为以下几个部分: - 分析 "autorun" 的使用方式; - 对比 "autorun" 与“发布订阅模式”的异同; - 实现 "autorun" 函数;

通过从0到1实现 autorun 函数以后,你可以了解以下知识:
  • autorun 与可观察对象的协作过程;
  • 为什么使用 autorun 的时候,所提供的函数会立即执行一次?
  • 为什么 autorun 不能跟踪到异步逻辑中的可观察对象取值?
autorun 使用方式
// 声明可观察对象 const message = observable({ title: 'title-01' })/* 执行autorun,传入监听函数 */ const dispose = autorun(() => { // 自动收集依赖,在依赖变更时执行注册函数 console.log(message.title) })// title-01 message.title = 'title-02' // title-02/* 注销autorun */ dispose() /* 注销以后,autorun 不再监听依赖变更 */ message.title = 'title-03'

autorun的使用流程如下:
  1. 声明可观察对象:autorun 仅会收集可观察对象作为依赖;
  2. 执行autorun:
    • 传入监听函数并执行autorun;
    • autorun 会自动收集函数中用到的可观察对象作为依赖;
    • autorun 返回一个注销函数,通过调用注销函数可以结束监听函数;
  3. 修改可观察对象:依赖变更,autorun自动执行监听函数;
  4. 注销autorun:注销之后再变更可观察对象将不再执行监听函数;
autorun VS 发布订阅模式 通过观察 autorun 的使用方式可以看出来,autorun 与传统的“发布订阅模式”很像。接下来我们对比下 autorun 与“发布订阅模式”的异同。
时序图
“发布订阅模式”涉及如下三种活动:
  • 注册:即订阅;
  • 触发:即发布;
  • 注销:即取消订阅;
用发布订阅者模式实现一次“注册-触发-注销”过程如下:
Mobx|Mobx autorun 原理解析
文章图片

用autorun实现一次“注册-触发-注销”过程如下:
Mobx|Mobx autorun 原理解析
文章图片

对比上述两张时序图,我们可以得出如下结论:
  1. 从开发者视角看:
    • 在“发布订阅模式”中,开发者需要参与注册、触发和注销;
    • 在autorun模式中,开发者只需要参与注册和注销,触发由autorun自动实现;
  2. 从对象视角看:
    • 在“发布订阅模式”中,对象不参与整个过程,对象是被动的;
    • 在autorun模式中,可观察对象会参与事件的绑定和解绑,对象是主动的;
  3. 从事件模型视角看:
    • 在“发布订阅模式”中,事件模型作为控制器调度整个过程;
    • 在autorun模式中,autorun和可观察对象协同调度整个过程;
  4. 从全局视角看:
    • “发布订阅模式”内部流程简单,但开发者使用复杂;
    • autorun模式内部流程复杂,但开发者使用简单;
autorun 模式对“发布订阅模式”做了一次改进:将事件触发自动化,从而减少开发成本。
Pros
autorun模式相比于“发布订阅模式”有以下好处:
  • autorun 将事件触发自动化,减少开发成本,提高开发效率;
Cons
autorun模式相比于“发布订阅模式”有以下坏处:
  • autorun 将事件触发自动化,增加了学习成本和理解成本;
如何实现auotorun? 根据上面的分析我们知道autorun是“发布订阅模式”的改进版:将事件触发自动化。这种自动化是从开发者的视角看的,即开发者在每次更新对象值之后无需再手动触发一次事件模型;从对象视角看就是每次被赋值之后对象都会执行一次监听函数:
Mobx|Mobx autorun 原理解析
文章图片

我们可以得到“自动触发”的以下信息:
  • 触发主体:可观察对象,事件触发由可观察对象发起;
  • 触发时机:属性赋值,在可观察对象的属性被赋值时触发事件;
我们需要解决如下问题:
  • 封装可观察对象:让普通对象的属性具有绑定和解绑监听函数的能力;
  • 代理对象属性的取值方法,在每次属性赋值时将监听函数绑定到对象属性上;
  • 代理对象属性的赋值方法,在每次属性取值时执行一次监听函数;
  • 解绑监听函数:需要提供一套机制解绑可观察对象属性上的监听函数;
封装可观察对象
【需求说明】
为了让对象的属性具有绑定和解绑监听函数的能力,我们需要将普通对象封装成可观察对象:
  1. 可观察对象属性支持绑定监听函数;
  2. 可观察对象属性支持解绑监听函数;
【代码示例】
通过调用observable方法可以使对象的所有属性都具备绑定和解绑事件的能力:
const message = observable({ title: 'title-01' })

【方案设计】
  1. 定义一个ObservableValue对象,用于将对象的属性封装成可观察属性:
class ObservableValue { observers = [] value = https://www.it610.com/article/undefined constructor(value) { this.value = value } addObserver(observer) { this.observers.push(observer) } removeObserver(observer) { const index = this.observers.findIndex(o => o === observer) this.observers.splice(index, 1) } trigger() { this.observers.forEach(observer => observer()) } }

  1. 为了减少对原始对象的侵入性,将observable扩展的功能限制在对象的一个不可枚举的symbol属性中:
const $mobx = Symbol("mobx administration") function observable(instance) { const mobxAdmin = {} Object.defineProperty(instance, $mobx, { enumerable: false, writable: true, configurable: true, value: mobxAdmin, }); ... }

  1. 将原始对象的所有属性封装成ObservableValue并赋值到mobxAdmin中;
... function observable(instance) { const mobxAdmin = {} ... for(const key in instance) { const value = https://www.it610.com/article/instance[key] mobxAdmin[key] = new ObservableValue(value) } }

  1. 将原始对象所有属性的取值和赋值都代理到 $mobx 中:
... function observable(instance) { ... for(const key in instance) { Object.defineProperty(instance, key, { configurable: true, enumerable: true, get() { return instance[$mobx][key].value; }, set(value) { instance[$mobx][key].value = https://www.it610.com/article/value; }, }) } ... }

绑定监听函数与对象
【需求说明】
现在我们已经有能力将普通对象上封装成可观察对象了。接下来我们实现如何将监听函数绑定到可观察对象上。
【代码示例】
autorun(() => { console.log(message.title) })

【方案设计】
通过autorun的使用示例,我们可以得到如下信息:
Mobx|Mobx autorun 原理解析
文章图片

  1. 监听函数作为参数传递给autorun函数;
  2. 对象的取值操作发生在监听函数内;
我们需要做的是在对象取值的时候将当前正在执行的监听函数绑定到对象的属性上:
... function observable(instance) { ... for(const key in instance) { Object.defineProperty(instance, key, { ... get() { const observableValue = https://www.it610.com/article/instance[$mobx][key] // 得到当前正在执行的监听函数 const observer = getCurrentObserver() if(observer) { observableValue.addObserver(observer) } return observableValue.value; }, ... }) } ... }

如何得到当前正在执行的监听函数?
对象的取值代理定义在observable中,但是监听函数的执行却是在autorun中,那要如何在 observable 中拿到 autorun 的运行时信息呢?
Mobx|Mobx autorun 原理解析
文章图片

答案就是:共享变量
observable和autorun都运行在mobx中,可以在mobx中定义一个共享变量管理全局状态:
Mobx|Mobx autorun 原理解析
文章图片

共享变量
让我们声明一个可以管理“当前正在执行的监听函数”的共享变量:
const globalState = { trackingObserver: undefined, };

让我们使用共享变量实现监听函数与可观察对象的绑定:
设置“当前正在执行的监听函数”
function autorun(observer) { globalState.trackingObserver = observer observer() globalState.trackingObserver = undefined }

分析上述代码我们可以知道:
  1. 调用autorun以后需要立即执行一次监听函数,用于绑定监听函数和对象;
  2. 在监听函数执行结束后会立即清除trackingObserver;
这两点可以分别解释mobx文档中的以下说明:
  1. 当使用 autorun 时,所提供的函数总是立即被触发一次;
  2. “过程(during)” 意味着只追踪那些在函数执行时被读取的 observable 。
得到并绑定“当前正在执行的监听函数”
... function observable(instance) { ... for(const key in instance) { Object.defineProperty(instance, key, { ... get() { const observableValue = https://www.it610.com/article/instance[$mobx][key] const observer = globalState.trackingObserver if(observer) { observableValue.addObserver(observer) } return observableValue.value; }, ... }) } ... }

触发“监听函数”
... function observable(instance) { ... for(const key in instance) { Object.defineProperty(instance, key, { ... set(value) { instance[$mobx][key].value = https://www.it610.com/article/value; instance[$mobx][key].trigger() }, }) } ... }

【用例测试】
const message = observable({ title: "title-01", }); autorun(() => { console.log(message.title); }); message.title = "title-02"; message.title = "title-03";

Mobx|Mobx autorun 原理解析
文章图片

解绑监听函数与对象
【需求说明】
将监听函数从可观察对象上解绑,解绑以后对象赋值操作将不再执行监听函数。
【代码示例】
const dispose = autorun(() => { console.log(message.title) })dispose()

【方案设计】
解绑函数从所有可观察对象的监听列表中移除监听函数:
function autorun(observer) { ... function dispose() { // 得到所有可观察对象 const observableValues = getObservableValues(); (observableValues || []).forEach(item => { item.removeObserver(observer) } }return dispose }

如何在autorun中获取“所有绑定了监听函数的对象”?
绑定监听函数的操作在observable中,但是解绑监听函数的操作却是在autorun中,那要如何在 autorun 中拿到 observable 的相关信息呢?
没错,答案还是:共享变量
我们之前使用的 globalState.trackingObserver 绑定的是监听函数本身,我们可以对它进行一些封装,让它可以收集“所有绑定了监听函数的对象”。为了说明它不再是仅仅代表监听函数,我们将它重命名为 trackingDerivation。
共享变量
const globalState = { trackingDerivation: undefined }

【Mobx|Mobx autorun 原理解析】封装 trackingDerivation
function autorun(observer) { const derivation = { observing: [], observer } globalState.trackingDerivation = observer observer() globalState.trackingDerivation = undefined }

在这里我们声明了一个 derivation 对象,它有以下属性:
  1. observing:代表所有绑定了监听函数的可观察对象;
  2. observer:监听函数;
设置“绑定了监听函数的对象”
... function observable(instance) { ... for(const key in instance) { Object.defineProperty(instance, key, { ... get() { const observableValue = https://www.it610.com/article/instance[$mobx][key] const derivation = globalState.trackingDerivation if(derivation) { observableValue.addObserver(derivation.observer) derivation.observing.push(observableValue) } return observableValue.value; }, ... }) } ... }

获取并解绑“所有绑定了监听函数的对象”
function autorun(observer) { const derivation = { observing: [], observer } ... function dispose() { const observableValues = derivation.observing; (observableValues || []).forEach(item => { item.removeObserver(observer) }) derivation.observing = [] }return dispose }

【用例测试】
const message = observable({ title: "title-01", }); const dispose = autorun(() => { console.log(message.title); }); message.title = "title-02"; dispose() message.title = "title-03";

Mobx|Mobx autorun 原理解析
文章图片

参考资料
  • 完整示例代码

    推荐阅读