-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from ice-lab/refactor-store-bindings
Refactor: bindings & performance
- Loading branch information
Showing
4 changed files
with
191 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); | ||
}); |