@nx-js/observer-util
Advanced tools
Comparing version 1.0.0 to 2.0.0
@@ -304,3 +304,3 @@ 'use strict' | ||
for (let nxSignal of nxSignals) { | ||
nx.unobserve(nxSignal) | ||
nxSignal.unobserve() | ||
} | ||
@@ -307,0 +307,0 @@ }) |
{ | ||
"name": "@nx-js/observer-util", | ||
"version": "1.0.0", | ||
"version": "2.0.0", | ||
"description": "An NX utility, responsible for powerful data observation with ES6 Proxies.", | ||
@@ -5,0 +5,0 @@ "scripts": { |
101
README.md
@@ -5,5 +5,5 @@ # The observer utility | ||
The purpose of this library is to allow powerful data observation/binding without any special syntax and with a 100% language observability coverage. | ||
It uses ES6 Proxies internally to create seamless data binding with a minimal interface. | ||
A more detailed blog post about this library can be found | ||
The purpose of this library is to provide transparent reactivity without any special syntax and with a 100% language observability coverage. | ||
It uses ES6 Proxies internally to work seamlessly with a minimal interface. | ||
A blog post about the inner working of this library can be found | ||
[here](https://blog.risingstack.com/writing-a-javascript-framework-data-binding-es6-proxy/) and a comparison with MobX can be found [here](http://www.nx-framework.com/blog/public/mobx-vs-nx/). | ||
@@ -38,3 +38,4 @@ | ||
This method creates and returns an observable object. If an object is passed as argument | ||
it wraps the passed object in an observable. | ||
it wraps the passed object in an observable. If an observable object is passed, it simply | ||
returns the passed observable object. | ||
@@ -45,23 +46,19 @@ ```js | ||
### const signal = observer.observe(function, [context], ...[args]) | ||
### observer.isObservable(Object) | ||
This method observes a function. An observed function automatically reruns when a property of an | ||
observable, which is used by the function changes (or is deleted). The function doesn't run | ||
immediately on property change, instead it runs after a small delay (when the current stack empties). | ||
Multiple synchronous changes won't cause the function to run multiple times. Changes that result in | ||
no value change (`state.prop = state.prop`) won't cause the function to run either. | ||
The function can observe any synchronous javascript code (nested data, iterations, function calls, | ||
getters/setters, etc.) | ||
Returns true if the passed object is an observable, otherwise returns false. | ||
`observe()` returns a signal, which can later be used to stop the observation. This is similar to | ||
the `const signal = setTimeout()`, `clearTimeout(signal)` pair. | ||
```js | ||
const signal = observer.observe(() => console.log(observable.prop)) | ||
const observable = observer.observable() | ||
const isObservable = observer.isObservable(observable) | ||
``` | ||
A `this` context and a list of argument can be passed after the observed function as arguments. | ||
In this case the observed function will always be called with the passed `this` context | ||
and arguments. | ||
### const signal = observer.observe(function, [context], ...[args]) | ||
This method observes the passed function. An observed function automatically reruns when a property of an observable - which is used by the function - changes (or is deleted). The function doesn't run immediately on property change, instead it runs after a small delay (when the current stack empties). | ||
Multiple synchronous changes won't cause the function to run multiple times. Changes that result in no value change - like `state.prop = state.prop` - won't cause the function to run either. | ||
The function can observe any synchronous javascript code (nested data, iterations, function calls, getters/setters, etc.) | ||
A `this` context and a list of argument can be passed after the observed function as arguments. In this case the observed function will always be called with the passed `this` context and arguments. | ||
```js | ||
@@ -75,38 +72,33 @@ observer.observe(printSum, context, arg1, arg2) | ||
### const signal = observer.queue(function, [context], ...[args]) | ||
`observe()` returns a signal object, which can be used to stop or modify the observation. | ||
This method queues a function to be executed together with the currently triggered observed functions. `queue()` returns a signal, which can later be used to remove the function from the queue. | ||
```js | ||
const signal = observer.queue(() => console.log(observable.prop)) | ||
const signal = observer.observe(() => console.log(observable.prop)) | ||
``` | ||
A `this` context and a list of argument can be passed after the queued function as arguments. | ||
In this case the queued function will be called with the passed `this` context and arguments. | ||
### signal.unobserve() | ||
Calling `signal.unobserve()` unobserves the observed function associated with the passed signal. Unobserved functions won't be rerun by observable changes anymore. | ||
```js | ||
observer.queue(printSum, context, arg1, arg2) | ||
function printSum (arg1, arg2) { | ||
console.log(arg1 + arg2) | ||
} | ||
const signal = observer.observe(() => console.log(observable.prop)) | ||
signal.unobserve() | ||
``` | ||
### observer.unobserve(signal) | ||
### signal.unqueue() | ||
If unobserves the observed or queued function associated with the assed signal. | ||
Unobserved functions won't be rerun by observable changes anymore. | ||
Calling `signal.unqueue()` removes the observed function from the set of triggered and queued observed functions, but it doesn't unobserve it. It can still be triggered and requeued by later observable changes. | ||
```js | ||
const signal = observer.observe(() => console.log(observable.prop)) | ||
observer.unobserve(signal) | ||
signal.unqueue() | ||
``` | ||
### observer.isObservable(Object) | ||
### signal.exec() | ||
Returns true if the passed object is an observable, otherwise returns false. | ||
Runs the observed function. Never run an observed function directly, use this method instead! | ||
```js | ||
const observable = observer.observable() | ||
const isObservable = observer.isObservable(observable) | ||
const signal = observer.observe(() => console.log(observable.prop)) | ||
signal.exec() | ||
``` | ||
@@ -123,4 +115,4 @@ | ||
// outputs 0 to the console after the stack empties | ||
// the arguments are: observer func, observer func 'this' context, observer func arguments | ||
// outputs 0 to the console | ||
// the passed parameters are: observed func, injected 'this' context, injected arguments | ||
const signal = observer.observe(printSum, undefined, observable1, observable2) | ||
@@ -138,4 +130,4 @@ | ||
// finish observing | ||
setTimeout(() => observer.unobserve(signal), 300) | ||
// finishes observing | ||
setTimeout(() => signal.unobserve(), 300) | ||
@@ -167,7 +159,6 @@ // observation is finished, doesn't trigger printSum, outputs nothing to the console | ||
And observer runs after every stack in which the observable properties used by it changes value. | ||
An observer runs maximum once per stack. Multiple synchronous changes of the observable | ||
properties won't trigger it more than once. Setting on observable property without a value change | ||
won't trigger it either. | ||
Every observed function runs once synchronously when it is passed to `observer.observe`. | ||
After that an observed function runs after every stack in which the observable properties used by it changed value. It runs maximum once per stack and multiple synchronous changes of the observable properties won't trigger it more than once. Setting on observable property without a value change won't trigger it either. | ||
```js | ||
@@ -178,3 +169,3 @@ const observer = require('@nx-js/observer-util') | ||
// outputs 'value' to the console after the stack empties | ||
// outputs 'value' to the console synchronously | ||
observer.observe(() => console.log(observable.prop)) | ||
@@ -201,3 +192,3 @@ | ||
// outputs 'undefined' to the console after the current stack empties | ||
// outputs 'undefined' to the console | ||
observer.observe(() => console.log(observable.expando)) | ||
@@ -222,3 +213,3 @@ | ||
// outputs 'prop1' to the console after the current stack empties | ||
// outputs 'prop1' to the console | ||
observer.observe(() => console.log(observable.condition ? observable.prop1 : observable.prop2)) | ||
@@ -242,3 +233,3 @@ | ||
// outputs 'nestedValue' to the console after the current stack empties | ||
// outputs 'nestedValue' to the console | ||
observer.observe(() => console.log(observable.prop.nested)) | ||
@@ -260,3 +251,3 @@ | ||
// outputs 'Hello World' to the console after the current stack empties | ||
// outputs 'Hello World' to the console | ||
observer.observe(() => console.log(observable.words.join(' '))) | ||
@@ -284,3 +275,3 @@ | ||
// outputs 'Hello World' to the console after the current stack empties | ||
// outputs 'Hello World' to the console | ||
observer.observe(() => console.log(observable.greeting + ' ' + observable.subject)) | ||
@@ -318,3 +309,3 @@ | ||
// outputs 0 to the console after the current stack empties | ||
// outputs 0 to the console | ||
observer.observe(() => console.log(observable.sum)) | ||
@@ -370,3 +361,3 @@ | ||
// outputs 'name: John, age: 25' to the console after the current stack empties | ||
// outputs 'name: John, age: 25' to the console | ||
observer.observe(() => console.log(`name: ${person.name}, age: ${person.$raw.age}`)) | ||
@@ -405,3 +396,3 @@ | ||
- 'Function cleanup' tests the cost of disposing observer/listener functions with `disposeFn()` or `nx.unobserve(fn)`. | ||
- 'Function cleanup' tests the cost of disposing observer/listener functions with `disposeFn()` or `signal.unobserve()`. | ||
@@ -408,0 +399,0 @@ Do not worry about the large difference between the vanilla and nx-observe / MobX results. |
@@ -17,4 +17,2 @@ 'use strict' | ||
observe, | ||
unobserve, | ||
queue, | ||
observable, | ||
@@ -26,29 +24,26 @@ isObservable | ||
if (typeof fn !== 'function') { | ||
throw new TypeError('first argument must be a function') | ||
throw new TypeError('First argument must be a function') | ||
} | ||
args = args.length ? args : undefined | ||
const observer = {fn, context, args, observedKeys: []} | ||
queueObserver(observer) | ||
const observer = {fn, context, args, observedKeys: [], exec, unobserve, unqueue} | ||
runObserver(observer) | ||
return observer | ||
} | ||
function unobserve (observer) { | ||
if (typeof observer === 'object') { | ||
if (observer.observedKeys) { | ||
observer.observedKeys.forEach(unobserveKey, observer) | ||
} | ||
observer.fn = observer.context = observer.args = observer.observedKeys = undefined | ||
} | ||
function exec () { | ||
runObserver(this) | ||
} | ||
function queue (fn, context, ...args) { | ||
if (typeof fn !== 'function') { | ||
throw new TypeError('first argument must be a function') | ||
function unobserve () { | ||
if (this.fn) { | ||
this.observedKeys.forEach(unobserveKey, this) | ||
this.fn = this.context = this.args = this.observedKeys = undefined | ||
queuedObservers.delete(this) | ||
} | ||
args = args.length ? args : undefined | ||
const observer = {fn, context, args, once: true} | ||
queueObserver(observer) | ||
return observer | ||
} | ||
function unqueue () { | ||
queuedObservers.delete(this) | ||
} | ||
function observable (obj) { | ||
@@ -143,4 +138,6 @@ obj = obj || {} | ||
const observersForKey = observers.get(target).get(key) | ||
if (observersForKey) { | ||
if (observersForKey && observersForKey.constructor === Set) { | ||
observersForKey.forEach(queueObserver) | ||
} else if (observersForKey) { | ||
queueObserver(observersForKey) | ||
} | ||
@@ -164,14 +161,7 @@ } | ||
function runObserver (observer) { | ||
if (observer.fn) { | ||
if (observer.once) { | ||
observer.fn.apply(observer.context, observer.args) | ||
unobserve(observer) | ||
} else { | ||
try { | ||
currentObserver = observer | ||
observer.fn.apply(observer.context, observer.args) | ||
} finally { | ||
currentObserver = undefined | ||
} | ||
} | ||
try { | ||
currentObserver = observer | ||
observer.fn.apply(observer.context, observer.args) | ||
} finally { | ||
currentObserver = undefined | ||
} | ||
@@ -178,0 +168,0 @@ } |
@@ -220,3 +220,3 @@ 'use strict' | ||
it('should not run synchronously after registration', () => { | ||
it('should run synchronously after registration', () => { | ||
let dummy | ||
@@ -231,11 +231,7 @@ const observable = observer.observable({prop: 'prop'}) | ||
expect(numOfRuns).to.equal(0) | ||
expect(dummy).to.equal(undefined) | ||
expect(numOfRuns).to.equal(1) | ||
expect(dummy).to.equal('prop') | ||
return Promise.resolve() | ||
.then(() => { | ||
expect(numOfRuns).to.equal(1) | ||
expect(dummy).to.equal('prop') | ||
}) | ||
.then(() => { | ||
observable.prop = 'new prop' | ||
@@ -565,3 +561,3 @@ }) | ||
describe('execution order', () => { | ||
it('should run in registration order the first time', () => { | ||
it('should be first-tigger order', () => { | ||
let dummy = '' | ||
@@ -571,19 +567,2 @@ const observable = observer.observable({prop1: 'prop1', prop2: 'prop2', prop3: 'prop3'}) | ||
observer.observe(() => dummy += observable.prop1) | ||
observer.queue(() => dummy += observable.prop2) | ||
observer.observe(() => dummy += observable.prop3) | ||
observable.prop2 = 'p' | ||
observable.prop1 = 'p1' | ||
observable.prop3 = 'p3' | ||
observable.prop2 = 'p2' | ||
return Promise.resolve() | ||
.then(() => expect(dummy).to.equal('p1p2p3')) | ||
}) | ||
it('should run in first-tigger order after the first time', () => { | ||
let dummy = '' | ||
const observable = observer.observable({prop1: 'prop1', prop2: 'prop2', prop3: 'prop3'}) | ||
observer.observe(() => dummy += observable.prop1) | ||
observer.observe(() => dummy += observable.prop2) | ||
@@ -606,109 +585,98 @@ observer.observe(() => dummy += observable.prop3) | ||
describe('queue', () => { | ||
it('should unobserve the observed function', () => { | ||
let dummy | ||
const observable = observer.observable({prop: 0}) | ||
describe('observer methods', () => { | ||
describe('exec', () => { | ||
it('should track the newly discovered function parts', () => { | ||
let condition = false | ||
let counter | ||
let numOfRuns = 0 | ||
function test() { | ||
dummy = observable.prop | ||
numOfRuns++ | ||
} | ||
const signal = observer.observe(test) | ||
const observable = observer.observable({condition: false, counter: 0}) | ||
const signal = observer.observe(conditionalIncrement) | ||
return Promise.resolve() | ||
.then(() => observable.prop = 'Hello') | ||
.then(() => observer.unobserve(signal)) | ||
.then(() => observable.prop = 'World') | ||
.then(() => observable.prop = '!') | ||
.then(() => expect(numOfRuns).to.equal(2)) | ||
function conditionalIncrement () { | ||
if (condition) { | ||
counter = observable.counter | ||
} | ||
} | ||
return Promise.resolve() | ||
.then(() => expect(counter).to.be.undefined) | ||
.then(() => observable.counter++) | ||
.then(() => expect(counter).to.be.undefined) | ||
.then(() => { | ||
condition = true | ||
signal.exec() | ||
}) | ||
.then(() => expect(counter).to.equal(1)) | ||
.then(() => observable.counter++) | ||
.then(() => expect(counter).to.equal(2)) | ||
}) | ||
}) | ||
it('should unobserve even if the function is registered for the stack', () => { | ||
let dummy | ||
const observable = observer.observable({prop: 0}) | ||
describe('unqueue', () => { | ||
it('should remove the observed function from the queue', () => { | ||
let dummy | ||
const observable = observer.observable({prop: 0}) | ||
let numOfRuns = 0 | ||
function test() { | ||
dummy = observable.prop | ||
numOfRuns++ | ||
} | ||
const signal = observer.observe(test) | ||
let numOfRuns = 0 | ||
function test() { | ||
dummy = observable.prop | ||
numOfRuns++ | ||
} | ||
const signal = observer.observe(test) | ||
return Promise.resolve() | ||
.then(() => { | ||
observable.prop = 2 | ||
observer.unobserve(signal) | ||
}) | ||
.then(() => expect(numOfRuns).to.equal(1)) | ||
return Promise.resolve() | ||
.then(() => { | ||
observable.prop = 2 | ||
signal.unqueue() | ||
}) | ||
.then(() => expect(numOfRuns).to.equal(1)) | ||
}) | ||
}) | ||
}) | ||
describe('unobserve', () => { | ||
it('should unobserve the observed function', () => { | ||
let dummy | ||
const observable = observer.observable({prop: 0}) | ||
describe('unobserve', () => { | ||
it('should unobserve the observed function', () => { | ||
let dummy | ||
const observable = observer.observable({prop: 0}) | ||
let numOfRuns = 0 | ||
function test() { | ||
dummy = observable.prop | ||
numOfRuns++ | ||
} | ||
const signal = observer.observe(test) | ||
let numOfRuns = 0 | ||
function test() { | ||
dummy = observable.prop | ||
numOfRuns++ | ||
} | ||
const signal = observer.observe(test) | ||
return Promise.resolve() | ||
.then(() => observable.prop = 'Hello') | ||
.then(() => observer.unobserve(signal)) | ||
.then(() => { | ||
expect(signal.fn).to.be.undefined | ||
expect(signal.context).to.be.undefined | ||
expect(signal.args).to.be.undefined | ||
expect(signal.observedKeys).to.be.undefined | ||
}) | ||
.then(() => observable.prop = 'World') | ||
.then(() => observable.prop = '!') | ||
.then(() => expect(numOfRuns).to.equal(2)) | ||
}) | ||
return Promise.resolve() | ||
.then(() => observable.prop = 'Hello') | ||
.then(() => signal.unobserve()) | ||
.then(() => { | ||
expect(signal.fn).to.be.undefined | ||
expect(signal.context).to.be.undefined | ||
expect(signal.args).to.be.undefined | ||
expect(signal.observedKeys).to.be.undefined | ||
}) | ||
.then(() => observable.prop = 'World') | ||
.then(() => observable.prop = '!') | ||
.then(() => expect(numOfRuns).to.equal(2)) | ||
}) | ||
it('should unobserve even if the function is registered for the stack', () => { | ||
let dummy | ||
const observable = observer.observable({prop: 0}) | ||
it('should unobserve even if the function is registered for the stack', () => { | ||
let dummy | ||
const observable = observer.observable({prop: 0}) | ||
let numOfRuns = 0 | ||
function test() { | ||
dummy = observable.prop | ||
numOfRuns++ | ||
} | ||
const signal = observer.observe(test) | ||
let numOfRuns = 0 | ||
function test() { | ||
dummy = observable.prop | ||
numOfRuns++ | ||
} | ||
const signal = observer.observe(test) | ||
return Promise.resolve() | ||
.then(() => { | ||
observable.prop = 2 | ||
observer.unobserve(signal) | ||
}) | ||
.then(() => expect(numOfRuns).to.equal(1)) | ||
}) | ||
it('should remove queued function', () => { | ||
let dummy | ||
const observable = observer.observable({prop: 0}) | ||
let numOfRuns = 0 | ||
const signal = observer.queue(() => { | ||
dummy = observable.prop | ||
numOfRuns++ | ||
return Promise.resolve() | ||
.then(() => { | ||
observable.prop = 2 | ||
signal.unobserve() | ||
}) | ||
.then(() => expect(numOfRuns).to.equal(1)) | ||
}) | ||
observer.unobserve(signal) | ||
return Promise.resolve() | ||
.then(() => expect(numOfRuns).to.equal(0)) | ||
.then(() => { | ||
expect(signal.fn).to.be.undefined | ||
expect(signal.context).to.be.undefined | ||
expect(signal.args).to.be.undefined | ||
expect(signal.observedKeys).to.be.undefined | ||
}) | ||
}) | ||
}) | ||
}) |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
110202
1240
403