From 110faa05a64596801bff0dea349ea82079b0d122 Mon Sep 17 00:00:00 2001 From: youchao liu Date: Sun, 6 Dec 2020 21:38:35 +0800 Subject: [PATCH 1/3] add basic when logic --- src/PathNode.ts | 65 +++++++++++++++++ src/PathTree.ts | 151 ++++++++++++++++++++++++++++++++++++++++ src/Runner.ts | 44 ++++++++++++ src/StateTrackerUtil.ts | 29 +++++++- src/collection.ts | 46 ++++++++++++ src/commons.ts | 63 ++++++++++++++++- src/es5.ts | 9 +++ src/proxy.ts | 9 +++ src/types/index.ts | 3 + src/types/pathNode.ts | 11 +++ src/types/pathTree.ts | 11 +++ src/types/runner.ts | 6 ++ src/when.ts | 38 ++++++++++ 13 files changed, 481 insertions(+), 4 deletions(-) create mode 100644 src/PathNode.ts create mode 100644 src/PathTree.ts create mode 100644 src/Runner.ts create mode 100644 src/collection.ts create mode 100644 src/types/pathNode.ts create mode 100644 src/types/pathTree.ts create mode 100644 src/types/runner.ts create mode 100644 src/when.ts diff --git a/src/PathNode.ts b/src/PathNode.ts new file mode 100644 index 0000000..157934a --- /dev/null +++ b/src/PathNode.ts @@ -0,0 +1,65 @@ +import { AccessPath, PathNodeChildren, PathNodeProps } from './types'; +import Runner from './Runner'; + +class PathNode { + private _parent?: PathNode; + private _type: string; + private _prop: string; + private _effects: Array; + public children: PathNodeChildren; + + constructor(options: PathNodeProps) { + const { parent, type, prop } = options; + this._parent = parent; + this.children = {}; + this._type = type; + this._prop = prop; + this._effects = []; + } + + getType() { + return this._type; + } + + getParent() { + return this._parent; + } + + getProp() { + return this._prop; + } + + getEffects() { + return this._effects; + } + + addRunner(path: AccessPath, runner: Runner) { + try { + const len = path.length; + path.reduce((node: PathNode, cur: string, index: number) => { + // path中前面的值都是为了让我们定位到最后的需要关心的位置 + if (!node.children[cur]) + node.children[cur] = new PathNode({ + type: this._type, + prop: cur, + parent: node, + }); + // 只有到达`path`的最后一个`prop`时,才会进行patcher的添加 + if (index === len - 1) { + runner.addRemover(() => { + const index = this._effects.indexOf(runner); + + if (index !== -1) { + this._effects.splice(index, 1); + } + }); + } + return node.children[cur]; + }, this); + } catch (err) { + // console.log('err ', err) + } + } +} + +export default PathNode; diff --git a/src/PathTree.ts b/src/PathTree.ts new file mode 100644 index 0000000..d886564 --- /dev/null +++ b/src/PathTree.ts @@ -0,0 +1,151 @@ +import PathNode from './PathNode'; +import Runner from './Runner'; +import StateTrackerContext from './StateTrackerContext'; +import { IStateTracker, PendingRunners, ProduceState } from './types'; +import { UPDATE_TYPE } from './types/pathTree'; +import { shallowEqual, isTypeEqual, isPrimitive, isMutable } from './commons'; + +class PathTree { + public node: PathNode; + readonly _state: IStateTracker; + readonly _base: ProduceState; + readonly _stateTrackerContext: StateTrackerContext; + private _updateType: UPDATE_TYPE | null; + public pendingRunners: Array; + + constructor({ + base, + proxyState, + stateTrackerContext, + }: { + proxyState: IStateTracker; + base: ProduceState; + stateTrackerContext: StateTrackerContext; + }) { + this.node = new PathNode({ + type: 'default', + prop: 'root', + }); + this._state = proxyState; + this._base = base; + this._stateTrackerContext = stateTrackerContext; + this.pendingRunners = []; + this._updateType = null; + } + + getUpdateType() { + return this._updateType; + } + + addRunner(runner: Runner) { + const accessPaths = runner.getAccessPaths(); + accessPaths.forEach(accessPath => { + this.node.addRunner(accessPath, runner); + }); + } + + peek(accessPath: Array) { + return accessPath.reduce((result, cur) => { + return result.children[cur]; + }, this.node); + } + + peekBaseValue(accessPath: Array) { + return accessPath.reduce((result, cur) => { + return result[cur]; + }, this._base); + } + + addEffects(runners: Array, updateType: UPDATE_TYPE) { + runners.forEach(runner => { + this.pendingRunners.push({ runner, updateType }); + }); + runners.forEach(runner => runner.markDirty()); + } + + diff({ + path, + value, + }: { + path: Array; + value: { + [key: string]: any; + }; + }): Array { + const affectedNode = this.peek(path); + const baseValue = this.peekBaseValue(path); + if (!affectedNode) return []; + + this.compare( + affectedNode, + baseValue, + value, + (pathNode: PathNode, updateType?: UPDATE_TYPE) => { + this.addEffects( + pathNode.getEffects(), + updateType || UPDATE_TYPE.BASIC_VALUE_CHANGE + ); + } + ); + return this.pendingRunners; + } + + compare( + branch: PathNode, + baseValue: { + [key: string]: any; + }, + nextValue: { + [key: string]: any; + }, + cb: { + (pathNode: PathNode, updateType?: UPDATE_TYPE): void; + } + ) { + const keysToCompare = Object.keys(branch.children); + + if (keysToCompare.indexOf('length') !== -1) { + const oldValue = baseValue.length; + const newValue = nextValue.length; + + if (newValue < oldValue) { + cb(branch.children['length'], UPDATE_TYPE.ARRAY_LENGTH_CHANGE); + return; + } + } + + if (branch.getType() === 'autoRun' && baseValue !== nextValue) { + cb(branch); + } + + keysToCompare.forEach(key => { + const oldValue = baseValue[key]; + const newValue = nextValue[key]; + + if (shallowEqual(oldValue, newValue)) return; + + if (isTypeEqual(oldValue, newValue)) { + if (isPrimitive(newValue)) { + if (oldValue !== newValue) { + const type = + key === 'length' + ? UPDATE_TYPE.ARRAY_LENGTH_CHANGE + : UPDATE_TYPE.BASIC_VALUE_CHANGE; + cb(branch.children[key], type); + } + } + + if (isMutable(newValue)) { + const childBranch = branch.children[key]; + this.compare(childBranch, oldValue, newValue, cb); + return; + } + + return; + } + cb(branch.children[key]); + }); + } +} + +export default PathTree; diff --git a/src/Runner.ts b/src/Runner.ts new file mode 100644 index 0000000..eeb01fa --- /dev/null +++ b/src/Runner.ts @@ -0,0 +1,44 @@ +import { isFunction } from './commons'; +import { AccessPath } from './types'; + +class Runner { + private _accessPaths: Array; + private _autoRun: Function; + private _removers: Array; + + constructor(props: { accessPaths?: Array; autoRun: Function }) { + const { accessPaths, autoRun } = props; + this._accessPaths = accessPaths || []; + this._autoRun = autoRun; + this._removers = []; + } + + getAccessPaths() { + return this._accessPaths; + } + + updateAccessPaths(accessPaths: Array) { + this._accessPaths = accessPaths; + if (this._removers.length) this.teardown(); + } + + // 将patcher从PathNode上删除 + teardown() { + this._removers.forEach(remover => remover()); + this._removers = []; + } + + markDirty() { + this.teardown(); + } + + addRemover(remove: Function) { + this._removers.push(remove); + } + + run() { + if (isFunction(this._autoRun)) this._autoRun(); + } +} + +export default Runner; diff --git a/src/StateTrackerUtil.ts b/src/StateTrackerUtil.ts index 96aeae6..9230cc5 100644 --- a/src/StateTrackerUtil.ts +++ b/src/StateTrackerUtil.ts @@ -4,10 +4,11 @@ import { TRACKER, canIUseProxy, } from './commons'; -import { IStateTracker, RelinkValue } from './types'; +import { IStateTracker, PendingRunners, RelinkValue } from './types'; import { createPlainTrackerObject } from './StateTracker'; import { produce as ES6Produce } from './proxy'; import { produce as ES5Produce } from './es5'; +import collection from './collection'; const StateTrackerUtil = { hasTracker: function(proxy: IStateTracker) { @@ -50,7 +51,11 @@ const StateTrackerUtil = { return tracker._stateTrackerContext; }, - relink: function(proxy: IStateTracker, path: Array, value: any) { + internalRelink: function( + proxy: IStateTracker, + path: Array, + value: any + ): Array { const tracker = proxy[TRACKER]; const stateContext = tracker._stateTrackerContext; stateContext.updateTime(); @@ -58,7 +63,21 @@ const StateTrackerUtil = { const last = copy.pop(); const front = copy; const parentState = this.peek(proxy, front); + const pathTree = collection.getPathTree(proxy); + let pendingRunners = [] as Array; + if (pathTree) { + pendingRunners = pathTree.diff({ + path, + value, + }); + } parentState[last!] = value; + return pendingRunners; + }, + + relink: function(proxy: IStateTracker, path: Array, value: any) { + const pendingRunners = this.internalRelink(proxy, path, value); + pendingRunners.forEach(({ runner }) => runner.run()); }, batchRelink: function(proxy: IStateTracker, values: Array) { @@ -93,13 +112,17 @@ const StateTrackerUtil = { ); const childProxies = Object.assign({}, tracker._childProxies); + let pendingRunners = [] as Array; values.forEach(({ path, value }) => { - this.relink(proxy, path, value); + const runners = this.internalRelink(proxy, path, value); + pendingRunners = pendingRunners.concat(runners); // unchanged object's proxy object will be preserved. delete childProxies[path[0]]; }); + pendingRunners.forEach(({ runner }) => runner.run()); + newTracker._childProxies = childProxies; return proxyStateCopy; diff --git a/src/collection.ts b/src/collection.ts new file mode 100644 index 0000000..f1725c7 --- /dev/null +++ b/src/collection.ts @@ -0,0 +1,46 @@ +import PathTree from './PathTree'; +import StateTrackerContext from './StateTrackerContext'; +import { IStateTracker, ProduceState } from './types'; +import StateTrackerUtil from './StateTrackerUtil'; + +class Collection { + private _trees: Map; + + constructor() { + this._trees = new Map(); + } + + register(props: { + base: ProduceState; + proxyState: IStateTracker; + stateTrackerContext: StateTrackerContext; + }) { + const { base, proxyState, stateTrackerContext } = props; + const id = stateTrackerContext.getId(); + if (this._trees.has(id)) + throw new Error( + `base value ${base} has been bound with ${stateTrackerContext}` + ); + this._trees.set( + id, + new PathTree({ + base, + proxyState, + stateTrackerContext, + }) + ); + } + + getPathTree(state: IStateTracker) { + const context = StateTrackerUtil.getTracker(state)._stateTrackerContext; + const contextId = context.getId(); + + if (!this._trees.has(contextId)) + throw new Error( + `state ${state} should be called with 'produce' function first` + ); + return this._trees.get(contextId); + } +} + +export default new Collection(); diff --git a/src/commons.ts b/src/commons.ts index f2c08c6..e8c3871 100644 --- a/src/commons.ts +++ b/src/commons.ts @@ -39,10 +39,16 @@ export const isTrackable = (o: any) => { // eslint-disable-line return ['[object Object]', '[object Array]'].indexOf(toString(o)) !== -1; }; +export const isNumber = (obj: any) => toString(obj) === '[object Number]'; +export const isString = (obj: any) => toString(obj) === '[object String]'; +export const isBoolean = (obj: any) => toString(obj) === '[object Boolean]'; +export const isMutable = (obj: any) => isObject(obj) || isArray(obj); +export const isPrimitive = (obj: any) => + isNumber(obj) || isString(obj) || isBoolean(obj); export const isTypeEqual = (a: any, b: any) => toString(a) === toString(b); export const isArray = (a: any) => Array.isArray(a); export const isPlainObject = (a: any) => toString(a) === '[object Object]'; - +export const isFunction = (a: any) => toString(a) === '[object Function]'; type EachArray = (index: number, entry: any, obj: T) => void; type EachObject = (key: K, entry: T[K], obj: T) => number; @@ -134,3 +140,58 @@ export const generateRandomKey = (prefix = '') => { export const generateRandomContextKey = () => generateRandomKey('__context_'); export const generateRandomFocusKey = () => generateRandomKey('__focus_'); + +/** + * inlined Object.is polyfill to avoid requiring consumers ship their own + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is + */ +function is(x: any, y: any) { + // SameValue algorithm + if (x === y) { + // Steps 1-5, 7-10 + // Steps 6.b-6.e: +0 != -0 + // Added the nonzero y check to make Flow happy, but it is redundant + return x !== 0 || y !== 0 || 1 / x === 1 / y; + } + // Step 6.a: NaN == NaN + return x !== x && y !== y; +} + +/** + * Performs equality by iterating through keys on an object and returning false + * when any key has values which are not strictly equal between the arguments. + * Returns true when the values of all keys are strictly equal. + */ +export function shallowEqual(objA: any, objB: any) { + if (is(objA, objB)) { + return true; + } + + if ( + typeof objA !== 'object' || + objA === null || + typeof objB !== 'object' || + objB === null + ) { + return false; + } + + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); + + if (keysA.length !== keysB.length) { + return false; + } + + // Test for A's keys different from B. + for (let i = 0; i < keysA.length; i++) { + if ( + !Object.prototype.hasOwnProperty.call(objB, keysA[i]) || + !is(objA[keysA[i]], objB[keysA[i]]) + ) { + return false; + } + } + + return true; +} diff --git a/src/es5.ts b/src/es5.ts index 1896daa..705a229 100644 --- a/src/es5.ts +++ b/src/es5.ts @@ -18,6 +18,7 @@ import { import StateTrackerContext from './StateTrackerContext'; import PathTracker from './PathTracker'; import StateTrackerUtil from './StateTrackerUtil'; +import collection from './collection'; function produce(state: ProduceState, options?: ProduceOptions): IStateTracker { const { @@ -296,6 +297,14 @@ function produce(state: ProduceState, options?: ProduceOptions): IStateTracker { }); } + if (!stateTrackerContext) { + collection.register({ + base: shadowBase, + proxyState: state as IStateTracker, + stateTrackerContext: trackerContext, + }); + } + return state as IStateTracker; } diff --git a/src/proxy.ts b/src/proxy.ts index 3b8fd42..7262096 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -20,6 +20,7 @@ import { } from './types'; import StateTrackerContext from './StateTrackerContext'; import StateTrackerUtil from './StateTrackerUtil'; +import collection from './collection'; function produce( state: ProduceState, @@ -242,6 +243,14 @@ function produce( return tracker._base; }); + if (!stateTrackerContext) { + collection.register({ + base: state, + proxyState: proxy, + stateTrackerContext: trackerContext, + }); + } + return proxy as IStateTracker; } diff --git a/src/types/index.ts b/src/types/index.ts index 0495367..273f198 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,6 @@ export * from './commons'; export * from './produce'; export * from './stateTracker'; +export * from './pathNode'; +export * from './runner'; +export * from './pathTree'; diff --git a/src/types/pathNode.ts b/src/types/pathNode.ts new file mode 100644 index 0000000..232780b --- /dev/null +++ b/src/types/pathNode.ts @@ -0,0 +1,11 @@ +import PathNode from '../PathNode'; + +export type PathNodeProps = { + parent?: PathNode; + prop: string; + type: string; +}; + +export type PathNodeChildren = { + [key: string]: PathNode; +}; diff --git a/src/types/pathTree.ts b/src/types/pathTree.ts new file mode 100644 index 0000000..3b46746 --- /dev/null +++ b/src/types/pathTree.ts @@ -0,0 +1,11 @@ +import Runner from '../Runner'; + +export enum UPDATE_TYPE { + ARRAY_LENGTH_CHANGE = 'array_length_change', + BASIC_VALUE_CHANGE = 'basic_value_change', +} + +export type PendingRunners = { + runner: Runner; + updateType: UPDATE_TYPE; +}; diff --git a/src/types/runner.ts b/src/types/runner.ts new file mode 100644 index 0000000..0bd8f28 --- /dev/null +++ b/src/types/runner.ts @@ -0,0 +1,6 @@ +export type AccessPath = Array; + +export type RunnerProps = { + autoRun: Function; + accessPath: AccessPath; +}; diff --git a/src/when.ts b/src/when.ts new file mode 100644 index 0000000..3ea45b1 --- /dev/null +++ b/src/when.ts @@ -0,0 +1,38 @@ +import { IStateTracker } from './types'; +import StateTrackerUtil from './StateTrackerUtil'; +import collection from './collection'; +import { isFunction } from './commons'; +import Runner from './Runner'; + +let count = 0; + +function when( + state: IStateTracker, + predicate: (state: IStateTracker) => boolean, + effect: () => void +) { + const id = `when_${count++}`; + const autoRunFn = () => { + state.enter(id); + const falsy = predicate(state); + if (!falsy) { + const tracker = StateTrackerUtil.getContext(state).getCurrent(); + const paths = tracker.getRemarkable(); + const pathTree = collection.getPathTree(state); + runner.updateAccessPaths(paths); + pathTree!.addRunner(runner); + } else if (isFunction(effect)) { + effect(); + } + state.leave(); + return falsy; + }; + + const runner = new Runner({ + autoRun: autoRunFn, + }); + + autoRunFn(); +} + +export default when; From 0648c6ef8e04bed65449c39c962657fd40bf041a Mon Sep 17 00:00:00 2001 From: youchao liu Date: Sun, 6 Dec 2020 22:58:27 +0800 Subject: [PATCH 2/3] add when test case --- src/PathNode.ts | 11 +++--- src/PathTree.ts | 5 ++- src/commons.ts | 2 +- src/index.ts | 1 + src/when.ts | 11 +++--- test/when.test.ts | 89 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 test/when.test.ts diff --git a/src/PathNode.ts b/src/PathNode.ts index 157934a..1767b89 100644 --- a/src/PathNode.ts +++ b/src/PathNode.ts @@ -2,7 +2,7 @@ import { AccessPath, PathNodeChildren, PathNodeProps } from './types'; import Runner from './Runner'; class PathNode { - private _parent?: PathNode; + private _parent: PathNode | null; private _type: string; private _prop: string; private _effects: Array; @@ -10,7 +10,7 @@ class PathNode { constructor(options: PathNodeProps) { const { parent, type, prop } = options; - this._parent = parent; + this._parent = parent || null; this.children = {}; this._type = type; this._prop = prop; @@ -46,11 +46,12 @@ class PathNode { }); // 只有到达`path`的最后一个`prop`时,才会进行patcher的添加 if (index === len - 1) { + const currentEffects = node.children[cur]._effects; + currentEffects.push(runner); runner.addRemover(() => { - const index = this._effects.indexOf(runner); - + const index = currentEffects.indexOf(runner); if (index !== -1) { - this._effects.splice(index, 1); + currentEffects.splice(index, 1); } }); } diff --git a/src/PathTree.ts b/src/PathTree.ts index d886564..3518e14 100644 --- a/src/PathTree.ts +++ b/src/PathTree.ts @@ -74,6 +74,7 @@ class PathTree { }): Array { const affectedNode = this.peek(path); const baseValue = this.peekBaseValue(path); + if (!affectedNode) return []; this.compare( @@ -87,7 +88,9 @@ class PathTree { ); } ); - return this.pendingRunners; + const copy = this.pendingRunners.slice(); + this.pendingRunners = []; + return copy; } compare( diff --git a/src/commons.ts b/src/commons.ts index e8c3871..2ed7ca3 100644 --- a/src/commons.ts +++ b/src/commons.ts @@ -154,7 +154,7 @@ function is(x: any, y: any) { return x !== 0 || y !== 0 || 1 / x === 1 / y; } // Step 6.a: NaN == NaN - return x !== x && y !== y; + return x !== x && y !== y; // eslint-disable-line } /** diff --git a/src/index.ts b/src/index.ts index b20ce45..1758c42 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,3 +15,4 @@ if (canIUseProxy()) { export default produce; export * from './types'; export { StateTrackerUtil }; +export { default as when } from './when'; diff --git a/src/when.ts b/src/when.ts index 3ea45b1..317c4b6 100644 --- a/src/when.ts +++ b/src/when.ts @@ -4,16 +4,13 @@ import collection from './collection'; import { isFunction } from './commons'; import Runner from './Runner'; -let count = 0; - function when( state: IStateTracker, predicate: (state: IStateTracker) => boolean, - effect: () => void + effect?: () => void ) { - const id = `when_${count++}`; const autoRunFn = () => { - state.enter(id); + StateTrackerUtil.enter(state); const falsy = predicate(state); if (!falsy) { const tracker = StateTrackerUtil.getContext(state).getCurrent(); @@ -22,9 +19,9 @@ function when( runner.updateAccessPaths(paths); pathTree!.addRunner(runner); } else if (isFunction(effect)) { - effect(); + effect!(); } - state.leave(); + StateTrackerUtil.leave(state); return falsy; }; diff --git a/test/when.test.ts b/test/when.test.ts new file mode 100644 index 0000000..c2ea6e3 --- /dev/null +++ b/test/when.test.ts @@ -0,0 +1,89 @@ +import { produce as ES5Produce } from '../src/es5'; +import { produce as ES6Produce } from '../src/proxy'; +import StateTrackerUtil from '../src/StateTrackerUtil'; +import when from '../src/when'; + +testTracker(true); +testTracker(false); + +function testTracker(useProxy: boolean) { + const produce = useProxy ? ES6Produce : ES5Produce; + const decorateDesc = (text: string) => + useProxy ? `proxy: ${text}` : `es5: ${text}`; + + describe(decorateDesc('basic'), () => { + it('clean if return true', () => { + const state = { + a: { + a1: [{ value: 0 }, { value: 1 }], + a2: 1, + }, + }; + let count = 0; + + const proxyState = produce(state); + when(proxyState, state => { + count++; + return state.a.a2 === 3; + }); + + StateTrackerUtil.relink(proxyState, ['a'], { + a2: 2, + }); + + expect(count).toBe(2); + + StateTrackerUtil.relink(proxyState, ['a'], { + a2: 3, + }); + expect(count).toBe(3); + + StateTrackerUtil.relink(proxyState, ['a'], { + a2: 4, + }); + expect(count).toBe(3); + }); + + it('effect will be call when return true', () => { + const state = { + a: { + a1: [{ value: 0 }, { value: 1 }], + a2: 1, + }, + }; + let count = 0; + let finished = false; + + const proxyState = produce(state); + when( + proxyState, + state => { + count++; + return state.a.a2 === 3; + }, + () => { + finished = true; + } + ); + + StateTrackerUtil.relink(proxyState, ['a'], { + a2: 2, + }); + + expect(count).toBe(2); + expect(finished).toBe(false); + + StateTrackerUtil.relink(proxyState, ['a'], { + a2: 3, + }); + expect(count).toBe(3); + expect(finished).toBe(true); + + StateTrackerUtil.relink(proxyState, ['a'], { + a2: 4, + }); + expect(count).toBe(3); + expect(finished).toBe(true); + }); + }); +} From 34f93ba5e4aeff1420b73b7e695271235224e850 Mon Sep 17 00:00:00 2001 From: youchao liu Date: Sun, 6 Dec 2020 23:31:49 +0800 Subject: [PATCH 3/3] predicate function only be called when used property changed --- src/PathTree.ts | 5 +++++ src/index.ts | 1 + src/watch.ts | 11 +++++++++++ test/watch.test.ts | 47 ++++++++++++++++++++++++++++++++++++++++++++++ test/when.test.ts | 32 +++++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+) create mode 100644 src/watch.ts create mode 100644 test/watch.test.ts diff --git a/src/PathTree.ts b/src/PathTree.ts index 3518e14..14cc411 100644 --- a/src/PathTree.ts +++ b/src/PathTree.ts @@ -121,6 +121,11 @@ class PathTree { cb(branch); } + if (!keysToCompare.length) { + if (shallowEqual(baseValue, nextValue)) return; + cb(branch); + } + keysToCompare.forEach(key => { const oldValue = baseValue[key]; const newValue = nextValue[key]; diff --git a/src/index.ts b/src/index.ts index 1758c42..552381c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,3 +16,4 @@ export default produce; export * from './types'; export { StateTrackerUtil }; export { default as when } from './when'; +export { default as watch } from './watch'; diff --git a/src/watch.ts b/src/watch.ts new file mode 100644 index 0000000..7946d41 --- /dev/null +++ b/src/watch.ts @@ -0,0 +1,11 @@ +import { IStateTracker } from './types'; +import when from './when'; + +function watch(state: IStateTracker, fn: Function) { + when(state, state => { + fn(state); + return false; + }); +} + +export default watch; diff --git a/test/watch.test.ts b/test/watch.test.ts new file mode 100644 index 0000000..9a0ea5e --- /dev/null +++ b/test/watch.test.ts @@ -0,0 +1,47 @@ +import { produce as ES5Produce } from '../src/es5'; +import { produce as ES6Produce } from '../src/proxy'; +import StateTrackerUtil from '../src/StateTrackerUtil'; +import watch from '../src/watch'; + +testTracker(true); +testTracker(false); + +function testTracker(useProxy: boolean) { + const produce = useProxy ? ES6Produce : ES5Produce; + const decorateDesc = (text: string) => + useProxy ? `proxy: ${text}` : `es5: ${text}`; + + describe(decorateDesc('basic'), () => { + it('fn will be called on every relink function', () => { + const state = { + a: { + a1: [{ value: 0 }, { value: 1 }], + a2: 1, + }, + }; + let count = 0; + + const proxyState = produce(state); + watch(proxyState, (state: any) => { + count++; + return state.a.a2 === 3; + }); + + StateTrackerUtil.relink(proxyState, ['a'], { + a2: 2, + }); + + expect(count).toBe(2); + + StateTrackerUtil.relink(proxyState, ['a'], { + a2: 3, + }); + expect(count).toBe(3); + + StateTrackerUtil.relink(proxyState, ['a'], { + a2: 4, + }); + expect(count).toBe(4); + }); + }); +} diff --git a/test/when.test.ts b/test/when.test.ts index c2ea6e3..7588960 100644 --- a/test/when.test.ts +++ b/test/when.test.ts @@ -44,6 +44,38 @@ function testTracker(useProxy: boolean) { expect(count).toBe(3); }); + it('predicate will be called only its watched variable value change', () => { + const state = { + a: { + a1: [{ value: 0 }, { value: 1 }], + a2: 1, + }, + }; + let count = 0; + + const proxyState = produce(state); + when(proxyState, state => { + count++; + return state.a.a2 === 3; + }); + + StateTrackerUtil.relink(proxyState, ['a', 'a2'], 2); + + expect(count).toBe(2); + + StateTrackerUtil.relink(proxyState, ['a', 'a1'], 2); + expect(count).toBe(2); + + StateTrackerUtil.relink(proxyState, ['a', 'a1'], 3); + expect(count).toBe(2); + + StateTrackerUtil.relink(proxyState, ['a', 'a2'], 3); + expect(count).toBe(3); + + StateTrackerUtil.relink(proxyState, ['a', 'a2'], 3); + expect(count).toBe(3); + }); + it('effect will be call when return true', () => { const state = { a: {