limu
Version:
A fast js lib of immutable data, based on shallow copy on read and mark modified on write mechanism
236 lines (207 loc) • 7.92 kB
text/typescript
/*---------------------------------------------------------------------------------------------
* Licensed under the MIT License.
*
* @Author: fantasticsoul
*--------------------------------------------------------------------------------------------*/
import type { DraftMeta, IApiCtx } from '../inner-types';
import { ARRAY, MAP, SET } from '../support/consts';
import { deepDrill, isObject } from '../support/util';
import { clearMultiRefData, getDraftMeta, getDraftMetaByCtx, getMetaVer, getMultiRefPaths, resetMultiRefData } from './meta';
import { getKeyStrByPath, getVal, setVal } from './path-util';
function ressignArrayItem(listMeta: DraftMeta, itemMeta: DraftMeta, ctx: { targetNode: any; key: any }) {
const { copy, isArrOrderChanged } = listMeta;
const { targetNode, key } = ctx;
// 数组顺序已变化
if (isArrOrderChanged) {
// fix issue https://github.com/tnfe/limu/issues/13
// 元素经过 sort 后,可能已变成 proxy 对象,所以这里需要比较 copy 和 proxyVal
const index = copy.findIndex((item: any) => item === itemMeta.copy || item === itemMeta.proxyVal);
if (index >= 0) {
copy[index] = targetNode;
}
return;
}
copy[key] = targetNode;
}
export function isInSameScope(mayDraftProxy: any, callerScopeVer: string) {
if (!isObject(mayDraftProxy)) {
return true;
}
return getMetaVer(mayDraftProxy) === callerScopeVer;
}
export function clearScopes(rootMeta: DraftMeta, apiCtx: IApiCtx) {
const { metaMap } = apiCtx;
// TODO 下钻有一定的性能损耗,允许用户关闭此逻辑 findDraftNodeNewRef=false
const drilledMap: Map<any, any> = new Map();
apiCtx.newNodeMap.forEach((v) => {
const { node, parent, key } = v;
const drilledNode = drilledMap.get(node);
if (drilledNode) {
// 同一个节点被多个父级引用了,只需要指向自身即可,无需再次下钻
parent[key] = drilledNode;
return;
}
const item = v;
deepDrill(node, parent, key, (obj: any, parentObj: any, key: any) => {
const meta = getDraftMetaByCtx(obj, apiCtx);
if (meta) {
const { modified, copy, self } = meta;
const targetNode = !modified ? self : copy;
parentObj[key] = targetNode;
}
});
item.target = parent[key]; // 此处可能已被替换为真正的目标节点
drilledMap.set(node, item.target);
});
rootMeta.scopes.forEach((meta) => {
const { modified, copy, parentMeta, key, self, revoke, proxyVal, isDel } = meta;
// LABEL: WAIT TEST(2025-05-25)
const leaveScope = () => {
metaMap.delete(self);
metaMap.delete(proxyVal);
// 这里还不能将 copy 的映射删除,否则会照成以下错误,留给 js 引擎自己清理即可
// fail at test/api/setAutoRevoke.ts -> set autoRevoke when call createDraft › read subNode
// metaMap.delete(copy);
revoke();
};
if (!copy || !parentMeta) return leaveScope();
const targetNode = !modified ? self : copy;
// 父节点是 Map、Set 时,parent 指向的是 ProxyItems,这里取到 copy 本体后再重新赋值
const parentCopy = parentMeta.copy as any;
const parentType = parentMeta.selfType;
if (parentType === MAP) {
parentCopy.set(key, targetNode);
return leaveScope();
}
if (parentType === SET) {
parentCopy.delete(proxyVal);
parentCopy.add(targetNode);
return leaveScope();
}
if (parentType === ARRAY) {
ressignArrayItem(parentMeta, meta, { targetNode, key });
return leaveScope();
}
if (isDel !== true) {
parentCopy[key] = targetNode;
return leaveScope();
}
});
rootMeta.scopes.length = 0;
}
/**
* TODO 将来使用 handleMultiRefV2 替换掉 handleMultiRef
* @param rootMeta T
* @param final
*/
export function handleMultiRefV2(rootMeta: DraftMeta, final: any) {
type Stat = { modified: boolean; keyPaths: string[][]; data: any };
const { sourceId, proxyVal } = rootMeta;
const keyPathsList = getMultiRefPaths(sourceId);
const keyPathsListNew: string[][][] = [];
const keyStr2KeyPaths: Record<string, string[][]> = {};
for (const keyPaths of keyPathsList) {
const resultStatMap = new Map<any, Stat>();
const getResultStat = (val: any) => {
let stat = resultStatMap.get(val);
if (!stat) {
stat = { modified: false, keyPaths: [], data: null };
resultStatMap.set(val, stat);
}
return stat;
};
// keyPaths 里每一个路径代表上一刻记录的多引用关系,
// 这里开始对比实际值并记录为新的多引用关系
for (const subIdx in keyPaths) {
const subIdxNum = Number(subIdx);
const keyPath = keyPaths[subIdxNum];
const { val } = getVal(proxyVal, keyPath);
const valMeta = getDraftMeta(val);
if (!valMeta) {
continue;
}
const result = valMeta.modified ? valMeta.copy : valMeta.self;
const stat = getResultStat(result);
stat.keyPaths.push(keyPath);
if (stat.modified) {
continue;
}
if (valMeta.modified) {
stat.modified = true;
stat.data = valMeta.copy;
} else {
stat.data = valMeta.self;
}
}
resultStatMap.forEach((stat) => {
const { keyPaths, data } = stat;
for (const keyPath of keyPaths) {
// 统一指到同一个 self 或者 copy
setVal(final, keyPath, data);
// 记录行新的 keyStr keyPaths 映射关系
keyStr2KeyPaths[getKeyStrByPath(keyPath)] = keyPaths;
}
// 添加到新的同一个数据多引用路径列表里
keyPathsListNew.push(keyPaths);
});
}
resetMultiRefData(sourceId, keyPathsListNew, keyStr2KeyPaths);
}
export function handleMultiRef(rootMeta: DraftMeta, final: any) {
const keyPathsList = getMultiRefPaths(rootMeta.sourceId);
let idx = -1;
const toClearIdxList: number[] = [];
const toClearKeyStrList: string[] = [];
for (const keyPaths of keyPathsList) {
idx += 1;
let changedMeta: any = null;
let fixedMeta: any = null;
const results: any[] = [];
for (const keyPath of keyPaths) {
const { val } = getVal(rootMeta.proxyVal, keyPath);
const valMeta = getDraftMeta(val);
if (!valMeta) continue;
if (valMeta.modified && !changedMeta) {
changedMeta = valMeta;
}
fixedMeta = valMeta;
results.push(valMeta.self);
}
// TODO 优化为分析所有 results,做部分清理,见 handleMultiRefV2
// prev: [ r1, r2, r3, r4, r5 ] 都一样
// now: [ r1, r2, r3 ] 一样,[ r4, r5 ] 一样
// 需要按照新的比较结果来记录新的,目前先实现简单的下标1和2比较
const isEq = results[0] === results[1];
if (!isEq) {
toClearIdxList.push(idx);
keyPaths.forEach((keyPath) => toClearKeyStrList.push(getKeyStrByPath(keyPath)));
} else if (changedMeta) {
for (const keyPath of keyPaths) {
setVal(final, keyPath, changedMeta.copy);
}
} else if (fixedMeta) {
for (const keyPath of keyPaths) {
setVal(final, keyPath, fixedMeta.self);
}
}
}
if (toClearIdxList.length) {
clearMultiRefData(rootMeta.sourceId, toClearIdxList, toClearKeyStrList);
}
}
export function extractFinalData(rootMeta: DraftMeta, apiCtx: IApiCtx) {
const { self, copy, modified } = rootMeta;
let final = self;
// 有 copy 不一定有修改行为,这里需做双重判断
if (copy && modified) {
final = rootMeta.copy;
}
// 这里 handleMultiRef 和 clearScopes 顺序很重要,必须先处理多引用,再清理 scopes
handleMultiRef(rootMeta, final);
// if put this on first line, fail at test/set-other/update-object-item.ts
clearScopes(rootMeta, apiCtx);
return final;
}
export function recordVerScope(meta: DraftMeta) {
meta.rootMeta.scopes.push(meta);
}