Skip to content

Commit

Permalink
Merge pull request #27 from ryuever/ft-when
Browse files Browse the repository at this point in the history
add when/watch support
  • Loading branch information
ryuever committed Dec 6, 2020
2 parents 2560cb5 + 34f93ba commit a66b0d4
Show file tree
Hide file tree
Showing 17 changed files with 668 additions and 4 deletions.
66 changes: 66 additions & 0 deletions src/PathNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { AccessPath, PathNodeChildren, PathNodeProps } from './types';
import Runner from './Runner';

class PathNode {
private _parent: PathNode | null;
private _type: string;
private _prop: string;
private _effects: Array<Runner>;
public children: PathNodeChildren;

constructor(options: PathNodeProps) {
const { parent, type, prop } = options;
this._parent = parent || null;
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<PathNode>((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) {
const currentEffects = node.children[cur]._effects;
currentEffects.push(runner);
runner.addRemover(() => {
const index = currentEffects.indexOf(runner);
if (index !== -1) {
currentEffects.splice(index, 1);
}
});
}
return node.children[cur];
}, this);
} catch (err) {
// console.log('err ', err)
}
}
}

export default PathNode;
159 changes: 159 additions & 0 deletions src/PathTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
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<PendingRunners>;

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<string>) {
return accessPath.reduce((result, cur) => {
return result.children[cur];
}, this.node);
}

peekBaseValue(accessPath: Array<string>) {
return accessPath.reduce((result, cur) => {
return result[cur];
}, this._base);
}

addEffects(runners: Array<Runner>, updateType: UPDATE_TYPE) {
runners.forEach(runner => {
this.pendingRunners.push({ runner, updateType });
});
runners.forEach(runner => runner.markDirty());
}

diff({
path,
value,
}: {
path: Array<string>;
value: {
[key: string]: any;
};
}): Array<PendingRunners> {
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
);
}
);
const copy = this.pendingRunners.slice();
this.pendingRunners = [];
return copy;
}

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);
}

if (!keysToCompare.length) {
if (shallowEqual(baseValue, nextValue)) return;
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;
44 changes: 44 additions & 0 deletions src/Runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { isFunction } from './commons';
import { AccessPath } from './types';

class Runner {
private _accessPaths: Array<AccessPath>;
private _autoRun: Function;
private _removers: Array<Function>;

constructor(props: { accessPaths?: Array<AccessPath>; autoRun: Function }) {
const { accessPaths, autoRun } = props;
this._accessPaths = accessPaths || [];
this._autoRun = autoRun;
this._removers = [];
}

getAccessPaths() {
return this._accessPaths;
}

updateAccessPaths(accessPaths: Array<AccessPath>) {
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;
29 changes: 26 additions & 3 deletions src/StateTrackerUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -50,15 +51,33 @@ const StateTrackerUtil = {
return tracker._stateTrackerContext;
},

relink: function(proxy: IStateTracker, path: Array<string>, value: any) {
internalRelink: function(
proxy: IStateTracker,
path: Array<string>,
value: any
): Array<PendingRunners> {
const tracker = proxy[TRACKER];
const stateContext = tracker._stateTrackerContext;
stateContext.updateTime();
const copy = path.slice();
const last = copy.pop();
const front = copy;
const parentState = this.peek(proxy, front);
const pathTree = collection.getPathTree(proxy);
let pendingRunners = [] as Array<PendingRunners>;
if (pathTree) {
pendingRunners = pathTree.diff({
path,
value,
});
}
parentState[last!] = value;
return pendingRunners;
},

relink: function(proxy: IStateTracker, path: Array<string>, value: any) {
const pendingRunners = this.internalRelink(proxy, path, value);
pendingRunners.forEach(({ runner }) => runner.run());
},

batchRelink: function(proxy: IStateTracker, values: Array<RelinkValue>) {
Expand Down Expand Up @@ -93,13 +112,17 @@ const StateTrackerUtil = {
);

const childProxies = Object.assign({}, tracker._childProxies);
let pendingRunners = [] as Array<PendingRunners>;

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;
Expand Down
46 changes: 46 additions & 0 deletions src/collection.ts
Original file line number Diff line number Diff line change
@@ -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<string, PathTree>;

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();
Loading

0 comments on commit a66b0d4

Please sign in to comment.