首发于大搜车技术博客:blog.souche.com/ru-he-zi-ji…javascript
mobx 是一个很是优雅的状态管理库,具备至关大的自由度,而且使用很是简单,本文经过本身实现一个 mini 版的 mobx 来探究一下相似的 FRP 模式在 js 中的实现。java
本文主要讲述了如何本身实现一个 mobx,主要是其核心几个 api 的实现。目的不是要从新造一个轮子,只是经过造轮子的过程,了解 mobx 的核心原理,以及一些具体实现的时候须要趟的坑,从而对RFP之类的编程范式有更深刻的了解。react
因此,不要将此项目应用于项目中,除非你真的想节省那一点点带宽(打包后 6K,GZip后 2.3K),用此项目了解 mobx 的原理便可。git
另外,s-mobx 的实现和 mobx 的实现细节可能并不一致。github
github: github.com/xinyu198736…npm
npm: npm install s-mobx --save
编程
s-mobx 最最核心是两个功能。api
整个 s-mobx 就是围绕这两个功能作包装。数组
autorun 是个神奇的函数,被他包装过的方法,就会变为观察者函数,而且这里有一个很重要的特性,这个函数只会观察本身依赖到的设为 observable 的值。函数
例如
autorun(function(){
console.log(person.name);
});复制代码
假设person对象身上有不少个属性是 observable 的,修改这些属性值的时候不会触发 autorun 包装过的函数,只有修改 name 属性的时候才会触发。
这里的原理就是依赖收集
这时候须要引伸出一个很简单的管理类,在 s-mobx 中,咱们叫作 dependenceManager,这个工具类中管理了一个依赖的 map,结构是一个全局惟一的 ID 和 对应的监听的函数的数组。
这个全局惟一的 ID 实际上表明的就是各个被设置为 observable 的属性值,是 Observable 类的一个属性 obID。
当一个被 observable 包装的属性值发生 set 行为的时候,就会触发 dependenceManager.trigger(obID); 从而触发遍历对应的监听函数列表,而且执行,这就是 autorun 的基本原理。
其实也很简单,也是 dependenceManager 的操做,在执行 autorun(handler) 的时候会执行如下的代码(实际上也就这三句代码):
dependenceManager.beginCollect(handler);
handler();
dependenceManager.endCollect();复制代码
这里 dependenceManager 标记如今开始收集依赖,而后执行 handler 函数,执行结束以后,标记当前收集结束。这里的收集操做能够嵌套。具体实现见 dependenceManager。
这个是经过 observable 的 get 动做来实现的,每一个被 observable 过的值在 get 的时候都会判断当前是否正在收集依赖,若是是的话,就会把这个值 和 当前正在收集依赖的 handler 关联起来存储在 dependenceManager 中。
这就是整个 s-mobx 核心的原理。
其余的代码大部分只是在实现如何包装 observable。
包装对象值的 Observable ,核心原理是 Object.defineProperty ,给被包装的属性套上 get 和 set 的钩子,在 get 中响应依赖收集,在 set 中触发监听函数。
数组的包装稍微麻烦,在 s-mobx 中使用 Proxy 来包装,可是兼容性不是很好,在 mobx 中,做者本身模拟了一个数组对象的操做,而后包装在原生数组上。
另外对于 Object 对象,为其进行了递归包装,每一级 Object 都绑定了一个 observable。
具体的代码见 s-observable (维护 Observable),s-extendObservable(包装到具体对象属性上)
Computed 是一种特殊的类型,他便是观察者,也是被观察者,而后它最大的特性是,他的计算不是每次调用的时候发生的,而是在每次依赖的值发生改变的时候计算的,调用只是简单的返回了最后一次的计算结果。
这样理解就明白了,其实在扮演观察者的时候, Computed 只是 autorun 的一个变种。
Computed 中有一个方法,叫作 _reComputed,当被 computed 包装的方法中依赖的 observable 值发生变化的时候,就会触发 _reComputed 方法从新计算 Computed 的值。这里的具体实现,其实就是把 _reComputed 当作 autorun 的handler 来处理,执行了一次依赖收集。
另外 Computed 还有一个特性就是能够被别人依赖,因此它也暴露了一个 get 的钩子,在钩子里的操做和 observable 中的 get 钩子作了一样的处理。
因此,当用 @computed 包装一个 class 的方法的时候,将其放入 autorun 中会执行两次依赖收集,一次是收集 computed 对其余 observable 的依赖,另外一次是收集 handler 对当前属性方法的依赖。这里 dependenceManager 提供了一种机制,能够嵌套收集依赖,采用了相似堆栈的机制。
在 mobx-react 中,可使用 @observer 对 react 对象进行包装,使其 render 方法成为一个观察者。
在 s-mobx 中直接集成这个功能,实现的代码:
var ReactMixin = {
componentWillMount: function() {
autorun(() => {
this.render();
this.forceUpdate();
});
}
};
function observer(target) {
const targetCWM = target.prototype.componentWillMount;
target.prototype.componentWillMount = function() {
targetCWM && targetCWM.call(this);
ReactMixin.componentWillMount.call(this);
};
}复制代码
这里给 react 组件的 prototype 作了一次 mixin,为其加入了一个 autorun,autorun的做用就是绑定组件 render 方法和其依赖的值的观察关系。当依赖的值发生变化的时候会触发 autorun 的参数 handler,handler中会强制执行 render() 方法和 forceUpdate()
这里每次都强制从新渲染,没有作很好的优化,在mobx中有个方法:isObjectShallowModified 来判断是否须要强制从新渲染,能够考虑直接引入进来。
mobx 的最大特点就是简单的注解使用方式,也就是 @observable @observer @computed 这些 decorator。
decorator 的实现其实很简单,不过有些坑须要规避,例如 在 decorator 中出现的target,是class 的prototype,而不是class的实例。可是在 return 出来的 descriptor 中,set 和 get 钩子中的this,则是 class 的实例。在实现一些复杂逻辑的时候要注意一下这个点。
具体的代码能够看 s-decorator
function observable(target, name, descriptor) {
var v = descriptor.initializer.call(this);
// 若是值是对象,为其值也建立observable
if(typeof (v) === 'object') {
createObservable(v);
}
var observable = new Observable(v);
return {
enumerable: true,
configurable: true,
get: function() {
return observable.get();
},
set: function(v) {
if(typeof (v) === 'object') {
createObservable(v);
}
return observable.set(v);
}
};
};复制代码
至此,一个简化版的 mobx 基本就完成了,mobx 中经常使用的功能基本都作了实现。原理的话其实也很简单,但愿之后有人问起来,你们可以说清楚 mobx 中的一些模式和实现,这就够了。