Skip to content

Commit 0b7f508

Browse files
committed
Improved looping; simplify assign; add test for listeners and unsubscribing
1 parent e8e468c commit 0b7f508

File tree

9 files changed

+121
-46
lines changed

9 files changed

+121
-46
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ package-lock.json
77
/preact.js.map
88
/react.js
99
/react.js.map
10+
.vscode/

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,10 @@ You can find the library on `window.unistore`.
6969
import createStore from 'unistore'
7070
import { Provider, connect } from 'unistore/preact'
7171

72-
let store = createStore({ count: 0 })
72+
const store = createStore({ count: 0 })
7373

7474
// If actions is a function, it gets passed the store:
75-
let actions = store => ({
75+
const actions = store => ({
7676
// Actions can just return a state update:
7777
increment(state) {
7878
return { count: state.count+1 }
@@ -90,7 +90,7 @@ let actions = store => ({
9090

9191
// Async actions can be pure async/promise functions:
9292
async getStuff(state) {
93-
let res = await fetch('/foo.json')
93+
const res = await fetch('/foo.json')
9494
return { stuff: await res.json() }
9595
},
9696

@@ -126,8 +126,8 @@ Make sure to have [Redux devtools extension](https://github.com/zalmoxisus/redux
126126
import createStore from 'unistore'
127127
import devtools from 'unistore/devtools'
128128

129-
let initialState = { count: 0 };
130-
let store = process.env.NODE_ENV === 'production' ? createStore(initialState) : devtools(createStore(initialState));
129+
const initialState = { count: 0 };
130+
const store = process.env.NODE_ENV === 'production' ? createStore(initialState) : devtools(createStore(initialState));
131131

132132
// ...
133133
```

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
"scripts": {
1111
"build": "npm-run-all --silent -p build:main build:integrations build:combined -s size docs",
1212
"build:main": "microbundle",
13-
"build:integrations": "microbundle src/integrations/*.js -o x.js -f cjs",
14-
"build:combined": "microbundle src/combined/*.js -o full/x.js",
13+
"build:integrations": "microbundle \"src/integrations/*.js\" -o x.js -f cjs",
14+
"build:combined": "microbundle \"src/combined/*.js\" -o full/x.js",
1515
"size": "strip-json-comments --no-whitespace dist/unistore.js | gzip-size && bundlesize",
1616
"docs": "documentation readme unistore.js -q --section API && npm run -s fixreadme",
1717
"fixreadme": "node -e 'var fs=require(\"fs\");fs.writeFileSync(\"README.md\", fs.readFileSync(\"README.md\", \"utf8\").replace(/^- /gm, \"- \"))'",
1818
"test": "eslint src && npm run build && jest",
19+
"test:jest": "jest",
1920
"prepare": "npm t",
2021
"release": "npm t && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish"
2122
},
@@ -28,7 +29,7 @@
2829
"bundlesize": [
2930
{
3031
"path": "full/preact.js",
31-
"maxSize": "750b"
32+
"maxSize": "744b"
3233
},
3334
{
3435
"path": "dist/unistore.js",

src/index.js

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,18 @@ import { assign } from './util';
1111
* store.setState({ c: 'd' }); // logs { a: 'b', c: 'd' }
1212
*/
1313
export default function createStore(state) {
14-
let listeners = [];
14+
const listeners = [];
1515
state = state || {};
1616

1717
function unsubscribe(listener) {
18-
let out = [];
19-
for (let i=0; i<listeners.length; i++) {
20-
if (listeners[i]===listener) {
21-
listener = null;
22-
}
23-
else {
24-
out.push(listeners[i]);
25-
}
26-
}
27-
listeners = out;
18+
const i = listeners.indexOf(listener);
19+
~i && listeners.splice(i, 1);
2820
}
2921

3022
function setState(update, overwrite, action) {
31-
state = overwrite ? update : assign(assign({}, state), update);
32-
let currentListeners = listeners;
33-
for (let i=0; i<currentListeners.length; i++) currentListeners[i](state, action);
23+
state = overwrite ? update : assign({}, state, update);
24+
let i = listeners.length;
25+
while (i-- > 0) listeners[i](state, action);
3426
}
3527

3628
/** An observable state container, returned from {@link createStore}
@@ -46,15 +38,15 @@ export default function createStore(state) {
4638
* @returns {Function} boundAction()
4739
*/
4840
action(action) {
49-
function apply(result) {
50-
setState(result, false, action);
41+
function apply(update) {
42+
setState(update, false, action);
5143
}
5244

5345
// Note: perf tests verifying this implementation: https://esbench.com/bench/5a295e6299634800a0349500
5446
return function() {
55-
let args = [state];
56-
for (let i=0; i<arguments.length; i++) args.push(arguments[i]);
57-
let ret = action.apply(this, args);
47+
const args = [state];
48+
for (let i = 0; i<arguments.length; i++) args.push(arguments[i]);
49+
const ret = action.apply(this, args);
5850
if (ret!=null) {
5951
if (ret.then) ret.then(apply);
6052
else apply(ret);

src/integrations/preact.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ export function connect(mapStateToProps, actions) {
2121
return Child => {
2222
function Wrapper(props, { store }) {
2323
let state = mapStateToProps(store ? store.getState() : {}, props);
24-
let boundActions = actions ? mapActions(actions, store) : { store };
25-
let update = () => {
24+
const boundActions = actions ? mapActions(actions, store) : { store };
25+
const update = () => {
2626
let mapped = mapStateToProps(store ? store.getState() : {}, this.props);
2727
for (let i in mapped) if (mapped[i]!==state[i]) {
2828
state = mapped;
@@ -40,7 +40,7 @@ export function connect(mapStateToProps, actions) {
4040
this.componentWillUnmount = () => {
4141
store.unsubscribe(update);
4242
};
43-
this.render = props => h(Child, assign(assign(assign({}, boundActions), props), state));
43+
this.render = props => h(Child, assign({}, boundActions, props, state));
4444
}
4545
return (Wrapper.prototype = new Component()).constructor = Wrapper;
4646
};

src/integrations/react.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ export function connect(mapStateToProps, actions) {
2727
Component.call(this, props, context);
2828
let { store } = context;
2929
let state = mapStateToProps(store ? store.getState() : {}, props);
30-
let boundActions = actions ? mapActions(actions, store) : { store };
31-
let update = () => {
32-
let mapped = mapStateToProps(store ? store.getState() : {}, this.props);
30+
const boundActions = actions ? mapActions(actions, store) : { store };
31+
const update = () => {
32+
const mapped = mapStateToProps(store ? store.getState() : {}, this.props);
3333
for (let i in mapped) if (mapped[i]!==state[i]) {
3434
state = mapped;
3535
return this.forceUpdate();
@@ -46,7 +46,7 @@ export function connect(mapStateToProps, actions) {
4646
this.componentWillUnmount = () => {
4747
store.unsubscribe(update);
4848
};
49-
this.render = () => createElement(Child, assign(assign(assign({}, boundActions), this.props), state));
49+
this.render = () => createElement(Child, assign({}, boundActions, this.props, state));
5050
}
5151
Wrapper.contextTypes = CONTEXT_TYPES;
5252
return (Wrapper.prototype = Object.create(Component.prototype)).constructor = Wrapper;

src/util.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
// Bind an object/factory of actions to the store and wrap them.
22
export function mapActions(actions, store) {
33
if (typeof actions==='function') actions = actions(store);
4-
let mapped = {};
5-
for (let i in actions) {
6-
mapped[i] = store.action(actions[i]);
7-
}
4+
const mapped = {};
5+
for (let i in actions) mapped[i] = store.action(actions[i]);
86
return mapped;
97
}
108

@@ -13,17 +11,19 @@ export function mapActions(actions, store) {
1311
export function select(properties) {
1412
if (typeof properties==='string') properties = properties.split(/\s*,\s*/);
1513
return state => {
16-
let selected = {};
17-
for (let i=0; i<properties.length; i++) {
18-
selected[properties[i]] = state[properties[i]];
19-
}
14+
const selected = {};
15+
let i = properties.length;
16+
while (i-- > 0) selected[properties[i]] = state[properties[i]];
2017
return selected;
2118
};
2219
}
2320

2421

25-
// Lighter Object.assign stand-in
26-
export function assign(obj, props) {
27-
for (let i in props) obj[i] = props[i];
28-
return obj;
22+
// Lighter Object.assign clone
23+
export function assign(_) {
24+
for (let i = 1; i<arguments.length; i++)
25+
for (let j in arguments[i])
26+
arguments[0][j] = arguments[i][j];
27+
28+
return arguments[0];
2929
}

test/preact/unistore.test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,52 @@ describe('createStore()', () => {
5151
expect(sub2).toBeCalledWith(store.getState(), action);
5252
});
5353

54+
it('should invoke all subscriptions at time of setState, no matter unsubscriptions', () => {
55+
let store = createStore();
56+
57+
let called = [];
58+
let unsub2;
59+
60+
let sub1 = jest.fn(() => {
61+
called.push(1);
62+
});
63+
let sub2 = jest.fn(() => {
64+
called.push(2);
65+
unsub2(); // unsubscribe during a listener callback
66+
});
67+
let sub3 = jest.fn(() => {
68+
called.push(3);
69+
});
70+
71+
let unsub1 = store.subscribe(sub1);
72+
unsub2 = store.subscribe(sub2);
73+
let unsub3 = store.subscribe(sub3);
74+
75+
store.setState({ a: 'a' });
76+
77+
expect(sub1).toHaveBeenCalledTimes(1);
78+
expect(sub2).toHaveBeenCalledTimes(1);
79+
expect(sub3).toHaveBeenCalledTimes(1);
80+
expect(called.sort()).toEqual([1, 2, 3]);
81+
82+
store.setState({ a: 'b' });
83+
84+
expect(sub1).toHaveBeenCalledTimes(2);
85+
expect(sub2).toHaveBeenCalledTimes(1);
86+
expect(sub3).toHaveBeenCalledTimes(2);
87+
expect(called.sort()).toEqual([1, 1, 2, 3, 3]);
88+
89+
unsub1();
90+
unsub3();
91+
92+
store.setState({ a: 'c' });
93+
94+
expect(sub1).toHaveBeenCalledTimes(2);
95+
expect(sub2).toHaveBeenCalledTimes(1);
96+
expect(sub3).toHaveBeenCalledTimes(2);
97+
expect(called.sort()).toEqual([1, 1, 2, 3, 3]);
98+
});
99+
54100
it('should unsubscribe', () => {
55101
let store = createStore();
56102

test/react/unistore.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,41 @@ describe('createStore()', () => {
5151
expect(sub1).toHaveBeenLastCalledWith(store.getState(), undefined);
5252
expect(sub2).toBeCalledWith(store.getState(), undefined);
5353
});
54+
it('should invoke all subscriptions at time of setState, no matter unsubscriptions', () => {
55+
let store = createStore();
56+
let called = [];
57+
let unsub2;
58+
let sub1 = jest.fn(() => {
59+
called.push(1);
60+
});
61+
let sub2 = jest.fn(() => {
62+
called.push(2);
63+
unsub2(); // unsubscribe during a listener callback
64+
});
65+
let sub3 = jest.fn(() => {
66+
called.push(3);
67+
});
68+
let unsub1 = store.subscribe(sub1);
69+
unsub2 = store.subscribe(sub2);
70+
let unsub3 = store.subscribe(sub3);
71+
store.setState({ a: 'a' });
72+
expect(sub1).toHaveBeenCalledTimes(1);
73+
expect(sub2).toHaveBeenCalledTimes(1);
74+
expect(sub3).toHaveBeenCalledTimes(1);
75+
expect(called.sort()).toEqual([1, 2, 3]);
76+
store.setState({ a: 'b' });
77+
expect(sub1).toHaveBeenCalledTimes(2);
78+
expect(sub2).toHaveBeenCalledTimes(1);
79+
expect(sub3).toHaveBeenCalledTimes(2);
80+
expect(called.sort()).toEqual([1, 1, 2, 3, 3]);
81+
unsub1();
82+
unsub3();
83+
store.setState({ a: 'c' });
84+
expect(sub1).toHaveBeenCalledTimes(2);
85+
expect(sub2).toHaveBeenCalledTimes(1);
86+
expect(sub3).toHaveBeenCalledTimes(2);
87+
expect(called.sort()).toEqual([1, 1, 2, 3, 3]);
88+
});
5489
it('should unsubscribe', () => {
5590
let store = createStore();
5691
let sub1 = jest.fn();

0 commit comments

Comments
 (0)