Add bundle to cut out unnecessary subscriber calls.
This commit is contained in:
parent
8c5e255db0
commit
6ce9ab7079
47
README.md
47
README.md
@ -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.
|
||||
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
spec/bundle.spec.js
Normal file
114
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
src/bundle.js
Normal file
43
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 val;
|
||||
|
||||
function _computedDirtyReporter() {
|
||||
function _computedDirtyReporter(_, skipPropagation) {
|
||||
if (!isDirty) {
|
||||
isDirty = true;
|
||||
dependents.forEach(d => d(_, skipPropagation));
|
||||
}
|
||||
dependents.forEach(runParam);
|
||||
|
||||
if (subscribers.size) {
|
||||
if (subscribers.size && !skipPropagation) {
|
||||
accessor();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export { observable } from './observable';
|
||||
export { computed } from './computed';
|
||||
export { bundle } from './bundle';
|
||||
|
||||
Reference in New Issue
Block a user