Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ft when #27

Merged
merged 3 commits into from
Dec 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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