├── docs ├── flow.png └── mobx.png ├── test ├── babel │ └── .babelrc ├── require.d.ts ├── perf │ ├── index.js │ ├── perf.txt │ └── transform-perf.js ├── untracked.js ├── mixed-versions.js ├── autorun.js ├── observe.js ├── api.js ├── whyrun.js ├── utils │ └── transform.js ├── autorunAsync.js ├── strict-mode.js ├── intercept.js ├── spy.js ├── cycles.js ├── reaction.js ├── tape.d.ts ├── action.js └── array.js ├── .gitignore ├── .editorconfig ├── sponsors.md ├── tsconfig.json ├── .travis.yml ├── src ├── api │ ├── iscomputed.ts │ ├── whyrun.ts │ ├── expr.ts │ ├── isobservable.ts │ ├── extras.ts │ ├── observabledecorator.ts │ ├── extendobservable.ts │ ├── tojs.ts │ ├── createtransformer.ts │ ├── intercept.ts │ ├── observe.ts │ ├── observable.ts │ ├── computeddecorator.ts │ ├── action.ts │ └── autorun.ts ├── utils │ ├── simpleeventemitter.ts │ ├── iterable.ts │ ├── decorators.ts │ └── utils.ts ├── types │ ├── listen-utils.ts │ ├── intercept-utils.ts │ ├── type-utils.ts │ ├── observablevalue.ts │ ├── modifiers.ts │ └── observableobject.ts ├── core │ ├── transaction.ts │ ├── spy.ts │ ├── action.ts │ ├── atom.ts │ ├── globalstate.ts │ ├── reaction.ts │ ├── computedvalue.ts │ ├── observable.ts │ └── derivation.ts └── mobx.ts ├── bower.json ├── LICENSE ├── scripts ├── publish.js └── single-file-build.sh ├── tslint.json └── package.json /docs/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corymsmith/mobx/master/docs/flow.png -------------------------------------------------------------------------------- /docs/mobx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corymsmith/mobx/master/docs/mobx.png -------------------------------------------------------------------------------- /test/babel/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-1" 6 | ], 7 | "plugins": ["transform-decorators-legacy"] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tscache 2 | .settings 3 | node_modules 4 | npm-debug.log 5 | coverage 6 | notes.md 7 | lib 8 | test/babel-tests.js 9 | test/typescript-tests.js* 10 | .vscode 11 | dist/ 12 | .build/ -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | indent_style = tab 7 | tab_width = 4 8 | 9 | [{package.json,.travis.yml}] 10 | indent_style = space 11 | indent_size = 2 -------------------------------------------------------------------------------- /sponsors.md: -------------------------------------------------------------------------------- 1 | MobX Sponsors 2 | =========== 3 | 4 | Want to sponser MobX as well? https://mobxjs.github.io/mobx/donate.html 5 | 6 | * Mendix 7 | * Mattia Manzati 8 | * Matt Ruby 9 | * Jeff Hansen 10 | * Steven Pérez 11 | * Anri Asaturov 12 | -------------------------------------------------------------------------------- /test/require.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * From DefinitelyTyped's `node.d.ts` file. 4 | */ 5 | 6 | declare var require: { 7 | (id: string): any; 8 | resolve(id:string): string; 9 | cache: any; 10 | extensions: any; 11 | main: any; 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.7.5", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "outDir": "lib", 6 | "sourceMap": false, 7 | "declaration": true, 8 | "module": "commonjs", 9 | "removeComments": false 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "test", 14 | "lib", 15 | ".build" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | install: 3 | - export DISPLAY=':99.0' 4 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 5 | - npm install 6 | - chmod +x scripts/single-file-build.sh 7 | script: npm run test-travis 8 | after_success: 9 | - cat ./coverage/lcov.info|./node_modules/coveralls/bin/coveralls.js 10 | node_js: 11 | - 4 12 | addons: 13 | apt: 14 | packages: 15 | - xvfb 16 | -------------------------------------------------------------------------------- /src/api/iscomputed.ts: -------------------------------------------------------------------------------- 1 | import {isObservableObject} from "../types/observableobject"; 2 | import {getAtom} from "../types/type-utils"; 3 | import {isComputedValue} from "../core/computedvalue"; 4 | 5 | export function isComputed(value, property?: string): boolean { 6 | if (value === null || value === undefined) 7 | return false; 8 | if (property !== undefined) { 9 | if (isObservableObject(value) === false) 10 | return false; 11 | const atom = getAtom(value, property); 12 | return isComputedValue(atom) 13 | } 14 | return isComputedValue(value); 15 | } 16 | -------------------------------------------------------------------------------- /test/perf/index.js: -------------------------------------------------------------------------------- 1 | var start = Date.now(); 2 | 3 | if (process.env.PERSIST) { 4 | var fs = require('fs'); 5 | var logFile = __dirname + "/perf.txt" 6 | // clear previous results 7 | if (fs.existsSync(logFile)) 8 | fs.unlinkSync(logFile) 9 | 10 | exports.logMeasurement = function (msg) { 11 | console.log(msg) 12 | fs.appendFileSync(logFile, "\n" + msg, "utf8") 13 | } 14 | } 15 | else { 16 | exports.logMeasurement = function (msg) { 17 | console.log(msg) 18 | } 19 | } 20 | 21 | require('./perf.js'); 22 | require('./transform-perf.js'); 23 | 24 | // This test runs last.. 25 | require('tape')(t => { 26 | exports.logMeasurement("\n\nCompleted performance suite in " + ((Date.now() - start) / 1000) + " sec.") 27 | t.end() 28 | }); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobx", 3 | "description": "Simple, scalable state management.", 4 | "main": "lib/mobx.umd.js", 5 | "authors": [ 6 | "Michel Weststrate" 7 | ], 8 | "license": "MIT", 9 | "keywords": [ 10 | "mobx", 11 | "mobservable", 12 | "observable", 13 | "react-component", 14 | "react", 15 | "reactjs", 16 | "reactive", 17 | "model", 18 | "frp", 19 | "functional-reactive-programming", 20 | "state management", 21 | "data flow" 22 | ], 23 | "homepage": "https://mobxjs.github.io/mobx", 24 | "moduleType": [ 25 | "amd", 26 | "globals", 27 | "node" 28 | ], 29 | "ignore": [ 30 | "**/.*", 31 | "node_modules", 32 | "bower_components", 33 | "test", 34 | "tests" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /test/untracked.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var m = require('..'); 3 | 4 | test('untracked 1', function(t) { 5 | var cCalcs = 0, dCalcs = 0; 6 | var a = m.observable(1); 7 | var b = m.observable(2); 8 | var c = m.observable(function() { 9 | cCalcs++; 10 | return a.get() + m.untracked(function() { 11 | return b.get(); 12 | }); 13 | }); 14 | var result; 15 | 16 | var d = m.autorun(function() { 17 | dCalcs++; 18 | result = c.get(); 19 | }); 20 | 21 | t.equal(result, 3); 22 | t.equal(cCalcs, 1); 23 | t.equal(dCalcs, 1); 24 | 25 | b.set(3); 26 | t.equal(result, 3); 27 | t.equal(cCalcs, 1); 28 | t.equal(dCalcs, 1); 29 | 30 | a.set(2); 31 | t.equal(result, 5); 32 | t.equal(cCalcs, 2); 33 | t.equal(dCalcs, 2); 34 | 35 | t.end(); 36 | }); -------------------------------------------------------------------------------- /test/mixed-versions.js: -------------------------------------------------------------------------------- 1 | const test = require("tape") 2 | const mobx1 = require("../") 3 | const mobx2 = require("../lib/mobx.umd.min.js") 4 | 5 | test("two versions should work together", (t) => { 6 | const a = mobx1.observable({ 7 | x: 1, 8 | get y() { 9 | return this.x + b.x 10 | } 11 | }) 12 | const b = mobx2.observable({ 13 | x: 3, 14 | get y() { 15 | return (this.x + a.x) * 2 16 | } 17 | }) 18 | 19 | const values = [] 20 | const d1 = mobx1.autorun(() => { 21 | values.push(a.y - b.y) 22 | }) 23 | const d2 = mobx2.autorun(() => { 24 | values.push(b.y - a.y) 25 | }) 26 | 27 | a.x = 2 28 | b.x = 4 29 | 30 | d1() 31 | d2() 32 | a.x = 87 33 | a.x = 23 34 | 35 | t.deepEqual(values, [ 36 | -4, 4, 37 | 5, -5, // n.b: depending execution order columns could be swapped 38 | 6, -6 39 | ]) 40 | 41 | t.end() 42 | }) -------------------------------------------------------------------------------- /src/utils/simpleeventemitter.ts: -------------------------------------------------------------------------------- 1 | import {once, Lambda, deprecated} from "./utils"; 2 | 3 | export type ISimpleEventListener = {(...data: any[]): void} 4 | 5 | export class SimpleEventEmitter { 6 | listeners: ISimpleEventListener[] = []; 7 | 8 | constructor() { 9 | deprecated("extras.SimpleEventEmitter is deprecated and will be removed in the next major release"); 10 | } 11 | 12 | emit(...data: any[]); 13 | emit() { 14 | const listeners = this.listeners.slice(); 15 | for (let i = 0, l = listeners.length; i < l; i++) 16 | listeners[i].apply(null, arguments); 17 | } 18 | 19 | on(listener: ISimpleEventListener): Lambda { 20 | this.listeners.push(listener); 21 | return once(() => { 22 | const idx = this.listeners.indexOf(listener); 23 | if (idx !== -1) 24 | this.listeners.splice(idx, 1); 25 | }); 26 | } 27 | 28 | once(listener: ISimpleEventListener): Lambda { 29 | const subscription = this.on(function() { 30 | subscription(); 31 | listener.apply(this, arguments); 32 | }); 33 | return subscription; 34 | } 35 | } -------------------------------------------------------------------------------- /test/autorun.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var m = require('..'); 3 | 4 | test('autorun passes Reaction as an argument to view function', function(t) { 5 | var a = m.observable(1); 6 | var values = []; 7 | 8 | m.autorun(r => { 9 | t.equal(typeof r.dispose, 'function'); 10 | if (a.get() === 'pleaseDispose') r.dispose(); 11 | values.push(a.get()); 12 | }); 13 | 14 | a.set(2); 15 | a.set(2); 16 | a.set('pleaseDispose'); 17 | a.set(3); 18 | a.set(4); 19 | 20 | t.deepEqual(values, [1, 2, 'pleaseDispose']); 21 | 22 | t.end() 23 | }); 24 | 25 | test('autorun can be disposed on first run', function(t) { 26 | var a = m.observable(1); 27 | var values = []; 28 | 29 | m.autorun(r => { 30 | r.dispose(); 31 | values.push(a.get()); 32 | }); 33 | 34 | a.set(2); 35 | 36 | t.deepEqual(values, [1]); 37 | 38 | t.end() 39 | }); 40 | 41 | test('autorun warns when passed an action', function(t) { 42 | var action = m.action(() => {}); 43 | t.plan(1); 44 | t.throws(() => m.autorun(action), /attempted to pass an action to autorun/); 45 | t.end(); 46 | }); 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Michel Weststrate 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/api/whyrun.ts: -------------------------------------------------------------------------------- 1 | import {globalState} from "../core/globalstate"; 2 | import {isComputedValue} from "../core/computedvalue"; 3 | import {isReaction} from "../core/reaction"; 4 | import {getAtom} from "../types/type-utils"; 5 | import {invariant} from "../utils/utils"; 6 | 7 | function log(msg: string): string { 8 | console.log(msg); 9 | return msg; 10 | } 11 | 12 | export function whyRun(thing?: any, prop?: string) { 13 | switch (arguments.length) { 14 | case 0: 15 | thing = globalState.trackingDerivation; 16 | if (!thing) 17 | return log("whyRun() can only be used if a derivation is active, or by passing an computed value / reaction explicitly. If you invoked whyRun from inside a computation; the computation is currently suspended but re-evaluating because somebody requested it's value."); 18 | break; 19 | case 2: 20 | thing = getAtom(thing, prop); 21 | break; 22 | } 23 | thing = getAtom(thing); 24 | if (isComputedValue(thing)) 25 | return log(thing.whyRun()); 26 | else if (isReaction(thing)) 27 | return log(thing.whyRun()); 28 | else 29 | invariant(false, "whyRun can only be used on reactions and computed values"); 30 | } 31 | -------------------------------------------------------------------------------- /src/api/expr.ts: -------------------------------------------------------------------------------- 1 | import {computed} from "../api/computeddecorator"; 2 | import {isComputingDerivation} from "../core/derivation"; 3 | 4 | /** 5 | * expr can be used to create temporarily views inside views. 6 | * This can be improved to improve performance if a value changes often, but usually doesn't affect the outcome of an expression. 7 | * 8 | * In the following example the expression prevents that a component is rerender _each time_ the selection changes; 9 | * instead it will only rerenders when the current todo is (de)selected. 10 | * 11 | * reactiveComponent((props) => { 12 | * const todo = props.todo; 13 | * const isSelected = mobx.expr(() => props.viewState.selection === todo); 14 | * return
{todo.title}
15 | * }); 16 | * 17 | */ 18 | export function expr(expr: () => T, scope?): T { 19 | // TODO: deprecate in 3.0? seems to be hardly used.. 20 | if (!isComputingDerivation()) 21 | console.warn("[mobx.expr] 'expr' should only be used inside other reactive functions."); 22 | // optimization: would be more efficient if the expr itself wouldn't be evaluated first on the next change, but just a 'changed' signal would be fired 23 | return computed(expr, scope).get(); 24 | } 25 | -------------------------------------------------------------------------------- /src/api/isobservable.ts: -------------------------------------------------------------------------------- 1 | import {isObservableArray} from "../types/observablearray"; 2 | import {isObservableMap} from "../types/observablemap"; 3 | import {isObservableObject, ObservableObjectAdministration} from "../types/observableobject"; 4 | import {isAtom} from "../core/atom"; 5 | import {isComputedValue} from "../core/computedvalue"; 6 | import {isReaction} from "../core/reaction"; 7 | 8 | /** 9 | * Returns true if the provided value is reactive. 10 | * @param value object, function or array 11 | * @param propertyName if propertyName is specified, checkes whether value.propertyName is reactive. 12 | */ 13 | export function isObservable(value, property?: string): boolean { 14 | if (value === null || value === undefined) 15 | return false; 16 | if (property !== undefined) { 17 | if (isObservableArray(value) || isObservableMap(value)) 18 | throw new Error("[mobx.isObservable] isObservable(object, propertyName) is not supported for arrays and maps. Use map.has or array.length instead."); 19 | else if (isObservableObject(value)) { 20 | const o = value.$mobx; 21 | return o.values && !!o.values[property]; 22 | } 23 | return false; 24 | } 25 | return !!value.$mobx || isAtom(value) || isReaction(value) || isComputedValue(value); 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/iterable.ts: -------------------------------------------------------------------------------- 1 | import {invariant, addHiddenFinalProp} from "./utils"; 2 | 3 | // inspired by https://github.com/leebyron/iterall/ 4 | 5 | declare var Symbol; 6 | 7 | function iteratorSymbol() { 8 | return (typeof Symbol === "function" && Symbol.iterator) || "@@iterator"; 9 | } 10 | 11 | export const IS_ITERATING_MARKER = "__$$iterating"; 12 | 13 | export interface Iterator { 14 | next(): { 15 | done: boolean; 16 | value?: T; 17 | }; 18 | } 19 | 20 | export function arrayAsIterator(array: T[]): T[] & Iterator { 21 | // TODO: this should be removed in the next major version of MobX 22 | // returning an array for entries(), values() etc for maps was a mis-interpretation of the specs.. 23 | invariant(array[IS_ITERATING_MARKER] !== true, "Illegal state: cannot recycle array as iterator"); 24 | addHiddenFinalProp(array, IS_ITERATING_MARKER, true); 25 | 26 | let idx = -1; 27 | addHiddenFinalProp(array, "next", function next() { 28 | idx++; 29 | return { 30 | done: idx >= this.length, 31 | value: idx < this.length ? this[idx] : undefined 32 | }; 33 | }); 34 | return array as any; 35 | } 36 | 37 | export function declareIterator(prototType, iteratorFactory: () => Iterator) { 38 | addHiddenFinalProp(prototType, iteratorSymbol(), iteratorFactory); 39 | } 40 | -------------------------------------------------------------------------------- /src/api/extras.ts: -------------------------------------------------------------------------------- 1 | import {IDepTreeNode, getObservers, hasObservers} from "../core/observable"; 2 | import {unique} from "../utils/utils"; 3 | import {getAtom} from "../types/type-utils"; 4 | 5 | export interface IDependencyTree { 6 | name: string; 7 | dependencies?: IDependencyTree[]; 8 | } 9 | 10 | export interface IObserverTree { 11 | name: string; 12 | observers?: IObserverTree[]; 13 | } 14 | 15 | export function getDependencyTree(thing: any, property?: string): IDependencyTree { 16 | return nodeToDependencyTree(getAtom(thing, property)); 17 | } 18 | 19 | function nodeToDependencyTree(node: IDepTreeNode): IDependencyTree { 20 | const result: IDependencyTree = { 21 | name: node.name 22 | }; 23 | if (node.observing && node.observing.length > 0) 24 | result.dependencies = unique(node.observing).map(nodeToDependencyTree); 25 | return result; 26 | } 27 | 28 | export function getObserverTree(thing: any, property?: string): IObserverTree { 29 | return nodeToObserverTree(getAtom(thing, property)); 30 | } 31 | 32 | function nodeToObserverTree(node: IDepTreeNode): IObserverTree { 33 | const result: IObserverTree = { 34 | name: node.name 35 | }; 36 | if (hasObservers(node as any)) 37 | result.observers = getObservers(node as any).map(nodeToObserverTree); 38 | return result; 39 | } 40 | -------------------------------------------------------------------------------- /src/types/listen-utils.ts: -------------------------------------------------------------------------------- 1 | import {Lambda, once} from "../utils/utils"; 2 | import {untrackedStart, untrackedEnd} from "../core/derivation"; 3 | 4 | export interface IListenable { 5 | changeListeners: Function[]; 6 | observe(handler: (change: any, oldValue?: any) => void, fireImmediately?: boolean): Lambda; 7 | } 8 | 9 | export function hasListeners(listenable: IListenable) { 10 | return listenable.changeListeners && listenable.changeListeners.length > 0; 11 | } 12 | 13 | export function registerListener(listenable: IListenable, handler: Function): Lambda { 14 | const listeners = listenable.changeListeners || (listenable.changeListeners = []); 15 | listeners.push(handler); 16 | return once(() => { 17 | const idx = listeners.indexOf(handler); 18 | if (idx !== -1) 19 | listeners.splice(idx, 1); 20 | }); 21 | } 22 | 23 | export function notifyListeners(listenable: IListenable, change: T | T[]) { 24 | const prevU = untrackedStart(); 25 | let listeners = listenable.changeListeners; 26 | if (!listeners) 27 | return; 28 | listeners = listeners.slice(); 29 | for (let i = 0, l = listeners.length; i < l; i++) { 30 | if (Array.isArray(change)) { 31 | listeners[i].apply(null, change); 32 | } 33 | else { 34 | listeners[i](change); 35 | } 36 | } 37 | untrackedEnd(prevU); 38 | } 39 | -------------------------------------------------------------------------------- /src/core/transaction.ts: -------------------------------------------------------------------------------- 1 | import {globalState} from "./globalstate"; 2 | import {runReactions} from "./reaction"; 3 | import {startBatch, endBatch} from "./observable"; 4 | import {isSpyEnabled, spyReportStart, spyReportEnd} from "./spy"; 5 | 6 | /** 7 | * During a transaction no views are updated until the end of the transaction. 8 | * The transaction will be run synchronously nonetheless. 9 | * @param action a function that updates some reactive state 10 | * @returns any value that was returned by the 'action' parameter. 11 | */ 12 | export function transaction(action: () => T, thisArg = undefined, report = true): T { 13 | transactionStart(((action as any).name) || "anonymous transaction", thisArg, report); 14 | const res = action.call(thisArg); 15 | transactionEnd(report); 16 | return res; 17 | } 18 | 19 | export function transactionStart(name: string, thisArg = undefined, report = true) { 20 | startBatch(); 21 | globalState.inTransaction += 1; 22 | if (report && isSpyEnabled()) { 23 | spyReportStart({ 24 | type: "transaction", 25 | target: thisArg, 26 | name: name 27 | }); 28 | } 29 | } 30 | 31 | export function transactionEnd(report = true) { 32 | if (--globalState.inTransaction === 0) { 33 | runReactions(); 34 | } 35 | if (report && isSpyEnabled()) 36 | spyReportEnd(); 37 | endBatch(); 38 | } 39 | -------------------------------------------------------------------------------- /test/observe.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var m = require('..'); 3 | 4 | test('observe object and map properties', function(t) { 5 | var map = m.map({ a : 1 }); 6 | var events = []; 7 | 8 | t.throws(function() { 9 | m.observe(map, "b", function() {}); 10 | }); 11 | 12 | var d1 = m.observe(map, "a", function(newV, oldV) { 13 | events.push([newV, oldV]); 14 | }); 15 | 16 | map.set("a", 2); 17 | map.set("a", 3); 18 | d1(); 19 | map.set("a", 4); 20 | 21 | var o = m.observable({ 22 | a: 5 23 | }); 24 | 25 | t.throws(function() { 26 | m.observe(o, "b", function() {}); 27 | }); 28 | var d2 = m.observe(o, "a", function(newV, oldV) { 29 | events.push([newV, oldV]); 30 | }); 31 | 32 | o.a = 6; 33 | o.a = 7; 34 | d2(); 35 | o.a = 8; 36 | 37 | t.deepEqual(events, [ 38 | [2, 1], 39 | [3, 2], 40 | [6, 5], 41 | [7, 6] 42 | ]); 43 | 44 | t.end(); 45 | }); 46 | 47 | test('observe computed values', function(t) { 48 | var events = []; 49 | 50 | var v = m.observable(0); 51 | var f = m.observable(0); 52 | var c = m.computed(function() { return v.get(); }); 53 | 54 | var d2 = c.observe(function(newV, oldV) { 55 | v.get(); 56 | f.get(); 57 | events.push([newV, oldV]); 58 | }); 59 | 60 | v.set(6); 61 | f.set(10); 62 | 63 | t.deepEqual(events, [ 64 | [6, 0] 65 | ]); 66 | 67 | t.end(); 68 | 69 | }); -------------------------------------------------------------------------------- /src/types/intercept-utils.ts: -------------------------------------------------------------------------------- 1 | import {Lambda, once, invariant} from "../utils/utils"; 2 | import {untrackedStart, untrackedEnd} from "../core/derivation"; 3 | 4 | export type IInterceptor = (change: T) => T; 5 | 6 | export interface IInterceptable { 7 | interceptors: IInterceptor[]; 8 | intercept(handler: IInterceptor): Lambda; 9 | } 10 | 11 | export function hasInterceptors(interceptable: IInterceptable) { 12 | return (interceptable.interceptors && interceptable.interceptors.length > 0); 13 | } 14 | 15 | export function registerInterceptor(interceptable: IInterceptable, handler: IInterceptor): Lambda { 16 | const interceptors = interceptable.interceptors || (interceptable.interceptors = []); 17 | interceptors.push(handler); 18 | return once(() => { 19 | const idx = interceptors.indexOf(handler); 20 | if (idx !== -1) 21 | interceptors.splice(idx, 1); 22 | }); 23 | } 24 | 25 | export function interceptChange(interceptable: IInterceptable, change: T): T { 26 | const prevU = untrackedStart(); 27 | const interceptors = interceptable.interceptors; 28 | for (let i = 0, l = interceptors.length; i < l; i++) { 29 | change = interceptors[i](change); 30 | invariant(!change || (change as any).type, "Intercept handlers should return nothing or a change object"); 31 | if (!change) 32 | return null; 33 | } 34 | untrackedEnd(prevU); 35 | return change; 36 | } 37 | -------------------------------------------------------------------------------- /scripts/publish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/nscript 2 | /* To run this script, nscript is needed: [sudo] npm install -g nscript 3 | /* Publish.js, publish a new version of the npm package as found in the current directory */ 4 | /* Run this file from the root of the repository */ 5 | module.exports = function(shell, npm, git) { 6 | var pkg = JSON.parse(shell.read('package.json')); 7 | 8 | // Bump version number 9 | var nrs = pkg.version.split("."); 10 | nrs[2] = 1 + parseInt(nrs[2], 10); 11 | var version = pkg.version = shell.prompt("Please specify the new package version of '" + pkg.name + "' (Ctrl^C to abort)", nrs.join(".")); 12 | if (!version.match(/^\d+\.\d+\.\d+$/)) 13 | shell.exit(1, "Invalid semantic version: " + version); 14 | 15 | // Check registery data 16 | if (npm.silent().test("info", pkg.name)) { 17 | //package is registered in npm? 18 | var publishedPackageInfo = JSON.parse(npm.get("info", pkg.name, "--json")); 19 | if (publishedPackageInfo.versions == version || publishedPackageInfo.versions.indexOf(version) != -1) 20 | shell.exit(2, "Version " + pkg.version + " is already published to npm"); 21 | 22 | shell.write('package.json', JSON.stringify(pkg, null, 2)); 23 | 24 | // Finally, commit and publish! 25 | npm("publish"); 26 | git("commit","-am","Published version " + version); 27 | git("tag", version); 28 | 29 | git("push"); 30 | git("push","--tags"); 31 | console.log("Published!"); 32 | } 33 | else 34 | shell.exit(1, pkg.name + " is not an existing npm package"); 35 | }; -------------------------------------------------------------------------------- /test/perf/perf.txt: -------------------------------------------------------------------------------- 1 | 2 | One observers many observes one - Started/Updated in 35/31 ms. 3 | 500 props observing sibling - Started/Updated in 3/4 ms. 4 | Late dependency change - Updated in 62ms. 5 | Unused computables - Updated in 0 ms. 6 | Unused observables - Updated in 11 ms. 7 | Array reduce - Started/Updated in 30/25 ms. 8 | Array loop - Started/Updated in 103/172 ms. 9 | Order system batched: false tracked: true Started/Updated in 550/73 ms. 10 | Order system batched: true tracked: true Started/Updated in 145/64 ms. 11 | Order system batched: false tracked: false Started/Updated in 158/51 ms. 12 | Order system batched: true tracked: false Started/Updated in 169/93 ms. 13 | 14 | Create array - Created in 516ms. 15 | 16 | Create array (non-recursive) Created in 252ms. 17 | Observable with many observers + dispose: 1919ms 18 | expensive sort: created 4935 19 | expensive sort: updated 17513 20 | expensive sort: disposed441 21 | native plain sort: updated 946 22 | computed memoization 0ms 23 | create folders 0ms. 24 | create displayfolders 0ms. 25 | create text 261ms. 26 | collapse folder 2ms. 27 | uncollapse folder 0ms. 28 | change name of folder 248ms. 29 | search 54ms. 30 | unsearch 245ms. 31 | reactive folder tree [total] 32 | 812ms. 33 | create folders 29ms. 34 | create displayfolders 2ms. 35 | create text 92ms. 36 | collapse folder 4ms. 37 | uncollapse folder 11ms. 38 | change name of folder 18ms. 39 | search 36ms. 40 | unsearch 39ms. 41 | reactive folder tree [total] 42 | 235ms. 43 | create boxes 120ms. 44 | mutations 464ms. 45 | total 602ms. 46 | create boxes 122ms. 47 | mutations 956ms. 48 | total 1160ms. 49 | 50 | 51 | Completed performance suite in 32.291 sec. -------------------------------------------------------------------------------- /src/core/spy.ts: -------------------------------------------------------------------------------- 1 | import {globalState} from "./globalstate"; 2 | import {objectAssign, deprecated, once, Lambda} from "../utils/utils"; 3 | 4 | export function isSpyEnabled() { 5 | return !!globalState.spyListeners.length; 6 | } 7 | 8 | export function spyReport(event) { 9 | if (!globalState.spyListeners.length) 10 | return false; 11 | const listeners = globalState.spyListeners; 12 | for (let i = 0, l = listeners.length; i < l; i++) 13 | listeners[i](event); 14 | } 15 | 16 | export function spyReportStart(event) { 17 | const change = objectAssign({}, event, { spyReportStart: true }); 18 | spyReport(change); 19 | } 20 | 21 | const END_EVENT = { spyReportEnd: true }; 22 | 23 | // TODO: change signature to spyReportEnd(time?: number) 24 | export function spyReportEnd(change?) { 25 | if (change) 26 | spyReport(objectAssign({}, change, END_EVENT)); 27 | else 28 | spyReport(END_EVENT); 29 | } 30 | 31 | export function spy(listener: (change: any) => void): Lambda { 32 | globalState.spyListeners.push(listener); 33 | return once(() => { 34 | const idx = globalState.spyListeners.indexOf(listener); 35 | if (idx !== -1) 36 | globalState.spyListeners.splice(idx, 1); 37 | }); 38 | } 39 | 40 | export function trackTransitions(onReport?: (c: any) => void): Lambda { 41 | deprecated("trackTransitions is deprecated. Use mobx.spy instead"); 42 | if (typeof onReport === "boolean") { 43 | deprecated("trackTransitions only takes a single callback function. If you are using the mobx-react-devtools, please update them first"); 44 | onReport = arguments[1]; 45 | } 46 | if (!onReport) { 47 | deprecated("trackTransitions without callback has been deprecated and is a no-op now. If you are using the mobx-react-devtools, please update them first"); 48 | return () => {}; 49 | } 50 | return spy(onReport); 51 | } -------------------------------------------------------------------------------- /test/api.js: -------------------------------------------------------------------------------- 1 | var mobx = require('../'); 2 | var test = require('tape'); 3 | 4 | test('correct api should be exposed', function(t) { 5 | t.deepEquals(Object.keys(mobx).sort(), [ 6 | 'Atom', 7 | 'BaseAtom', // TODO: remove somehow 8 | 'IDerivationState', 9 | 'ObservableMap', 10 | 'Reaction', 11 | 'SimpleEventEmitter', 12 | '_', 13 | 'action', 14 | 'asFlat', 15 | 'asMap', 16 | 'asReference', 17 | 'asStructure', 18 | 'autorun', 19 | 'autorunAsync', 20 | 'autorunUntil', 21 | 'computed', 22 | 'createTransformer', 23 | 'expr', 24 | 'extendObservable', 25 | 'extras', 26 | 'fastArray', 27 | 'intercept', 28 | 'isAction', 29 | 'isComputed', 30 | 'isObservable', 31 | 'isObservableArray', 32 | 'isObservableMap', 33 | 'isObservableObject', 34 | 'isStrictModeEnabled', 35 | 'map', 36 | 'observable', 37 | 'observe', 38 | 'reaction', 39 | 'runInAction', 40 | 'spy', 41 | 'toJS', 42 | 'toJSON', 43 | 'transaction', 44 | 'untracked', 45 | 'useStrict', 46 | 'when', 47 | 'whyRun' 48 | ]); 49 | t.equals(Object.keys(mobx).filter(function(key) { 50 | return mobx[key] == undefined; 51 | }).length, 0); 52 | 53 | t.deepEquals(Object.keys(mobx._).sort(), [ 54 | 'getAdministration', 55 | 'resetGlobalState' 56 | ]); 57 | t.equals(Object.keys(mobx._).filter(function(key) { 58 | return mobx._[key] == undefined; 59 | }).length, 0); 60 | 61 | t.deepEquals(Object.keys(mobx.extras).sort(), [ 62 | 'allowStateChanges', 63 | 'getAtom', 64 | 'getDebugName', 65 | 'getDependencyTree', 66 | 'getObserverTree', 67 | 'isComputingDerivation', 68 | 'isSpyEnabled', 69 | 'resetGlobalState', 70 | 'spyReport', 71 | 'spyReportEnd', 72 | 'spyReportStart', 73 | 'trackTransitions' 74 | ]); 75 | t.equals(Object.keys(mobx.extras).filter(function(key) { 76 | return mobx.extras[key] == undefined; 77 | }).length, 0); 78 | 79 | t.end(); 80 | }); 81 | -------------------------------------------------------------------------------- /src/api/observabledecorator.ts: -------------------------------------------------------------------------------- 1 | import {ValueMode, asReference} from "../types/modifiers"; 2 | import {allowStateChangesStart, allowStateChangesEnd} from "../core/action"; 3 | import {asObservableObject, defineObservableProperty, setPropertyValue} from "../types/observableobject"; 4 | import {invariant, assertPropertyConfigurable} from "../utils/utils"; 5 | import {createClassPropertyDecorator} from "../utils/decorators"; 6 | 7 | const decoratorImpl = createClassPropertyDecorator( 8 | (target, name, baseValue) => { 9 | // might happen lazily (on first read), so temporarily allow state changes.. 10 | const prevA = allowStateChangesStart(true); 11 | if (typeof baseValue === "function") 12 | baseValue = asReference(baseValue); 13 | const adm = asObservableObject(target, undefined, ValueMode.Recursive); 14 | defineObservableProperty(adm, name, baseValue, true, undefined); 15 | allowStateChangesEnd(prevA); 16 | }, 17 | function (name) { 18 | const observable = this.$mobx.values[name]; 19 | if (observable === undefined) // See #505 20 | return undefined; 21 | return observable.get(); 22 | }, 23 | function (name, value) { 24 | setPropertyValue(this, name, value); 25 | }, 26 | true, 27 | false 28 | ); 29 | 30 | /** 31 | * ESNext / Typescript decorator which can to make class properties and getter functions reactive. 32 | * Use this annotation to wrap properties of an object in an observable, for example: 33 | * class OrderLine { 34 | * @observable amount = 3; 35 | * @observable price = 2; 36 | * @observable total() { 37 | * return this.amount * this.price; 38 | * } 39 | * } 40 | */ 41 | export function observableDecorator(target: Object, key: string, baseDescriptor: PropertyDescriptor) { 42 | invariant(arguments.length >= 2 && arguments.length <= 3, "Illegal decorator config", key); 43 | assertPropertyConfigurable(target, key); 44 | invariant(!baseDescriptor || !baseDescriptor.get, "@observable can not be used on getters, use @computed instead"); 45 | return decoratorImpl.apply(null, arguments); 46 | } 47 | -------------------------------------------------------------------------------- /src/api/extendobservable.ts: -------------------------------------------------------------------------------- 1 | import {ValueMode} from "../types/modifiers"; 2 | import {isObservableMap} from "../types/observablemap"; 3 | import {asObservableObject, setObservableObjectInstanceProperty} from "../types/observableobject"; 4 | import {isObservable} from "../api/isobservable"; 5 | import {invariant, isPropertyConfigurable, hasOwnProperty} from "../utils/utils"; 6 | 7 | /** 8 | * Extends an object with reactive capabilities. 9 | * @param target the object to which reactive properties should be added 10 | * @param properties the properties that should be added and made reactive 11 | * @returns targer 12 | */ 13 | export function extendObservable(target: A, ...properties: B[]): A & B { 14 | invariant(arguments.length >= 2, "extendObservable expected 2 or more arguments"); 15 | invariant(typeof target === "object", "extendObservable expects an object as first argument"); 16 | invariant(!(isObservableMap(target)), "extendObservable should not be used on maps, use map.merge instead"); 17 | properties.forEach(propSet => { 18 | invariant(typeof propSet === "object", "all arguments of extendObservable should be objects"); 19 | 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"); 20 | extendObservableHelper(target, propSet, ValueMode.Recursive, null); 21 | }); 22 | return target; 23 | } 24 | 25 | export function extendObservableHelper(target, properties, mode: ValueMode, name: string): Object { 26 | const adm = asObservableObject(target, name, mode); 27 | for (let key in properties) if (hasOwnProperty(properties, key)) { 28 | if (target === properties && !isPropertyConfigurable(target, key)) 29 | continue; // see #111, skip non-configurable or non-writable props for `observable(object)`. 30 | const descriptor = Object.getOwnPropertyDescriptor(properties, key); 31 | setObservableObjectInstanceProperty(adm, key, descriptor); 32 | } 33 | return target; 34 | } 35 | -------------------------------------------------------------------------------- /src/api/tojs.ts: -------------------------------------------------------------------------------- 1 | import {isObservableArray} from "../types/observablearray"; 2 | import {isObservableMap} from "../types/observablemap"; 3 | import {isObservableValue} from "../types/observablevalue"; 4 | import {deprecated} from "../utils/utils"; 5 | 6 | /** 7 | * Basically, a deep clone, so that no reactive property will exist anymore. 8 | */ 9 | export function toJS(source, detectCycles: boolean = true, __alreadySeen: [any, any][] = null) { 10 | // optimization: using ES6 map would be more efficient! 11 | function cache(value) { 12 | if (detectCycles) 13 | __alreadySeen.push([source, value]); 14 | return value; 15 | } 16 | if (source instanceof Date || source instanceof RegExp) 17 | return source; 18 | 19 | if (detectCycles && __alreadySeen === null) 20 | __alreadySeen = []; 21 | if (detectCycles && source !== null && typeof source === "object") { 22 | for (let i = 0, l = __alreadySeen.length; i < l; i++) 23 | if (__alreadySeen[i][0] === source) 24 | return __alreadySeen[i][1]; 25 | } 26 | 27 | if (!source) 28 | return source; 29 | if (Array.isArray(source) || isObservableArray(source)) { 30 | const res = cache([]); 31 | const toAdd = source.map(value => toJS(value, detectCycles, __alreadySeen)); 32 | res.length = toAdd.length; 33 | for (let i = 0, l = toAdd.length; i < l; i++) 34 | res[i] = toAdd[i]; 35 | return res; 36 | } 37 | if (isObservableMap(source)) { 38 | const res = cache({}); 39 | source.forEach( 40 | (value, key) => res[key] = toJS(value, detectCycles, __alreadySeen) 41 | ); 42 | return res; 43 | } 44 | if (isObservableValue(source)) 45 | return toJS(source.get(), detectCycles, __alreadySeen); 46 | if (typeof source === "object") { 47 | const res = cache({}); 48 | for (let key in source) 49 | res[key] = toJS(source[key], detectCycles, __alreadySeen); 50 | return res; 51 | } 52 | return source; 53 | } 54 | 55 | export function toJSON(source, detectCycles: boolean = true, __alreadySeen: [any, any][] = null) { 56 | deprecated("toJSON is deprecated. Use toJS instead"); 57 | return toJS.apply(null, arguments); 58 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "curly": false, 9 | "indent": [ 10 | true, 11 | "tabs" 12 | ], 13 | "interface-name": true, 14 | "jsdoc-format": true, 15 | "no-consecutive-blank-lines" : true, 16 | "no-debugger": true, 17 | "no-duplicate-key": true, 18 | "no-duplicate-variable": true, 19 | "no-eval": true, 20 | "no-internal-module": true, 21 | "no-trailing-whitespace": true, 22 | "no-shadowed-variable": true, 23 | "no-switch-case-fall-through": true, 24 | "no-unreachable": true, 25 | "no-unused-expression": true, 26 | "no_unused-variable": [ 27 | true, 28 | "check-parameters" 29 | ], 30 | "no-use-before-declare": false, 31 | "no-var-keyword": true, 32 | "one-line": [ 33 | true, 34 | "check-open-brace", 35 | "check-whitespace", 36 | "check-catch" 37 | ], 38 | "quotemark": [ 39 | true, 40 | "double" 41 | ], 42 | "semicolon": true, 43 | "trailing-comma": [ 44 | true, 45 | { 46 | "multiline": "never", 47 | "singleline": "never" 48 | } 49 | ], 50 | "triple-equals": [ 51 | true, 52 | "allow-null-check" 53 | ], 54 | "typedef-whitespace": [ 55 | true, 56 | { 57 | "call-signature": "nospace", 58 | "index-signature": "nospace", 59 | "parameter": "nospace", 60 | "property-declaration": "nospace", 61 | "variable-declaration": "nospace" 62 | } 63 | ], 64 | "variable-name": [ 65 | true, 66 | "ban-keywords" 67 | ], 68 | "whitespace": [ 69 | true, 70 | "check-branch", 71 | "check-decl", 72 | "check-operator", 73 | "check-separator", 74 | "check-type" 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/api/createtransformer.ts: -------------------------------------------------------------------------------- 1 | import {ComputedValue} from "../core/computedvalue"; 2 | import {invariant, getNextId, addHiddenProp} from "../utils/utils"; 3 | import {globalState} from "../core/globalstate"; 4 | 5 | export type ITransformer = (object: A) => B; 6 | 7 | export function createTransformer(transformer: ITransformer, onCleanup?: (resultObject: B, sourceObject?: A) => void): ITransformer { 8 | invariant(typeof transformer === "function" && transformer.length === 1, "createTransformer expects a function that accepts one argument"); 9 | 10 | // Memoizes: object id -> reactive view that applies transformer to the object 11 | let objectCache: {[id: number]: ComputedValue} = {}; 12 | 13 | // If the resetId changes, we will clear the object cache, see #163 14 | // This construction is used to avoid leaking refs to the objectCache directly 15 | let resetId = globalState.resetId; 16 | 17 | // Local transformer class specifically for this transformer 18 | class Transformer extends ComputedValue { 19 | constructor(private sourceIdentifier: string, private sourceObject: A) { 20 | super(() => transformer(sourceObject), null, false, `Transformer-${(transformer).name}-${sourceIdentifier}`, undefined); 21 | } 22 | onBecomeUnobserved() { 23 | const lastValue = this.value; 24 | super.onBecomeUnobserved(); 25 | delete objectCache[this.sourceIdentifier]; 26 | if (onCleanup) 27 | onCleanup(lastValue, this.sourceObject); 28 | } 29 | } 30 | 31 | return (object: A) => { 32 | if (resetId !== globalState.resetId) { 33 | objectCache = {}; 34 | resetId = globalState.resetId; 35 | } 36 | 37 | const identifier = getMemoizationId(object); 38 | let reactiveTransformer = objectCache[identifier]; 39 | if (reactiveTransformer) 40 | return reactiveTransformer.get(); 41 | // Not in cache; create a reactive view 42 | reactiveTransformer = objectCache[identifier] = new Transformer(identifier, object); 43 | return reactiveTransformer.get(); 44 | }; 45 | } 46 | 47 | function getMemoizationId(object) { 48 | if (object === null || typeof object !== "object") 49 | throw new Error("[mobx] transform expected some kind of object, got: " + object); 50 | let tid = object.$transformId; 51 | if (tid === undefined) { 52 | tid = getNextId(); 53 | addHiddenProp(object, "$transformId", tid); 54 | } 55 | return tid; 56 | } -------------------------------------------------------------------------------- /scripts/single-file-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # to be invoked from the root of mobx, by using `npm run` to be able to resolve binaries 4 | 5 | # This script takes all typescript files, concatenates it to one big file and removes import and export statements. 6 | # This makes the library a factor 2 - 3 small, both minified and unminified, because 7 | # 1) when having many source files, there are a lot of webpack require calls lingering around 8 | # 2) when export functions from typescript files, they cannot be minified anymore because they are exported 9 | # (or added as prop to a namespace if using typescript namespaces), 10 | # while actually they can be minified as long as they are internal to the module 11 | 12 | # prelude 13 | set -e 14 | rm -rf lib .build 15 | mkdir -p .build 16 | 17 | echo '/** MobX - (c) Michel Weststrate 2015, 2016 - MIT Licensed */' > .build/mobx.ts 18 | 19 | # generate exports config 20 | cat src/mobx.ts | grep -v '^import' | sed -e 's/from.*$//g' >> .build/mobx.ts 21 | 22 | # find all ts files, concat them (with newlines), remove all import statements, remove export keyword 23 | ls src/{core,types,api,utils}/*.ts | xargs awk 'BEGINFILE {print "/* file:", FILENAME, "*/"} {print $0}' | grep -v '^import ' | sed -e 's/^export //g' >> .build/mobx.ts 24 | 25 | # compile to commonjs, generate declaration, no comments 26 | tsc -m commonjs -t es5 -d --removeComments --outDir lib .build/mobx.ts 27 | 28 | # make an umd build as well 29 | browserify -s mobx -e lib/mobx.js -o lib/mobx.umd.js 30 | 31 | # idea: strip invariants from compiled result. However, difference is not really significant in speed and size, disabled for now. 32 | # cat lib/mobx.js | grep -v -P '^\s+invariant' > .build/mobx-prod.js 33 | 34 | # minify, mangle, compress, wrap in function, use build without invariant 35 | # N.B: don't worry about the dead code warnings, see https://github.com/Microsoft/TypeScript/issues/7017#issuecomment-182789529 36 | uglifyjs -m sort,toplevel -c --screw-ie8 --preamble '/** MobX - (c) Michel Weststrate 2015, 2016 - MIT Licensed */' --source-map lib/mobx.min.js.map -o lib/mobx.min.js lib/mobx.js 37 | # -- OR -- (see above) 38 | # .build/mobx-prod.js 39 | 40 | # ..and a minified UMD build 41 | uglifyjs -m sort,toplevel -c --screw-ie8 --preamble '/** MobX - (c) Michel Weststrate 2015, 2016 - MIT Licensed */' --source-map lib/mobx.umd.min.js.map -o lib/mobx.umd.min.js lib/mobx.umd.js -------------------------------------------------------------------------------- /src/api/intercept.ts: -------------------------------------------------------------------------------- 1 | import {IInterceptor} from "../types/intercept-utils"; 2 | import {IObservableArray, IArrayWillChange, IArrayWillSplice} from "../types/observablearray"; 3 | import {ObservableMap, IMapWillChange} from "../types/observablemap"; 4 | import {IObjectWillChange, isObservableObject} from "../types/observableobject"; 5 | import {observable} from "./observable"; 6 | import {IValueWillChange, IObservableValue} from "../types/observablevalue"; 7 | import {Lambda, isPlainObject, deprecated} from "../utils/utils"; 8 | import {extendObservable} from "./extendobservable"; 9 | import {getAdministration} from "../types/type-utils"; 10 | 11 | export function intercept(value: IObservableValue, handler: IInterceptor>): Lambda; 12 | export function intercept(observableArray: IObservableArray, handler: IInterceptor | IArrayWillSplice>): Lambda; 13 | export function intercept(observableMap: ObservableMap, handler: IInterceptor>): Lambda; 14 | export function intercept(observableMap: ObservableMap, property: string, handler: IInterceptor>): Lambda; 15 | export function intercept(object: Object, handler: IInterceptor): Lambda; 16 | export function intercept(object: Object, property: string, handler: IInterceptor>): Lambda; 17 | export function intercept(thing, propOrHandler?, handler?): Lambda { 18 | if (typeof handler === "function") 19 | return interceptProperty(thing, propOrHandler, handler); 20 | else 21 | return interceptInterceptable(thing, propOrHandler); 22 | } 23 | 24 | function interceptInterceptable(thing, handler) { 25 | if (isPlainObject(thing) && !isObservableObject(thing)) { 26 | deprecated("Passing plain objects to intercept / observe is deprecated and will be removed in 3.0"); 27 | return getAdministration(observable(thing) as any).intercept(handler); 28 | } 29 | return getAdministration(thing).intercept(handler); 30 | } 31 | 32 | function interceptProperty(thing, property, handler) { 33 | if (isPlainObject(thing) && !isObservableObject(thing)) { 34 | deprecated("Passing plain objects to intercept / observe is deprecated and will be removed in 3.0"); 35 | extendObservable(thing, { 36 | property: thing[property] 37 | }); 38 | return interceptProperty(thing, property, handler); 39 | } 40 | return getAdministration(thing, property).intercept(handler); 41 | } 42 | -------------------------------------------------------------------------------- /test/whyrun.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const test = require('tape'); 4 | const mobx = require('../'); 5 | const noop = () => {}; 6 | 7 | test("whyrun", t => { 8 | const baselog = console.log; 9 | let lastButOneLine = ""; 10 | let lastLine = ""; 11 | 12 | const whyRun = function () { 13 | lastButOneLine = lastLine; 14 | console.log = noop; 15 | lastLine = mobx.whyRun.apply(null, arguments); 16 | console.log = baselog; 17 | return lastLine; 18 | } 19 | 20 | const x = mobx.observable({ 21 | firstname: "Michel", 22 | lastname: "Weststrate", 23 | fullname: function() { 24 | var res = this.firstname + " " + this.lastname; 25 | whyRun(); 26 | return res; 27 | } 28 | }); 29 | 30 | x.fullname; 31 | t.ok(lastLine.match(/suspended/), "just accessed fullname"); // no normal report, just a notification that nothing is being derived atm 32 | 33 | t.ok(whyRun(x, "fullname").match(/\[idle\]/)); 34 | t.ok(whyRun(x, "fullname").match(/suspended/)); 35 | 36 | const d = mobx.autorun("loggerzz", () => { 37 | x.fullname; 38 | whyRun(); 39 | }) 40 | 41 | t.ok(lastButOneLine.match(/\[active\]/)); 42 | t.ok(lastButOneLine.match(/\.firstname/)); 43 | t.ok(lastButOneLine.match(/\.lastname/)); 44 | 45 | t.ok(lastLine.match(/loggerzz/)); 46 | t.ok(lastLine.match(/\[running\]/)); 47 | t.ok(lastLine.match(/\.fullname/)); 48 | 49 | t.ok(whyRun(x, "fullname").match(/\[idle\]/)); 50 | t.ok(whyRun(x, "fullname").match(/\.firstname/)); 51 | t.ok(whyRun(x, "fullname").match(/\.lastname/)); 52 | t.ok(whyRun(x, "fullname").match(/loggerzz/)); 53 | 54 | t.ok(whyRun(d).match(/\[idle\]/)); 55 | t.ok(whyRun(d).match(/\.fullname/)); 56 | 57 | t.ok(whyRun(d).match(/loggerzz/)); 58 | 59 | mobx.transaction(() => { 60 | x.firstname = "Veria"; 61 | t.ok(whyRun(x, "fullname").match(/\[idle\]/), "made change in transaction"); 62 | 63 | t.ok(whyRun(d).match(/\[scheduled\]/)); 64 | }) 65 | 66 | t.ok(lastButOneLine.match(/will re-run/)); 67 | t.ok(lastButOneLine.match(/\.firstname/)); 68 | t.ok(lastButOneLine.match(/\.lastname/)); 69 | t.ok(lastButOneLine.match(/\loggerzz/)); 70 | 71 | t.ok(lastLine.match(/\[running\]/)); 72 | t.ok(lastLine.match(/\.fullname/)); 73 | 74 | t.ok(whyRun(x, "fullname").match(/\[idle\]/)); 75 | t.ok(whyRun(x, "fullname").match(/\.firstname/)); 76 | t.ok(whyRun(x, "fullname").match(/\.lastname/)); 77 | t.ok(whyRun(x, "fullname").match(/loggerzz/)); 78 | 79 | t.ok(whyRun(d).match(/\[idle\]/)); 80 | t.ok(whyRun(d).match(/\.fullname/)); 81 | t.ok(whyRun(d).match(/loggerzz/)); 82 | 83 | d(); 84 | 85 | t.ok(whyRun(d).match(/\[stopped\]/)); 86 | t.ok(whyRun(x, "fullname").match(/\[idle\]/)); 87 | t.ok(whyRun(x, "fullname").match(/suspended/)); 88 | 89 | t.end(); 90 | }) 91 | -------------------------------------------------------------------------------- /src/api/observe.ts: -------------------------------------------------------------------------------- 1 | import {IObservableArray, IArrayChange, IArraySplice} from "../types/observablearray"; 2 | import {ObservableMap, IMapChange} from "../types/observablemap"; 3 | import {IObjectChange, isObservableObject} from "../types/observableobject"; 4 | import {IComputedValue} from "../core/computedvalue"; 5 | 6 | import {IObservableValue} from "../types/observablevalue"; 7 | import {observable} from "./observable"; 8 | import {Lambda, isPlainObject, deprecated} from "../utils/utils"; 9 | import {extendObservable} from "./extendobservable"; 10 | import {getAdministration} from "../types/type-utils"; 11 | 12 | export function observe(value: IObservableValue | IComputedValue, listener: (newValue: T, oldValue: T) => void, fireImmediately?: boolean): Lambda; 13 | export function observe(observableArray: IObservableArray, listener: (change: IArrayChange | IArraySplice) => void, fireImmediately?: boolean): Lambda; 14 | export function observe(observableMap: ObservableMap, listener: (change: IMapChange) => void, fireImmediately?: boolean): Lambda; 15 | export function observe(observableMap: ObservableMap, property: string, listener: (newValue: T, oldValue: T) => void, fireImmediately?: boolean): Lambda; 16 | export function observe(object: Object, listener: (change: IObjectChange) => void, fireImmediately?: boolean): Lambda; 17 | export function observe(object: Object, property: string, listener: (newValue: any, oldValue: any) => void, fireImmediately?: boolean): Lambda; 18 | export function observe(thing, propOrCb?, cbOrFire?, fireImmediately?): Lambda { 19 | if (typeof cbOrFire === "function") 20 | return observeObservableProperty(thing, propOrCb, cbOrFire, fireImmediately); 21 | else 22 | return observeObservable(thing, propOrCb, cbOrFire); 23 | } 24 | 25 | function observeObservable(thing, listener, fireImmediately: boolean) { 26 | if (isPlainObject(thing) && !isObservableObject(thing)) { 27 | deprecated("Passing plain objects to intercept / observe is deprecated and will be removed in 3.0"); 28 | return getAdministration(observable(thing) as any).observe(listener, fireImmediately); 29 | } 30 | return getAdministration(thing).observe(listener, fireImmediately); 31 | } 32 | 33 | function observeObservableProperty(thing, property, listener, fireImmediately: boolean) { 34 | if (isPlainObject(thing) && !isObservableObject(thing)) { 35 | deprecated("Passing plain objects to intercept / observe is deprecated and will be removed in 3.0"); 36 | extendObservable(thing, { 37 | property: thing[property] 38 | }); 39 | return observeObservableProperty(thing, property, listener, fireImmediately); 40 | } 41 | return getAdministration(thing, property).observe(listener, fireImmediately); 42 | } -------------------------------------------------------------------------------- /test/utils/transform.js: -------------------------------------------------------------------------------- 1 | var m = require('../../'); 2 | 3 | module.exports.intersection = require('lodash.intersection'); 4 | 5 | module.exports.pluckFn = function(key) { 6 | return function(obj) { 7 | var keys = key.split('.'), value = obj; 8 | for (var i = 0, l = keys.length; i < l; i++) { if (!value) return; value = value[keys[i]]; } 9 | return value; 10 | } 11 | } 12 | module.exports.identity = function(value) { return value; } 13 | 14 | module.exports.testSet = function() { 15 | var testSet = {}; 16 | 17 | var state = testSet.state = m.observable({ 18 | root: null, 19 | renderedNodes: m.asStructure([]), 20 | collapsed: new m.map() // KM: ideally, I would like to use a set 21 | }); 22 | 23 | var stats = testSet.stats = { 24 | refCount: 0 25 | } 26 | 27 | var TreeNode = testSet.TreeNode = function(name, extensions) { 28 | this.children = m.observable(m.asStructure([])); 29 | this.icon = m.observable('folder'); 30 | 31 | this.parent = null; // not observed 32 | this.name = name; // not observed 33 | 34 | // optional extensions 35 | if (extensions) { for (var key in extensions) { this[key] = extensions[key]; } } 36 | } 37 | TreeNode.prototype.addChild = function(node) { node.parent = this; this.children.push(node); } 38 | TreeNode.prototype.addChildren = function(nodes) { 39 | var _this = this; 40 | nodes.map(function(node) { node.parent = _this; }); 41 | this.children.splice.apply(this.children, [this.children.length, 0].concat(nodes)); 42 | } 43 | 44 | TreeNode.prototype.path = function() { 45 | var node = this, parts = []; 46 | while (node) { 47 | parts.push(node.name); 48 | node = node.parent; 49 | } 50 | return parts.join('/'); 51 | } 52 | 53 | TreeNode.prototype.map = function(iteratee, results) { 54 | results = results || []; 55 | results.push(iteratee(this)); 56 | this.children.forEach(function(child) { child.map(iteratee, results); }); 57 | return results; 58 | } 59 | 60 | TreeNode.prototype.find = function(predicate) { 61 | if (predicate(this)) return this; 62 | 63 | var result; 64 | for (var i = 0, l = this.children.length; i < l; i++) { 65 | result = this.children[i].find(predicate); 66 | if (result) return result; 67 | } 68 | return null; 69 | } 70 | 71 | var DisplayNode = testSet.DisplayNode = function(node) { 72 | stats.refCount++; 73 | this.node = node; 74 | } 75 | DisplayNode.prototype.destroy = function() { stats.refCount--; } 76 | 77 | DisplayNode.prototype.toggleCollapsed = function() { 78 | var path = this.node.path(); 79 | state.collapsed.has(path) ? state.collapsed.delete(path) : state.collapsed.set(path, true); // KM: ideally, I would like to use a set 80 | } 81 | 82 | return testSet; 83 | } -------------------------------------------------------------------------------- /src/types/type-utils.ts: -------------------------------------------------------------------------------- 1 | import {IDepTreeNode} from "../core/observable"; 2 | import {invariant} from "../utils/utils"; 3 | import {runLazyInitializers} from "../utils/decorators"; 4 | import {isAtom} from "../core/atom"; 5 | import {isComputedValue} from "../core/computedvalue"; 6 | import {isReaction} from "../core/reaction"; 7 | import {isObservableArray} from "../types/observablearray"; 8 | import {isObservableMap} from "../types/observablemap"; 9 | import {isObservableObject} from "../types/observableobject"; 10 | 11 | export function getAtom(thing: any, property?: string): IDepTreeNode { 12 | if (typeof thing === "object" && thing !== null) { 13 | if (isObservableArray(thing)) { 14 | invariant(property === undefined, "It is not possible to get index atoms from arrays"); 15 | return thing.$mobx.atom; 16 | } 17 | if (isObservableMap(thing)) { 18 | if (property === undefined) 19 | return getAtom(thing._keys); 20 | const observable = thing._data[property] || thing._hasMap[property]; 21 | invariant(!!observable, `the entry '${property}' does not exist in the observable map '${getDebugName(thing)}'`); 22 | return observable; 23 | } 24 | // Initializers run lazily when transpiling to babel, so make sure they are run... 25 | runLazyInitializers(thing); 26 | if (isObservableObject(thing)) { 27 | invariant(!!property, `please specify a property`); 28 | const observable = thing.$mobx.values[property]; 29 | invariant(!!observable, `no observable property '${property}' found on the observable object '${getDebugName(thing)}'`); 30 | return observable; 31 | } 32 | if (isAtom(thing) || isComputedValue(thing) || isReaction(thing)) { 33 | return thing; 34 | } 35 | } else if (typeof thing === "function") { 36 | if (isReaction(thing.$mobx)) { 37 | // disposer function 38 | return thing.$mobx; 39 | } 40 | } 41 | invariant(false, "Cannot obtain atom from " + thing); 42 | } 43 | 44 | export function getAdministration(thing: any, property?: string) { 45 | invariant(thing, "Expection some object"); 46 | if (property !== undefined) 47 | return getAdministration(getAtom(thing, property)); 48 | if (isAtom(thing) || isComputedValue(thing) || isReaction(thing)) 49 | return thing; 50 | if (isObservableMap(thing)) 51 | return thing; 52 | // Initializers run lazily when transpiling to babel, so make sure they are run... 53 | runLazyInitializers(thing); 54 | if (thing.$mobx) 55 | return thing.$mobx; 56 | invariant(false, "Cannot obtain administration from " + thing); 57 | } 58 | 59 | export function getDebugName(thing: any, property?: string): string { 60 | let named; 61 | if (property !== undefined) 62 | named = getAtom(thing, property); 63 | else if (isObservableObject(thing) || isObservableMap(thing)) 64 | named = getAdministration(thing); 65 | else 66 | named = getAtom(thing); // valid for arrays as well 67 | return named.name; 68 | } 69 | -------------------------------------------------------------------------------- /src/api/observable.ts: -------------------------------------------------------------------------------- 1 | import {ObservableValue, IObservableValue} from "../types/observablevalue"; 2 | import {ValueMode, getValueModeFromValue, makeChildObservable} from "../types/modifiers"; 3 | import {computed} from "./computeddecorator"; 4 | import {isPlainObject, invariant, deprecated} from "../utils/utils"; 5 | import {observableDecorator} from "./observabledecorator"; 6 | import {isObservable} from "./isobservable"; 7 | import {IObservableObject} from "../types/observableobject"; 8 | import {IObservableArray, isObservableArray} from "../types/observablearray"; 9 | 10 | /** 11 | * Turns an object, array or function into a reactive structure. 12 | * @param value the value which should become observable. 13 | */ 14 | export function observable(): IObservableValue; 15 | export function observable(target: Object, key: string, baseDescriptor?: PropertyDescriptor): any; 16 | export function observable(value: T[]): IObservableArray; 17 | export function observable(value: () => T, thisArg?: S): IObservableValue; 18 | export function observable(value: T): IObservableValue; 19 | export function observable(value: T): T & IObservableObject; 20 | export function observable(v: any = undefined, keyOrScope?: string | any) { 21 | if (typeof arguments[1] === "string") 22 | return observableDecorator.apply(null, arguments); 23 | 24 | invariant(arguments.length < 3, "observable expects zero, one or two arguments"); 25 | if (isObservable(v)) 26 | return v; 27 | 28 | let [mode, value] = getValueModeFromValue(v, ValueMode.Recursive); 29 | const sourceType = mode === ValueMode.Reference ? ValueType.Reference : getTypeOfValue(value); 30 | 31 | switch (sourceType) { 32 | case ValueType.Array: 33 | case ValueType.PlainObject: 34 | return makeChildObservable(value, mode); 35 | case ValueType.Reference: 36 | case ValueType.ComplexObject: 37 | return new ObservableValue(value, mode); 38 | case ValueType.ComplexFunction: 39 | throw new Error("[mobx.observable] To be able to make a function reactive it should not have arguments. If you need an observable reference to a function, use `observable(asReference(f))`"); 40 | case ValueType.ViewFunction: 41 | deprecated("Use `computed(expr)` instead of `observable(expr)`"); 42 | return computed(v, keyOrScope); 43 | } 44 | invariant(false, "Illegal State"); 45 | } 46 | 47 | export enum ValueType { Reference, PlainObject, ComplexObject, Array, ViewFunction, ComplexFunction } 48 | 49 | export function getTypeOfValue(value): ValueType { 50 | if (value === null || value === undefined) 51 | return ValueType.Reference; 52 | if (typeof value === "function") 53 | return value.length ? ValueType.ComplexFunction : ValueType.ViewFunction; 54 | if (Array.isArray(value) || isObservableArray(value)) 55 | return ValueType.Array; 56 | if (typeof value === "object") 57 | return isPlainObject(value) ? ValueType.PlainObject : ValueType.ComplexObject; 58 | return ValueType.Reference; // safe default, only refer by reference.. 59 | } 60 | -------------------------------------------------------------------------------- /src/api/computeddecorator.ts: -------------------------------------------------------------------------------- 1 | import {ValueMode, getValueModeFromValue, asStructure} from "../types/modifiers"; 2 | import {IObservableValue} from "../types/observablevalue"; 3 | import {asObservableObject, defineObservableProperty} from "../types/observableobject"; 4 | import {invariant} from "../utils/utils"; 5 | import {createClassPropertyDecorator} from "../utils/decorators"; 6 | import {ComputedValue, IComputedValue} from "../core/computedvalue"; 7 | 8 | export interface IComputedValueOptions { 9 | asStructure: boolean; 10 | } 11 | 12 | const computedDecorator = createClassPropertyDecorator( 13 | (target, name, _, decoratorArgs, originalDescriptor) => { 14 | invariant(typeof originalDescriptor !== "undefined", "@computed can only be used on getter functions, like: '@computed get myProps() { return ...; }'. It looks like it was used on a property."); 15 | const baseValue = originalDescriptor.get; 16 | const setter = originalDescriptor.set; 17 | invariant(typeof baseValue === "function", "@computed can only be used on getter functions, like: '@computed get myProps() { return ...; }'"); 18 | 19 | let compareStructural = false; 20 | if (decoratorArgs && decoratorArgs.length === 1 && decoratorArgs[0].asStructure === true) 21 | compareStructural = true; 22 | 23 | const adm = asObservableObject(target, undefined, ValueMode.Recursive); 24 | defineObservableProperty(adm, name, compareStructural ? asStructure(baseValue) : baseValue, false, setter); 25 | }, 26 | function (name) { 27 | const observable = this.$mobx.values[name]; 28 | if (observable === undefined) // See #505 29 | return undefined; 30 | return observable.get(); 31 | }, 32 | function (name, value) { 33 | this.$mobx.values[name].set(value); 34 | }, 35 | false, 36 | true 37 | ); 38 | 39 | /** 40 | * Decorator for class properties: @computed get value() { return expr; }. 41 | * For legacy purposes also invokable as ES5 observable created: `computed(() => expr)`; 42 | */ 43 | export function computed(func: () => T, setter: (value: T) => void): IComputedValue; 44 | export function computed(func: () => T, scope?: any): IComputedValue; 45 | export function computed(opts: IComputedValueOptions): (target: Object, key: string, baseDescriptor?: PropertyDescriptor) => void; 46 | export function computed(target: Object, key: string | symbol, baseDescriptor?: PropertyDescriptor): void; 47 | export function computed(targetOrExpr: any, keyOrScopeOrSetter?: any, baseDescriptor?: PropertyDescriptor, options?: IComputedValueOptions) { 48 | if (typeof targetOrExpr === "function" && arguments.length < 3) { 49 | if (typeof keyOrScopeOrSetter === "function") 50 | return computedExpr(targetOrExpr, keyOrScopeOrSetter, undefined); 51 | else 52 | return computedExpr(targetOrExpr, undefined, keyOrScopeOrSetter); 53 | } 54 | return computedDecorator.apply(null, arguments); 55 | } 56 | 57 | function computedExpr(expr: () => T, setter, scope: any) { 58 | const [mode, value] = getValueModeFromValue(expr, ValueMode.Recursive); 59 | return new ComputedValue(value, scope, mode === ValueMode.Structure, value.name, setter); 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobx", 3 | "version": "2.6.0", 4 | "description": "Simple, scalable state management.", 5 | "main": "lib/mobx.js", 6 | "typings": "lib/mobx.d.ts", 7 | "scripts": { 8 | "test": "npm run quick-build && npm run tape", 9 | "full-test": "npm run small-build && npm run build-tests && npm run use-minified && npm run tape && npm run perf", 10 | "tape": "tape test/*.js | faucet", 11 | "perf": "npm run small-build && PERSIST=true time node --expose-gc test/perf/index.js", 12 | "prepublish": "npm run small-build", 13 | "quick-build": "tsc", 14 | "small-build": "scripts/single-file-build.sh", 15 | "test-browser-electron": "npm run small-build && ( browserify test/*.js | tape-run )", 16 | "test-browser-chrome": "npm run small-build && ( browserify test/*.js | tape-run --browser chrome )", 17 | "test-browser-safari": "npm run small-build && ( browserify test/*.js -t [ babelify --presets [ es2015 ] ] | tape-run --browser safari )", 18 | "test-browser-firefox": "npm run small-build && ( browserify test/*.js | tape-run --browser firefox )", 19 | "test-travis": "npm run small-build && npm run build-tests && tape test/*.js test/perf/index.js && tsc && istanbul cover tape test/*.js", 20 | "coverage": "npm run quick-build && npm run build-tests && istanbul cover tape test/*.js", 21 | "build-tests": "npm run build-typescript-tests && npm run build-babel-tests", 22 | "build-typescript-tests": "tsc -m commonjs -t es5 --experimentalDecorators --noImplicitAny --outDir test test/typescript-tests.ts", 23 | "build-babel-tests": "babel test/babel/babel-tests.js -o test/babel-tests.js", 24 | "use-minified": "cp lib/mobx.min.js lib/mobx.js", 25 | "lint": "tslint -c tslint.json src/*.ts src/types/*.ts src/api/*.ts src/core/*.ts src/utils/*.ts" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/mobxjs/mobx.git" 30 | }, 31 | "author": "Michel Weststrate", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/mobxjs/mobx/issues" 35 | }, 36 | "files": [ 37 | "lib", 38 | "LICENSE", 39 | "bower.json" 40 | ], 41 | "homepage": "https://mobxjs.github.io/mobx", 42 | "devDependencies": { 43 | "babel-cli": "^6.4.5", 44 | "babel-core": "^6.4.5", 45 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 46 | "babel-preset-es2015": "^6.3.13", 47 | "babel-preset-react": "^6.3.13", 48 | "babel-preset-stage-1": "^6.3.13", 49 | "babelify": "^7.3.0", 50 | "browserify": "^12.0.1", 51 | "coveralls": "^2.11.4", 52 | "faucet": "0.0.1", 53 | "istanbul": "^0.3.21", 54 | "iterall": "^1.0.2", 55 | "lodash.intersection": "^3.2.0", 56 | "tape": "^4.2.2", 57 | "tape-run": "^2.1.0", 58 | "typescript": "^1.8.10", 59 | "uglify-js": "^2.6.1" 60 | }, 61 | "dependencies": {}, 62 | "keywords": [ 63 | "mobx", 64 | "mobservable", 65 | "observable", 66 | "react-component", 67 | "react", 68 | "reactjs", 69 | "reactive", 70 | "model", 71 | "frp", 72 | "functional-reactive-programming", 73 | "state management", 74 | "data flow" 75 | ] 76 | } -------------------------------------------------------------------------------- /src/core/action.ts: -------------------------------------------------------------------------------- 1 | import {transactionStart, transactionEnd} from "../core/transaction"; 2 | import {invariant, deprecated} from "../utils/utils"; 3 | import {untrackedStart, untrackedEnd} from "../core/derivation"; 4 | import {isSpyEnabled, spyReportStart, spyReportEnd} from "../core/spy"; 5 | import {isComputedValue} from "../core/computedvalue"; 6 | import {globalState} from "../core/globalstate"; 7 | 8 | export function createAction(actionName: string, fn: Function): Function { 9 | invariant(typeof fn === "function", "`action` can only be invoked on functions"); 10 | invariant(typeof actionName === "string" && actionName.length > 0, `actions should have valid names, got: '${actionName}'`); 11 | const res = function () { 12 | return executeAction(actionName, fn, this, arguments); 13 | }; 14 | (res as any).isMobxAction = true; 15 | return res; 16 | } 17 | 18 | export function executeAction(actionName: string, fn: Function, scope: any, args: IArguments) { 19 | // actions should not be called from computeds. check only works if the computed is actively observed, but that is fine enough as heuristic 20 | invariant(!isComputedValue(globalState.trackingDerivation), "Computed values or transformers should not invoke actions or trigger other side effects"); 21 | 22 | const notifySpy = isSpyEnabled(); 23 | let startTime: number; 24 | if (notifySpy) { 25 | startTime = Date.now(); 26 | const l = (args && args.length) || 0; 27 | const flattendArgs = new Array(l); 28 | if (l > 0) for (let i = 0; i < l; i++) 29 | flattendArgs[i] = args[i]; 30 | spyReportStart({ 31 | type: "action", 32 | name: actionName, 33 | fn, 34 | target: scope, 35 | arguments: flattendArgs 36 | }); 37 | } 38 | const prevUntracked = untrackedStart(); 39 | transactionStart(actionName, scope, false); 40 | const prevAllowStateChanges = allowStateChangesStart(true); 41 | 42 | try { 43 | return fn.apply(scope, args); 44 | } 45 | finally { 46 | allowStateChangesEnd(prevAllowStateChanges); 47 | transactionEnd(false); 48 | untrackedEnd(prevUntracked); 49 | if (notifySpy) 50 | spyReportEnd({ time: Date.now() - startTime }); 51 | } 52 | } 53 | 54 | export function useStrict(): boolean; 55 | export function useStrict(strict: boolean); 56 | export function useStrict(strict?: boolean): any { 57 | if (arguments.length === 0) { 58 | deprecated("`useStrict` without arguments is deprecated, use `isStrictModeEnabled()` instead"); 59 | return globalState.strictMode; 60 | } else { 61 | invariant(globalState.trackingDerivation === null, "It is not allowed to set `useStrict` when a derivation is running"); 62 | globalState.strictMode = strict; 63 | globalState.allowStateChanges = !strict; 64 | } 65 | } 66 | 67 | export function isStrictModeEnabled(): boolean { 68 | return globalState.strictMode; 69 | } 70 | 71 | export function allowStateChanges(allowStateChanges: boolean, func: () => T): T { 72 | const prev = allowStateChangesStart(allowStateChanges); 73 | const res = func(); 74 | allowStateChangesEnd(prev); 75 | return res; 76 | } 77 | 78 | export function allowStateChangesStart(allowStateChanges: boolean) { 79 | const prev = globalState.allowStateChanges; 80 | globalState.allowStateChanges = allowStateChanges; 81 | return prev; 82 | } 83 | 84 | export function allowStateChangesEnd(prev: boolean) { 85 | globalState.allowStateChanges = prev; 86 | } 87 | -------------------------------------------------------------------------------- /test/autorunAsync.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var m = require('..'); 3 | 4 | test('autorun 1', function(t) { 5 | var _fired = 0; 6 | var _result = null; 7 | var _cCalcs = 0; 8 | var to = setTimeout; 9 | 10 | function expect(fired, cCalcs, result) { 11 | t.equal(_fired, fired, "autorun fired"); 12 | t.equal(_cCalcs, cCalcs, "'c' fired"); 13 | if (fired) 14 | t.equal(_result, result, "result"); 15 | _fired = 0; 16 | _cCalcs = 0; 17 | } 18 | 19 | var a = m.observable(2); 20 | var b = m.observable(3); 21 | var c = m.observable(function() { 22 | _cCalcs++; 23 | return a.get() * b.get(); 24 | }); 25 | var d = m.observable(1); 26 | var autorun = function() { 27 | _fired++; 28 | _result = d.get() > 0 ? a.get() * c.get() : d.get(); 29 | }; 30 | var disp = m.autorunAsync(autorun, 20); 31 | 32 | expect(0, 0, null); 33 | disp(); 34 | to(function() { 35 | expect(0, 0, null); 36 | disp = m.autorunAsync(autorun, 20); 37 | 38 | to(function() { 39 | expect(1, 1, 12); 40 | a.set(4); 41 | b.set(5); 42 | a.set(6); 43 | expect(0, 0, null); // a change triggered async rerun, compute will trigger after 20ms of async timeout 44 | to(function() { 45 | expect(1, 1, 180); 46 | d.set(2); 47 | 48 | to(function() { 49 | expect(1, 0, 180); 50 | 51 | d.set(-2); 52 | to(function() { 53 | expect(1, 0, -2); 54 | 55 | a.set(7); 56 | to(function() { 57 | expect(0, 0, 0); // change a has no effect 58 | 59 | a.set(4); 60 | b.set(2); 61 | d.set(2) 62 | 63 | to(function() { 64 | expect(1, 1, 32); 65 | 66 | disp(); 67 | a.set(1); 68 | b.set(2); 69 | d.set(4); 70 | to(function() { 71 | expect(0, 0, 0); 72 | t.end(); 73 | },30) 74 | }, 30); 75 | }, 30); 76 | }, 30); 77 | }, 30); 78 | }, 30); 79 | }, 30); 80 | }, 30); 81 | }); 82 | 83 | test('autorun should not result in loop', function(t) { 84 | var i = 0; 85 | var a = m.observable({ 86 | x: i 87 | }); 88 | 89 | var autoRunsCalled = 0; 90 | var d = m.autorunAsync("named async", function() { 91 | autoRunsCalled++; 92 | a.x = ++i; 93 | setTimeout(function() { 94 | a.x = ++i; 95 | }, 10); 96 | }, 10); 97 | 98 | setTimeout(function() { 99 | t.equal(autoRunsCalled, 1); 100 | t.end(); 101 | 102 | t.equal(d.$mobx.name, "named async"); 103 | d(); 104 | }, 100); 105 | }); 106 | 107 | test('autorunAsync passes Reaction as an argument to view function', function(t) { 108 | var a = m.observable(1); 109 | 110 | var autoRunsCalled = 0; 111 | 112 | m.autorunAsync(r => { 113 | t.equal(typeof r.dispose, 'function'); 114 | autoRunsCalled++; 115 | if (a.get() === 'pleaseDispose') r.dispose(); 116 | }, 10); 117 | 118 | setTimeout(() => a.set(2), 25); 119 | setTimeout(() => a.set('pleaseDispose'), 40); 120 | setTimeout(() => a.set(3), 55); 121 | setTimeout(() => a.set(4), 70); 122 | 123 | setTimeout(function() { 124 | t.equal(autoRunsCalled, 3); 125 | t.end(); 126 | }, 100); 127 | }); 128 | 129 | test('autorunAsync warns when passed an action', function(t) { 130 | var action = m.action(() => {}); 131 | t.plan(1); 132 | t.throws(() => m.autorunAsync(action), /attempted to pass an action to autorunAsync/); 133 | t.end(); 134 | }); 135 | -------------------------------------------------------------------------------- /src/core/atom.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IAtom extends IObservable { 3 | } 4 | 5 | /** 6 | * Anything that can be used to _store_ state is an Atom in mobx. Atom's have two important jobs 7 | * 8 | * 1) detect when they are being _used_ and report this (using reportObserved). This allows mobx to make the connection between running functions and the data they used 9 | * 2) they should notify mobx whenever they have _changed_. This way mobx can re-run any functions (derivations) that are using this atom. 10 | */ 11 | export class BaseAtom implements IAtom { 12 | isPendingUnobservation = true; // for effective unobserving. BaseAtom has true, for extra optimization, so it's onBecomeUnobserved never get's called, because it's not needed 13 | observers = []; 14 | observersIndexes = {}; 15 | 16 | diffValue = 0; 17 | lastAccessedBy = 0; 18 | lowestObserverState = IDerivationState.NOT_TRACKING; 19 | /** 20 | * Create a new atom. For debugging purposes it is recommended to give it a name. 21 | * The onBecomeObserved and onBecomeUnobserved callbacks can be used for resource management. 22 | */ 23 | constructor(public name = "Atom@" + getNextId()) { } 24 | 25 | public onBecomeUnobserved() { 26 | // noop 27 | } 28 | 29 | /** 30 | * Invoke this method to notify mobx that your atom has been used somehow. 31 | */ 32 | public reportObserved() { 33 | reportObserved(this); 34 | } 35 | 36 | /** 37 | * Invoke this method _after_ this method has changed to signal mobx that all its observers should invalidate. 38 | */ 39 | public reportChanged() { 40 | transactionStart("propagatingAtomChange", null, false); 41 | propagateChanged(this); 42 | transactionEnd(false); 43 | } 44 | 45 | toString() { 46 | return this.name; 47 | } 48 | } 49 | 50 | export class Atom extends BaseAtom implements IAtom { 51 | isPendingUnobservation = false; // for effective unobserving. 52 | public isBeingTracked = false; 53 | 54 | /** 55 | * Create a new atom. For debugging purposes it is recommended to give it a name. 56 | * The onBecomeObserved and onBecomeUnobserved callbacks can be used for resource management. 57 | */ 58 | constructor(public name = "Atom@" + getNextId(), public onBecomeObservedHandler: () => void = noop, public onBecomeUnobservedHandler: () => void = noop) { 59 | super(name); 60 | } 61 | 62 | public reportObserved(): boolean { 63 | startBatch(); 64 | 65 | super.reportObserved(); 66 | 67 | if (!this.isBeingTracked) { 68 | this.isBeingTracked = true; 69 | this.onBecomeObservedHandler(); 70 | } 71 | 72 | endBatch(); 73 | return !!globalState.trackingDerivation; 74 | // return doesn't really give usefull info, because it can be as well calling computed which calls atom (no reactions) 75 | // also it could not trigger when calculating reaction dependent on Atom because Atom's value was cached by computed called by given reaction. 76 | } 77 | 78 | public onBecomeUnobserved() { 79 | this.isBeingTracked = false; 80 | this.onBecomeUnobservedHandler(); 81 | } 82 | } 83 | 84 | 85 | import {globalState} from "./globalstate"; 86 | import {IObservable, propagateChanged, reportObserved, startBatch, endBatch} from "./observable"; 87 | import {IDerivationState} from "./derivation"; 88 | import {transactionStart, transactionEnd} from "../core/transaction"; 89 | import {createInstanceofPredicate, noop, getNextId, isObject} from "../utils/utils"; 90 | 91 | export const isAtom = createInstanceofPredicate("Atom", BaseAtom); 92 | -------------------------------------------------------------------------------- /src/core/globalstate.ts: -------------------------------------------------------------------------------- 1 | import {IDerivation} from "./derivation"; 2 | import {Reaction} from "./reaction"; 3 | import {IObservable} from "./observable"; 4 | 5 | declare const global: any; 6 | 7 | /** 8 | * These values will persist if global state is reset 9 | */ 10 | const persistentKeys = ["mobxGuid", "resetId", "spyListeners", "strictMode", "runId"]; 11 | 12 | export class MobXGlobals { 13 | /** 14 | * MobXGlobals version. 15 | * MobX compatiblity with other versions loaded in memory as long as this version matches. 16 | * It indicates that the global state still stores similar information 17 | */ 18 | version = 4; 19 | 20 | /** 21 | * Stack of currently running derivations 22 | */ 23 | trackingDerivation: IDerivation = null; 24 | 25 | /** 26 | * Each time a derivation is tracked, it is assigned a unique run-id 27 | */ 28 | runId = 0; 29 | 30 | /** 31 | * 'guid' for general purpose. Will be persisted amongst resets. 32 | */ 33 | mobxGuid = 0; 34 | 35 | /** 36 | * Are we in a transaction block? (and how many of them) 37 | */ 38 | inTransaction = 0; 39 | 40 | /** 41 | * Are we currently running reactions? 42 | * Reactions are run after derivations using a trampoline. 43 | */ 44 | isRunningReactions = false; 45 | 46 | /** 47 | * Are we in a batch block? (and how many of them) 48 | */ 49 | inBatch: number = 0; 50 | 51 | /** 52 | * Observables that don't have observers anymore, and are about to be 53 | * suspended, unless somebody else accesses it in the same batch 54 | * 55 | * @type {IObservable[]} 56 | */ 57 | pendingUnobservations: IObservable[] = []; 58 | 59 | /** 60 | * List of scheduled, not yet executed, reactions. 61 | */ 62 | pendingReactions: Reaction[] = []; 63 | 64 | /** 65 | * Is it allowed to change observables at this point? 66 | * In general, MobX doesn't allow that when running computations and React.render. 67 | * To ensure that those functions stay pure. 68 | */ 69 | allowStateChanges = true; 70 | /** 71 | * If strict mode is enabled, state changes are by default not allowed 72 | */ 73 | strictMode = false; 74 | 75 | /** 76 | * Used by createTransformer to detect that the global state has been reset. 77 | */ 78 | resetId = 0; 79 | 80 | /** 81 | * Spy callbacks 82 | */ 83 | spyListeners: {(change: any): void}[] = []; 84 | } 85 | 86 | export const globalState: MobXGlobals = (() => { 87 | const res = new MobXGlobals(); 88 | /** 89 | * Backward compatibility check 90 | */ 91 | if (global.__mobservableTrackingStack || global.__mobservableViewStack) 92 | throw new Error("[mobx] An incompatible version of mobservable is already loaded."); 93 | if (global.__mobxGlobal && global.__mobxGlobal.version !== res.version) 94 | throw new Error("[mobx] An incompatible version of mobx is already loaded."); 95 | if (global.__mobxGlobal) 96 | return global.__mobxGlobal; 97 | return global.__mobxGlobal = res; 98 | })(); 99 | 100 | export function registerGlobals() { 101 | // no-op to make explicit why this file is loaded 102 | } 103 | 104 | /** 105 | * For testing purposes only; this will break the internal state of existing observables, 106 | * but can be used to get back at a stable state after throwing errors 107 | */ 108 | export function resetGlobalState() { 109 | globalState.resetId++; 110 | const defaultGlobals = new MobXGlobals(); 111 | for (let key in defaultGlobals) 112 | if (persistentKeys.indexOf(key) === -1) 113 | globalState[key] = defaultGlobals[key]; 114 | globalState.allowStateChanges = !globalState.strictMode; 115 | } 116 | -------------------------------------------------------------------------------- /src/api/action.ts: -------------------------------------------------------------------------------- 1 | import {invariant, addHiddenProp} from "../utils/utils"; 2 | import {createClassPropertyDecorator} from "../utils/decorators"; 3 | import {createAction, executeAction} from "../core/action"; 4 | 5 | const actionFieldDecorator = createClassPropertyDecorator( 6 | function (target, key, value, args, originalDescriptor) { 7 | const actionName = (args && args.length === 1) ? args[0] : (value.name || key || ""); 8 | const wrappedAction = action(actionName, value); 9 | addHiddenProp(target, key, wrappedAction); 10 | }, 11 | function (key) { 12 | return this[key]; 13 | }, 14 | function () { 15 | invariant(false, "It is not allowed to assign new values to @action fields"); 16 | }, 17 | false, 18 | true 19 | ); 20 | 21 | export function action R>(fn: T): T; 22 | export function action R>(fn: T): T; 23 | export function action R>(fn: T): T; 24 | export function action R>(fn: T): T; 25 | export function action R>(name: string, fn: T): T; 26 | export function action R>(name: string, fn: T): T; 27 | export function action R>(name: string, fn: T): T; 28 | export function action R>(name: string, fn: T): T; 29 | export function action(fn: T): T; 30 | export function action(name: string, fn: T): T; 31 | export function action(customName: string): (target: Object, key: string, baseDescriptor?: PropertyDescriptor) => void; 32 | export function action(target: Object, propertyKey: string, descriptor?: PropertyDescriptor): void; 33 | export function action(arg1, arg2?, arg3?, arg4?): any { 34 | if (arguments.length === 1 && typeof arg1 === "function") 35 | return createAction(arg1.name || "", arg1); 36 | if (arguments.length === 2 && typeof arg2 === "function") 37 | return createAction(arg1, arg2); 38 | 39 | if (arguments.length === 1 && typeof arg1 === "string") 40 | return namedActionDecorator(arg1); 41 | 42 | return namedActionDecorator(arg2).apply(null, arguments); 43 | } 44 | 45 | function namedActionDecorator(name: string) { 46 | return function (target, prop, descriptor) { 47 | if (descriptor && typeof descriptor.value === "function") { 48 | // TypeScript @action method() { }. Defined on proto before being decorated 49 | // Don't use the field decorator if we are just decorating a method 50 | descriptor.value = createAction(name, descriptor.value); 51 | descriptor.enumerable = false; 52 | descriptor.configurable = true; 53 | return descriptor; 54 | } 55 | // bound instance methods 56 | return actionFieldDecorator(name).apply(this, arguments); 57 | }; 58 | } 59 | 60 | export function runInAction(block: () => T, scope?: any): T; 61 | export function runInAction(name: string, block: () => T, scope?: any): T; 62 | export function runInAction(arg1, arg2?, arg3?) { 63 | const actionName = typeof arg1 === "string" ? arg1 : arg1.name || ""; 64 | const fn = typeof arg1 === "function" ? arg1 : arg2; 65 | const scope = typeof arg1 === "function" ? arg2 : arg3; 66 | 67 | invariant(typeof fn === "function", "`runInAction` expects a function"); 68 | invariant(fn.length === 0, "`runInAction` expects a function without arguments"); 69 | invariant(typeof actionName === "string" && actionName.length > 0, `actions should have valid names, got: '${actionName}'`); 70 | 71 | return executeAction(actionName, fn, scope, undefined); 72 | } 73 | 74 | export function isAction(thing: any) { 75 | return typeof thing === "function" && thing.isMobxAction === true; 76 | } 77 | 78 | -------------------------------------------------------------------------------- /test/strict-mode.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var mobx = require('../'); 3 | 4 | var strictError = /It is not allowed to create or change state outside an `action` when MobX is in strict mode. Wrap the current method in `action` if this state change is intended/; 5 | 6 | test('strict mode should not allow changes outside action', t => { 7 | var a = mobx.observable(2); 8 | t.equal(mobx.isStrictModeEnabled(), false) 9 | mobx.useStrict(true); 10 | t.equal(mobx.isStrictModeEnabled(), true) 11 | t.throws(() => a.set(3), strictError); 12 | mobx.useStrict(false); 13 | t.equal(mobx.isStrictModeEnabled(), false) 14 | a.set(4); 15 | t.equal(a.get(), 4); 16 | t.end(); 17 | }); 18 | 19 | test('actions can modify state in strict mode', t => { 20 | var a = mobx.observable(2); 21 | 22 | mobx.useStrict(true); 23 | mobx.action(() => { 24 | a.set(3); 25 | var b = mobx.observable(4); 26 | })(); 27 | 28 | mobx.useStrict(false); 29 | t.end(); 30 | }); 31 | 32 | test('reactions cannot modify state in strict mode', t => { 33 | var a = mobx.observable(3); 34 | var b = mobx.observable(4); 35 | mobx.useStrict(true); 36 | mobx.extras.resetGlobalState(); // should preserve strict mode 37 | 38 | var d = mobx.autorun(() => { 39 | t.throws(() => { 40 | a.get(); 41 | b.set(3) 42 | }, strictError); 43 | }); 44 | 45 | d = mobx.autorun(() => { 46 | if (a.get() > 5) 47 | b.set(7); 48 | }); 49 | 50 | mobx.action(() => a.set(4))(); // ok 51 | 52 | t.throws(() => a.set(5), strictError); 53 | 54 | mobx.useStrict(false); 55 | t.end(); 56 | }); 57 | 58 | 59 | test('action inside reaction in strict mode can modify state', t => { 60 | var a = mobx.observable(1); 61 | var b = mobx.observable(2); 62 | 63 | mobx.useStrict(true); 64 | var act = mobx.action(() => b.set(b.get() + 1)); 65 | 66 | var d = mobx.autorun(() => { 67 | if (a.get() % 2 === 0) 68 | act(); 69 | if (a.get() == 16) { 70 | t.throws(() => b.set(55), strictError, "finishing act should restore strict mode again"); 71 | } 72 | }); 73 | 74 | var setA = mobx.action(val => a.set(val)); 75 | t.equal(b.get(), 2); 76 | setA(4); 77 | t.equal(b.get(), 3); 78 | setA(5); 79 | t.equal(b.get(), 3); 80 | setA(16); 81 | t.equal(b.get(), 4, "b should not be 55"); 82 | 83 | mobx.useStrict(false); 84 | t.end(); 85 | }); 86 | 87 | test('cannot create or modify objects in strict mode without action', t => { 88 | var obj = mobx.observable({a: 2}); 89 | var ar = mobx.observable([1]); 90 | var map = mobx.map({ a: 2}); 91 | 92 | mobx.useStrict(true); 93 | 94 | // introducing new observables is ok! 95 | mobx.observable({ a: 2, b: function() { return this.a }}); 96 | mobx.observable({ b: function() { return this.a } }); 97 | mobx.map({ a: 2}); 98 | mobx.observable([1, 2, 3]); 99 | mobx.extendObservable(obj, { b: 4}); 100 | 101 | t.throws(() => obj.a = 3, strictError); 102 | t.throws(() => ar[0] = 2, strictError); 103 | t.throws(() => ar.push(3), strictError); 104 | t.throws(() => map.set("a", 3), strictError); 105 | t.throws(() => map.set("b", 4), strictError); 106 | t.throws(() => map.delete("a"), strictError); 107 | 108 | mobx.useStrict(false); 109 | 110 | // can modify again 111 | obj.a = 42; 112 | 113 | t.end(); 114 | }) 115 | 116 | test('can create objects in strict mode with action', t => { 117 | var obj = mobx.observable({a: 2}); 118 | var ar = mobx.observable([1]); 119 | var map = mobx.map({ a: 2}); 120 | 121 | mobx.useStrict(true); 122 | 123 | mobx.action(() => { 124 | mobx.observable({ a: 2, b: function() { return this.a }}); 125 | mobx.map({ a: 2}); 126 | mobx.observable([1, 2, 3]); 127 | 128 | obj.a = 3; 129 | mobx.extendObservable(obj, { b: 4}); 130 | ar[0] = 2; 131 | ar.push(3); 132 | map.set("a", 3); 133 | map.set("b", 4); 134 | map.delete("a"); 135 | })(); 136 | 137 | mobx.useStrict(false); 138 | t.end(); 139 | }) -------------------------------------------------------------------------------- /src/types/observablevalue.ts: -------------------------------------------------------------------------------- 1 | import {BaseAtom} from "../core/atom"; 2 | import {checkIfStateModificationsAreAllowed} from "../core/derivation"; 3 | import {ValueMode, getValueModeFromValue, makeChildObservable, assertUnwrapped} from "./modifiers"; 4 | import {valueDidChange, Lambda, getNextId, createInstanceofPredicate} from "../utils/utils"; 5 | import {hasInterceptors, IInterceptable, IInterceptor, registerInterceptor, interceptChange} from "./intercept-utils"; 6 | import {IListenable, registerListener, hasListeners, notifyListeners} from "./listen-utils"; 7 | import {isSpyEnabled, spyReportStart, spyReportEnd, spyReport} from "../core/spy"; 8 | 9 | export interface IValueWillChange { 10 | object: any; 11 | type: "update"; 12 | newValue: T; 13 | } 14 | 15 | // Introduce in 3.0 16 | // export interface IValueDidChange { 17 | // object: any; 18 | // type: "update" | "create"; 19 | // newValue: T; 20 | // oldValue: T; 21 | // } 22 | 23 | export type IUNCHANGED = {}; 24 | 25 | export const UNCHANGED: IUNCHANGED = {}; 26 | 27 | export interface IObservableValue { 28 | get(): T; 29 | set(value: T): void; 30 | intercept(handler: IInterceptor>): Lambda; 31 | observe(listener: (newValue: T, oldValue: T) => void, fireImmediately?: boolean): Lambda; 32 | } 33 | 34 | export class ObservableValue extends BaseAtom implements IObservableValue, IInterceptable>, IListenable { 35 | hasUnreportedChange = false; 36 | interceptors; 37 | changeListeners; 38 | protected value: T = undefined; 39 | 40 | constructor(value: T, protected mode: ValueMode, name = "ObservableValue@" + getNextId(), notifySpy = true) { 41 | super(name); 42 | const [childmode, unwrappedValue] = getValueModeFromValue(value, ValueMode.Recursive); 43 | // If the value mode is recursive, modifiers like 'structure', 'reference', or 'flat' could apply 44 | if (this.mode === ValueMode.Recursive) 45 | this.mode = childmode; 46 | this.value = makeChildObservable(unwrappedValue, this.mode, this.name); 47 | if (notifySpy && isSpyEnabled()) { 48 | // only notify spy if this is a stand-alone observable 49 | spyReport({ type: "create", object: this, newValue: this.value }); 50 | } 51 | } 52 | 53 | public set(newValue: T) { 54 | const oldValue = this.value; 55 | newValue = this.prepareNewValue(newValue) as any; 56 | if (newValue !== UNCHANGED) { 57 | const notifySpy = isSpyEnabled(); 58 | if (notifySpy) { 59 | spyReportStart({ 60 | type: "update", 61 | object: this, 62 | newValue, oldValue 63 | }); 64 | } 65 | this.setNewValue(newValue); 66 | if (notifySpy) 67 | spyReportEnd(); 68 | } 69 | } 70 | 71 | prepareNewValue(newValue): T | IUNCHANGED { 72 | assertUnwrapped(newValue, "Modifiers cannot be used on non-initial values."); 73 | checkIfStateModificationsAreAllowed(); 74 | if (hasInterceptors(this)) { 75 | const change = interceptChange>(this, { object: this, type: "update", newValue }); 76 | if (!change) 77 | return UNCHANGED; 78 | newValue = change.newValue; 79 | } 80 | const changed = valueDidChange(this.mode === ValueMode.Structure, this.value, newValue); 81 | if (changed) 82 | return makeChildObservable(newValue, this.mode, this.name); 83 | return UNCHANGED; 84 | } 85 | 86 | setNewValue(newValue: T) { 87 | const oldValue = this.value; 88 | this.value = newValue; 89 | this.reportChanged(); 90 | if (hasListeners(this)) 91 | notifyListeners(this, [newValue, oldValue]); // in 3.0, use an object instead! 92 | } 93 | 94 | public get(): T { 95 | this.reportObserved(); 96 | return this.value; 97 | } 98 | 99 | public intercept(handler: IInterceptor>): Lambda { 100 | return registerInterceptor(this, handler); 101 | } 102 | 103 | public observe(listener: (newValue: T, oldValue: T) => void, fireImmediately?: boolean): Lambda { 104 | if (fireImmediately) 105 | listener(this.value, undefined); 106 | return registerListener(this, listener); 107 | } 108 | 109 | toJSON() { 110 | return this.get(); 111 | } 112 | 113 | toString() { 114 | return `${this.name}[${this.value}]`; 115 | } 116 | } 117 | 118 | export const isObservableValue = createInstanceofPredicate("ObservableValue", ObservableValue); 119 | -------------------------------------------------------------------------------- /test/intercept.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var m = require('..'); 3 | var intercept = m.intercept; 4 | 5 | test('intercept observable value', t => { 6 | var a = m.observable(1); 7 | 8 | var d = intercept(a, () => { 9 | return null; 10 | }); 11 | 12 | a.set(2); 13 | 14 | t.equal(a.get(), 1, "should be unchanged"); 15 | 16 | d(); 17 | 18 | a.set(3); 19 | t.equal(a.get(), 3, "should be changed"); 20 | 21 | d = intercept(a, (c) => { 22 | if (c.newValue % 2 === 0) 23 | throw "value should be odd!"; 24 | return c; 25 | }); 26 | 27 | t.throws(() => { 28 | a.set(4); 29 | }, "value should be odd!"); 30 | 31 | t.equal(a.get(), 3, "unchanged"); 32 | a.set(5); 33 | t.equal(a.get(), 5, "changed"); 34 | 35 | d(); 36 | d = intercept(a, c => { 37 | c.newValue *= 2; 38 | return c; 39 | }); 40 | 41 | a.set(6); 42 | t.equal(a.get(), 12, "should be doubled"); 43 | 44 | var d2 = intercept(a, c => { 45 | c.newValue += 1; 46 | return c; 47 | }); 48 | 49 | a.set(7); 50 | t.equal(a.get(), 15, "doubled and added"); 51 | 52 | d(); 53 | a.set(8); 54 | t.equal(a.get(), 9, "just added"); 55 | 56 | t.end(); 57 | }); 58 | 59 | test('intercept array', t => { 60 | var a = m.observable([1, 2]); 61 | 62 | var d = a.intercept(c => null); 63 | a.push(2); 64 | t.deepEqual(a.slice(), [1, 2]); 65 | 66 | d(); 67 | 68 | d = intercept(a, c => { 69 | if (c.type === "splice") { 70 | c.added.push(c.added[0] * 2); 71 | c.removedCount = 1; 72 | return c; 73 | } else if (c.type === "update") { 74 | c.newValue = c.newValue * 3; 75 | return c; 76 | } 77 | }); 78 | 79 | a.unshift(3,4); 80 | 81 | t.deepEqual(a.slice(), [3, 4, 6, 2], "splice has been modified"); 82 | a[2] = 5; 83 | t.deepEqual(a.slice(), [3, 4, 15, 2], "update has tripled"); 84 | 85 | t.end(); 86 | }); 87 | 88 | test('intercept object', t => { 89 | var a = { 90 | b: 3 91 | } 92 | 93 | // bit magical, but intercept makes a observable, to be consistent with observe. 94 | // deprecated immediately :) 95 | var d = intercept(a, c => { 96 | c.newValue *= 3; 97 | return c; 98 | }); 99 | 100 | a.b = 4; 101 | 102 | t.equal(a.b, 12, "intercept applied"); 103 | 104 | var d2 = intercept(a, "b", c => { 105 | c.newValue += 1; 106 | return c; 107 | }); 108 | 109 | a.b = 5; 110 | t.equal(a.b, 16, "attribute selector applied last"); 111 | 112 | var d3 = intercept(a, c => { 113 | t.equal(c.name, "b"), 114 | t.equal(c.object, a); 115 | t.equal(c.type, "update"); 116 | return null; 117 | }) 118 | 119 | a.b = 7; 120 | t.equal(a.b, 16, "interceptor not applied"); 121 | 122 | d3(); 123 | a.b = 7; 124 | t.equal(a.b, 22, "interceptor applied again"); 125 | 126 | var d4 = intercept(a, c => { 127 | if (c.type === "add") { 128 | return null; 129 | } 130 | return c; 131 | }); 132 | 133 | m.extendObservable(a, { c: 1 }); 134 | t.equal(a.c, undefined, "extension intercepted"); 135 | t.equal(m.isObservable(a, "c"), false); 136 | 137 | d4(); 138 | 139 | m.extendObservable(a, { c: 2 }); 140 | t.equal(a.c, 6, "extension not intercepted"); 141 | t.equal(m.isObservable(a, "c"), true); 142 | 143 | t.end(); 144 | }); 145 | 146 | test('intercept map', t => { 147 | var a = m.map({ 148 | b: 3 149 | }) 150 | 151 | var d = intercept(a, c => { 152 | c.newValue *= 3; 153 | return c; 154 | }); 155 | 156 | a.set("b", 4); 157 | 158 | t.equal(a.get("b"), 12, "intercept applied"); 159 | 160 | var d2 = intercept(a, "b", c => { 161 | c.newValue += 1; 162 | return c; 163 | }); 164 | 165 | a.set("b", 5); 166 | t.equal(a.get("b"), 16, "attribute selector applied last"); 167 | 168 | var d3 = intercept(a, c => { 169 | t.equal(c.name, "b"), 170 | t.equal(c.object, a); 171 | t.equal(c.type, "update"); 172 | return null; 173 | }) 174 | 175 | a.set("b", 7); 176 | t.equal(a.get("b"), 16, "interceptor not applied"); 177 | 178 | d3(); 179 | a.set("b", 7); 180 | t.equal(a.get("b"), 22, "interceptor applied again"); 181 | 182 | var d4 = intercept(a, c => { 183 | if (c.type === "delete") 184 | return null; 185 | return c; 186 | }); 187 | 188 | a.delete("b"); 189 | t.equal(a.has("b"), true); 190 | t.equal(a.get("b"), 22, "delete intercepted"); 191 | 192 | d4(); 193 | a.delete("b"); 194 | t.equal(a.has("b"), false); 195 | t.equal(a.get("c"), undefined, "delete not intercepted"); 196 | 197 | t.end(); 198 | }); -------------------------------------------------------------------------------- /test/spy.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var mobx = require('..'); 3 | 4 | test('spy output', t => { 5 | var events = []; 6 | 7 | var stop = mobx.spy(c => events.push(c)); 8 | 9 | doStuff(); 10 | 11 | stop(); 12 | 13 | doStuff(); 14 | 15 | events.forEach(ev => { delete ev.object; delete ev.fn; delete ev.time; }); 16 | 17 | t.equal(events.length, doStuffEvents.length, "amount of events doesn't match"); 18 | //t.deepEqual(events, doStuffEvents); 19 | 20 | events.forEach((ev, idx) => { 21 | t.deepEqual(ev, doStuffEvents[idx], "expected event #" + (1 + idx) + " to be equal"); 22 | }); 23 | 24 | t.ok(events.filter(ev => ev.spyReportStart === true).length > 0, "spy report start count should be larger then zero"); 25 | 26 | t.equal( 27 | events.filter(ev => ev.spyReportStart === true).length, 28 | events.filter(ev => ev.spyReportEnd === true).length, 29 | "amount of start and end events differs" 30 | ); 31 | 32 | t.end(); 33 | }) 34 | 35 | function doStuff() { 36 | var a = mobx.observable(2); 37 | a.set(3); 38 | 39 | var b = mobx.observable({ 40 | c: 4 41 | }); 42 | b.c = 5; 43 | mobx.extendObservable(b, { d: 6 }); 44 | b.d = 7; 45 | 46 | var e = mobx.observable([1, 2]); 47 | e.push(3, 4); 48 | e.shift(); 49 | e[2] = 5; 50 | 51 | var f = mobx.map({ g: 1 }); 52 | f.delete("h"); 53 | f.delete("g"); 54 | f.set("i", 5); 55 | f.set("i", 6); 56 | 57 | var j = mobx.computed(() => a.get() * 2); 58 | 59 | var stop = mobx.autorun(() => { j.get() }); 60 | 61 | a.set(4); 62 | 63 | mobx.transaction(function myTransaction() { 64 | a.set(5); 65 | a.set(6); 66 | }); 67 | 68 | mobx.action("myTestAction", (newValue) => { 69 | a.set(newValue) 70 | })(7); 71 | } 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | const doStuffEvents = [ 80 | { newValue: 2, type: 'create' }, 81 | { newValue: 3, oldValue: 2, type: 'update', spyReportStart: true }, 82 | { spyReportEnd: true }, 83 | { name: 'c', newValue: 4, spyReportStart: true, type: 'add' }, 84 | { spyReportEnd: true }, 85 | { name: 'c', newValue: 5, oldValue: 4, spyReportStart: true, type: 'update' }, 86 | { spyReportEnd: true }, 87 | { name: 'd', newValue: 6, spyReportStart: true, type: 'add' }, 88 | { spyReportEnd: true }, 89 | { name: 'd', newValue: 7, oldValue: 6, spyReportStart: true, type: 'update' }, 90 | { spyReportEnd: true }, 91 | { added: [ 1, 2 ], addedCount: 2, index: 0, removed: [], removedCount: 0, spyReportStart: true, type: 'splice' }, 92 | { spyReportEnd: true }, 93 | { added: [ 3, 4 ], addedCount: 2, index: 2, removed: [], removedCount: 0, spyReportStart: true, type: 'splice' }, 94 | { spyReportEnd: true }, 95 | { added: [], addedCount: 0, index: 0, removed: [ 1 ], removedCount: 1, spyReportStart: true, type: 'splice' }, 96 | { spyReportEnd: true }, 97 | { index: 2, newValue: 5, oldValue: 4, spyReportStart: true, type: 'update' }, 98 | { spyReportEnd: true }, 99 | { name: 'g', newValue: 1, spyReportStart: true, type: 'add' }, 100 | { spyReportEnd: true }, 101 | { name: 'g', oldValue: 1, spyReportStart: true, type: 'delete' }, 102 | { spyReportEnd: true }, 103 | { name: 'i', newValue: 5, spyReportStart: true, type: 'add' }, 104 | { spyReportEnd: true }, 105 | { name: 'i', newValue: 6, oldValue: 5, spyReportStart: true, type: 'update' }, 106 | { spyReportEnd: true }, 107 | { spyReportStart: true, type: 'reaction' }, 108 | { target: undefined, type: 'compute' }, 109 | { spyReportEnd: true }, 110 | { newValue: 4, oldValue: 3, spyReportStart: true, type: 'update' }, 111 | { target: undefined, type: 'compute' }, 112 | { spyReportStart: true, type: 'reaction' }, 113 | { spyReportEnd: true }, 114 | { spyReportEnd: true }, 115 | { name: 'myTransaction', spyReportStart: true, target: undefined, type: 'transaction' }, 116 | { newValue: 5, oldValue: 4, spyReportStart: true, type: 'update' }, 117 | { spyReportEnd: true }, 118 | { newValue: 6, oldValue: 5, spyReportStart: true, type: 'update' }, 119 | { spyReportEnd: true }, 120 | { target: undefined, type: 'compute' }, 121 | { spyReportStart: true, type: 'reaction' }, 122 | { spyReportEnd: true }, 123 | { spyReportEnd: true }, 124 | { name: 'myTestAction', spyReportStart: true, arguments: [7], type: 'action', target: undefined }, 125 | { newValue: 7, oldValue: 6, spyReportStart: true, type: 'update' }, 126 | { spyReportEnd: true }, 127 | { target: undefined, type: 'compute' }, 128 | { spyReportStart: true, type: 'reaction' }, 129 | { spyReportEnd: true }, 130 | { spyReportEnd: true } 131 | ] -------------------------------------------------------------------------------- /src/mobx.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) Michel Weststrate 2015 - 2016 3 | * MIT Licensed 4 | * 5 | * Welcome to the mobx sources! To get an global overview of how MobX internally works, 6 | * this is a good place to start: 7 | * https://medium.com/@mweststrate/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254#.xvbh6qd74 8 | * 9 | * Source folders: 10 | * =============== 11 | * 12 | * - api/ Most of the public static methods exposed by the module can be found here. 13 | * - core/ Implementation of the MobX algorithm; atoms, derivations, reactions, dependency trees, optimizations. Cool stuff can be found here. 14 | * - types/ All the magic that is need to have observable objects, arrays and values is in this folder. Including the modifiers like `asFlat`. 15 | * - utils/ Utility stuff. 16 | * 17 | */ 18 | 19 | import {registerGlobals} from "./core/globalstate"; 20 | registerGlobals(); 21 | 22 | export { IAtom, Atom, BaseAtom } from "./core/atom"; 23 | export { IObservable, IDepTreeNode } from "./core/observable"; 24 | export { Reaction, IReactionPublic } from "./core/reaction"; 25 | export { IDerivation, untracked, IDerivationState } from "./core/derivation"; 26 | export { useStrict, isStrictModeEnabled } from "./core/action"; 27 | export { spy } from "./core/spy"; 28 | export { transaction } from "./core/transaction"; 29 | export { IComputedValue } from "./core/computedvalue"; 30 | 31 | export { asReference, asFlat, asStructure, asMap } from "./types/modifiers"; 32 | export { IInterceptable, IInterceptor } from "./types/intercept-utils"; 33 | export { IListenable } from "./types/listen-utils"; 34 | export { IObjectWillChange, IObjectChange, IObservableObject, isObservableObject } from "./types/observableobject"; 35 | export { /* 3.0: IValueDidChange, */ IValueWillChange, IObservableValue } from "./types/observablevalue"; 36 | 37 | export { IObservableArray, IArrayWillChange, IArrayWillSplice, IArrayChange, IArraySplice, isObservableArray, fastArray } from "./types/observablearray"; 38 | export { IKeyValueMap, ObservableMap, IMapEntries, IMapEntry, IMapWillChange, IMapChange, isObservableMap, map } from "./types/observablemap" 39 | 40 | export { observable } from "./api/observable"; 41 | export { computed, IComputedValueOptions } from "./api/computeddecorator"; 42 | export { isObservable } from "./api/isobservable"; 43 | export { isComputed } from "./api/iscomputed"; 44 | export { extendObservable } from "./api/extendobservable"; 45 | export { observe } from "./api/observe"; 46 | export { intercept } from "./api/intercept"; 47 | export { autorun, autorunAsync, autorunUntil, when, reaction } from "./api/autorun"; 48 | export { action, isAction, runInAction } from "./api/action"; 49 | 50 | export { expr } from "./api/expr"; 51 | export { toJSON, toJS } from "./api/tojs"; 52 | export { ITransformer, createTransformer } from "./api/createtransformer"; 53 | export { whyRun } from "./api/whyrun"; 54 | 55 | export { Lambda } from "./utils/utils"; 56 | export { Iterator } from "./utils/iterable"; 57 | export { SimpleEventEmitter, ISimpleEventListener } from "./utils/simpleeventemitter"; 58 | export { IObserverTree, IDependencyTree } from "./api/extras"; 59 | 60 | import { resetGlobalState } from "./core/globalstate"; 61 | 62 | import { IDepTreeNode } from "./core/observable"; 63 | import { IObserverTree, IDependencyTree, getDependencyTree, getObserverTree } from "./api/extras"; 64 | import { getDebugName, getAtom, getAdministration } from "./types/type-utils"; 65 | import { allowStateChanges } from "./core/action"; 66 | import { trackTransitions, spyReport, spyReportEnd, spyReportStart, isSpyEnabled } from "./core/spy"; 67 | import { Lambda } from "./utils/utils"; 68 | import { isComputingDerivation } from "./core/derivation"; 69 | 70 | export const extras = { 71 | allowStateChanges, 72 | getAtom, 73 | getDebugName, 74 | getDependencyTree, 75 | getObserverTree, 76 | isComputingDerivation, 77 | isSpyEnabled, 78 | resetGlobalState, 79 | spyReport, 80 | spyReportEnd, 81 | spyReportStart, 82 | trackTransitions 83 | }; 84 | 85 | // Experimental or internal api's (exposed for testing for example) 86 | export const _ = { 87 | getAdministration, 88 | resetGlobalState 89 | }; 90 | 91 | declare var __MOBX_DEVTOOLS_GLOBAL_HOOK__: { injectMobx: ((any) => void)}; 92 | declare var module: { exports: any }; 93 | if (typeof __MOBX_DEVTOOLS_GLOBAL_HOOK__ === 'object') { 94 | __MOBX_DEVTOOLS_GLOBAL_HOOK__.injectMobx(module.exports) 95 | } 96 | -------------------------------------------------------------------------------- /src/utils/decorators.ts: -------------------------------------------------------------------------------- 1 | import {invariant, addHiddenProp, hasOwnProperty} from "./utils"; 2 | 3 | /** 4 | * Construcs a decorator, that normalizes the differences between 5 | * TypeScript and Babel. Mainly caused by the fact that legacy-decorator cannot assign 6 | * values during instance creation to properties that have a getter setter. 7 | * 8 | * - Sigh - 9 | * 10 | * Also takes care of the difference between @decorator field and @decorator(args) field, and different forms of values. 11 | * For performance (cpu and mem) reasons the properties are always defined on the prototype (at least initially). 12 | * This means that these properties despite being enumerable might not show up in Object.keys() (but they will show up in for...in loops). 13 | */ 14 | export function createClassPropertyDecorator( 15 | /** 16 | * This function is invoked once, when the property is added to a new instance. 17 | * When this happens is not strictly determined due to differences in TS and Babel: 18 | * Typescript: Usually when constructing the new instance 19 | * Babel, sometimes Typescript: during the first get / set 20 | * Both: when calling `runLazyInitializers(instance)` 21 | */ 22 | onInitialize: (target, property, initialValue, customArgs?: IArguments, originalDescriptor?) => void, 23 | get: (name) => any, 24 | set: (name, newValue) => void, 25 | enumerable: boolean, 26 | /** 27 | * Can this decorator invoked with arguments? e.g. @decorator(args) 28 | */ 29 | allowCustomArguments: boolean 30 | ): any { 31 | function classPropertyDecorator(target: any, key: string, descriptor, customArgs?: IArguments, argLen?: number) { 32 | invariant(allowCustomArguments || quacksLikeADecorator(arguments), "This function is a decorator, but it wasn't invoked like a decorator"); 33 | if (!descriptor) { 34 | // typescript (except for getter / setters) 35 | const newDescriptor = { 36 | enumerable, 37 | configurable: true, 38 | get: function() { 39 | if (!this.__mobxInitializedProps || this.__mobxInitializedProps[key] !== true) 40 | typescriptInitializeProperty(this, key, undefined, onInitialize, customArgs, descriptor); 41 | return get.call(this, key); 42 | }, 43 | set: function(v) { 44 | if (!this.__mobxInitializedProps || this.__mobxInitializedProps[key] !== true) { 45 | typescriptInitializeProperty(this, key, v, onInitialize, customArgs, descriptor); 46 | } else { 47 | set.call(this, key, v); 48 | } 49 | } 50 | }; 51 | if (arguments.length < 3 || arguments.length === 5 && argLen < 3) { 52 | // Typescript target is ES3, so it won't define property for us 53 | // or using Reflect.decorate polyfill, which will return no descriptor 54 | // (see https://github.com/mobxjs/mobx/issues/333) 55 | Object.defineProperty(target, key, newDescriptor); 56 | } 57 | return newDescriptor; 58 | } else { 59 | // babel and typescript getter / setter props 60 | if (!hasOwnProperty(target, "__mobxLazyInitializers")) { 61 | addHiddenProp(target, "__mobxLazyInitializers", 62 | (target.__mobxLazyInitializers && target.__mobxLazyInitializers.slice()) || [] // support inheritance 63 | ); 64 | } 65 | 66 | const {value, initializer} = descriptor; 67 | target.__mobxLazyInitializers.push(instance => { 68 | onInitialize( 69 | instance, 70 | key, 71 | (initializer ? initializer.call(instance) : value), 72 | customArgs, 73 | descriptor 74 | ); 75 | }); 76 | 77 | return { 78 | enumerable, configurable: true, 79 | get : function() { 80 | if (this.__mobxDidRunLazyInitializers !== true) 81 | runLazyInitializers(this); 82 | return get.call(this, key); 83 | }, 84 | set : function(v) { 85 | if (this.__mobxDidRunLazyInitializers !== true) 86 | runLazyInitializers(this); 87 | set.call(this, key, v); 88 | } 89 | }; 90 | } 91 | } 92 | 93 | if (allowCustomArguments) { 94 | /** If custom arguments are allowed, we should return a function that returns a decorator */ 95 | return function() { 96 | /** Direct invocation: @decorator bla */ 97 | if (quacksLikeADecorator(arguments)) 98 | return classPropertyDecorator.apply(null, arguments); 99 | /** Indirect invocation: @decorator(args) bla */ 100 | const outerArgs = arguments; 101 | const argLen = arguments.length; 102 | return (target, key, descriptor) => classPropertyDecorator(target, key, descriptor, outerArgs, argLen); 103 | }; 104 | } 105 | return classPropertyDecorator; 106 | } 107 | 108 | function typescriptInitializeProperty(instance, key, v, onInitialize, customArgs, baseDescriptor) { 109 | if (!hasOwnProperty(instance, "__mobxInitializedProps")) 110 | addHiddenProp(instance, "__mobxInitializedProps", {}); 111 | instance.__mobxInitializedProps[key] = true; 112 | onInitialize(instance, key, v, customArgs, baseDescriptor); 113 | } 114 | 115 | export function runLazyInitializers(instance) { 116 | if (instance.__mobxDidRunLazyInitializers === true) 117 | return; 118 | if (instance.__mobxLazyInitializers) { 119 | addHiddenProp(instance, "__mobxDidRunLazyInitializers", true); 120 | instance.__mobxDidRunLazyInitializers && instance.__mobxLazyInitializers.forEach(initializer => initializer(instance)); 121 | } 122 | } 123 | 124 | function quacksLikeADecorator(args: IArguments): boolean { 125 | return (args.length === 2 || args.length === 3) && typeof args[1] === "string"; 126 | } 127 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export const EMPTY_ARRAY = []; 2 | Object.freeze(EMPTY_ARRAY); 3 | 4 | export interface Lambda { 5 | (): void; 6 | name?: string; 7 | } 8 | 9 | export function getNextId() { 10 | return ++globalState.mobxGuid; 11 | } 12 | 13 | export function invariant(check: boolean, message: string, thing?) { 14 | if (!check) 15 | throw new Error("[mobx] Invariant failed: " + message + (thing ? ` in '${thing}'` : "")); 16 | } 17 | 18 | const deprecatedMessages = []; 19 | export function deprecated(msg: string) { 20 | if (deprecatedMessages.indexOf(msg) !== -1) 21 | return; 22 | deprecatedMessages.push(msg); 23 | console.error("[mobx] Deprecated: " + msg); 24 | } 25 | 26 | /** 27 | * Makes sure that the provided function is invoked at most once. 28 | */ 29 | export function once(func: Lambda): Lambda { 30 | let invoked = false; 31 | return function() { 32 | if (invoked) 33 | return; 34 | invoked = true; 35 | return func.apply(this, arguments); 36 | }; 37 | } 38 | 39 | export const noop = () => {}; 40 | 41 | export function unique(list: T[]): T[] { 42 | const res = []; 43 | list.forEach(item => { 44 | if (res.indexOf(item) === -1) 45 | res.push(item); 46 | }); 47 | return res; 48 | } 49 | 50 | export function joinStrings(things: string[], limit: number = 100, separator = " - "): string { 51 | if (!things) 52 | return ""; 53 | const sliced = things.slice(0, limit); 54 | return `${sliced.join(separator)}${things.length > limit ? " (... and " + (things.length - limit) + "more)" : ""}`; 55 | } 56 | 57 | export function isObject(value: any): boolean { 58 | return value !== null && typeof value === "object"; 59 | } 60 | 61 | export function isPlainObject(value) { 62 | if (value === null || typeof value !== "object") 63 | return false; 64 | const proto = Object.getPrototypeOf(value); 65 | return proto === Object.prototype || proto === null; 66 | } 67 | 68 | export function objectAssign(...objs: Object[]): Object; 69 | export function objectAssign() { 70 | const res = arguments[0]; 71 | for (let i = 1, l = arguments.length; i < l; i++) { 72 | const source = arguments[i]; 73 | for (let key in source) if (hasOwnProperty(source, key)) { 74 | res[key] = source[key]; 75 | } 76 | } 77 | return res; 78 | } 79 | 80 | export function valueDidChange(compareStructural: boolean, oldValue, newValue): boolean { 81 | return compareStructural 82 | ? !deepEquals(oldValue, newValue) 83 | : oldValue !== newValue; 84 | } 85 | 86 | const prototypeHasOwnProperty = Object.prototype.hasOwnProperty; 87 | export function hasOwnProperty(object: Object, propName: string) { 88 | return prototypeHasOwnProperty.call(object, propName); 89 | } 90 | 91 | export function makeNonEnumerable(object: any, propNames: string[]) { 92 | for (let i = 0; i < propNames.length; i++) { 93 | addHiddenProp(object, propNames[i], object[propNames[i]]); 94 | } 95 | } 96 | 97 | export function addHiddenProp(object: any, propName: string, value: any) { 98 | Object.defineProperty(object, propName, { 99 | enumerable: false, 100 | writable: true, 101 | configurable: true, 102 | value 103 | }); 104 | } 105 | 106 | export function addHiddenFinalProp(object: any, propName: string, value: any) { 107 | Object.defineProperty(object, propName, { 108 | enumerable: false, 109 | writable: false, 110 | configurable: true, 111 | value 112 | }); 113 | } 114 | 115 | export function isPropertyConfigurable(object: any, prop: string): boolean { 116 | const descriptor = Object.getOwnPropertyDescriptor(object, prop); 117 | return !descriptor || (descriptor.configurable !== false && descriptor.writable !== false); 118 | } 119 | 120 | export function assertPropertyConfigurable(object: any, prop: string) { 121 | invariant( 122 | isPropertyConfigurable(object, prop), 123 | `Cannot make property '${prop}' observable, it is not configurable and writable in the target object` 124 | ); 125 | } 126 | 127 | export function getEnumerableKeys(obj) { 128 | const res = []; 129 | for (let key in obj) 130 | res.push(key); 131 | return res; 132 | } 133 | 134 | /** 135 | * Naive deepEqual. Doesn't check for prototype, non-enumerable or out-of-range properties on arrays. 136 | * If you have such a case, you probably should use this function but something fancier :). 137 | */ 138 | export function deepEquals(a, b) { 139 | if (a === null && b === null) 140 | return true; 141 | if (a === undefined && b === undefined) 142 | return true; 143 | const aIsArray = Array.isArray(a) || isObservableArray(a); 144 | if (aIsArray !== (Array.isArray(b) || isObservableArray(b))) { 145 | return false; 146 | } else if (aIsArray) { 147 | if (a.length !== b.length) 148 | return false; 149 | for (let i = a.length -1; i >= 0; i--) 150 | if (!deepEquals(a[i], b[i])) 151 | return false; 152 | return true; 153 | } else if (typeof a === "object" && typeof b === "object") { 154 | if (a === null || b === null) 155 | return false; 156 | if (getEnumerableKeys(a).length !== getEnumerableKeys(b).length) 157 | return false; 158 | for (let prop in a) { 159 | if (!(prop in b)) 160 | return false; 161 | if (!deepEquals(a[prop], b[prop])) 162 | return false; 163 | } 164 | return true; 165 | } 166 | return a === b; 167 | } 168 | 169 | export function createInstanceofPredicate(name: string, clazz: new (...args:any[]) => T): (x: any) => x is T { 170 | // TODO: this is quite a slow aproach, find something faster? 171 | const propName = "isMobX" + name; 172 | clazz.prototype[propName] = true; 173 | return function (x) { 174 | return isObject(x) && x[propName] === true; 175 | } as any; 176 | } 177 | 178 | import {globalState} from "../core/globalstate"; 179 | import {isObservableArray} from "../types/observablearray"; -------------------------------------------------------------------------------- /src/types/modifiers.ts: -------------------------------------------------------------------------------- 1 | import {isPlainObject, invariant, isObject} from "../utils/utils"; 2 | import {isObservable} from "../api/isobservable"; 3 | import {extendObservableHelper} from "../api/extendobservable"; 4 | import {createObservableArray} from "../types/observablearray"; 5 | import {map, ObservableMap, IMapEntries, IKeyValueMap} from "../types/observablemap"; 6 | 7 | export enum ValueMode { 8 | Recursive, // If the value is an plain object, it will be made reactive, and so will all its future children. 9 | Reference, // Treat this value always as a reference, without any further processing. 10 | Structure, // Similar to recursive. However, this structure can only exist of plain arrays and objects. 11 | // No observers will be triggered if a new value is assigned (to a part of the tree) that deeply equals the old value. 12 | Flat // If the value is an plain object, it will be made reactive, and so will all its future children. 13 | } 14 | 15 | export interface IModifierWrapper { 16 | mobxModifier: ValueMode; 17 | value: any; 18 | } 19 | 20 | function withModifier(modifier: ValueMode, value: any): IModifierWrapper { 21 | assertUnwrapped(value, "Modifiers are not allowed to be nested"); 22 | return { 23 | mobxModifier: modifier, 24 | value 25 | }; 26 | } 27 | 28 | export function getModifier(value: any): ValueMode { 29 | if (value) { // works for both objects and functions 30 | return (value.mobxModifier as ValueMode) || null; 31 | } 32 | return null; 33 | } 34 | 35 | 36 | /** 37 | * Can be used in combination with makeReactive / extendReactive. 38 | * Enforces that a reference to 'value' is stored as property, 39 | * but that 'value' itself is not turned into something reactive. 40 | * Future assignments to the same property will inherit this behavior. 41 | * @param value initial value of the reactive property that is being defined. 42 | */ 43 | export function asReference(value: T): T { 44 | // unsound typecast, but in combination with makeReactive, the end result should be of the correct type this way 45 | // e.g: makeReactive({ x : asReference(number)}) -> { x : number } 46 | return withModifier(ValueMode.Reference, value) as any as T; 47 | } 48 | (asReference as any).mobxModifier = ValueMode.Reference; 49 | 50 | /** 51 | * Can be used in combination with makeReactive / extendReactive. 52 | * Enforces that values that are deeply equalled identical to the previous are considered to unchanged. 53 | * (the default equality used by mobx is reference equality). 54 | * Values that are still reference equal, but not deep equal, are considered to be changed. 55 | * asStructure can only be used incombinations with arrays or objects. 56 | * It does not support cyclic structures. 57 | * Future assignments to the same property will inherit this behavior. 58 | * @param value initial value of the reactive property that is being defined. 59 | */ 60 | export function asStructure(value: T): T { 61 | return withModifier(ValueMode.Structure, value) as any as T; 62 | } 63 | (asStructure as any).mobxModifier = ValueMode.Structure; 64 | 65 | /** 66 | * Can be used in combination with makeReactive / extendReactive. 67 | * The value will be made reactive, but, if the value is an object or array, 68 | * children will not automatically be made reactive as well. 69 | */ 70 | export function asFlat(value: T): T { 71 | return withModifier(ValueMode.Flat, value) as any as T; 72 | } 73 | (asFlat as any).mobxModifier = ValueMode.Flat; 74 | 75 | export function asMap(): ObservableMap; 76 | export function asMap(): ObservableMap; 77 | export function asMap(entries: IMapEntries, modifierFunc?: Function): ObservableMap; 78 | export function asMap(data: IKeyValueMap, modifierFunc?: Function): ObservableMap; 79 | export function asMap(data?, modifierFunc?): ObservableMap { 80 | return map(data, modifierFunc); 81 | } 82 | 83 | export function getValueModeFromValue(value: any, defaultMode: ValueMode): [ValueMode, any] { 84 | const mode = getModifier(value); 85 | if (mode) 86 | return [mode, value.value]; 87 | return [defaultMode, value]; 88 | } 89 | 90 | export function getValueModeFromModifierFunc(func?: Function): ValueMode { 91 | if (func === undefined) 92 | return ValueMode.Recursive; 93 | const mod = getModifier(func); 94 | invariant(mod !== null, "Cannot determine value mode from function. Please pass in one of these: mobx.asReference, mobx.asStructure or mobx.asFlat, got: " + func); 95 | return mod; 96 | } 97 | 98 | 99 | export function makeChildObservable(value, parentMode: ValueMode, name?: string) { 100 | let childMode: ValueMode; 101 | if (isObservable(value)) 102 | return value; 103 | 104 | switch (parentMode) { 105 | case ValueMode.Reference: 106 | return value; 107 | case ValueMode.Flat: 108 | assertUnwrapped(value, "Items inside 'asFlat' cannot have modifiers"); 109 | childMode = ValueMode.Reference; 110 | break; 111 | case ValueMode.Structure: 112 | assertUnwrapped(value, "Items inside 'asStructure' cannot have modifiers"); 113 | childMode = ValueMode.Structure; 114 | break; 115 | case ValueMode.Recursive: 116 | [childMode, value] = getValueModeFromValue(value, ValueMode.Recursive); 117 | break; 118 | default: 119 | invariant(false, "Illegal State"); 120 | } 121 | 122 | if (Array.isArray(value)) 123 | return createObservableArray(value as any[], childMode, name); 124 | if (isPlainObject(value) && Object.isExtensible(value)) 125 | return extendObservableHelper(value, value, childMode, name); 126 | return value; 127 | } 128 | 129 | export function assertUnwrapped(value, message) { 130 | if (getModifier(value) !== null) 131 | throw new Error(`[mobx] asStructure / asReference / asFlat cannot be used here. ${message}`); 132 | } 133 | -------------------------------------------------------------------------------- /test/cycles.js: -------------------------------------------------------------------------------- 1 | var m = require('../'); 2 | var test = require('tape'); 3 | 4 | test('cascading active state (form 1)', function(t) { 5 | var Store = function() { 6 | m.extendObservable(this, {_activeItem: null}); 7 | } 8 | Store.prototype.activeItem = function(item) { 9 | var _this = this; 10 | 11 | if (arguments.length === 0) return this._activeItem; 12 | 13 | m.transaction(function() { 14 | if (_this._activeItem === item) return; 15 | if (_this._activeItem) _this._activeItem.isActive = false; 16 | _this._activeItem = item; 17 | if (_this._activeItem) _this._activeItem.isActive = true; 18 | }); 19 | } 20 | 21 | var Item = function() { 22 | m.extendObservable(this, {isActive: false}); 23 | } 24 | 25 | var store = new Store(); 26 | var item1 = new Item(), item2 = new Item(); 27 | t.equal(store.activeItem(), null); 28 | t.equal(item1.isActive, false); 29 | t.equal(item2.isActive, false); 30 | 31 | store.activeItem(item1); 32 | t.equal(store.activeItem(), item1); 33 | t.equal(item1.isActive, true); 34 | t.equal(item2.isActive, false); 35 | 36 | store.activeItem(item2); 37 | t.equal(store.activeItem(), item2); 38 | t.equal(item1.isActive, false); 39 | t.equal(item2.isActive, true); 40 | 41 | store.activeItem(null); 42 | t.equal(store.activeItem(), null); 43 | t.equal(item1.isActive, false); 44 | t.equal(item2.isActive, false); 45 | 46 | t.end(); 47 | }); 48 | 49 | test('cascading active state (form 2)', function(t) { 50 | var Store = function() { 51 | var _this = this; 52 | m.extendObservable(this, {activeItem: null}); 53 | 54 | m.autorun(function() { 55 | if (_this._activeItem === _this.activeItem) return; 56 | if (_this._activeItem) _this._activeItem.isActive = false; 57 | _this._activeItem = _this.activeItem; 58 | if (_this._activeItem) _this._activeItem.isActive = true; 59 | }); 60 | } 61 | 62 | var Item = function() { 63 | m.extendObservable(this, {isActive: false}); 64 | } 65 | 66 | var store = new Store(); 67 | var item1 = new Item(), item2 = new Item(); 68 | t.equal(store.activeItem, null); 69 | t.equal(item1.isActive, false); 70 | t.equal(item2.isActive, false); 71 | 72 | store.activeItem = item1; 73 | t.equal(store.activeItem, item1); 74 | t.equal(item1.isActive, true); 75 | t.equal(item2.isActive, false); 76 | 77 | store.activeItem = item2; 78 | t.equal(store.activeItem, item2); 79 | t.equal(item1.isActive, false); 80 | t.equal(item2.isActive, true); 81 | 82 | store.activeItem = null; 83 | t.equal(store.activeItem, null); 84 | t.equal(item1.isActive, false); 85 | t.equal(item2.isActive, false); 86 | 87 | t.end(); 88 | }); 89 | 90 | test('emulate rendering', function(t) { 91 | var renderCount = 0; 92 | 93 | var Component = function(props) { 94 | var _this = this; 95 | this.props = props; 96 | } 97 | Component.prototype.destroy = function() { 98 | if (this.handler) { this.handler(); this.handler = null; } 99 | } 100 | 101 | Component.prototype.render = function() { 102 | var _this = this; 103 | 104 | if (this.handler) { this.handler(); this.handler = null; } 105 | this.handler = m.autorun(function() { 106 | if (!_this.props.data.title) _this.props.data.title = 'HELLO'; 107 | renderCount++; 108 | }); 109 | } 110 | 111 | var data = {}; 112 | m.extendObservable(data, {title: null}); 113 | var component = new Component({data: data}); 114 | t.equal(renderCount, 0); 115 | 116 | component.render(); 117 | t.equal(renderCount, 1); 118 | 119 | data.title = 'WORLD'; 120 | t.equal(renderCount, 2); 121 | 122 | data.title = null; 123 | // Note that this causes two invalidations 124 | // however, the real mobx-react binding optimizes this as well 125 | // see mobx-react #12, so maybe this ain't the best test 126 | t.equal(renderCount, 4); 127 | 128 | data.title = 'WORLD'; 129 | t.equal(renderCount, 5); 130 | 131 | component.destroy(); 132 | data.title = 'HELLO'; 133 | t.equal(renderCount, 5); 134 | 135 | t.end(); 136 | }); 137 | 138 | 139 | test('efficient selection', function(t) { 140 | 141 | function Item(value) { 142 | m.extendObservable(this, { 143 | selected: false, 144 | value: value 145 | }); 146 | } 147 | 148 | function Store() { 149 | this.prevSelection = null; 150 | m.extendObservable(this, { 151 | selection: null, 152 | items: [ 153 | new Item(1), 154 | new Item(2), 155 | new Item(3) 156 | ] 157 | }); 158 | m.autorun(function() { 159 | m.transaction(function() { 160 | if (this.previousSelection === this.selection) 161 | return true; // converging condition 162 | if (this.previousSelection) 163 | this.previousSelection.selected = false; 164 | if (this.selection) 165 | this.selection.selected = true; 166 | this.previousSelection = this.selection; 167 | }, this); 168 | }, this); 169 | } 170 | 171 | var store = new Store(); 172 | 173 | t.equal(store.selection, null); 174 | t.equal(store.items.filter(function (i) { return i.selected }).length, 0); 175 | 176 | store.selection = store.items[1]; 177 | t.equal(store.items.filter(function (i) { return i.selected }).length, 1); 178 | t.equal(store.selection, store.items[1]); 179 | t.equal(store.items[1].selected, true); 180 | 181 | store.selection = store.items[2]; 182 | t.equal(store.items.filter(function (i) { return i.selected }).length, 1); 183 | t.equal(store.selection, store.items[2]); 184 | t.equal(store.items[2].selected, true); 185 | 186 | store.selection = null; 187 | t.equal(store.items.filter(function (i) { return i.selected }).length, 0); 188 | t.equal(store.selection, null); 189 | 190 | t.end(); 191 | }); -------------------------------------------------------------------------------- /test/reaction.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var mobx = require('..') 3 | var reaction = mobx.reaction 4 | 5 | test('basic', t => { 6 | var a = mobx.observable(1); 7 | var values = []; 8 | 9 | var d = reaction(() => a.get(), newValue => { 10 | values.push(newValue); 11 | }) 12 | 13 | a.set(2); 14 | a.set(3); 15 | d(); 16 | a.set(4); 17 | 18 | t.deepEqual(values, [2, 3]); 19 | t.end(); 20 | }) 21 | 22 | test('effect fireImmediately is honored', t => { 23 | var a = mobx.observable(1); 24 | var values = []; 25 | 26 | var d = reaction(() => a.get(), newValue => { 27 | values.push(newValue); 28 | }, true) 29 | 30 | a.set(2); 31 | a.set(3); 32 | d(); 33 | a.set(4); 34 | 35 | t.deepEqual(values, [1, 2, 3]); 36 | t.end() 37 | }) 38 | 39 | test('effect is untracked', t => { 40 | var a = mobx.observable(1); 41 | var b = mobx.observable(2); 42 | var values = []; 43 | 44 | var d = reaction(() => a.get(), newValue => { 45 | values.push(newValue * b.get()); 46 | }, true) 47 | 48 | a.set(2); 49 | b.set(7); // shoudn't trigger a new change 50 | a.set(3); 51 | d(); 52 | a.set(4); 53 | 54 | t.deepEqual(values, [2, 4, 21]); 55 | t.end() 56 | }) 57 | 58 | test('effect debounce is honored', t => { 59 | t.plan(2) 60 | 61 | var a = mobx.observable(1); 62 | var values = []; 63 | var exprCount = 0; 64 | 65 | var d = reaction(() => { 66 | exprCount ++; 67 | return a.get() 68 | }, newValue => { 69 | values.push(newValue); 70 | }, false, 100) 71 | 72 | setTimeout(() => a.set(2), 30); // should not become visible nor evaluate expr; first is skipped 73 | setTimeout(() => a.set(3), 150); // should not be visible, combined with the next 74 | setTimeout(() => a.set(4), 160); 75 | setTimeout(() => a.set(5), 300); 76 | setTimeout(() => d(), 500); 77 | setTimeout(() => a.set(6), 700); 78 | 79 | setTimeout(() => { 80 | t.deepEqual(values, [4, 5]) 81 | t.equal(exprCount, 3) 82 | }, 900) 83 | }) 84 | 85 | test('effect debounce + fire immediately is honored', t => { 86 | t.plan(2) 87 | 88 | var a = mobx.observable(1); 89 | var values = []; 90 | var exprCount = 0; 91 | 92 | var d = reaction(() => { 93 | exprCount ++; 94 | return a.get() 95 | }, newValue => { 96 | values.push(newValue); 97 | }, true, 100) 98 | 99 | setTimeout(() => a.set(3), 150); 100 | setTimeout(() => a.set(4), 300); 101 | 102 | setTimeout(() => { 103 | d(); 104 | t.deepEqual(values, [1, 3, 4]); 105 | t.equal(exprCount, 3) 106 | }, 500) 107 | }) 108 | 109 | test('passes Reaction as an argument to expression function', t => { 110 | var a = mobx.observable(1); 111 | var values = []; 112 | 113 | reaction(r => { 114 | if (a.get() === 'pleaseDispose') r.dispose(); 115 | return a.get(); 116 | }, newValue => { 117 | values.push(newValue); 118 | }, true); 119 | 120 | a.set(2); 121 | a.set(2); 122 | a.set('pleaseDispose'); 123 | a.set(3); 124 | a.set(4); 125 | 126 | t.deepEqual(values, [1, 2, 'pleaseDispose']); 127 | t.end(); 128 | }); 129 | 130 | test('passes Reaction as an argument to effect function', t => { 131 | var a = mobx.observable(1); 132 | var values = []; 133 | 134 | reaction(() => a.get(), (newValue, r) => { 135 | if (a.get() === 'pleaseDispose') r.dispose(); 136 | values.push(newValue); 137 | }, true); 138 | 139 | a.set(2); 140 | a.set(2); 141 | a.set('pleaseDispose'); 142 | a.set(3); 143 | a.set(4); 144 | 145 | t.deepEqual(values, [1, 2, 'pleaseDispose']); 146 | t.end(); 147 | }); 148 | 149 | test('can dispose reaction on first run', t => { 150 | var a = mobx.observable(1); 151 | 152 | var valuesExpr1st = []; 153 | reaction(() => a.get(), (newValue, r) => { 154 | r.dispose(); 155 | valuesExpr1st.push(newValue); 156 | }, true); 157 | 158 | var valuesEffect1st = []; 159 | reaction(r => { 160 | r.dispose(); 161 | return a.get(); 162 | }, newValue => { 163 | valuesEffect1st.push(newValue); 164 | }, true); 165 | 166 | var valuesExpr = []; 167 | reaction(() => a.get(), (newValue, r) => { 168 | r.dispose(); 169 | valuesExpr.push(newValue); 170 | }); 171 | 172 | var valuesEffect = []; 173 | reaction(r => { 174 | r.dispose(); 175 | return a.get(); 176 | }, newValue => { 177 | valuesEffect.push(newValue); 178 | }); 179 | 180 | a.set(2); 181 | a.set(3); 182 | 183 | t.deepEqual(valuesExpr1st, [1]); 184 | t.deepEqual(valuesEffect1st, [1]); 185 | t.deepEqual(valuesExpr, [2]); 186 | t.deepEqual(valuesEffect, []); 187 | t.end(); 188 | }); 189 | 190 | test("#278 do not rerun if expr output doesn't change", t => { 191 | var a = mobx.observable(1); 192 | var values = []; 193 | 194 | var d = reaction(() => a.get() < 10 ? a.get() : 11, newValue => { 195 | values.push(newValue); 196 | }) 197 | 198 | a.set(2); 199 | a.set(3); 200 | a.set(10); 201 | a.set(11); 202 | a.set(12); 203 | a.set(4); 204 | a.set(5); 205 | a.set(13); 206 | 207 | d(); 208 | a.set(4); 209 | 210 | t.deepEqual(values, [2, 3, 11, 4, 5, 11]); 211 | t.end(); 212 | }) 213 | 214 | test("#278 do not rerun if expr output doesn't change structurally", t => { 215 | var users = mobx.observable([ 216 | { 217 | name: "jan", 218 | uppername: function() { return this.name.toUpperCase() } 219 | }, 220 | { 221 | name: "piet", 222 | uppername: function() { return this.name.toUpperCase() } 223 | } 224 | ]); 225 | var values = []; 226 | 227 | var d = reaction(mobx.asStructure( 228 | () => users.map(user => user.uppername) 229 | ), newValue => { 230 | values.push(newValue); 231 | }, true) 232 | 233 | users[0].name = "john"; 234 | users[0].name = "JoHn"; 235 | users[0].name = "jOHN"; 236 | users[1].name = "johan"; 237 | 238 | d(); 239 | users[1].name = "w00t"; 240 | 241 | t.deepEqual(values, [ 242 | ["JAN", "PIET"], 243 | ["JOHN", "PIET"], 244 | ["JOHN", "JOHAN"] 245 | ]); 246 | t.end(); 247 | }) 248 | 249 | test("throws when the max iterations over reactions are done", t => { 250 | var foo = mobx.observable({ 251 | a: 1, 252 | }); 253 | 254 | mobx.autorun("bar", () => { 255 | var x = foo.a; 256 | foo.a = Math.random(); 257 | }); 258 | 259 | t.throws( 260 | () => foo.a++, 261 | new RegExp("Reaction doesn't converge to a stable state after 100 iterations\\. " 262 | + "Probably there is a cycle in the reactive function: Reaction\\[bar\\]") 263 | ); 264 | mobx.extras.resetGlobalState(); 265 | t.end(); 266 | }) 267 | -------------------------------------------------------------------------------- /src/core/reaction.ts: -------------------------------------------------------------------------------- 1 | import {IDerivation, IDerivationState, trackDerivedFunction, clearObserving, shouldCompute} from "./derivation"; 2 | import {globalState, resetGlobalState} from "./globalstate"; 3 | import {createInstanceofPredicate, getNextId, Lambda, unique, joinStrings} from "../utils/utils"; 4 | import {isSpyEnabled, spyReport, spyReportStart, spyReportEnd} from "./spy"; 5 | import {startBatch, endBatch} from "./observable"; 6 | 7 | /** 8 | * Reactions are a special kind of derivations. Several things distinguishes them from normal reactive computations 9 | * 10 | * 1) They will always run, whether they are used by other computations or not. 11 | * This means that they are very suitable for triggering side effects like logging, updating the DOM and making network requests. 12 | * 2) They are not observable themselves 13 | * 3) They will always run after any 'normal' derivations 14 | * 4) They are allowed to change the state and thereby triggering themselves again, as long as they make sure the state propagates to a stable state in a reasonable amount of iterations. 15 | * 16 | * The state machine of a Reaction is as follows: 17 | * 18 | * 1) after creating, the reaction should be started by calling `runReaction` or by scheduling it (see also `autorun`) 19 | * 2) the `onInvalidate` handler should somehow result in a call to `this.track(someFunction)` 20 | * 3) all observables accessed in `someFunction` will be observed by this reaction. 21 | * 4) as soon as some of the dependencies has changed the Reaction will be rescheduled for another run (after the current mutation or transaction). `isScheduled` will yield true once a dependency is stale and during this period 22 | * 5) `onInvalidate` will be called, and we are back at step 1. 23 | * 24 | */ 25 | 26 | export interface IReactionPublic { 27 | dispose: () => void; 28 | } 29 | 30 | export class Reaction implements IDerivation, IReactionPublic { 31 | observing = []; // nodes we are looking at. Our value depends on these nodes 32 | newObserving = []; 33 | dependenciesState = IDerivationState.NOT_TRACKING; 34 | diffValue = 0; 35 | runId = 0; 36 | unboundDepsCount = 0; 37 | __mapid = "#" + getNextId(); 38 | isDisposed = false; 39 | _isScheduled = false; 40 | _isTrackPending = false; 41 | _isRunning = false; 42 | 43 | constructor(public name: string = "Reaction@" + getNextId(), private onInvalidate: () => void) { } 44 | 45 | onBecomeStale() { 46 | this.schedule(); 47 | } 48 | 49 | schedule() { 50 | if (!this._isScheduled) { 51 | this._isScheduled = true; 52 | globalState.pendingReactions.push(this); 53 | startBatch(); 54 | runReactions(); 55 | endBatch(); 56 | } 57 | } 58 | 59 | isScheduled() { 60 | return this._isScheduled; 61 | } 62 | 63 | /** 64 | * internal, use schedule() if you intend to kick off a reaction 65 | */ 66 | runReaction() { 67 | if (!this.isDisposed) { 68 | this._isScheduled = false; 69 | if (shouldCompute(this)) { 70 | this._isTrackPending = true; 71 | 72 | this.onInvalidate(); 73 | if (this._isTrackPending && isSpyEnabled()) { 74 | // onInvalidate didn't trigger track right away.. 75 | spyReport({ 76 | object: this, 77 | type: "scheduled-reaction" 78 | }); 79 | } 80 | } 81 | } 82 | } 83 | 84 | track(fn: () => void) { 85 | startBatch(); 86 | const notify = isSpyEnabled(); 87 | let startTime; 88 | if (notify) { 89 | startTime = Date.now(); 90 | spyReportStart({ 91 | object: this, 92 | type: "reaction", 93 | fn 94 | }); 95 | } 96 | this._isRunning = true; 97 | trackDerivedFunction(this, fn); 98 | this._isRunning = false; 99 | this._isTrackPending = false; 100 | if (this.isDisposed) { 101 | // disposed during last run. Clean up everything that was bound after the dispose call. 102 | clearObserving(this); 103 | } 104 | if (notify) { 105 | spyReportEnd({ 106 | time: Date.now() - startTime 107 | }); 108 | } 109 | endBatch(); 110 | } 111 | 112 | recoverFromError() { 113 | this._isRunning = false; 114 | this._isTrackPending = false; 115 | } 116 | 117 | dispose() { 118 | if (!this.isDisposed) { 119 | this.isDisposed = true; 120 | if (!this._isRunning) { 121 | startBatch(); 122 | clearObserving(this); // if disposed while running, clean up later. Maybe not optimal, but rare case 123 | endBatch(); 124 | } 125 | } 126 | } 127 | 128 | getDisposer(): Lambda & { $mosbservable: Reaction } { 129 | const r = this.dispose.bind(this); 130 | r.$mobx = this; 131 | return r; 132 | } 133 | 134 | toString() { 135 | return `Reaction[${this.name}]`; 136 | } 137 | 138 | whyRun() { 139 | const observing = unique(this._isRunning ? this.newObserving : this.observing).map(dep => dep.name); 140 | 141 | return (` 142 | WhyRun? reaction '${this.name}': 143 | * Status: [${this.isDisposed ? "stopped" : this._isRunning ? "running" : this.isScheduled() ? "scheduled" : "idle"}] 144 | * This reaction will re-run if any of the following observables changes: 145 | ${joinStrings(observing)} 146 | ${(this._isRunning) ? " (... or any observable accessed during the remainder of the current run)" : ""} 147 | Missing items in this list? 148 | 1. Check whether all used values are properly marked as observable (use isObservable to verify) 149 | 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. 150 | ` 151 | ); 152 | } 153 | } 154 | 155 | /** 156 | * Magic number alert! 157 | * Defines within how many times a reaction is allowed to re-trigger itself 158 | * until it is assumed that this is gonna be a never ending loop... 159 | */ 160 | const MAX_REACTION_ITERATIONS = 100; 161 | 162 | export function runReactions() { 163 | // invariant(globalState.inBatch > 0, "INTERNAL ERROR runReactions should be called only inside batch"); 164 | if (globalState.isRunningReactions === true || globalState.inTransaction > 0) 165 | return; 166 | globalState.isRunningReactions = true; 167 | const allReactions = globalState.pendingReactions; 168 | let iterations = 0; 169 | 170 | // While running reactions, new reactions might be triggered. 171 | // Hence we work with two variables and check whether 172 | // we converge to no remaining reactions after a while. 173 | while (allReactions.length > 0) { 174 | if (++iterations === MAX_REACTION_ITERATIONS) { 175 | resetGlobalState(); 176 | throw new Error(`Reaction doesn't converge to a stable state after ${MAX_REACTION_ITERATIONS} iterations.` 177 | + ` Probably there is a cycle in the reactive function: ${allReactions[0]}`); 178 | } 179 | let remainingReactions = allReactions.splice(0); 180 | for (let i = 0, l = remainingReactions.length; i < l; i++) 181 | remainingReactions[i].runReaction(); 182 | } 183 | globalState.isRunningReactions = false; 184 | } 185 | 186 | export const isReaction = createInstanceofPredicate("Reaction", Reaction); 187 | -------------------------------------------------------------------------------- /test/tape.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for tape v4.2.2 2 | // Project: https://github.com/substack/tape 3 | // Definitions by: Bart van der Schoor , Haoqun Jiang 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | declare module 'tape' { 7 | export = tape; 8 | 9 | /** 10 | * Create a new test with an optional name string and optional opts object. 11 | * cb(t) fires with the new test object t once all preceeding tests have finished. 12 | * Tests execute serially. 13 | */ 14 | function tape(name: string, cb: tape.TestCase): void; 15 | function tape(name: string, opts: tape.TestOptions, cb: tape.TestCase): void; 16 | function tape(cb: tape.TestCase): void; 17 | function tape(opts: tape.TestOptions, cb: tape.TestCase): void; 18 | 19 | module tape { 20 | 21 | interface TestCase { 22 | (test: Test): void; 23 | } 24 | 25 | /** 26 | * Available opts options for the tape function. 27 | */ 28 | interface TestOptions { 29 | skip?: boolean; // See tape.skip. 30 | timeout?: number; // Set a timeout for the test, after which it will fail. See tape.timeoutAfter. 31 | } 32 | 33 | /** 34 | * Options for the createStream function. 35 | */ 36 | interface StreamOptions { 37 | objectMode?: boolean; 38 | } 39 | 40 | /** 41 | * Generate a new test that will be skipped over. 42 | */ 43 | export function skip(name: string, cb: tape.TestCase): void; 44 | 45 | /** 46 | * Like test(name, cb) except if you use .only this is the only test case that will run for the entire process, all other test cases using tape will be ignored. 47 | */ 48 | export function only(name: string, cb: tape.TestCase): void; 49 | 50 | /** 51 | * Create a new test harness instance, which is a function like test(), but with a new pending stack and test state. 52 | */ 53 | export function createHarness(): typeof tape; 54 | /** 55 | * Create a stream of output, bypassing the default output stream that writes messages to console.log(). 56 | * By default stream will be a text stream of TAP output, but you can get an object stream instead by setting opts.objectMode to true. 57 | */ 58 | export function createStream(opts?: tape.StreamOptions): any; 59 | 60 | interface Test { 61 | /** 62 | * Create a subtest with a new test handle st from cb(st) inside the current test. 63 | * cb(st) will only fire when t finishes. 64 | * Additional tests queued up after t will not be run until all subtests finish. 65 | */ 66 | test(name: string, cb: tape.TestCase): void; 67 | 68 | /** 69 | * Declare that n assertions should be run. end() will be called automatically after the nth assertion. 70 | * If there are any more assertions after the nth, or after end() is called, they will generate errors. 71 | */ 72 | plan(n: number): void; 73 | 74 | /** 75 | * Declare the end of a test explicitly. 76 | * If err is passed in t.end will assert that it is falsey. 77 | */ 78 | end(err?: any): void; 79 | 80 | /** 81 | * Generate a failing assertion with a message msg. 82 | */ 83 | fail(msg?: string): void; 84 | 85 | /** 86 | * Generate a passing assertion with a message msg. 87 | */ 88 | pass(msg?: string): void; 89 | 90 | /** 91 | * Automatically timeout the test after X ms. 92 | */ 93 | timeoutAfter(ms: number): void; 94 | 95 | /** 96 | * Generate an assertion that will be skipped over. 97 | */ 98 | skip(msg?: string): void; 99 | 100 | /** 101 | * Assert that value is truthy with an optional description message msg. 102 | */ 103 | ok(value: any, msg?: string): void; 104 | true(value: any, msg?: string): void; 105 | assert(value: any, msg?: string): void; 106 | 107 | /** 108 | * Assert that value is falsy with an optional description message msg. 109 | */ 110 | notOk(value: any, msg?: string): void; 111 | false(value: any, msg?: string): void; 112 | notok(value: any, msg?: string): void; 113 | 114 | /** 115 | * Assert that err is falsy. 116 | * If err is non-falsy, use its err.message as the description message. 117 | */ 118 | error(err: any, msg?: string): void; 119 | ifError(err: any, msg?: string): void; 120 | ifErr(err: any, msg?: string): void; 121 | iferror(err: any, msg?: string): void; 122 | 123 | /** 124 | * Assert that a === b with an optional description msg. 125 | */ 126 | equal(a: any, b: any, msg?: string): void; 127 | equals(a: any, b: any, msg?: string): void; 128 | isEqual(a: any, b: any, msg?: string): void; 129 | is(a: any, b: any, msg?: string): void; 130 | strictEqual(a: any, b: any, msg?: string): void; 131 | strictEquals(a: any, b: any, msg?: string): void; 132 | 133 | /** 134 | * Assert that a !== b with an optional description msg. 135 | */ 136 | notEqual(a: any, b: any, msg?: string): void; 137 | notEquals(a: any, b: any, msg?: string): void; 138 | notStrictEqual(a: any, b: any, msg?: string): void; 139 | notStrictEquals(a: any, b: any, msg?: string): void; 140 | isNotEqual(a: any, b: any, msg?: string): void; 141 | isNot(a: any, b: any, msg?: string): void; 142 | not(a: any, b: any, msg?: string): void; 143 | doesNotEqual(a: any, b: any, msg?: string): void; 144 | isInequal(a: any, b: any, msg?: string): void; 145 | 146 | /** 147 | * Assert that a and b have the same structure and nested values using node's deepEqual() algorithm with strict comparisons (===) on leaf nodes and an optional description msg. 148 | */ 149 | deepEqual(a: any, b: any, msg?: string): void; 150 | deepEquals(a: any, b: any, msg?: string): void; 151 | isEquivalent(a: any, b: any, msg?: string): void; 152 | same(a: any, b: any, msg?: string): void; 153 | 154 | /** 155 | * Assert that a and b do not have the same structure and nested values using node's deepEqual() algorithm with strict comparisons (===) on leaf nodes and an optional description msg. 156 | */ 157 | notDeepEqual(a: any, b: any, msg?: string): void; 158 | notEquivalent(a: any, b: any, msg?: string): void; 159 | notDeeply(a: any, b: any, msg?: string): void; 160 | notSame(a: any, b: any, msg?: string): void; 161 | isNotDeepEqual(a: any, b: any, msg?: string): void; 162 | isNotDeeply(a: any, b: any, msg?: string): void; 163 | isNotEquivalent(a: any, b: any, msg?: string): void; 164 | isInequivalent(a: any, b: any, msg?: string): void; 165 | 166 | /** 167 | * Assert that a and b have the same structure and nested values using node's deepEqual() algorithm with loose comparisons (==) on leaf nodes and an optional description msg. 168 | */ 169 | deepLooseEqual(a: any, b: any, msg?: string): void; 170 | looseEqual(a: any, b: any, msg?: string): void; 171 | looseEquals(a: any, b: any, msg?: string): void; 172 | 173 | /** 174 | * Assert that a and b do not have the same structure and nested values using node's deepEqual() algorithm with loose comparisons (==) on leaf nodes and an optional description msg. 175 | */ 176 | notDeepLooseEqual(a: any, b: any, msg?: string): void; 177 | notLooseEqual(a: any, b: any, msg?: string): void; 178 | notLooseEquals(a: any, b: any, msg?: string): void; 179 | 180 | /** 181 | * Assert that the function call fn() throws an exception. 182 | * expected, if present, must be a RegExp or Function, which is used to test the exception object. 183 | */ 184 | throws(fn: () => void, msg?: string): void; 185 | throws(fn: () => void, exceptionExpected: RegExp | (() => void), msg?: string): void; 186 | 187 | /** 188 | * Assert that the function call fn() does not throw an exception. 189 | */ 190 | doesNotThrow(fn: () => void, msg?: string): void; 191 | doesNotThrow(fn: () => void, exceptionExpected: RegExp | (() => void), msg?: string): void; 192 | 193 | /** 194 | * Print a message without breaking the tap output. 195 | * (Useful when using e.g. tap-colorize where output is buffered & console.log will print in incorrect order vis-a-vis tap output.) 196 | */ 197 | comment(msg: string): void; 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /test/action.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | var test = require('tape'); 4 | var mobx = require('../'); 5 | 6 | test('action should wrap in transaction', t => { 7 | var values = []; 8 | 9 | var observable = mobx.observable(0); 10 | var d = mobx.autorun(() => values.push(observable.get())); 11 | 12 | var increment = mobx.action("increment", (amount) => { 13 | observable.set(observable.get() + amount * 2); 14 | observable.set(observable.get() - amount); // oops 15 | }); 16 | 17 | t.equal(mobx.isAction(increment), true); 18 | t.equal(mobx.isAction(function () {}), false); 19 | 20 | increment(7); 21 | 22 | t.deepEqual(values, [0, 7]); 23 | 24 | t.end(); 25 | }); 26 | 27 | 28 | test('action modifications should be picked up 1', t => { 29 | var a = mobx.observable(1); 30 | var i = 3; 31 | var b = 0; 32 | 33 | mobx.autorun(() => { 34 | b = a.get() * 2; 35 | }); 36 | 37 | t.equal(b, 2); 38 | 39 | var action = mobx.action(() => { 40 | a.set(++i); 41 | }); 42 | 43 | action(); 44 | t.equal(b, 8); 45 | 46 | action(); 47 | t.equal(b, 10); 48 | 49 | t.end(); 50 | }); 51 | 52 | test('action modifications should be picked up 1', t => { 53 | var a = mobx.observable(1); 54 | var b = 0; 55 | 56 | mobx.autorun(() => { 57 | b = a.get() * 2; 58 | }); 59 | 60 | t.equal(b, 2); 61 | 62 | var action = mobx.action(() => { 63 | a.set(a.get() + 1); // ha, no loop! 64 | }); 65 | 66 | action(); 67 | t.equal(b, 4); 68 | 69 | action(); 70 | t.equal(b, 6); 71 | 72 | t.end(); 73 | }); 74 | 75 | test('action modifications should be picked up 3', t => { 76 | var a = mobx.observable(1); 77 | var b = 0; 78 | 79 | var doubler = mobx.computed(() => a.get() * 2); 80 | 81 | doubler.observe(() => { 82 | b = doubler.get(); 83 | }, true); 84 | 85 | t.equal(b, 2); 86 | 87 | var action = mobx.action(() => { 88 | a.set(a.get() + 1); // ha, no loop! 89 | }); 90 | 91 | action(); 92 | t.equal(b, 4); 93 | 94 | action(); 95 | t.equal(b, 6); 96 | 97 | t.end(); 98 | }); 99 | 100 | 101 | test('test action should be untracked', t => { 102 | var a = mobx.observable(3); 103 | var b = mobx.observable(4); 104 | var latest = 0; 105 | var runs = 0; 106 | 107 | var action = mobx.action((baseValue) => { 108 | b.set(baseValue * 2); 109 | latest = b.get(); // without action this would trigger loop 110 | }); 111 | 112 | var d = mobx.autorun(() => { 113 | runs++; 114 | var current = a.get(); 115 | action(current); 116 | }); 117 | 118 | t.equal(b.get(), 6); 119 | t.equal(latest, 6); 120 | 121 | a.set(7); 122 | t.equal(b.get(), 14); 123 | t.equal(latest, 14); 124 | 125 | a.set(8); 126 | t.equal(b.get(), 16); 127 | t.equal(latest, 16); 128 | 129 | b.set(7); // should have no effect 130 | t.equal(a.get(), 8) 131 | t.equal(b.get(), 7); 132 | t.equal(latest, 16); // effect not triggered 133 | 134 | a.set(3); 135 | t.equal(b.get(), 6); 136 | t.equal(latest, 6); 137 | 138 | t.equal(runs, 4); 139 | 140 | d(); 141 | t.end(); 142 | }); 143 | 144 | test('should be possible to create autorun in ation', t => { 145 | var a = mobx.observable(1); 146 | var values = []; 147 | 148 | var adder = mobx.action(inc => { 149 | return mobx.autorun(() => { 150 | values.push(a.get() + inc); 151 | }) 152 | }); 153 | 154 | var d1 = adder(2); 155 | a.set(3); 156 | var d2 = adder(17); 157 | a.set(24); 158 | d1(); 159 | a.set(11); 160 | d2(); 161 | a.set(100); 162 | 163 | t.deepEqual(values, [ 164 | 3, 165 | 5, 166 | 20, 167 | 41, 168 | 26, 169 | 28 170 | ]); 171 | t.end(); 172 | }) 173 | 174 | test('should not be possible to invoke action in a computed block', t => { 175 | var a = mobx.observable(2); 176 | 177 | var noopAction = mobx.action(() => {}); 178 | 179 | var c = mobx.computed(() => { 180 | noopAction(); 181 | return a.get(); 182 | }); 183 | 184 | t.throws(() => { 185 | mobx.autorun(() => c.get()); 186 | }, /Computed values or transformers should not invoke actions or trigger other side effects/, 'expected throw'); 187 | t.end(); 188 | }); 189 | 190 | test('action in autorun should be untracked', t => { 191 | var a = mobx.observable(2); 192 | var b = mobx.observable(3); 193 | 194 | var data = []; 195 | var multiplier = mobx.action(val => val * b.get()); 196 | 197 | var d = mobx.autorun(() => { 198 | data.push(multiplier(a.get())); 199 | }); 200 | 201 | a.set(3); 202 | b.set(4); 203 | a.set(5); 204 | 205 | d(); 206 | 207 | a.set(6); 208 | 209 | t.deepEqual(data, [ 210 | 6, 9, 20 211 | ]); 212 | 213 | t.end(); 214 | }) 215 | 216 | test('action should not be converted to computed when using (extend)observable', t => { 217 | var a = mobx.observable({ 218 | a: 1, 219 | b: mobx.action(function() { 220 | this.a++; 221 | }) 222 | }) 223 | 224 | t.equal(mobx.isAction(a.b), true); 225 | a.b(); 226 | t.equal(a.a, 2); 227 | 228 | mobx.extendObservable(a, { 229 | c: mobx.action(function() { 230 | this.a *= 3; 231 | }) 232 | }); 233 | 234 | t.equal(mobx.isAction(a.c), true); 235 | a.c(); 236 | t.equal(a.a, 6); 237 | 238 | t.end(); 239 | }) 240 | 241 | test('#286 exceptions in actions should not affect global state', t => { 242 | var autorunTimes = 0; 243 | function Todos() { 244 | mobx.extendObservable(this, { 245 | count: 0, 246 | add: mobx.action(function() { 247 | this.count++; 248 | if (this.count === 2) { 249 | throw new Error('An Action Error!'); 250 | } 251 | }) 252 | }) 253 | } 254 | const todo = new Todos; 255 | mobx.autorun(() => { 256 | autorunTimes++; 257 | return todo.count; 258 | }); 259 | try { 260 | todo.add(); 261 | t.equal(autorunTimes, 2); 262 | todo.add(); 263 | } catch (e) { 264 | t.equal(autorunTimes, 3); 265 | todo.add(); 266 | t.equal(autorunTimes, 4); 267 | } 268 | t.end(); 269 | }) 270 | 271 | test('runInAction', t => { 272 | mobx.useStrict(true); 273 | var values = []; 274 | var events = []; 275 | var spyDisposer = mobx.spy(ev => { 276 | if (ev.type === 'action') events.push({ 277 | name: ev.name, 278 | arguments: ev.arguments 279 | }) 280 | }); 281 | 282 | var observable = mobx.observable(0); 283 | var d = mobx.autorun(() => values.push(observable.get())); 284 | 285 | var res = mobx.runInAction("increment", () => { 286 | observable.set(observable.get() + 6 * 2); 287 | observable.set(observable.get() - 3); // oops 288 | return 2; 289 | }); 290 | 291 | t.equal(res, 2); 292 | t.deepEqual(values, [0, 9]); 293 | 294 | res = mobx.runInAction(() => { 295 | observable.set(observable.get() + 5 * 2); 296 | observable.set(observable.get() - 4); // oops 297 | return 3; 298 | }); 299 | 300 | t.equal(res, 3); 301 | t.deepEqual(values, [0, 9, 15]); 302 | t.deepEqual(events, [ 303 | { arguments: [], name: 'increment' }, 304 | { arguments: [], name: '' } 305 | ]); 306 | 307 | mobx.useStrict(false); 308 | spyDisposer(); 309 | 310 | d(); 311 | t.end(); 312 | }) 313 | 314 | test('action in autorun does not keep / make computed values alive', t => { 315 | let calls = 0 316 | const myComputed = mobx.computed(() => calls++) 317 | const callComputedTwice = () => { 318 | myComputed.get() 319 | myComputed.get() 320 | } 321 | 322 | const runWithMemoizing = fun => { mobx.autorun(fun)() } 323 | 324 | callComputedTwice() 325 | t.equal(calls, 2) 326 | 327 | runWithMemoizing(callComputedTwice) 328 | t.equal(calls, 3) 329 | 330 | callComputedTwice() 331 | t.equal(calls, 5) 332 | 333 | runWithMemoizing(function() { 334 | mobx.runInAction(callComputedTwice) 335 | }) 336 | t.equal(calls, 6) 337 | 338 | callComputedTwice() 339 | t.equal(calls, 8) 340 | 341 | t.end() 342 | }) 343 | 344 | test('computed values and actions', t => { 345 | let calls = 0 346 | 347 | const number = mobx.observable(1) 348 | const squared = mobx.computed(() => { 349 | calls++ 350 | return number.get() * number.get() 351 | }) 352 | const changeNumber10Times = mobx.action(() => { 353 | squared.get() 354 | squared.get() 355 | for (let i = 0; i < 10; i++) 356 | number.set(number.get() + 1) 357 | }) 358 | 359 | changeNumber10Times() 360 | t.equal(calls, 1) 361 | 362 | mobx.autorun(() => { 363 | changeNumber10Times() 364 | t.equal(calls, 2) 365 | })() 366 | t.equal(calls, 2) 367 | 368 | changeNumber10Times() 369 | t.equal(calls, 3) 370 | 371 | t.end() 372 | }) 373 | -------------------------------------------------------------------------------- /src/api/autorun.ts: -------------------------------------------------------------------------------- 1 | import {Lambda, getNextId, deprecated, invariant, valueDidChange} from "../utils/utils"; 2 | import {assertUnwrapped, ValueMode, getValueModeFromValue} from "../types/modifiers"; 3 | import {Reaction, IReactionPublic} from "../core/reaction"; 4 | import {untrackedStart, untrackedEnd} from "../core/derivation"; 5 | import {action, isAction} from "../api/action"; 6 | 7 | /** 8 | * Creates a reactive view and keeps it alive, so that the view is always 9 | * updated if one of the dependencies changes, even when the view is not further used by something else. 10 | * @param view The reactive view 11 | * @param scope (optional) 12 | * @returns disposer function, which can be used to stop the view from being updated in the future. 13 | */ 14 | export function autorun(view: (r: IReactionPublic) => void, scope?: any); 15 | 16 | /** 17 | * Creates a named reactive view and keeps it alive, so that the view is always 18 | * updated if one of the dependencies changes, even when the view is not further used by something else. 19 | * @param name The view name 20 | * @param view The reactive view 21 | * @param scope (optional) 22 | * @returns disposer function, which can be used to stop the view from being updated in the future. 23 | */ 24 | export function autorun(name: string, view: (r: IReactionPublic) => void, scope?: any); 25 | 26 | export function autorun(arg1: any, arg2: any, arg3?: any) { 27 | let name: string, view: (r: IReactionPublic) => void, scope: any; 28 | if (typeof arg1 === "string") { 29 | name = arg1; 30 | view = arg2; 31 | scope = arg3; 32 | } else if (typeof arg1 === "function") { 33 | name = arg1.name || ("Autorun@" + getNextId()); 34 | view = arg1; 35 | scope = arg2; 36 | } 37 | 38 | assertUnwrapped(view, "autorun methods cannot have modifiers"); 39 | invariant(typeof view === "function", "autorun expects a function"); 40 | invariant( 41 | isAction(view) === false, 42 | "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." 43 | ); 44 | if (scope) 45 | view = view.bind(scope); 46 | 47 | const reaction = new Reaction(name, function () { 48 | this.track(reactionRunner); 49 | }); 50 | 51 | function reactionRunner() { 52 | view(reaction); 53 | } 54 | 55 | reaction.schedule(); 56 | 57 | return reaction.getDisposer(); 58 | } 59 | 60 | /** 61 | * Similar to 'observer', observes the given predicate until it returns true. 62 | * Once it returns true, the 'effect' function is invoked an the observation is cancelled. 63 | * @param name 64 | * @param predicate 65 | * @param effect 66 | * @param scope (optional) 67 | * @returns disposer function to prematurely end the observer. 68 | */ 69 | export function when(name: string, predicate: () => boolean, effect: Lambda, scope?: any); 70 | 71 | /** 72 | * Similar to 'observer', observes the given predicate until it returns true. 73 | * Once it returns true, the 'effect' function is invoked an the observation is cancelled. 74 | * @param predicate 75 | * @param effect 76 | * @param scope (optional) 77 | * @returns disposer function to prematurely end the observer. 78 | */ 79 | export function when(predicate: () => boolean, effect: Lambda, scope?: any); 80 | 81 | export function when(arg1: any, arg2: any, arg3?: any, arg4?: any) { 82 | let name: string, predicate: () => boolean, effect: Lambda, scope: any; 83 | if (typeof arg1 === "string") { 84 | name = arg1; 85 | predicate = arg2; 86 | effect = arg3; 87 | scope = arg4; 88 | } else if (typeof arg1 === "function") { 89 | name = ("When@" + getNextId()); 90 | predicate = arg1; 91 | effect = arg2; 92 | scope = arg3; 93 | } 94 | 95 | const disposer = autorun(name, r => { 96 | if (predicate.call(scope)) { 97 | r.dispose(); 98 | const prevUntracked = untrackedStart(); 99 | effect.call(scope); 100 | untrackedEnd(prevUntracked); 101 | } 102 | }); 103 | return disposer; 104 | } 105 | 106 | export function autorunUntil(predicate: () => boolean, effect: (r: IReactionPublic) => void, scope?: any) { 107 | deprecated("`autorunUntil` is deprecated, please use `when`."); 108 | return when.apply(null, arguments); 109 | } 110 | 111 | export function autorunAsync(name: string, func: (r: IReactionPublic) => void, delay?: number, scope?: any); 112 | 113 | export function autorunAsync(func: (r: IReactionPublic) => void, delay?: number, scope?: any); 114 | 115 | export function autorunAsync(arg1: any, arg2: any, arg3?: any, arg4?: any) { 116 | let name: string, func: (r: IReactionPublic) => void, delay: number, scope: any; 117 | if (typeof arg1 === "string") { 118 | name = arg1; 119 | func = arg2; 120 | delay = arg3; 121 | scope = arg4; 122 | } else if (typeof arg1 === "function") { 123 | name = arg1.name || ("AutorunAsync@" + getNextId()); 124 | func = arg1; 125 | delay = arg2; 126 | scope = arg3; 127 | } 128 | invariant( 129 | isAction(func) === false, 130 | "Warning: attempted to pass an action to autorunAsync. Actions are untracked and will not trigger on state changes. Use `reaction` or wrap only your state modification code in an action." 131 | ); 132 | if (delay === void 0) 133 | delay = 1; 134 | 135 | if (scope) 136 | func = func.bind(scope); 137 | 138 | let isScheduled = false; 139 | 140 | const r = new Reaction(name, () => { 141 | if (!isScheduled) { 142 | isScheduled = true; 143 | setTimeout(() => { 144 | isScheduled = false; 145 | if (!r.isDisposed) 146 | r.track(reactionRunner); 147 | }, delay); 148 | } 149 | }); 150 | 151 | function reactionRunner() { func(r); } 152 | 153 | r.schedule(); 154 | return r.getDisposer(); 155 | } 156 | 157 | /** 158 | * 159 | * Basically sugar for computed(expr).observe(action(effect)) 160 | * or 161 | * autorun(() => action(effect)(expr)); 162 | */ 163 | export function reaction(name: string, expression: () => T, effect: (arg: T, r: IReactionPublic) => void, fireImmediately?: boolean, delay?: number, scope?: any); 164 | 165 | /** 166 | * 167 | * Basically sugar for computed(expr).observe(action(effect)) 168 | * or 169 | * autorun(() => action(effect)(expr)); 170 | */ 171 | export function reaction(expression: () => T, effect: (arg: T, r: IReactionPublic) => void, fireImmediately?: boolean, delay?: number, scope?: any); 172 | 173 | export function reaction(arg1: any, arg2: any, arg3: any, arg4?: any, arg5?: any, arg6?: any) { 174 | let name: string, expression: () => T, effect: (arg: T, r: IReactionPublic) => void, fireImmediately: boolean, delay: number, scope: any; 175 | if (typeof arg1 === "string") { 176 | name = arg1; 177 | expression = arg2; 178 | effect = arg3; 179 | fireImmediately = arg4; 180 | delay = arg5; 181 | scope = arg6; 182 | } else { 183 | name = arg1.name || arg2.name || ("Reaction@" + getNextId()); 184 | expression = arg1; 185 | effect = arg2; 186 | fireImmediately = arg3; 187 | delay = arg4; 188 | scope = arg5; 189 | } 190 | 191 | if (fireImmediately === void 0) 192 | fireImmediately = false; 193 | 194 | if (delay === void 0) 195 | delay = 0; 196 | 197 | let [valueMode, unwrappedExpression] = getValueModeFromValue(expression, ValueMode.Reference); 198 | const compareStructural = valueMode === ValueMode.Structure; 199 | 200 | if (scope) { 201 | unwrappedExpression = unwrappedExpression.bind(scope); 202 | effect = action(name, effect.bind(scope)); 203 | } 204 | 205 | let firstTime = true; 206 | let isScheduled = false; 207 | let nextValue = undefined; 208 | 209 | const r = new Reaction(name, () => { 210 | if (delay < 1) { 211 | reactionRunner(); 212 | } else if (!isScheduled) { 213 | isScheduled = true; 214 | setTimeout(() => { 215 | isScheduled = false; 216 | reactionRunner(); 217 | }, delay); 218 | } 219 | }); 220 | 221 | function reactionRunner () { 222 | if (r.isDisposed) 223 | return; 224 | let changed = false; 225 | r.track(() => { 226 | const v = unwrappedExpression(r); 227 | changed = valueDidChange(compareStructural, nextValue, v); 228 | nextValue = v; 229 | }); 230 | if (firstTime && fireImmediately) 231 | effect(nextValue, r); 232 | if (!firstTime && changed === true) 233 | effect(nextValue, r); 234 | if (firstTime) 235 | firstTime = false; 236 | } 237 | 238 | 239 | r.schedule(); 240 | return r.getDisposer(); 241 | } 242 | -------------------------------------------------------------------------------- /src/types/observableobject.ts: -------------------------------------------------------------------------------- 1 | import {ObservableValue, UNCHANGED} from "./observablevalue"; 2 | import {isComputedValue, ComputedValue} from "../core/computedvalue"; 3 | import {isAction} from "../api/action"; 4 | import {ValueMode, getModifier} from "./modifiers"; 5 | import {createInstanceofPredicate, isObject, Lambda, getNextId, invariant, assertPropertyConfigurable, isPlainObject, addHiddenFinalProp} from "../utils/utils"; 6 | import {runLazyInitializers} from "../utils/decorators"; 7 | import {hasInterceptors, IInterceptable, registerInterceptor, interceptChange} from "./intercept-utils"; 8 | import {IListenable, registerListener, hasListeners, notifyListeners} from "./listen-utils"; 9 | import {isSpyEnabled, spyReportStart, spyReportEnd} from "../core/spy"; 10 | 11 | export interface IObservableObject { 12 | "observable-object": IObservableObject; 13 | } 14 | 15 | // In 3.0, change to IObjectDidChange 16 | export interface IObjectChange { 17 | name: string; 18 | object: any; 19 | type: "update" | "add"; 20 | oldValue?: any; 21 | newValue: any; 22 | } 23 | 24 | export interface IObjectWillChange { 25 | object: any; 26 | type: "update" | "add"; 27 | name: string; 28 | newValue: any; 29 | } 30 | 31 | export class ObservableObjectAdministration implements IInterceptable, IListenable { 32 | values: {[key: string]: ObservableValue|ComputedValue} = {}; 33 | changeListeners = null; 34 | interceptors = null; 35 | 36 | constructor(public target: any, public name: string, public mode: ValueMode) { } 37 | 38 | /** 39 | * Observes this object. Triggers for the events 'add', 'update' and 'delete'. 40 | * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe 41 | * for callback details 42 | */ 43 | observe(callback: (changes: IObjectChange) => void, fireImmediately?: boolean): Lambda { 44 | invariant(fireImmediately !== true, "`observe` doesn't support the fire immediately property for observable objects."); 45 | return registerListener(this, callback); 46 | } 47 | 48 | 49 | intercept(handler): Lambda { 50 | return registerInterceptor(this, handler); 51 | } 52 | } 53 | 54 | export interface IIsObservableObject { 55 | $mobx: ObservableObjectAdministration; 56 | } 57 | 58 | export function asObservableObject(target, name: string, mode: ValueMode = ValueMode.Recursive): ObservableObjectAdministration { 59 | if (isObservableObject(target)) 60 | return target.$mobx; 61 | 62 | if (!isPlainObject(target)) 63 | name = target.constructor.name + "@" + getNextId(); 64 | if (!name) 65 | name = "ObservableObject@" + getNextId(); 66 | 67 | const adm = new ObservableObjectAdministration(target, name, mode); 68 | addHiddenFinalProp(target, "$mobx", adm); 69 | return adm; 70 | } 71 | 72 | export function setObservableObjectInstanceProperty(adm: ObservableObjectAdministration, propName: string, descriptor: PropertyDescriptor) { 73 | if (adm.values[propName]) { 74 | invariant("value" in descriptor, "cannot redefine property " + propName); 75 | adm.target[propName] = descriptor.value; // the property setter will make 'value' reactive if needed. 76 | } else { 77 | if ("value" in descriptor) 78 | defineObservableProperty(adm, propName, descriptor.value, true, undefined); 79 | else 80 | defineObservableProperty(adm, propName, descriptor.get, true, descriptor.set); 81 | } 82 | } 83 | 84 | export function defineObservableProperty(adm: ObservableObjectAdministration, propName: string, newValue, asInstanceProperty: boolean, setter) { 85 | if (asInstanceProperty) 86 | assertPropertyConfigurable(adm.target, propName); 87 | 88 | let observable: ComputedValue|ObservableValue; 89 | let name = `${adm.name}.${propName}`; 90 | let isComputed = true; 91 | 92 | if (isComputedValue(newValue)) { 93 | // desugger computed(getter, setter) 94 | // TODO: deprecate this and remove in 3.0, to keep them boxed 95 | // get / set is now the idiomatic syntax for non-boxed computed values 96 | observable = newValue; 97 | newValue.name = name; 98 | if (!newValue.scope) 99 | newValue.scope = adm.target; 100 | } else if (typeof newValue === "function" && newValue.length === 0 && !isAction(newValue)) { 101 | // TODO: add warning in 2.6, see https://github.com/mobxjs/mobx/issues/421 102 | // TODO: remove in 3.0 103 | observable = new ComputedValue(newValue, adm.target, false, name, setter); 104 | } else if (getModifier(newValue) === ValueMode.Structure && typeof newValue.value === "function" && newValue.value.length === 0) { 105 | observable = new ComputedValue(newValue.value, adm.target, true, name, setter); 106 | } else { 107 | isComputed = false; 108 | if (hasInterceptors(adm)) { 109 | const change = interceptChange(adm, { 110 | object: adm.target, 111 | name: propName, 112 | type: "add", 113 | newValue 114 | }); 115 | if (!change) 116 | return; 117 | newValue = change.newValue; 118 | } 119 | observable = new ObservableValue(newValue, adm.mode, name, false); 120 | newValue = (observable as any).value; // observableValue might have changed it 121 | } 122 | 123 | adm.values[propName] = observable; 124 | if (asInstanceProperty) { 125 | Object.defineProperty(adm.target, propName, isComputed ? generateComputedPropConfig(propName) : generateObservablePropConfig(propName)); 126 | } 127 | if (!isComputed) 128 | notifyPropertyAddition(adm, adm.target, propName, newValue); 129 | } 130 | 131 | const observablePropertyConfigs = {}; 132 | const computedPropertyConfigs = {}; 133 | 134 | export function generateObservablePropConfig(propName) { 135 | const config = observablePropertyConfigs[propName]; 136 | if (config) 137 | return config; 138 | return observablePropertyConfigs[propName] = { 139 | configurable: true, 140 | enumerable: true, 141 | get: function() { 142 | return this.$mobx.values[propName].get(); 143 | }, 144 | set: function(v) { 145 | setPropertyValue(this, propName, v); 146 | } 147 | }; 148 | } 149 | 150 | export function generateComputedPropConfig(propName) { 151 | const config = computedPropertyConfigs[propName]; 152 | if (config) 153 | return config; 154 | return computedPropertyConfigs[propName] = { 155 | configurable: true, 156 | enumerable: false, 157 | get: function() { 158 | return this.$mobx.values[propName].get(); 159 | }, 160 | set: function(v) { 161 | return this.$mobx.values[propName].set(v); 162 | } 163 | }; 164 | } 165 | 166 | export function setPropertyValue(instance, name: string, newValue) { 167 | const adm = instance.$mobx; 168 | const observable = adm.values[name]; 169 | 170 | // intercept 171 | if (hasInterceptors(adm)) { 172 | const change = interceptChange(adm, { 173 | type: "update", 174 | object: instance, 175 | name, newValue 176 | }); 177 | if (!change) 178 | return; 179 | newValue = change.newValue; 180 | } 181 | newValue = observable.prepareNewValue(newValue); 182 | 183 | // notify spy & observers 184 | if (newValue !== UNCHANGED) { 185 | const notify = hasListeners(adm); 186 | const notifySpy = isSpyEnabled(); 187 | const change = notify || notifySpy ? { 188 | type: "update", 189 | object: instance, 190 | oldValue: (observable as any).value, 191 | name, newValue 192 | } : null; 193 | 194 | if (notifySpy) 195 | spyReportStart(change); 196 | observable.setNewValue(newValue); 197 | if (notify) 198 | notifyListeners(adm, change); 199 | if (notifySpy) 200 | spyReportEnd(); 201 | } 202 | } 203 | 204 | function notifyPropertyAddition(adm, object, name: string, newValue) { 205 | const notify = hasListeners(adm); 206 | const notifySpy = isSpyEnabled(); 207 | const change = notify || notifySpy ? { 208 | type: "add", 209 | object, name, newValue 210 | } : null; 211 | 212 | if (notifySpy) 213 | spyReportStart(change); 214 | if (notify) 215 | notifyListeners(adm, change); 216 | if (notifySpy) 217 | spyReportEnd(); 218 | } 219 | 220 | 221 | const isObservableObjectAdministration = createInstanceofPredicate("ObservableObjectAdministration", ObservableObjectAdministration); 222 | 223 | export function isObservableObject(thing: T): thing is T & IObservableObject { 224 | if (isObject(thing)) { 225 | // Initializers run lazily when transpiling to babel, so make sure they are run... 226 | runLazyInitializers(thing); 227 | return isObservableObjectAdministration((thing as any).$mobx); 228 | } 229 | return false; 230 | } 231 | -------------------------------------------------------------------------------- /src/core/computedvalue.ts: -------------------------------------------------------------------------------- 1 | import {IObservable, reportObserved, propagateMaybeChanged, propagateChangeConfirmed, startBatch, endBatch, getObservers} from "./observable"; 2 | import {IDerivation, IDerivationState, trackDerivedFunction, clearObserving, untrackedStart, untrackedEnd, shouldCompute, handleExceptionInDerivation} from "./derivation"; 3 | import {globalState} from "./globalstate"; 4 | import {allowStateChangesStart, allowStateChangesEnd, createAction} from "./action"; 5 | import {createInstanceofPredicate, getNextId, valueDidChange, invariant, Lambda, unique, joinStrings} from "../utils/utils"; 6 | import {isSpyEnabled, spyReport} from "../core/spy"; 7 | import {autorun} from "../api/autorun"; 8 | 9 | export interface IComputedValue { 10 | get(): T; 11 | set(value: T): void; 12 | observe(listener: (newValue: T, oldValue: T) => void, fireImmediately?: boolean): Lambda; 13 | } 14 | 15 | /** 16 | * A node in the state dependency root that observes other nodes, and can be observed itself. 17 | * 18 | * ComputedValue will remember result of the computation for duration of a batch, or being observed 19 | * During this time it will recompute only when one of it's direct dependencies changed, 20 | * but only when it is being accessed with `ComputedValue.get()`. 21 | * 22 | * Implementation description: 23 | * 1. First time it's being accessed it will compute and remember result 24 | * give back remembered result until 2. happens 25 | * 2. First time any deep dependency change, propagate POSSIBLY_STALE to all observers, wait for 3. 26 | * 3. When it's being accessed, recompute if any shallow dependency changed. 27 | * if result changed: propagate STALE to all observers, that were POSSIBLY_STALE from the last step. 28 | * go to step 2. either way 29 | * 30 | * If at any point it's outside batch and it isn't observed: reset everything and go to 1. 31 | */ 32 | export class ComputedValue implements IObservable, IComputedValue, IDerivation { 33 | dependenciesState = IDerivationState.NOT_TRACKING; 34 | observing = []; // nodes we are looking at. Our value depends on these nodes 35 | newObserving = null; // during tracking it's an array with new observed observers 36 | 37 | isPendingUnobservation: boolean = false; 38 | observers = []; 39 | observersIndexes = {}; 40 | diffValue = 0; 41 | runId = 0; 42 | lastAccessedBy = 0; 43 | lowestObserverState = IDerivationState.UP_TO_DATE; 44 | unboundDepsCount = 0; 45 | __mapid = "#" + getNextId(); 46 | protected value: T = undefined; 47 | name: string; 48 | isComputing: boolean = false; // to check for cycles 49 | isRunningSetter: boolean = false; // TODO optimize, see: https://reaktor.com/blog/javascript-performance-fundamentals-make-bluebird-fast/ 50 | setter: (value: T) => void; 51 | 52 | /** 53 | * Create a new computed value based on a function expression. 54 | * 55 | * The `name` property is for debug purposes only. 56 | * 57 | * The `compareStructural` property indicates whether the return values should be compared structurally. 58 | * Normally, a computed value will not notify an upstream observer if a newly produced value is strictly equal to the previously produced value. 59 | * However, enabling compareStructural can be convienent if you always produce an new aggregated object and don't want to notify observers if it is structurally the same. 60 | * This is useful for working with vectors, mouse coordinates etc. 61 | */ 62 | constructor(public derivation: () => T, private scope: Object, private compareStructural: boolean, name: string, setter: (v: T) => void) { 63 | this.name = name || "ComputedValue@" + getNextId(); 64 | if (setter) 65 | this.setter = createAction(name + "-setter", setter) as any; 66 | } 67 | 68 | peek() { 69 | this.isComputing = true; 70 | const prevAllowStateChanges = allowStateChangesStart(false); 71 | const res = this.derivation.call(this.scope); 72 | allowStateChangesEnd(prevAllowStateChanges); 73 | this.isComputing = false; 74 | return res; 75 | }; 76 | 77 | peekUntracked() { 78 | let hasError = true; 79 | try { 80 | const res = this.peek(); 81 | hasError = false; 82 | return res; 83 | } finally { 84 | if (hasError) 85 | handleExceptionInDerivation(this); 86 | } 87 | 88 | } 89 | 90 | onBecomeStale() { 91 | propagateMaybeChanged(this); 92 | } 93 | 94 | onBecomeUnobserved() { 95 | invariant(this.dependenciesState !== IDerivationState.NOT_TRACKING, "INTERNAL ERROR only onBecomeUnobserved shouldn't be called twice in a row"); 96 | clearObserving(this); 97 | this.value = undefined; 98 | } 99 | 100 | /** 101 | * Returns the current value of this computed value. 102 | * Will evaluate it's computation first if needed. 103 | */ 104 | public get(): T { 105 | invariant(!this.isComputing, `Cycle detected in computation ${this.name}`, this.derivation); 106 | startBatch(); 107 | if (globalState.inBatch === 1) { // just for small optimization, can be droped for simplicity 108 | // computed called outside of any mobx stuff. batch observing shuold be enough, don't need tracking 109 | // because it will never be called again inside this batch 110 | if (shouldCompute(this)) 111 | this.value = this.peekUntracked(); 112 | } else { 113 | 114 | reportObserved(this); 115 | if (shouldCompute(this)) 116 | if (this.trackAndCompute()) 117 | propagateChangeConfirmed(this); 118 | 119 | } 120 | const result = this.value; 121 | endBatch(); 122 | 123 | return result; 124 | } 125 | 126 | public recoverFromError() { 127 | // this.derivation.call(this.scope) in peek returned error, let's run all cleanups, that would be run 128 | // note that resetGlobalState will run afterwards 129 | this.isComputing = false; 130 | } 131 | 132 | public set(value: T) { 133 | if (this.setter) { 134 | invariant(!this.isRunningSetter, `The setter of computed value '${this.name}' is trying to update itself. Did you intend to update an _observable_ value, instead of the computed property?`); 135 | this.isRunningSetter = true; 136 | try { 137 | this.setter.call(this.scope, value); 138 | } finally { 139 | this.isRunningSetter = false; 140 | } 141 | } 142 | else 143 | invariant(false, `[ComputedValue '${this.name}'] It is not possible to assign a new value to a computed value.`); 144 | } 145 | 146 | private trackAndCompute(): boolean { 147 | if (isSpyEnabled()) { 148 | spyReport({ 149 | object: this, 150 | type: "compute", 151 | fn: this.derivation, 152 | target: this.scope 153 | }); 154 | } 155 | const oldValue = this.value; 156 | const newValue = this.value = trackDerivedFunction(this, this.peek); 157 | return valueDidChange(this.compareStructural, newValue, oldValue); 158 | } 159 | 160 | observe(listener: (newValue: T, oldValue: T) => void, fireImmediately?: boolean): Lambda { 161 | let firstTime = true; 162 | let prevValue = undefined; 163 | return autorun(() => { 164 | let newValue = this.get(); 165 | if (!firstTime || fireImmediately) { 166 | const prevU = untrackedStart(); 167 | listener(newValue, prevValue); 168 | untrackedEnd(prevU); 169 | } 170 | firstTime = false; 171 | prevValue = newValue; 172 | }); 173 | } 174 | 175 | toJSON() { 176 | return this.get(); 177 | } 178 | 179 | toString() { 180 | return `${this.name}[${this.derivation.toString()}]`; 181 | } 182 | 183 | whyRun() { 184 | const isTracking = Boolean(globalState.trackingDerivation); 185 | const observing = unique(this.isComputing ? this.newObserving : this.observing).map((dep: any) => dep.name); 186 | const observers = unique(getObservers(this).map(dep => dep.name)); 187 | // TODO: use issue 188 | // TOOD; expand wiht more states 189 | return (` 190 | WhyRun? computation '${this.name}': 191 | * Running because: ${isTracking ? "[active] the value of this computation is needed by a reaction" : this.isComputing ? "[get] The value of this computed was requested outside a reaction" : "[idle] not running at the moment"} 192 | ` + 193 | (this.dependenciesState === IDerivationState.NOT_TRACKING 194 | ? 195 | ` * This computation is suspended (not in use by any reaction) and won't run automatically. 196 | Didn't expect this computation to be suspended at this point? 197 | 1. Make sure this computation is used by a reaction (reaction, autorun, observer). 198 | 2. Check whether you are using this computation synchronously (in the same stack as they reaction that needs it). 199 | ` 200 | : 201 | ` * This computation will re-run if any of the following observables changes: 202 | ${joinStrings(observing)} 203 | ${(this.isComputing && isTracking) ? " (... or any observable accessed during the remainder of the current run)" : ""} 204 | Missing items in this list? 205 | 1. Check whether all used values are properly marked as observable (use isObservable to verify) 206 | 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. 207 | * If the outcome of this computation changes, the following observers will be re-run: 208 | ${joinStrings(observers)} 209 | ` 210 | ) 211 | ); 212 | } 213 | } 214 | 215 | export const isComputedValue = createInstanceofPredicate("ComputedValue", ComputedValue); 216 | -------------------------------------------------------------------------------- /src/core/observable.ts: -------------------------------------------------------------------------------- 1 | import {IDerivation, IDerivationState} from "./derivation"; 2 | import {globalState} from "./globalstate"; 3 | import {invariant} from "../utils/utils"; 4 | 5 | export interface IDepTreeNode { 6 | name: string; 7 | observing?: IObservable[]; 8 | } 9 | 10 | export interface IObservable extends IDepTreeNode { 11 | diffValue: number; 12 | /** 13 | * Id of the derivation *run* that last accesed this observable. 14 | * If this id equals the *run* id of the current derivation, 15 | * the dependency is already established 16 | */ 17 | lastAccessedBy: number; 18 | 19 | lowestObserverState: IDerivationState; // Used to avoid redundant propagations 20 | isPendingUnobservation: boolean; // Used to push itself to global.pendingUnobservations at most once per batch. 21 | 22 | observers: IDerivation[]; // mantain _observers in raw array for for way faster iterating in propagation. 23 | observersIndexes: {}; // map derivation.__mapid to _observers.indexOf(derivation) (see removeObserver) 24 | 25 | onBecomeUnobserved(); 26 | } 27 | 28 | export function hasObservers(observable: IObservable): boolean { 29 | return observable.observers && observable.observers.length > 0; 30 | } 31 | 32 | export function getObservers(observable: IObservable): IDerivation[] { 33 | return observable.observers; 34 | } 35 | 36 | function invariantObservers(observable: IObservable) { 37 | const list = observable.observers; 38 | const map = observable.observersIndexes; 39 | const l = list.length; 40 | for (let i = 0; i < l; i++) { 41 | const id = list[i].__mapid; 42 | if (i) { 43 | invariant(map[id] === i, "INTERNAL ERROR maps derivation.__mapid to index in list"); // for performance 44 | } else { 45 | invariant(!(id in map), "INTERNAL ERROR observer on index 0 shouldnt be held in map."); // for performance 46 | } 47 | } 48 | invariant(list.length === 0 || Object.keys(map).length === list.length - 1, "INTERNAL ERROR there is no junk in map"); 49 | } 50 | export function addObserver(observable: IObservable, node: IDerivation) { 51 | // invariant(node.dependenciesState !== -1, "INTERNAL ERROR, can add only dependenciesState !== -1"); 52 | // invariant(observable._observers.indexOf(node) === -1, "INTERNAL ERROR add already added node"); 53 | // invariantObservers(observable); 54 | 55 | const l = observable.observers.length; 56 | if (l) { // because object assignment is relatively expensive, let's not store data about index 0. 57 | observable.observersIndexes[node.__mapid] = l; 58 | } 59 | observable.observers[l] = node; 60 | 61 | if (observable.lowestObserverState > node.dependenciesState) observable.lowestObserverState = node.dependenciesState; 62 | 63 | // invariantObservers(observable); 64 | // invariant(observable._observers.indexOf(node) !== -1, "INTERNAL ERROR didnt add node"); 65 | } 66 | 67 | export function removeObserver(observable: IObservable, node: IDerivation) { 68 | // invariant(globalState.inBatch > 0, "INTERNAL ERROR, remove should be called only inside batch"); 69 | // invariant(observable._observers.indexOf(node) !== -1, "INTERNAL ERROR remove already removed node"); 70 | // invariantObservers(observable); 71 | 72 | if (observable.observers.length === 1) { 73 | // deleting last observer 74 | observable.observers.length = 0; 75 | 76 | queueForUnobservation(observable); 77 | } else { 78 | // deleting from _observersIndexes is straight forward, to delete from _observers, let's swap `node` with last element 79 | const list = observable.observers; 80 | const map = observable.observersIndexes; 81 | const filler = list.pop(); // get last element, which should fill the place of `node`, so the array doesnt have holes 82 | if (filler !== node) { // otherwise node was the last element, which already got removed from array 83 | const index = map[node.__mapid] || 0; // getting index of `node`. this is the only place we actually use map. 84 | if (index) { // map store all indexes but 0, see comment in `addObserver` 85 | map[filler.__mapid] = index; 86 | } else { 87 | delete map[filler.__mapid]; 88 | } 89 | list[index] = filler; 90 | } 91 | delete map[node.__mapid]; 92 | } 93 | // invariantObservers(observable); 94 | // invariant(observable._observers.indexOf(node) === -1, "INTERNAL ERROR remove already removed node2"); 95 | } 96 | 97 | export function queueForUnobservation(observable: IObservable) { 98 | if (!observable.isPendingUnobservation) { 99 | // invariant(globalState.inBatch > 0, "INTERNAL ERROR, remove should be called only inside batch"); 100 | // invariant(observable._observers.length === 0, "INTERNAL ERROR, shuold only queue for unobservation unobserved observables"); 101 | observable.isPendingUnobservation = true; 102 | globalState.pendingUnobservations.push(observable); 103 | } 104 | } 105 | 106 | /** 107 | * Batch is a pseudotransaction, just for purposes of memoizing ComputedValues when nothing else does. 108 | * During a batch `onBecomeUnobserved` will be called at most once per observable. 109 | * Avoids unnecessary recalculations. 110 | */ 111 | export function startBatch() { 112 | globalState.inBatch++; 113 | } 114 | 115 | export function endBatch() { 116 | if (globalState.inBatch === 1) { 117 | // the batch is actually about to finish, all unobserving should happen here. 118 | const list = globalState.pendingUnobservations; 119 | for (let i = 0; i < list.length; i++) { 120 | const observable = list[i]; 121 | observable.isPendingUnobservation = false; 122 | if (observable.observers.length === 0) { 123 | observable.onBecomeUnobserved(); 124 | // NOTE: onBecomeUnobserved might push to `pendingUnobservations` 125 | } 126 | } 127 | globalState.pendingUnobservations = []; 128 | } 129 | globalState.inBatch--; 130 | } 131 | 132 | export function reportObserved(observable: IObservable) { 133 | const derivation = globalState.trackingDerivation; 134 | if (derivation !== null) { 135 | /** 136 | * Simple optimization, give each derivation run an unique id (runId) 137 | * Check if last time this observable was accessed the same runId is used 138 | * if this is the case, the relation is already known 139 | */ 140 | if (derivation.runId !== observable.lastAccessedBy) { 141 | observable.lastAccessedBy = derivation.runId; 142 | derivation.newObserving[derivation.unboundDepsCount++] = observable; 143 | } 144 | } else if (observable.observers.length === 0) { 145 | queueForUnobservation(observable); 146 | } 147 | } 148 | 149 | function invariantLOS(observable: IObservable, msg) { 150 | // it's expensive so better not run it in produciton. but temporarily helpful for testing 151 | const min = getObservers(observable).reduce( 152 | (a, b) => Math.min(a, b.dependenciesState), 153 | 2 154 | ); 155 | if (min >= observable.lowestObserverState) return; // <- the only assumption about `lowestObserverState` 156 | throw new Error("lowestObserverState is wrong for " + msg + " because " + min + " < " + observable.lowestObserverState); 157 | } 158 | 159 | /** 160 | * NOTE: current propagation mechanism will in case of self reruning autoruns behave unexpectedly 161 | * It will propagate changes to observers from previous run 162 | * It's hard or maybe inpossible (with reasonable perf) to get it right with current approach 163 | * Hopefully self reruning autoruns aren't a feature people shuold depend on 164 | * Also most basic use cases shuold be ok 165 | * TODO: create description of autorun behaviour or change this behaviour? 166 | */ 167 | 168 | // Called by Atom when it's value changes 169 | export function propagateChanged(observable: IObservable) { 170 | // invariantLOS(observable, "changed start"); 171 | if (observable.lowestObserverState === IDerivationState.STALE) return; 172 | observable.lowestObserverState = IDerivationState.STALE; 173 | 174 | const observers = observable.observers; 175 | let i = observers.length; 176 | while (i--) { 177 | const d = observers[i]; 178 | if (d.dependenciesState === IDerivationState.UP_TO_DATE) 179 | d.onBecomeStale(); 180 | d.dependenciesState = IDerivationState.STALE; 181 | } 182 | // invariantLOS(observable, "changed end"); 183 | } 184 | 185 | // Called by ComputedValue when it recalculate and it's value changed 186 | export function propagateChangeConfirmed(observable: IObservable) { 187 | // invariantLOS(observable, "confirmed start"); 188 | if (observable.lowestObserverState === IDerivationState.STALE) return; 189 | observable.lowestObserverState = IDerivationState.STALE; 190 | 191 | const observers = observable.observers; 192 | let i = observers.length; 193 | while (i--) { 194 | const d = observers[i]; 195 | if (d.dependenciesState === IDerivationState.POSSIBLY_STALE) 196 | d.dependenciesState = IDerivationState.STALE; 197 | else if (d.dependenciesState === IDerivationState.UP_TO_DATE) // this happens during computing of `d`, just keep lowestObserverState up to date. 198 | observable.lowestObserverState = IDerivationState.UP_TO_DATE; 199 | } 200 | // invariantLOS(observable, "confirmed end"); 201 | } 202 | 203 | // Used by computed when it's dependency changed, but we don't wan't to immidiately recompute. 204 | export function propagateMaybeChanged(observable: IObservable) { 205 | // invariantLOS(observable, "maybe start"); 206 | if (observable.lowestObserverState !== IDerivationState.UP_TO_DATE) return; 207 | observable.lowestObserverState = IDerivationState.POSSIBLY_STALE; 208 | 209 | const observers = observable.observers; 210 | let i = observers.length; 211 | while (i--) { 212 | const d = observers[i]; 213 | if (d.dependenciesState === IDerivationState.UP_TO_DATE) { 214 | d.dependenciesState = IDerivationState.POSSIBLY_STALE; 215 | d.onBecomeStale(); 216 | } 217 | } 218 | // invariantLOS(observable, "maybe end"); 219 | } 220 | -------------------------------------------------------------------------------- /test/perf/transform-perf.js: -------------------------------------------------------------------------------- 1 | var m = require('../../'); 2 | var test = require('tape'); 3 | var log = require('./index.js').logMeasurement; 4 | 5 | function gc() { 6 | if (typeof global.gc === "function") 7 | global.gc(); 8 | } 9 | 10 | /** 11 | * This file compares creating a project tree view using a reactive graph transformation 12 | * against the same in a plain js implementation (the baseline). 13 | * Surely the plain version could be optimized further, 14 | * but even in this comparison the plain version is already significantly more complex than the reactive version, 15 | * yet twice as slow. 16 | */ 17 | 18 | function measure(title, func) { 19 | var start = Date.now(); 20 | var res = func(); 21 | log(title + " " + (Date.now() - start) + "ms."); 22 | return res; 23 | } 24 | 25 | function flatten() { 26 | var res = []; 27 | for(var i = 0, l = arguments.length; i < l; i++) 28 | for(var j = 0, l2 = arguments[i].length; j < l2; j++) 29 | res.push(arguments[i][j]); 30 | return res; 31 | } 32 | 33 | test('non-reactive folder tree', function(t) { 34 | gc(); 35 | function Folder(parent, name) { 36 | this.parent = parent; 37 | this.name = "" + name; 38 | this.children = []; 39 | } 40 | 41 | function DisplayFolder(folder, state) { 42 | this.state = state; 43 | this.folder = folder; 44 | this.collapsed = false; 45 | this._children = folder.children.map(transformFolder); 46 | } 47 | 48 | Object.defineProperties(DisplayFolder.prototype, { 49 | name: { get: function() { 50 | return this.folder.name; 51 | } }, 52 | isVisible: { get: function() { 53 | return !this.state.filter || this.name.indexOf(this.state.filter) !== -1 || this.children.some(child => child.isVisible); 54 | } }, 55 | children: { get: function() { 56 | if (this.collapsed) 57 | return []; 58 | return this._children.filter(function(child) { 59 | return child.isVisible; 60 | }); 61 | } }, 62 | path: { get: function() { 63 | return this.folder.parent === null ? this.name : transformFolder(this.folder.parent).path + "/" + this.name; 64 | } } 65 | }); 66 | 67 | var state = { 68 | root: new Folder(null, "root"), 69 | filter: null, 70 | displayRoot: null 71 | }; 72 | 73 | var transformFolder = function (folder) { 74 | return new DisplayFolder(folder, state); 75 | }; 76 | 77 | // returns list of strings per folder 78 | DisplayFolder.prototype.rebuild = function () { 79 | var path = this.path; 80 | this.asText = path + "\n" + 81 | this.children.filter(function(child) { 82 | return child.isVisible; 83 | }).map(child => child.asText).join(''); 84 | }; 85 | 86 | DisplayFolder.prototype.rebuildAll = function() { 87 | this.children.forEach(child => child.rebuildAll()); 88 | this.rebuild(); 89 | } 90 | 91 | function createFolders(parent, recursion) { 92 | if (recursion === 0) 93 | return; 94 | for (var i = 0; i < 10; i++) { 95 | var folder = new Folder(parent, i); 96 | parent.children.push(folder); 97 | createFolders(folder, recursion - 1); 98 | } 99 | } 100 | 101 | measure("reactive folder tree [total]\n", () => { 102 | measure("create folders", () => createFolders(state.root, 3)); // 10^3 103 | measure("create displayfolders", () => state.displayRoot = transformFolder(state.root)); 104 | measure("create text", () => { 105 | state.displayRoot.rebuildAll(); 106 | state.text = state.displayRoot.asText.split("\n"); 107 | t.equal(state.text.length, 1112); 108 | t.equal(state.text[0], "root"); 109 | t.equal(state.text[state.text.length - 2], "root/9/9/9"); 110 | }); 111 | 112 | measure("collapse folder", () => { 113 | state.displayRoot.children[9].collapsed = true; 114 | state.displayRoot.children[9].rebuild(); 115 | state.displayRoot.rebuild(); 116 | state.text = state.displayRoot.asText.split("\n"); 117 | t.equal(state.text.length, 1002); 118 | t.equal(state.text[state.text.length - 3], "root/8/9/9"); 119 | t.equal(state.text[state.text.length - 2], "root/9"); 120 | }); 121 | 122 | measure("uncollapse folder", () => { 123 | state.displayRoot.children[9].collapsed = false; 124 | state.displayRoot.children[9].rebuild(); 125 | state.displayRoot.rebuild(); 126 | state.text = state.displayRoot.asText.split("\n"); 127 | t.equal(state.text.length, 1112); 128 | t.equal(state.text[state.text.length - 2], "root/9/9/9"); 129 | }); 130 | 131 | measure("change name of folder", () => { 132 | state.root.name = "wow"; 133 | state.displayRoot.rebuildAll(); 134 | state.text = state.displayRoot.asText.split("\n"); 135 | t.equal(state.text.length, 1112); 136 | t.equal(state.text[state.text.length - 2], "wow/9/9/9"); 137 | }); 138 | 139 | measure("search", () => { 140 | state.filter = "8"; 141 | state.displayRoot.rebuildAll(); 142 | state.text = state.displayRoot.asText.split("\n"); 143 | t.deepEqual(state.text.slice(0, 4), [ 'wow', 'wow/0', 'wow/0/0', 'wow/0/0/8' ]); 144 | t.deepEqual(state.text.slice(-5), [ 'wow/9/8', 'wow/9/8/8', 'wow/9/9', 'wow/9/9/8', '' ]); 145 | t.equal(state.text.length, 212); 146 | }); 147 | 148 | measure("unsearch", () => { 149 | state.filter = null; 150 | state.displayRoot.rebuildAll(); 151 | state.text = state.displayRoot.asText.split("\n"); 152 | t.equal(state.text.length, 1112); 153 | }); 154 | }); 155 | 156 | t.end(); 157 | }); 158 | 159 | test('reactive folder tree', function(t) { 160 | gc(); 161 | function Folder(parent, name) { 162 | this.parent = parent; 163 | m.extendObservable(this, { 164 | name: "" + name, 165 | children: m.fastArray(), 166 | }); 167 | } 168 | 169 | function DisplayFolder(folder, state) { 170 | this.state = state; 171 | this.folder = folder; 172 | m.extendObservable(this, { 173 | collapsed: false, 174 | name: function() { 175 | return this.folder.name; 176 | }, 177 | isVisible: function() { 178 | return !this.state.filter || this.name.indexOf(this.state.filter) !== -1 || this.children.some(child => child.isVisible); 179 | }, 180 | children: function() { 181 | if (this.collapsed) 182 | return []; 183 | return this.folder.children.map(transformFolder).filter(function(child) { 184 | return child.isVisible; 185 | }) 186 | }, 187 | path: function() { 188 | return this.folder.parent === null ? this.name : transformFolder(this.folder.parent).path + "/" + this.name; 189 | } 190 | }); 191 | } 192 | 193 | var state = m.observable({ 194 | root: new Folder(null, "root"), 195 | filter: null, 196 | displayRoot: null 197 | }); 198 | 199 | var transformFolder = m.createTransformer(function (folder) { 200 | return new DisplayFolder(folder, state); 201 | }); 202 | 203 | // returns list of strings per folder 204 | var stringTransformer = m.createTransformer(function (displayFolder) { 205 | var path = displayFolder.path; 206 | return path + "\n" + 207 | displayFolder.children.filter(function(child) { 208 | return child.isVisible; 209 | }).map(stringTransformer).join(''); 210 | }); 211 | 212 | function createFolders(parent, recursion) { 213 | if (recursion === 0) 214 | return; 215 | for (var i = 0; i < 10; i++) { 216 | var folder = new Folder(parent, i); 217 | parent.children.push(folder); 218 | createFolders(folder, recursion - 1); 219 | } 220 | } 221 | 222 | measure("reactive folder tree [total]\n", () => { 223 | measure("create folders", () => createFolders(state.root, 3)); // 10^3 224 | measure("create displayfolders", () => state.displayRoot = transformFolder(state.root)); 225 | measure("create text", () => { 226 | m.autorun(function() { 227 | state.text = stringTransformer(state.displayRoot).split("\n"); 228 | }); 229 | t.equal(state.text.length, 1112); 230 | t.equal(state.text[0], "root"); 231 | t.equal(state.text[state.text.length - 2], "root/9/9/9"); 232 | }); 233 | 234 | measure("collapse folder", () => { 235 | state.displayRoot.children[9].collapsed = true; 236 | t.equal(state.text.length, 1002); 237 | t.equal(state.text[state.text.length - 3], "root/8/9/9"); 238 | t.equal(state.text[state.text.length - 2], "root/9"); 239 | }); 240 | 241 | measure("uncollapse folder", () => { 242 | state.displayRoot.children[9].collapsed = false; 243 | t.equal(state.text.length, 1112); 244 | t.equal(state.text[state.text.length - 2], "root/9/9/9"); 245 | }); 246 | 247 | measure("change name of folder", () => { 248 | state.root.name = "wow"; 249 | t.equal(state.text.length, 1112); 250 | t.equal(state.text[state.text.length - 2], "wow/9/9/9"); 251 | }); 252 | 253 | measure("search", () => { 254 | state.filter = "8"; 255 | t.deepEqual(state.text.slice(0, 4), [ 'wow', 'wow/0', 'wow/0/0', 'wow/0/0/8' ]); 256 | t.deepEqual(state.text.slice(-5), [ 'wow/9/8', 'wow/9/8/8', 'wow/9/9', 'wow/9/9/8', '' ]); 257 | t.equal(state.text.length, 212); 258 | }); 259 | 260 | measure("unsearch", () => { 261 | state.filter = null; 262 | t.equal(state.text.length, 1112); 263 | }); 264 | }); 265 | 266 | t.end(); 267 | }); 268 | 269 | var BOX_COUNT = 10000; 270 | var BOX_MUTATIONS = 100; 271 | 272 | test('non-reactive state serialization', function(t) { 273 | gc(); 274 | serializationTester(t, x => x); 275 | }); 276 | 277 | test('reactive state serialization', function(t) { 278 | gc(); 279 | serializationTester(t, m.createTransformer); 280 | }); 281 | 282 | function serializationTester(t, transformerFactory) { 283 | function Box(x, y) { 284 | m.extendObservable(this, { 285 | x: x, 286 | y: y 287 | }); 288 | } 289 | 290 | var boxes = m.fastArray(); 291 | var states = []; 292 | 293 | var serializeBox = transformerFactory(box => { 294 | return { x: box.x, y: box.y }; 295 | }); 296 | 297 | var serializeBoxes = transformerFactory(boxes => { 298 | // NB. would be a lot faster if partitioning or liveMap would be applied..! 299 | return boxes.map(serializeBox); 300 | }) 301 | 302 | measure("total", () => { 303 | measure("create boxes", () => { 304 | for(var i = 0; i < BOX_COUNT; i++) 305 | boxes.push(new Box(i, i)); 306 | }); 307 | 308 | m.autorun(() => { 309 | states.push(serializeBoxes(boxes)); 310 | }); 311 | 312 | measure("mutations", () => { 313 | for(var i = 0; i < BOX_MUTATIONS; i++) 314 | boxes[boxes.length -1 - BOX_MUTATIONS][i % 2 === 0 ? "x" : "y"] = i * 3; 315 | }); 316 | 317 | t.equal(boxes.length, BOX_COUNT); 318 | t.equal(states.length, BOX_MUTATIONS + 1); 319 | t.equal(states[states.length -1].length, BOX_COUNT); 320 | }); 321 | 322 | t.end(); 323 | } -------------------------------------------------------------------------------- /test/array.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var mobx = require('..'); 3 | var observable = mobx.observable; 4 | var iterall = require('iterall'); 5 | 6 | function buffer() { 7 | var b = []; 8 | var res = function(newValue) { 9 | b.push(newValue); 10 | }; 11 | res.toArray = function() { 12 | return b; 13 | } 14 | return res; 15 | } 16 | 17 | test('test1', function(t) { 18 | var a = observable([]); 19 | t.equal(a.length, 0); 20 | t.deepEqual(Object.keys(a), []); 21 | t.deepEqual(a.slice(), []); 22 | 23 | a.push(1); 24 | t.equal(a.length, 1); 25 | t.deepEqual(a.slice(), [1]); 26 | 27 | a[1] = 2; 28 | t.equal(a.length, 2); 29 | t.deepEqual(a.slice(), [1,2]); 30 | 31 | var sum = observable(function() { 32 | return -1 + a.reduce(function(a,b) { 33 | return a + b; 34 | }, 1); 35 | }); 36 | 37 | t.equal(sum.get(), 3); 38 | 39 | a[1] = 3; 40 | t.equal(a.length, 2); 41 | t.deepEqual(a.slice(), [1,3]); 42 | t.equal(sum.get(), 4); 43 | 44 | a.splice(1,1,4,5); 45 | t.equal(a.length, 3); 46 | t.deepEqual(a.slice(), [1,4,5]); 47 | t.equal(sum.get(), 10); 48 | 49 | a.replace([2,4]); 50 | t.equal(sum.get(), 6); 51 | 52 | a.splice(1,1); 53 | t.equal(sum.get(), 2); 54 | t.deepEqual(a.slice(), [2]) 55 | 56 | a.splice(0,0,4,3); 57 | t.equal(sum.get(), 9); 58 | t.deepEqual(a.slice(), [4,3,2]); 59 | 60 | a.clear(); 61 | t.equal(sum.get(), 0); 62 | t.deepEqual(a.slice(), []); 63 | 64 | a.length = 4; 65 | t.equal(isNaN(sum.get()), true); 66 | t.deepEqual(a.length, 4); 67 | 68 | t.deepEqual(a.slice(), [undefined, undefined, undefined, undefined]); 69 | 70 | a.replace([1,2, 2,4]); 71 | t.equal(sum.get(), 9); 72 | a.length = 4; 73 | t.equal(sum.get(), 9); 74 | 75 | 76 | a.length = 2; 77 | t.equal(sum.get(), 3); 78 | t.deepEqual(a.slice(), [1,2]); 79 | 80 | t.deepEqual(a.reverse(), [2,1]); 81 | t.deepEqual(a.slice(), [1,2]); 82 | 83 | a.unshift(3); 84 | t.deepEqual(a.sort(), [1,2,3]); 85 | t.deepEqual(a.slice(), [3,1,2]); 86 | 87 | t.equal(JSON.stringify(a), "[3,1,2]"); 88 | 89 | // t.deepEqual(Object.keys(a), ['0', '1', '2']); // ideally.... 90 | t.deepEqual(Object.keys(a), []); 91 | 92 | t.end(); 93 | }) 94 | 95 | test('array should support iterall / iterable ', t => { 96 | var a = observable([1,2,3]) 97 | 98 | t.equal(iterall.isIterable(a), true); 99 | t.equal(iterall.isArrayLike(a), true); 100 | 101 | var values = []; 102 | iterall.forEach(a, v => values.push(v)) 103 | 104 | t.deepEqual(values, [1,2,3]) 105 | 106 | var iter = iterall.getIterator(a) 107 | t.deepEqual(iter.next(), { value: 1, done: false }) 108 | t.deepEqual(iter.next(), { value: 2, done: false }) 109 | t.deepEqual(iter.next(), { value: 3, done: false }) 110 | t.deepEqual(iter.next(), { value: undefined, done: true }) 111 | 112 | a.replace([]) 113 | iter = iterall.getIterator(a) 114 | t.deepEqual(iter.next(), { value: undefined, done: true }) 115 | 116 | t.end() 117 | }) 118 | 119 | test('find and remove', function(t) { 120 | var a = mobx.observable([10,20,20]); 121 | var idx = -1; 122 | function predicate(item, index) { 123 | if (item === 20) { 124 | idx = index; 125 | return true; 126 | } 127 | return false; 128 | } 129 | 130 | t.equal(a.find(predicate), 20); 131 | t.equal(idx, 1); 132 | t.equal(a.find(predicate, null, 1), 20); 133 | t.equal(idx, 1); 134 | t.equal(a.find(predicate, null, 2), 20); 135 | t.equal(idx, 2); 136 | idx = -1; 137 | t.equal(a.find(predicate, null, 3), undefined); 138 | t.equal(idx, -1); 139 | 140 | t.equal(a.remove(20), true); 141 | t.equal(a.find(predicate), 20); 142 | t.equal(idx, 1); 143 | idx = -1; 144 | t.equal(a.remove(20), true); 145 | t.equal(a.find(predicate), undefined); 146 | t.equal(idx, -1); 147 | 148 | t.equal(a.remove(20), false); 149 | 150 | t.end(); 151 | }) 152 | 153 | test('concat should automatically slice observable arrays, #260', t => { 154 | var a1 = mobx.observable([1,2]) 155 | var a2 = mobx.observable([3,4]) 156 | t.deepEqual(a1.concat(a2), [1,2,3,4]) 157 | t.end() 158 | }) 159 | 160 | test('observe', function(t) { 161 | var ar = mobx.observable([1,4]); 162 | var buf = []; 163 | var disposer = ar.observe(function(changes) { 164 | buf.push(changes); 165 | }, true); 166 | 167 | ar[1] = 3; // 1,3 168 | ar[2] = 0; // 1, 3, 0 169 | ar.shift(); // 3, 0 170 | ar.push(1,2); // 3, 0, 1, 2 171 | ar.splice(1,2,3,4); // 3, 3, 4, 2 172 | t.deepEqual(ar.slice(), [3,3,4,2]); 173 | ar.splice(6); 174 | ar.splice(6,2); 175 | ar.replace(['a']); 176 | ar.pop(); 177 | ar.pop(); // does not fire anything 178 | 179 | // check the object param 180 | buf.forEach(function(change) { 181 | t.equal(change.object, ar); 182 | delete change.object; 183 | }); 184 | 185 | var result = [ 186 | { type: "splice", index: 0, addedCount: 2, removed: [], added: [1, 4], removedCount: 0 }, 187 | { type: "update", index: 1, oldValue: 4, newValue: 3 }, 188 | { type: "splice", index: 2, addedCount: 1, removed: [], added: [0], removedCount: 0 }, 189 | { type: "splice", index: 0, addedCount: 0, removed: [1], added: [], removedCount: 1 }, 190 | { type: "splice", index: 2, addedCount: 2, removed: [], added: [1,2], removedCount: 0 }, 191 | { type: "splice", index: 1, addedCount: 2, removed: [0,1], added: [3, 4], removedCount: 2 }, 192 | { type: "splice", index: 0, addedCount: 1, removed: [3,3,4,2], added:['a'], removedCount: 4 }, 193 | { type: "splice", index: 0, addedCount: 0, removed: ['a'], added: [], removedCount: 1 }, 194 | ] 195 | 196 | t.deepEqual(buf, result); 197 | 198 | disposer(); 199 | ar[0] = 5; 200 | t.deepEqual(buf, result); 201 | 202 | t.end(); 203 | }) 204 | 205 | test('array modification1', function(t) { 206 | var a = mobx.observable([1,2,3]); 207 | var r = a.splice(-10, 5, 4,5,6); 208 | t.deepEqual(a.slice(), [4,5,6]); 209 | t.deepEqual(r, [1,2,3]); 210 | t.end(); 211 | }) 212 | 213 | test('serialize', function(t) { 214 | var a = [1,2,3]; 215 | var m = mobx.observable(a); 216 | 217 | t.deepEqual(JSON.stringify(m), JSON.stringify(a)); 218 | t.deepEqual(a, m.peek()); 219 | 220 | a = [4]; 221 | m.replace(a); 222 | t.deepEqual(JSON.stringify(m), JSON.stringify(a)); 223 | t.deepEqual(a, m.toJSON()); 224 | 225 | t.end(); 226 | }) 227 | 228 | test('array modification functions', function(t) { 229 | var ars = [[], [1,2,3]]; 230 | var funcs = ["push","pop","shift","unshift"]; 231 | funcs.forEach(function(f) { 232 | ars.forEach(function (ar) { 233 | var a = ar.slice(); 234 | var b = mobx.observable(a); 235 | var res1 = a[f](4); 236 | var res2 = b[f](4); 237 | t.deepEqual(res1, res2); 238 | t.deepEqual(a, b.slice()); 239 | }); 240 | }); 241 | t.end(); 242 | }) 243 | 244 | test('array modifications', function(t) { 245 | 246 | var a2 = mobx.fastArray([]); 247 | var inputs = [undefined, -10, -4, -3, -1, 0, 1, 3, 4, 10]; 248 | var arrays = [[], [1], [1,2,3,4], [1,2,3,4,5,6,7,8,9,10,11],[1,undefined],[undefined]] 249 | for (var i = 0; i < inputs.length; i++) 250 | for (var j = 0; j< inputs.length; j++) 251 | for (var k = 0; k < arrays.length; k++) 252 | for (var l = 0; l < arrays.length; l++) { 253 | var msg = ["array mod: [", arrays[k].toString(),"] i: ",inputs[i]," d: ", inputs[j]," [", arrays[l].toString(),"]"].join(' '); 254 | var a1 = arrays[k].slice(); 255 | a2.replace(a1); 256 | var res1 = a1.splice.apply(a1, [inputs[i], inputs[j]].concat(arrays[l])); 257 | var res2 = a2.splice.apply(a2, [inputs[i], inputs[j]].concat(arrays[l])); 258 | t.deepEqual(a1.slice(), a2.slice(), "values wrong: " + msg); 259 | t.deepEqual(res1, res2, "results wrong: " + msg); 260 | t.equal(a1.length, a2.length, "length wrong: " + msg); 261 | } 262 | 263 | t.end(); 264 | }) 265 | 266 | test('new fast array values won\'t be observable', function(t) { 267 | // See: https://mobxjs.github.io/mobx/refguide/fast-array.html#comment-2486090381 268 | var booksA = mobx.fastArray([]); 269 | var rowling = { name: 'J.K.Rowling', birth: 1965 }; 270 | booksA.push(rowling) 271 | t.equal(mobx.isObservable(booksA[0], "name"), false); 272 | var removed = booksA.splice(0, 1); 273 | t.equal(mobx.isObservable(removed[0], "name"), false); 274 | t.end(); 275 | }); 276 | 277 | test('is array', function(t) { 278 | var x = mobx.observable([]); 279 | t.equal(x instanceof Array, true); 280 | 281 | // would be cool if these two would return true... 282 | t.equal(typeof x === "array", false); 283 | t.equal(Array.isArray(x), false); 284 | t.end(); 285 | }) 286 | 287 | test('peek', function(t) { 288 | var x = mobx.observable([1, 2, 3]); 289 | t.deepEqual(x.peek(), [1, 2, 3]); 290 | t.equal(x.$mobx.values, x.peek()); 291 | 292 | x.peek().push(4); //noooo! 293 | t.throws(function() { 294 | x.push(5); // detect alien change 295 | }, "modification exception"); 296 | t.end(); 297 | }) 298 | 299 | test('react to sort changes', function(t) { 300 | var x = mobx.observable([4, 2, 3]); 301 | var sortedX = mobx.observable(function() { 302 | return x.sort(); 303 | }); 304 | var sorted; 305 | 306 | mobx.autorun(function() { 307 | sorted = sortedX.get(); 308 | }); 309 | 310 | t.deepEqual(x.slice(), [4,2,3]); 311 | t.deepEqual(sorted, [2,3,4]); 312 | x.push(1); 313 | t.deepEqual(x.slice(), [4,2,3,1]); 314 | t.deepEqual(sorted, [1,2,3,4]); 315 | x.shift(); 316 | t.deepEqual(x.slice(), [2,3,1]); 317 | t.deepEqual(sorted, [1,2,3]); 318 | t.end(); 319 | }) 320 | 321 | test('autoextend buffer length', function(t) { 322 | var ar = observable(new Array(1000)); 323 | var changesCount = 0; 324 | ar.observe(changes => ++changesCount); 325 | 326 | ar[ar.length] = 0; 327 | ar.push(0); 328 | 329 | t.equal(changesCount, 2); 330 | 331 | t.end(); 332 | }) 333 | 334 | test('array exposes correct keys', t => { 335 | var keys = [] 336 | var ar = observable([1,2]) 337 | for (var key in ar) 338 | keys.push(key) 339 | 340 | t.deepEqual(keys, []) 341 | t.end() 342 | }) -------------------------------------------------------------------------------- /src/core/derivation.ts: -------------------------------------------------------------------------------- 1 | import {IObservable, IDepTreeNode, addObserver, removeObserver, endBatch} from "./observable"; 2 | import {globalState, resetGlobalState} from "./globalstate"; 3 | import {invariant} from "../utils/utils"; 4 | import {isSpyEnabled, spyReport} from "./spy"; 5 | import {isComputedValue} from "./computedvalue"; 6 | 7 | export enum IDerivationState { 8 | // before being run or (outside batch and not being observed) 9 | // at this point derivation is not holding any data about dependency tree 10 | NOT_TRACKING = -1, 11 | // no shallow dependency changed since last computation 12 | // won't recalculate derivation 13 | // this is what makes mobx fast 14 | UP_TO_DATE = 0, 15 | // some deep dependency changed, but don't know if shallow dependency changed 16 | // will require to check first if UP_TO_DATE or POSSIBLY_STALE 17 | // currently only ComputedValue will propagate POSSIBLY_STALE 18 | // 19 | // having this state is second big optimization: 20 | // don't have to recompute on every dependency change, but only when it's needed 21 | POSSIBLY_STALE = 1, 22 | // shallow dependency changed 23 | // will need to recompute when it's needed 24 | STALE = 2 25 | } 26 | 27 | /** 28 | * A derivation is everything that can be derived from the state (all the atoms) in a pure manner. 29 | * See https://medium.com/@mweststrate/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254#.xvbh6qd74 30 | * TODO: the one above is outdated, new one? 31 | */ 32 | export interface IDerivation extends IDepTreeNode { 33 | observing: IObservable[]; 34 | newObserving: IObservable[]; 35 | dependenciesState: IDerivationState; 36 | /** 37 | * Id of the current run of a derivation. Each time the derivation is tracked 38 | * this number is increased by one. This number is globally unique 39 | */ 40 | runId: number; 41 | /** 42 | * amount of dependencies used by the derivation in this run, which has not been bound yet. 43 | */ 44 | unboundDepsCount: number; 45 | __mapid: string; 46 | onBecomeStale(); 47 | recoverFromError(); // TODO: revisit implementation of error handling 48 | } 49 | 50 | /** 51 | * Finds out wether any dependency of derivation actually changed 52 | * If dependenciesState is 1 it will recalculate dependencies, 53 | * if any dependency changed it will propagate it by changing dependenciesState to 2. 54 | * 55 | * By iterating over dependencies in the same order they were reported and stoping on first change 56 | * all recalculations are called only for ComputedValues that will be tracked anyway by derivation. 57 | * That is because we assume that if first x dependencies of derivation doesn't change 58 | * than derivation shuold run the same way up until accessing x-th dependency. 59 | */ 60 | export function shouldCompute(derivation: IDerivation): boolean { 61 | switch (derivation.dependenciesState) { 62 | case IDerivationState.UP_TO_DATE: return false; 63 | case IDerivationState.NOT_TRACKING: case IDerivationState.STALE: return true; 64 | case IDerivationState.POSSIBLY_STALE: { 65 | let hasError = true; 66 | const prevUntracked = untrackedStart(); // no need for those computeds to be reported, they will be picked up in trackDerivedFunction. 67 | try { 68 | const obs = derivation.observing, l = obs.length; 69 | for (let i = 0; i < l; i++) { 70 | const obj = obs[i]; 71 | if (isComputedValue(obj)) { 72 | obj.get(); 73 | // if ComputedValue `obj` actually changed it will be computed and propagated to its observers. 74 | // and `derivation` is an observer of `obj` 75 | if (derivation.dependenciesState === IDerivationState.STALE) { 76 | hasError = false; 77 | untrackedEnd(prevUntracked); 78 | return true; 79 | } 80 | } 81 | } 82 | hasError = false; 83 | changeDependenciesStateTo0(derivation); 84 | untrackedEnd(prevUntracked); 85 | return false; 86 | } finally { 87 | if (hasError) { 88 | changeDependenciesStateTo0(derivation); 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | export function isComputingDerivation() { 96 | return globalState.trackingDerivation !== null; // filter out actions inside computations 97 | } 98 | 99 | export function checkIfStateModificationsAreAllowed() { 100 | if (!globalState.allowStateChanges) { 101 | invariant(false, globalState.strictMode 102 | ? "It is not allowed to create or change state outside an `action` when MobX is in strict mode. Wrap the current method in `action` if this state change is intended" 103 | : "It is not allowed to change the state when a computed value or transformer is being evaluated. Use 'autorun' to create reactive functions with side-effects." 104 | ); 105 | } 106 | } 107 | 108 | /** 109 | * Executes the provided function `f` and tracks which observables are being accessed. 110 | * The tracking information is stored on the `derivation` object and the derivation is registered 111 | * as observer of any of the accessed observables. 112 | */ 113 | export function trackDerivedFunction(derivation: IDerivation, f: () => T) { 114 | // pre allocate array allocation + room for variation in deps 115 | // array will be trimmed by bindDependencies 116 | changeDependenciesStateTo0(derivation); 117 | derivation.newObserving = new Array(derivation.observing.length + 100); 118 | derivation.unboundDepsCount = 0; 119 | derivation.runId = ++globalState.runId; 120 | const prevTracking = globalState.trackingDerivation; 121 | globalState.trackingDerivation = derivation; 122 | let hasException = true; 123 | let result: T; 124 | try { 125 | result = f.call(derivation); 126 | hasException = false; 127 | } finally { 128 | if (hasException) { 129 | handleExceptionInDerivation(derivation); 130 | } else { 131 | globalState.trackingDerivation = prevTracking; 132 | bindDependencies(derivation); 133 | } 134 | } 135 | return result; 136 | } 137 | 138 | export function handleExceptionInDerivation(derivation: IDerivation) { 139 | const message = ( 140 | `[mobx] An uncaught exception occurred while calculating your computed value, autorun or transformer. Or inside the render() method of an observer based React component. ` + 141 | `These functions should never throw exceptions as MobX will not always be able to recover from them. ` + 142 | `Please fix the error reported after this message or enable 'Pause on (caught) exceptions' in your debugger to find the root cause. In: '${derivation.name}'. ` + 143 | `For more details see https://github.com/mobxjs/mobx/issues/462` 144 | ); 145 | if (isSpyEnabled()) { 146 | spyReport({ 147 | type: "error", 148 | message 149 | }); 150 | } 151 | console.warn(message); // In next major, maybe don't emit this message at all? 152 | // Poor mans recovery attempt 153 | // Assumption here is that this is the only exception handler in MobX. 154 | // So functions higher up in the stack (like transanction) won't be modifying the globalState anymore after this call. 155 | // (Except for other trackDerivedFunction calls of course, but that is just) 156 | changeDependenciesStateTo0(derivation); 157 | derivation.newObserving = null; 158 | derivation.unboundDepsCount = 0; 159 | derivation.recoverFromError(); 160 | // close current batch, make sure pending unobservations are executed 161 | endBatch(); 162 | resetGlobalState(); 163 | } 164 | 165 | /** 166 | * diffs newObserving with obsering. 167 | * update observing to be newObserving with unique observables 168 | * notify observers that become observed/unobserved 169 | */ 170 | function bindDependencies(derivation: IDerivation) { 171 | // invariant(derivation.dependenciesState !== IDerivationState.NOT_TRACKING, "INTERNAL ERROR bindDependencies expects derivation.dependenciesState !== -1"); 172 | 173 | const prevObserving = derivation.observing; 174 | const observing = derivation.observing = derivation.newObserving; 175 | 176 | derivation.newObserving = null; // newObserving shouldn't be needed outside tracking 177 | 178 | // Go through all new observables and check diffValue: (this list can contain duplicates): 179 | // 0: first occurence, change to 1 and keep it 180 | // 1: extra occurence, drop it 181 | let i0 = 0, l = derivation.unboundDepsCount; 182 | for (let i = 0; i < l; i++) { 183 | const dep = observing[i]; 184 | if (dep.diffValue === 0) { 185 | dep.diffValue = 1; 186 | if (i0 !== i) observing[i0] = dep; 187 | i0++; 188 | } 189 | } 190 | observing.length = i0; 191 | 192 | // Go through all old observables and check diffValue: (it is unique after last bindDependencies) 193 | // 0: it's not in new observables, unobserve it 194 | // 1: it keeps being observed, don't want to notify it. change to 0 195 | l = prevObserving.length; 196 | while (l--) { 197 | const dep = prevObserving[l]; 198 | if (dep.diffValue === 0) { 199 | removeObserver(dep, derivation); 200 | } 201 | dep.diffValue = 0; 202 | } 203 | 204 | // Go through all new observables and check diffValue: (now it should be unique) 205 | // 0: it was set to 0 in last loop. don't need to do anything. 206 | // 1: it wasn't observed, let's observe it. set back to 0 207 | while (i0--) { 208 | const dep = observing[i0]; 209 | if (dep.diffValue === 1) { 210 | dep.diffValue = 0; 211 | addObserver(dep, derivation); 212 | } 213 | } 214 | } 215 | 216 | export function clearObserving(derivation: IDerivation) { 217 | // invariant(globalState.inBatch > 0, "INTERNAL ERROR clearObserving should be called only inside batch"); 218 | const obs = derivation.observing; 219 | let i = obs.length; 220 | while (i--) 221 | removeObserver(obs[i], derivation); 222 | 223 | derivation.dependenciesState = IDerivationState.NOT_TRACKING; 224 | obs.length = 0; 225 | } 226 | 227 | export function untracked(action: () => T): T { 228 | const prev = untrackedStart(); 229 | const res = action(); 230 | untrackedEnd(prev); 231 | return res; 232 | } 233 | 234 | export function untrackedStart(): IDerivation { 235 | const prev = globalState.trackingDerivation; 236 | globalState.trackingDerivation = null; 237 | return prev; 238 | } 239 | 240 | export function untrackedEnd(prev: IDerivation) { 241 | globalState.trackingDerivation = prev; 242 | } 243 | 244 | /** 245 | * needed to keep `lowestObserverState` correct. when changing from (2 or 1) to 0 246 | * 247 | */ 248 | export function changeDependenciesStateTo0(derivation: IDerivation) { 249 | if (derivation.dependenciesState === IDerivationState.UP_TO_DATE) return; 250 | derivation.dependenciesState = IDerivationState.UP_TO_DATE; 251 | 252 | const obs = derivation.observing; 253 | let i = obs.length; 254 | while (i--) 255 | obs[i].lowestObserverState = IDerivationState.UP_TO_DATE; 256 | } 257 | --------------------------------------------------------------------------------