Skip to content

Commit

Permalink
Merge pull request #15 from ice-lab/refactor-store-bindings
Browse files Browse the repository at this point in the history
Refactor: bindings & performance
  • Loading branch information
alvinhui authored Jul 3, 2019
2 parents 3c05293 + a5b5240 commit ed13d50
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 25 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ice/store",
"version": "0.1.4",
"version": "0.1.5",
"description": "基于 React Hooks 的数据流方案",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down Expand Up @@ -55,7 +55,8 @@
"typescript": "^3.4.4"
},
"dependencies": {
"lodash.isfunction": "^3.0.9"
"lodash.isfunction": "^3.0.9",
"lodash.isobject": "^3.0.2"
},
"peerDependencies": {
"react": "^16.7.0"
Expand Down
93 changes: 70 additions & 23 deletions src/store.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,105 @@

import * as isFunction from 'lodash.isfunction';
import { useState, useEffect } from 'react';
import { addProxy } from './util';

interface MethodFunc {
(): void;
}

export default class Store {
private state: {[name: string]: any} = {};

private methods: {[name: string]: MethodFunc} = {};
// store state and actions user defined
private bindings: {[name: string]: any} = {};

// queue of setState method from useState hook
private queue = [];

// flag of whether allow state mutate
private allowMutate = false;

// flag of whether state changed after mutation
private stateChanged = false;

public constructor(bindings: object) {
Object.keys(bindings).forEach((key) => {
const value = bindings[key];

if (isFunction(value)) {
this.methods[key] = this.createMethod(value);
} else {
this.state[key] = value;
}
this.bindings[key] = isFunction(value) ? this.createAction(value) : value;
});
}

private getBindings() {
return { ...this.state, ...this.methods };
const handler = {
set: (target, prop, value) => {
if (!this.allowMutate) {
console.error('Forbid modifying state directly, use action to modify instead.');
return false;
}
if (target[prop] !== value) {
this.stateChanged = true;
}
/* eslint no-param-reassign: 0 */
target[prop] = addProxy(value, handler);
return true;
},
};

this.bindings = addProxy(this.bindings, handler);
}

private createMethod(fun): MethodFunc {
/**
* Create action which will trigger state update after mutation
* @param {func} fun - original method user defined
* @return {func} action function
*/
private createAction(fun): MethodFunc {
return async (...args) => {
const newState = { ...this.state };
await fun.apply(newState, args);
this.setState(newState);
return this.getBindings();
this.allowMutate = true;
await fun.apply(this.bindings, args);
if (this.stateChanged) {
this.setState();
}
this.allowMutate = false;
this.stateChanged = false;
return this.bindings;
};
}

private setState(newState: object): void {
this.state = newState;
/**
* Get state from bindings
* @return {object} state
*/
private getState(): object {
const { bindings } = this;
const state = {};
Object.keys(bindings).forEach((key) => {
const value = bindings[key];
if (!isFunction(value)) {
state[key] = value;
}
});
return state;
}

/**
* Trigger setState method in queue
*/
private setState(): void {
const state = this.getState();
const newState = { ...state };
this.queue.forEach(setState => setState(newState));
}

/**
* Hook used to register setState and expose bindings
* @return {object} bindings of store
*/
public useStore(): object {
const [, setState] = useState(this.state);
const state = this.getState();
const [, setState] = useState(state);
useEffect(() => {
this.queue.push(setState);
return () => {
const index = this.queue.indexOf(setState);
this.queue.splice(index, 1);
};
}, []);

return this.getBindings();
return this.bindings;
}
}
33 changes: 33 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as isObject from 'lodash.isobject';
import * as isFunction from 'lodash.isfunction';

const { isArray } = Array;

/**
* Recursively add proxy to object and array
* @param {*} value - variable of any type
* @param {object} handler - proxy handler
* @return {object} new proxy object
*/
/* eslint no-param-reassign: 0, import/prefer-default-export: 0 */
export function addProxy(value: any, handler: object): any {
// primitive type (number, boolean, string, null, undefined) and function type
if (!isObject(value) || isFunction(value)) {
return value;
}

if (isArray(value)) { // array type
value.forEach((item, index) => {
if (isObject(item)) { // object and array type
value[index] = addProxy(item, handler);
}
});
} else if (isObject(value)) { // object type
Object.keys(value).forEach((key) => {
if (isObject(value[key])) { // object and array type
value[key] = addProxy(value[key], handler);
}
});
}
return new Proxy(value, handler);
}
85 changes: 85 additions & 0 deletions tests/util.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { addProxy } from '../src/util';

describe('#util', () => {
let handler;

beforeEach(() => {
handler = {
set: (target, prop): boolean => {
/* eslint no-param-reassign: 0 */
target[prop] = 'foo';
return true;
},
};
});

describe('#addProxy', () => {
test('should boolean type not affected', () => {
const value = false;
expect(addProxy(value, handler)).toBe(value);
});

test('should number type not affected', () => {
const value = 1;
expect(addProxy(value, handler)).toBe(value);
});

test('should string type not affected', () => {
const value = 'abc';
expect(addProxy(value, handler)).toBe(value);
});

test('should null type not affected', () => {
const value = null;
expect(addProxy(value, handler)).toBe(value);
});

test('should undefined type not affected', () => {
const value = undefined;
expect(addProxy(value, handler)).toBe(value);
});

test('should function type not affected', () => {
const value = () => {};
expect(addProxy(value, handler)).toBe(value);
});

test('should object proxy set success', () => {
const value = {
a: 1,
b: 2,
};
const result = addProxy(value, handler);
result.a = 100;
expect(result.a).toBe('foo');
});

test('should object proxy recursively set success', () => {
const value = {
a: [
{ c: 3 },
],
b: 2,
};
const result = addProxy(value, handler);
result.a[0].c = 4;
expect(result.a[0].c).toBe('foo');
});

test('should array proxy set success', () => {
const value = [1, 2];
const result = addProxy(value, handler);
result[0] = 4;
expect(result[0]).toBe('foo');
});

test('should array proxy recursively set success', () => {
const value = [
{ a: 1 },
];
const result = addProxy(value, handler);
result[0].a = 4;
expect(result[0].a).toBe('foo');
});
});
});

0 comments on commit ed13d50

Please sign in to comment.