Add bundle to cut out unnecessary subscriber calls.
This commit is contained in:
parent
49259828af
commit
3230a3ef5a
@ -108,3 +108,50 @@ inViewport(false); // showDialog result recomputed and `false` is written to the
|
|||||||
shouldShow(false); // showDialog result recomputed, console.log is not called.
|
shouldShow(false); // showDialog result recomputed, console.log is not called.
|
||||||
showDialog(); // showDialog does not recompute, console.log is not called. `false` is returned.
|
showDialog(); // showDialog does not recompute, console.log is not called. `false` is returned.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# [bundle](./src/bundle.js)
|
||||||
|
|
||||||
|
`bundle` is a wrapper around a group of `observables` for the purpose of applying changes to all of
|
||||||
|
them at once without having to trigger a subscription that may depend on more than observable in the
|
||||||
|
group.
|
||||||
|
|
||||||
|
Another way to think of a `bundle` is an `observable` that takes an object and exposes the
|
||||||
|
properties as individual observables.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
A `bundle` wraps observables to intercept dependency hooks in such a way that updating all
|
||||||
|
observables can happen at once before any downstream `computeds` are evaluated. A bundle returns a
|
||||||
|
function that can be called with an object to set values for the mapped member observables.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Creation
|
||||||
|
|
||||||
|
```js
|
||||||
|
const layoutEventBundle = bundle({
|
||||||
|
width: observable(1),
|
||||||
|
height: observable(2)
|
||||||
|
});
|
||||||
|
const ratio = computed((a, b) => a / b, [layoutEventBundle.width, layoutEventBundle.height]);
|
||||||
|
ratio.subscribe(render);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change Member Observables atomically
|
||||||
|
|
||||||
|
```js
|
||||||
|
layoutEventBundle({ width: 640, height: 480 });
|
||||||
|
```
|
||||||
|
|
||||||
|
`ratio` would normally be evaluated twice and `render` would be called after each intermediate
|
||||||
|
change. But bundle allows both values to change and `ratio` will only be evaluated once and `render`
|
||||||
|
called once.
|
||||||
|
|
||||||
|
### Change Member Observables individually
|
||||||
|
|
||||||
|
```js
|
||||||
|
layoutEventBundle.width(640);
|
||||||
|
layoutEventBundle.height(480);
|
||||||
|
```
|
||||||
|
|
||||||
|
The observables exposed by the bundle can also be updated apart from their grouping.
|
||||||
|
|||||||
114
packages/frptools/spec/bundle.spec.js
Normal file
114
packages/frptools/spec/bundle.spec.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
const { observable, computed, bundle } = require('../lib/index.js');
|
||||||
|
|
||||||
|
describe('bundle', () => {
|
||||||
|
const methods = {
|
||||||
|
add: (a, b) => a + b,
|
||||||
|
square: a => a * a,
|
||||||
|
getVal: val => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(methods, 'add').and.callThrough();
|
||||||
|
spyOn(methods, 'square').and.callThrough();
|
||||||
|
spyOn(methods, 'getVal').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bundles observable changes together', () => {
|
||||||
|
const a = bundle({
|
||||||
|
a: observable(0),
|
||||||
|
b: observable(10)
|
||||||
|
});
|
||||||
|
const b = computed(methods.square, [a.a]);
|
||||||
|
const c = computed(methods.add, [a.a, a.b]);
|
||||||
|
|
||||||
|
b.subscribe(methods.getVal);
|
||||||
|
c.subscribe(methods.getVal);
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
expect(b()).toEqual(0);
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(1);
|
||||||
|
b();
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(1);
|
||||||
|
expect(c()).toEqual(10);
|
||||||
|
c();
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(2);
|
||||||
|
expect(methods.add).toHaveBeenCalledTimes(1);
|
||||||
|
expect(methods.square).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
a({ a: 2, b: 20 });
|
||||||
|
expect(methods.add).toHaveBeenCalledTimes(2);
|
||||||
|
expect(methods.square).toHaveBeenCalledTimes(2);
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(4);
|
||||||
|
expect(b()).toEqual(4);
|
||||||
|
expect(c()).toEqual(22);
|
||||||
|
expect(methods.add).toHaveBeenCalledTimes(2);
|
||||||
|
expect(methods.square).toHaveBeenCalledTimes(2);
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unbundled changes are less efficient', () => {
|
||||||
|
const a = observable(0);
|
||||||
|
const _b = observable(10);
|
||||||
|
const b = computed(methods.square, [a]);
|
||||||
|
const c = computed(methods.add, [a, _b]);
|
||||||
|
|
||||||
|
b.subscribe(methods.getVal);
|
||||||
|
c.subscribe(methods.getVal);
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
expect(b()).toEqual(0);
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(1);
|
||||||
|
b();
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(1);
|
||||||
|
expect(c()).toEqual(10);
|
||||||
|
c();
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(2);
|
||||||
|
expect(methods.add).toHaveBeenCalledTimes(1);
|
||||||
|
expect(methods.square).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
a(2);
|
||||||
|
_b(20);
|
||||||
|
expect(methods.add).toHaveBeenCalledTimes(3);
|
||||||
|
expect(methods.square).toHaveBeenCalledTimes(2);
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(5);
|
||||||
|
expect(b()).toEqual(4);
|
||||||
|
expect(c()).toEqual(22);
|
||||||
|
expect(methods.add).toHaveBeenCalledTimes(3);
|
||||||
|
expect(methods.square).toHaveBeenCalledTimes(2);
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows individual members to be updated', () => {
|
||||||
|
const a = bundle({
|
||||||
|
a: observable(0),
|
||||||
|
b: observable(10)
|
||||||
|
});
|
||||||
|
const b = computed(methods.square, [a.a]);
|
||||||
|
const c = computed(methods.add, [a.a, a.b]);
|
||||||
|
|
||||||
|
b.subscribe(methods.getVal);
|
||||||
|
c.subscribe(methods.getVal);
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
expect(b()).toEqual(0);
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(1);
|
||||||
|
b();
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(1);
|
||||||
|
expect(c()).toEqual(10);
|
||||||
|
c();
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(2);
|
||||||
|
expect(methods.add).toHaveBeenCalledTimes(1);
|
||||||
|
expect(methods.square).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
a.a(2);
|
||||||
|
a.b(20);
|
||||||
|
expect(methods.add).toHaveBeenCalledTimes(3);
|
||||||
|
expect(methods.square).toHaveBeenCalledTimes(2);
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(5);
|
||||||
|
expect(b()).toEqual(4);
|
||||||
|
expect(c()).toEqual(22);
|
||||||
|
expect(methods.add).toHaveBeenCalledTimes(3);
|
||||||
|
expect(methods.square).toHaveBeenCalledTimes(2);
|
||||||
|
expect(methods.getVal).toHaveBeenCalledTimes(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
43
packages/frptools/src/bundle.js
Normal file
43
packages/frptools/src/bundle.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
export function bundle(observables) {
|
||||||
|
const activeSubscribers = new Set();
|
||||||
|
let activeUpdate = false;
|
||||||
|
|
||||||
|
const accessor = function _bundle(values) {
|
||||||
|
const result = {};
|
||||||
|
activeUpdate = true;
|
||||||
|
Object.keys(values)
|
||||||
|
.filter(k => typeof observables[k] === 'function')
|
||||||
|
.forEach(k => {
|
||||||
|
result[k] = observables[k](values[k]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const subscribers = Array.from(activeSubscribers);
|
||||||
|
// Set them dirty but don't propagate.
|
||||||
|
subscribers.forEach(s => s(result, true));
|
||||||
|
// Now propagate.
|
||||||
|
subscribers.forEach(s => s(result));
|
||||||
|
activeSubscribers.clear();
|
||||||
|
activeUpdate = false;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const subscriptionFactory = obsFn => fn => {
|
||||||
|
return obsFn(v => {
|
||||||
|
if (activeUpdate) {
|
||||||
|
activeSubscribers.add(fn);
|
||||||
|
} else {
|
||||||
|
fn(v);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(observables).forEach(k => {
|
||||||
|
const obs = observables[k];
|
||||||
|
|
||||||
|
accessor[k] = obs;
|
||||||
|
obs._d = subscriptionFactory(obs._d);
|
||||||
|
});
|
||||||
|
|
||||||
|
return accessor;
|
||||||
|
}
|
||||||
@ -4,13 +4,13 @@ export function computed(fn, dependencies = []) {
|
|||||||
let isDirty = true;
|
let isDirty = true;
|
||||||
let val;
|
let val;
|
||||||
|
|
||||||
function _computedDirtyReporter() {
|
function _computedDirtyReporter(_, skipPropagation) {
|
||||||
if (!isDirty) {
|
if (!isDirty) {
|
||||||
isDirty = true;
|
isDirty = true;
|
||||||
|
dependents.forEach(d => d(_, skipPropagation));
|
||||||
}
|
}
|
||||||
dependents.forEach(runParam);
|
|
||||||
|
|
||||||
if (subscribers.size) {
|
if (subscribers.size && !skipPropagation) {
|
||||||
accessor();
|
accessor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export { observable } from './observable';
|
export { observable } from './observable';
|
||||||
export { computed } from './computed';
|
export { computed } from './computed';
|
||||||
|
export { bundle } from './bundle';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user