Mobx|Mobx autorun 原理解析
本次分享主题为 "mobx autorun" 原理解析,主要分为以下几个部分:
- 分析 "autorun" 的使用方式;
- 对比 "autorun" 与“发布订阅模式”的异同;
- 实现 "autorun" 函数;
通过从0到1实现 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的使用流程如下:
- 声明可观察对象:autorun 仅会收集可观察对象作为依赖;
- 执行autorun:
- 传入监听函数并执行autorun;
- autorun 会自动收集函数中用到的可观察对象作为依赖;
- autorun 返回一个注销函数,通过调用注销函数可以结束监听函数;
- 修改可观察对象:依赖变更,autorun自动执行监听函数;
- 注销autorun:注销之后再变更可观察对象将不再执行监听函数;
时序图
“发布订阅模式”涉及如下三种活动:
- 注册:即订阅;
- 触发:即发布;
- 注销:即取消订阅;
文章图片
用autorun实现一次“注册-触发-注销”过程如下:
文章图片
对比上述两张时序图,我们可以得出如下结论:
- 从开发者视角看:
- 在“发布订阅模式”中,开发者需要参与注册、触发和注销;
- 在autorun模式中,开发者只需要参与注册和注销,触发由autorun自动实现;
- 从对象视角看:
- 在“发布订阅模式”中,对象不参与整个过程,对象是被动的;
- 在autorun模式中,可观察对象会参与事件的绑定和解绑,对象是主动的;
- 从事件模型视角看:
- 在“发布订阅模式”中,事件模型作为控制器调度整个过程;
- 在autorun模式中,autorun和可观察对象协同调度整个过程;
- 从全局视角看:
- “发布订阅模式”内部流程简单,但开发者使用复杂;
- autorun模式内部流程复杂,但开发者使用简单;
Pros
autorun模式相比于“发布订阅模式”有以下好处:
- autorun 将事件触发自动化,减少开发成本,提高开发效率;
autorun模式相比于“发布订阅模式”有以下坏处:
- autorun 将事件触发自动化,增加了学习成本和理解成本;
文章图片
我们可以得到“自动触发”的以下信息:
- 触发主体:可观察对象,事件触发由可观察对象发起;
- 触发时机:属性赋值,在可观察对象的属性被赋值时触发事件;
- 封装可观察对象:让普通对象的属性具有绑定和解绑监听函数的能力;
- 代理对象属性的取值方法,在每次属性赋值时将监听函数绑定到对象属性上;
- 代理对象属性的赋值方法,在每次属性取值时执行一次监听函数;
- 解绑监听函数:需要提供一套机制解绑可观察对象属性上的监听函数;
【需求说明】
为了让对象的属性具有绑定和解绑监听函数的能力,我们需要将普通对象封装成可观察对象:
- 可观察对象属性支持绑定监听函数;
- 可观察对象属性支持解绑监听函数;
通过调用observable方法可以使对象的所有属性都具备绑定和解绑事件的能力:
const message = observable({
title: 'title-01'
})
【方案设计】
- 定义一个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())
}
}
- 为了减少对原始对象的侵入性,将observable扩展的功能限制在对象的一个不可枚举的symbol属性中:
const $mobx = Symbol("mobx administration")
function observable(instance) {
const mobxAdmin = {}
Object.defineProperty(instance, $mobx, {
enumerable: false,
writable: true,
configurable: true,
value: mobxAdmin,
});
...
}
- 将原始对象的所有属性封装成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)
}
}
- 将原始对象所有属性的取值和赋值都代理到 $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的使用示例,我们可以得到如下信息:
文章图片
- 监听函数作为参数传递给autorun函数;
- 对象的取值操作发生在监听函数内;
...
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 的运行时信息呢?
文章图片
答案就是:共享变量
observable和autorun都运行在mobx中,可以在mobx中定义一个共享变量管理全局状态:
文章图片
共享变量
让我们声明一个可以管理“当前正在执行的监听函数”的共享变量:
const globalState = {
trackingObserver: undefined,
};
让我们使用共享变量实现监听函数与可观察对象的绑定:
设置“当前正在执行的监听函数”
function autorun(observer) {
globalState.trackingObserver = observer
observer()
globalState.trackingObserver = undefined
}
分析上述代码我们可以知道:
- 调用autorun以后需要立即执行一次监听函数,用于绑定监听函数和对象;
- 在监听函数执行结束后会立即清除trackingObserver;
- 当使用 autorun 时,所提供的函数总是立即被触发一次;
- “过程(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";
文章图片
解绑监听函数与对象
【需求说明】
将监听函数从可观察对象上解绑,解绑以后对象赋值操作将不再执行监听函数。
【代码示例】
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 对象,它有以下属性:
- observing:代表所有绑定了监听函数的可观察对象;
- 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";
文章图片
参考资料
- 完整示例代码
推荐阅读
- 做一件事情的基本原理是什么()
- 【读书笔记】贝叶斯原理
- SG平滑轨迹算法的原理和实现
- “写作宝典”《金字塔原理》之读书笔记
- Spring|Spring 框架之 AOP 原理剖析已经出炉!!!预定的童鞋可以识别下发二维码去看了
- Spring|Spring Boot 自动配置的原理、核心注解以及利用自动配置实现了自定义 Starter 组件
- Vue源码分析—响应式原理(二)
- MYSQL主从同步的实现
- (1)redis集群原理及搭建与使用(1)
- React.js学习笔记(17)|React.js学习笔记(17) Mobx