limu
Version:
A fast js lib of immutable data, based on shallow copy on read and mark modified on write mechanism
184 lines (183 loc) • 6.94 kB
JavaScript
import { ARRAY, IS_RAW, MAP, PROXYITEM_FNKEYS, SET } from '../support/consts';
import { isFn, isObject, isPrimitive, noop } from '../support/util';
import { makeCopyWithMeta } from './copy';
import { attachMeta, getDraftMeta, getSafeDraftMeta, markModified, newMeta } from './meta';
import { recordVerScope } from './scope';
export function createScopedMeta(key, baseData, options) {
const { traps, parentType, fastModeRange, immutBase, apiCtx, autoRevoke } = options;
// new meta data for current data node
const meta = newMeta(key, baseData, options);
const { copy, fast } = makeCopyWithMeta(baseData, meta, {
immutBase,
parentType,
fastModeRange,
apiCtx,
});
meta.copy = copy;
meta.isFast = fast;
if (immutBase) {
const ret = new Proxy(copy, traps);
meta.proxyVal = ret;
meta.revoke = noop;
}
else {
const ret = Proxy.revocable(copy, traps);
meta.proxyVal = ret.proxy;
meta.revoke = autoRevoke ? ret.revoke : noop;
}
apiCtx.metaMap.set(copy, meta);
// apiCtx.metaMap.set(baseData, meta);
apiCtx.metaMap.set(meta.proxyVal, meta);
return meta;
}
export function shouldGenerateProxyItems(parentType, key) {
// !!! 对于 Array,直接生成 proxyItems
if (parentType === ARRAY)
return true;
const fnKeys = PROXYITEM_FNKEYS[parentType] || [];
return fnKeys.includes(key);
}
export function getMayProxiedVal(val, options) {
const { key, parentMeta, parent, parentType, fastModeRange, readOnly, apiCtx } = options;
let curVal = val;
// keep copy always same with self when readOnly = true
if (readOnly && parentMeta && !isFn(val)) {
const { copy, self } = parentMeta;
const latestVal = self[key];
if (curVal !== latestVal) {
const meta = apiCtx.metaMap.get(curVal);
if (meta) {
apiCtx.metaMap.delete(curVal);
apiCtx.metaMap.delete(meta.proxyVal);
}
copy[key] = latestVal;
curVal = latestVal;
}
}
const mayCreateProxyVal = (val, inputKey) => {
const key = inputKey || '';
if (isPrimitive(val) || !val) {
return val;
}
if (!parentMeta) {
throw new Error('[[ createMeta ]]: meta should not be null');
}
if (!isFn(val)) {
if (
// 是一个全新的节点,不必生成代理,以便提高性能
parentMeta.newNodeStats[key] ||
// 已被 markRaw 标记,不需转为代理
val[IS_RAW]) {
return val;
}
let valMeta = getSafeDraftMeta(val, apiCtx);
// 惰性生成代理对象和其元数据
if (!valMeta) {
valMeta = createScopedMeta(key, val, options);
recordVerScope(valMeta);
// child value 指向 copy
if (parentMeta.selfType === MAP) {
parent.set(key, valMeta.copy);
}
else {
parent[key] = valMeta.copy;
}
}
return valMeta.proxyVal;
}
if (!shouldGenerateProxyItems(parentType, key)) {
return val;
}
if (parentMeta.proxyItems) {
return val;
}
// 提前完成遍历,为所有 item 生成代理
let proxyItems = [];
if (parentType === SET) {
const tmp = new Set();
parent.forEach((val) => tmp.add(mayCreateProxyVal(val)));
replaceSetOrMapMethods(tmp, parentMeta, {
dataType: SET,
apiCtx,
});
proxyItems = attachMeta(tmp, parentMeta, { fast: fastModeRange, apiCtx });
// 区别于 2.0.2 版本,这里提前把 copy 指回来
parentMeta.copy = proxyItems;
}
else if (parentType === MAP) {
const tmp = new Map();
parent.forEach((val, key) => tmp.set(key, mayCreateProxyVal(val, key)));
replaceSetOrMapMethods(tmp, parentMeta, {
dataType: MAP,
apiCtx,
});
proxyItems = attachMeta(tmp, parentMeta, { fast: fastModeRange, apiCtx });
// 区别于 2.0.2 版本,这里提前把copy指回来
parentMeta.copy = proxyItems;
}
else if (parentType === ARRAY && key !== 'sort') {
parentMeta.copy = parentMeta.copy || parent.slice();
proxyItems = parentMeta.proxyVal;
}
parentMeta.proxyItems = proxyItems;
return val;
};
return mayCreateProxyVal(curVal, key);
}
export function getUnProxyValue(value, apiCtx) {
if (!isObject(value)) {
return value;
}
const valueMeta = getSafeDraftMeta(value, apiCtx);
if (!valueMeta)
return value;
return valueMeta.copy;
}
/**
* 拦截 set delete clear add
* 支持用户使用 callback 的第三位参数 (val, key, mapOrSet) 的 mapOrSet 当做 draft 使用
*/
export function replaceSetOrMapMethods(mapOrSet, meta, options) {
const { dataType, apiCtx } = options;
// 拦截 set delete clear add,注意 set,add 在末尾判断后添加
// 支持用户使用 callback 的第三位参数 (val, key, map) 的 map 当做 draft 使用
const oriDel = mapOrSet.delete.bind(mapOrSet);
const oriClear = mapOrSet.clear.bind(mapOrSet);
mapOrSet.delete = function limuDelete(...args) {
markModified(meta);
return oriDel(...args);
};
mapOrSet.clear = function limuClear(...args) {
markModified(meta);
return oriClear(...args);
};
if (dataType === SET) {
const oriAdd = mapOrSet.add.bind(mapOrSet);
mapOrSet.add = function limuAdd(...args) {
markModified(meta);
return oriAdd(...args);
};
}
if (dataType === MAP) {
const oriSet = mapOrSet.set.bind(mapOrSet);
const oriGet = mapOrSet.get.bind(mapOrSet);
mapOrSet.set = function limuSet(...args) {
markModified(meta);
if (meta.hasOnOperate) {
const value = args[1];
meta.rootMeta.execOnOperate('set', args[0], { mayProxyVal: value, value, parentMeta: meta });
}
// @ts-ignore
return oriSet(...args);
};
mapOrSet.get = function limuGet(...args) {
const mayProxyVal = oriGet(...args);
if (meta.hasOnOperate) {
const draftMeta = getDraftMeta(mayProxyVal, apiCtx);
const value = draftMeta ? draftMeta.copy || draftMeta.self : mayProxyVal;
meta.rootMeta.execOnOperate('get', args[0], { mayProxyVal, value, parentMeta: meta, isChanged: false });
}
return mayProxyVal;
};
}
}