mobx是redux的代替品,其本身就是一个很好的MVVM框架。因此花点力气研究一下它。
网上下最新的2.75
function Todo() {
this.id = Math.random()
mobx.extendObservable(this, {
aaa: 111,
bbb: 222
})
}
var vm = new Todo
mobx.autorun(function () {
console.log(vm.aaa + " " + vm.bbb)
})
这是es5的写法,可以改成更酷的es7写法
import {observable,autorun} from "mobx";
class Todo {
@observable aaa:number = 0;
@observable bbb:number = 1;
}
cont vm = new Todo
autorun(() => console.log(vm.aaa+" "+ vm.bbb ) );
语法怎么也没关系,重要的是思想。我们看这里面出现 的两个方法extendObservable与autorun
function extendObservable(target) {
//将其他参数混入第一个参数中,有点像jQuery的extend
var properties = [];
for (var _i = 1; _i < arguments.length; _i++) {
properties[_i - 1] = arguments[_i];
}
invariant(arguments.length >= 2, "extendObservable expected 2 or more arguments");
invariant(typeof target === "object", "extendObservable expects an object as first argument");
invariant(!(isObservableMap(target)), "extendObservable should not be used on maps, use map.merge instead");
properties.forEach(function (propSet) {
invariant(typeof propSet === "object", "all arguments of extendObservable should be objects");
invariant(!isObservable(propSet), "extending an object with another observable (object) is not supported. Please construct an explicit propertymap, using `toJS` if need. See issue #540");
extendObservableHelper(target, propSet, ValueMode.Recursive, null);
});
return target;
}
mobx从react那里借鉴了invariant,如果第一个参数为false时,它就会将第二个参数打印出来。通过babel等编译工具,可能在生产环境中将它们从最终代码里完全剔除。这么密集的提示信息,是目前所有JS中绝无紧有的。这在我们开发阶段非常有帮助。
这个方法无非是告诉我们,第一个参数必须是对象,并且不能被Observable化(isObservableMap),第二,第三,第四个参数也是如此。后面的对象的个数是随机,但必须保证有一个。最后是调用了extendObservableHelper方法。
function extendObservableHelper(target, properties, mode, name) {
var adm = asObservableObject(target, name, mode);
for (var key in properties)
if (hasOwnProperty(properties, key)) {
if (target === properties && !isPropertyConfigurable(target, key))
continue;
var descriptor = Object.getOwnPropertyDescriptor(properties, key);
setObservableObjectInstanceProperty(adm, key, descriptor);
}
return target;
}
extendObservableHelpery方法,首先根据第一个对象,产生一个特殊的对象,这与avalon的策略一样。不在原对象上修改,而是生成另一个可观察对象(观察者模式中,Observable, 对应的是Observe),这样就有机会用到Proxy这样更高层次的魔术对象。
这个步骤由asObservableObject方法处理,它有三个参数,第二个应该用来生成UUID的,第三个是产生模式。
function asObservableObject(target, name, mode) {
if (mode === void 0) { mode = ValueMode.Recursive; }
if (isObservableObject(target))
return target.$mobx;
if (!isPlainObject(target))
name = (target.constructor.name || "ObservableObject") + "@" + getNextId();
if (!name)
name = "ObservableObject@" + getNextId();
var adm = new ObservableObjectAdministration(target, name, mode);
addHiddenFinalProp(target, "$mobx", adm);
return adm;
}
我猜得不错,asObservableObject里面,如果它已经被改造,立即返回,否则就将它变成ObservableObjectAdministration实例。其中name,它会先定义目标对象是用户类的实例,是就取其构造器名+UUID,否则就是ObservableObject
+UUID。然后为原对象添加一个属性$mobx,这个属性是隐藏的,不可修改的。我们通过es5 的方法就可以添加此类属性:
function addHiddenFinalProp(object, propName, value) {
Object.defineProperty(object, propName, {
enumerable: false,
writable: false,
configurable: true,
value: value
});
}
ObservableObjectAdministration这个类如下:
function ObservableObjectAdministration(target, name, mode) {
this.target = target;
this.name = name;
this.mode = mode;
this.values = {};
this.changeListeners = null;
this.interceptors = null;
}
ObservableObjectAdministration.prototype.observe = function (callback, fireImmediately) {
invariant(fireImmediately !== true, "`observe` doesn't support the fire immediately property for observable objects.");
return registerListener(this, callback);
};
ObservableObjectAdministration.prototype.intercept = function (handler) {
return registerInterceptor(this, handler);
};
这里最重要的是用到两个内部方法registerListener与registerInterceptor。不过以后讲吧,我们回过头来extendObservableHelper.得到ooa实例后,框架遍历对象的每人属性,然后将当中的可以修改的属性再改造一下,放到ooa实例上。
筛选可用属性通过两个方法hasOwnProperty与isPropertyConfigurable。
setObservableObjectInstanceProperty需要用一个属性的描述对象。描述对象也是es5的,不懂的人可以翻看MSDN。一个属性描述对象有6个可用的配置项:value, writable, enumerable, configurable, get, set。注意,这里是说可用,并不代表一个对象就有6个属性,它们最多有4个属性,有了value, writable就不能get,set,反之亦然。因此是存在两种形态的描述对象。我们可以姑且称之为,数据描述与方法描述(也叫访问器描述)
function setObservableObjectInstanceProperty(adm, propName, descriptor) {
if (adm.values[propName]) {//如果它已经在实例是注册了,就改变其数据值
invariant("value" in descriptor, "cannot redefine property " + propName);
adm.target[propName] = descriptor.value;
}
else {
if ("value" in descriptor) {//如果是数据描述
if (handleAsComputedValue(descriptor.value)) {
if (deprecated(COMPUTED_FUNC_DEPRECATED)) {
console.error("in: " + adm.name + "." + propName);
console.trace();
}
}
defineObservableProperty(adm, propName, descriptor.value, true, undefined);
} else {
defineObservableProperty(adm, propName, descriptor.get, true, descriptor.set);
}
}
}
比如本文最初的例子,aaa和bbb,它们的描述对象就是数据描述。
function defineObservableProperty(adm, propName, newValue, asInstanceProperty, setter) {
if (asInstanceProperty)
assertPropertyConfigurable(adm.target, propName);
var observable;
var name = adm.name + "." + propName;
var isComputed = true;
if (isComputedValue(newValue)) {
observable = newValue;
newValue.name = name;
if (!newValue.scope)
newValue.scope = adm.target;
}
else if (handleAsComputedValue(newValue)) {
observable = new ComputedValue(newValue, adm.target, false, name, setter);
}
else if (getModifier(newValue) === ValueMode.Structure && typeof newValue.value === "function" && newValue.value.length === 0) {
observable = new ComputedValue(newValue.value, adm.target, true, name, setter);
}
else {
isComputed = false;
if (hasInterceptors(adm)) {
var change = interceptChange(adm, {
object: adm.target,
name: propName,
type: "add",
newValue: newValue
});
if (!change)
return;
newValue = change.newValue;
}
observable = new ObservableValue(newValue, adm.mode, name, false);
newValue = observable.value;
}
adm.values[propName] = observable;
if (asInstanceProperty) {
Object.defineProperty(adm.target, propName, isComputed ? generateComputedPropConfig(propName) : generateObservablePropConfig(propName));
}
if (!isComputed)
notifyPropertyAddition(adm, adm.target, propName, newValue);
}
这方法特复杂,其实与avalon2.1的modelAdaptor的功能一样,将一个属性值,转换为各种类型的可观察对象。可观察对象是有许多各形式的,在avalon中就分为用户VM,子VM,代理VM,复合VM,监控数组,监控属性与计算属性。就像mobx将各类VM合并一下,也有4类。mobx的分类基本与avalon一致。我们还是回到源码上,搞定几个内部方法吧。
assertPropertyConfigurable是用发出警告,不让用户修改描述对象的 configurable配置项。
isComputedValue是对象已经是计算属性了。这个判定非常复杂,本身是由createInstanceofPredicate方法创建的。从源码上看,只要这个对象有一个叫isMobXComputedValue的属性,其值为true就行了。
var isComputedValue = createInstanceofPredicate("ComputedValue", ComputedValue);
function createInstanceofPredicate(name, clazz) {
var propName = "isMobX" + name;
clazz.prototype[propName] = true;
return function (x) {
return isObject(x) && x[propName] === true;
};
}
handleAsComputedValue则用来判定用户的原始数据是否有资格转换为计算属性。虽然说属性,但里面却是一个个对象。mobx里面尽是这样笨重的对象组成。
function handleAsComputedValue(value) {//要求是函数,不能在定义时指定参数,不能是Action
return typeof value === "function" && value.length === 0 && !isAction(value);
}
ObservableValue 这个类巨复杂,暂时跳过,我们看一下行的getModifier,这是用来判定用户函数是否加上了一个特殊的属性。
function getModifier(value) {
if (value) {
return value.mobxModifier || null;
}
return null;
}
否则它就会将属性转换为监控属性(ObservableValue的实例)。
最后为监控属性与计算属性生成新的描述对象
var observablePropertyConfigs = {};
var computedPropertyConfigs = {};
function generateObservablePropConfig(propName) {
var config = observablePropertyConfigs[propName];
if (config)
return config;
return observablePropertyConfigs[propName] = {
configurable: true,
enumerable: true,
get: function () {
return this.$mobx.values[propName].get();
},
set: function (v) {
setPropertyValue(this, propName, v);
}
};
}
function generateComputedPropConfig(propName) {
var config = computedPropertyConfigs[propName];
if (config)
return config;
return computedPropertyConfigs[propName] = {
configurable: true,
enumerable: false,
get: function () {
return this.$mobx.values[propName].get();
},
set: function (v) {
return this.$mobx.values[propName].set(v);
}
};
}
下面这句应该是将消息发到事件总线上,然后用触发视图更新什么的,也是巨复杂。
function notifyPropertyAddition(adm, object, name, newValue) {
var notify = hasListeners(adm);
var notifySpy = isSpyEnabled();
var change = notify || notifySpy ? {
type: "add",
object: object, name: name, newValue: newValue
} : null;
if (notifySpy)
spyReportStart(change);
if (notify)
notifyListeners(adm, change);
if (notifySpy)
spyReportEnd();
}
这是Todo类生成的实例,被硬塞了许多东西:
最后我们看autorun,是不是有一种崩溃的感觉,就像发掘某个人的黑历史,目不睱接!
function autorun(arg1, arg2, arg3) {
var name, view, scope;
if (typeof arg1 === "string") {
name = arg1;
view = arg2;
scope = arg3;
}
else if (typeof arg1 === "function") {
name = arg1.name || ("Autorun@" + getNextId());
view = arg1;
scope = arg2;
}
assertUnwrapped(view, "autorun methods cannot have modifiers");
invariant(typeof view === "function", "autorun expects a function");
invariant(isAction(view) === false, "Warning: attempted to pass an action to autorun. Actions are untracked and will not trigger on state changes. Use `reaction` or wrap only your state modification code in an action.");
if (scope)
view = view.bind(scope);
var reaction = new Reaction(name, function () {
this.track(reactionRunner);
});
function reactionRunner() {
view(reaction);
}
reaction.schedule();
return reaction.getDisposer();
}
看源码,我们的例子就走第二个分支,并且没有传入scope,相当于直接
var reaction = new Reaction('AutoRun@11', function () {
this.track(reactionRunner);
});
var vew = function () {
console.log(vm.aaa + " " + vm.bbb)
}
function reactionRunner() {
view(reaction);
}
Reaction也是巨复杂的,我们看一下它的真身吧,下一节详解
function Reaction(name, onInvalidate) {
if (name === void 0) { name = "Reaction@" + getNextId(); }
this.name = name;
this.onInvalidate = onInvalidate;
this.observing = [];
this.newObserving = [];
this.dependenciesState = IDerivationState.NOT_TRACKING;
this.diffValue = 0;
this.runId = 0;
this.unboundDepsCount = 0;
this.__mapid = "#" + getNextId();
this.isDisposed = false;
this._isScheduled = false;
this._isTrackPending = false;
this._isRunning = false;
}
Reaction.prototype.onBecomeStale = function () {
this.schedule();
};
Reaction.prototype.schedule = function () {
if (!this._isScheduled) {
this._isScheduled = true;
globalState.pendingReactions.push(this);
startBatch();
runReactions();
endBatch();
}
};
Reaction.prototype.isScheduled = function () {
return this._isScheduled;
};
Reaction.prototype.runReaction = function () {
if (!this.isDisposed) {
this._isScheduled = false;
if (shouldCompute(this)) {
this._isTrackPending = true;
this.onInvalidate();
if (this._isTrackPending && isSpyEnabled()) {
spyReport({
object: this,
type: "scheduled-reaction"
});
}
}
}
};
Reaction.prototype.track = function (fn) {
startBatch();
var notify = isSpyEnabled();
var startTime;
if (notify) {
startTime = Date.now();
spyReportStart({
object: this,
type: "reaction",
fn: fn
});
}
this._isRunning = true;
trackDerivedFunction(this, fn);
this._isRunning = false;
this._isTrackPending = false;
if (this.isDisposed) {
clearObserving(this);
}
if (notify) {
spyReportEnd({
time: Date.now() - startTime
});
}
endBatch();
};
Reaction.prototype.recoverFromError = function () {
this._isRunning = false;
this._isTrackPending = false;
};
Reaction.prototype.dispose = function () {
if (!this.isDisposed) {
this.isDisposed = true;
if (!this._isRunning) {
startBatch();
clearObserving(this);
endBatch();
}
}
};
Reaction.prototype.getDisposer = function () {
var r = this.dispose.bind(this);
r.$mobx = this;
return r;
};
Reaction.prototype.toString = function () {
return "Reaction[" + this.name + "]";
};
Reaction.prototype.whyRun = function () {
var observing = unique(this._isRunning ? this.newObserving : this.observing).map(function (dep) { return dep.name; });
return ("
WhyRun? reaction '" + this.name + "':
* Status: [" + (this.isDisposed ? "stopped" : this._isRunning ? "running" : this.isScheduled() ? "scheduled" : "idle") + "]
* This reaction will re-run if any of the following observables changes:
" + joinStrings(observing) + "
" + ((this._isRunning) ? " (... or any observable accessed during the remainder of the current run)" : "") + "
Missing items in this list?
1. Check whether all used values are properly marked as observable (use isObservable to verify)
2. Make sure you didn't dereference values too early. MobX observes props, not primitives. E.g: use 'person.name' instead of 'name' in your computation.
");
};
return Reaction;
总而言之,你看完这篇,你还是无法了解它是怎么运作的。每个人实现MVVM的方式都不一样。但MVVM都有一个共同点,就是收集依赖与触发通知。目前,我们已经看到notify这样的字眼了。我们下篇就是寻找它收集依赖的根据!