├── 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 |
--------------------------------------------------------------------------------