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 ("\nWhyRun? reaction '" + this.name + "':\n * Status: [" + (this.isDisposed ? "stopped" : this._isRunning ? "running" : this.isScheduled() ? "scheduled" : "idle") + "]\n * This reaction will re-run if any of the following observables changes:\n " + joinStrings(observing) + "\n " + ((this._isRunning) ? " (... or any observable accessed during the remainder of the current run)" : "") + "\n\tMissing items in this list?\n\t 1. Check whether all used values are properly marked as observable (use isObservable to verify)\n\t 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.\n");
};
return Reaction;
總而言之,你看完這篇,你還是無法了解它是怎么運作的。每個人實現MVVM的方式都不一樣。但MVVM都有一個共同點,就是收集依賴與觸發通知。目前,我們已經看到notify這樣的字眼了。我們下篇就是尋找它收集依賴的根據!