diff --git a/store.js b/store.js index 3a4a516e46..91603bb15b 100644 --- a/store.js +++ b/store.js @@ -8,10 +8,20 @@ import { } from './shared.js'; function Store(state) { - this._state = state ? assign({}, state) : {}; this._observers = { pre: blankObject(), post: blankObject() }; this._changeHandlers = []; this._dependents = []; + + this._proto = blankObject(); + this._changed = blankObject(); + this._dependentProps = blankObject(); + this._dirty = blankObject(); + this._state = Object.create(this._proto); + + for (var key in state) { + this._changed[key] = true; + this._state[key] = state[key]; + } } assign(Store.prototype, { @@ -25,6 +35,17 @@ assign(Store.prototype, { }); }, + _makeDirty: function(prop) { + var dependentProps = this._dependentProps[prop]; + if (dependentProps) { + for (var i = 0; i < dependentProps.length; i += 1) { + var dependentProp = dependentProps[i]; + this._dirty[dependentProp] = this._changed[dependentProp] = true; + this._makeDirty(dependentProp); + } + } + }, + _init: function(props) { var state = {}; for (let i = 0; i < props.length; i += 1) { @@ -44,6 +65,52 @@ assign(Store.prototype, { } }, + compute: function(key, deps, fn) { + var store = this; + var value; + + store._dirty[key] = true; + + for (var i = 0; i < deps.length; i += 1) { + var dep = deps[i]; + if (!this._dependentProps[dep]) this._dependentProps[dep] = []; + this._dependentProps[dep].push(key); + } + + Object.defineProperty(this._proto, key, { + get: function() { + if (store._dirty[key]) { + var values = deps.map(function(dep) { + if (dep in store._changed) changed = true; + return store._state[dep]; + }); + + var newValue = fn.apply(null, values); + + if (differs(newValue, value)) { + value = newValue; + store._changed[key] = true; + + var dependentProps = store._dependentProps[key]; + if (dependentProps) { + for (var i = 0; i < dependentProps.length; i += 1) { + var prop = dependentProps[i]; + store._dirty[prop] = store._changed[prop] = true; + } + } + } + + store._dirty[key] = false; + } + + return value; + }, + set: function() { + throw new Error(`'${key}' is a read-only property`); + } + }); + }, + onchange: function(callback) { this._changeHandlers.push(callback); return { @@ -56,7 +123,7 @@ assign(Store.prototype, { set: function(newState) { var oldState = this._state, - changed = {}, + changed = this._changed = {}, dirty = false; for (var key in newState) { @@ -64,7 +131,9 @@ assign(Store.prototype, { } if (!dirty) return; - this._state = assign({}, oldState, newState); + this._state = assign(Object.create(this._proto), oldState, newState); + + for (var key in changed) this._makeDirty(key); for (var i = 0; i < this._changeHandlers.length; i += 1) { this._changeHandlers[i](this._state, changed); diff --git a/test/store/index.js b/test/store/index.js index 414e49bae4..50b6572774 100644 --- a/test/store/index.js +++ b/test/store/index.js @@ -92,4 +92,55 @@ describe('store', () => { }); }); }); + + describe('computed', () => { + it('computes a property based on data', () => { + const store = new Store({ + foo: 1 + }); + + store.compute('bar', ['foo'], foo => foo * 2); + assert.equal(store.get('bar'), 2); + + const values = []; + + store.observe('bar', bar => { + values.push(bar); + }); + + store.set({ foo: 2 }); + assert.deepEqual(values, [2, 4]); + }); + + it('computes a property based on another computed property', () => { + const store = new Store({ + foo: 1 + }); + + store.compute('bar', ['foo'], foo => foo * 2); + store.compute('baz', ['bar'], bar => bar * 2); + assert.equal(store.get('baz'), 4); + + const values = []; + + store.observe('baz', baz => { + values.push(baz); + }); + + store.set({ foo: 2 }); + assert.deepEqual(values, [4, 8]); + }); + + it('prevents computed properties from being set', () => { + const store = new Store({ + foo: 1 + }); + + store.compute('bar', ['foo'], foo => foo * 2); + + assert.throws(() => { + store.set({ bar: 'whatever' }); + }, /'bar' is a read-only property/); + }); + }); });