mobx是redux的代替品,其自己就是一個很好的MVVM框架。所以花點力氣研究一下它。javascript
網上下最新的2.75java
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寫法react
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與autorunredux
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中絕無緊有的。這在咱們開發階段很是有幫助。api
這個方法無非是告訴咱們,第一個參數必須是對象,而且不能被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這樣更高層次的魔術對象。babel
這個步驟由asObservableObject方法處理,它有三個參數,第二個應該用來生成UUID的,第三個是產生模式。app
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這個類以下:dom
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這樣的字眼了。咱們下篇就是尋找它收集依賴的根據!