limu
Version:
A fast js lib of immutable data, based on shallow copy on read and mark modified on write mechanism
363 lines (362 loc) • 16.6 kB
JavaScript
import { ARRAY, CAREFUL_FNKEYS, CAREFUL_TYPES, CHANGE_FNKEYS, IMMUT_BASE, JS_SYM_KEYS, MAP, META_VER, SET } from '../support/consts';
import { conf } from '../support/inner-data';
import { canBeNum, has, isFn, isPrimitive } from '../support/util';
import { handleDataNode } from './data-node-processor';
import { deepFreeze } from './freeze';
import { createScopedMeta, getMayProxiedVal, getUnProxyValue } from './helper';
import { genMetaVer, getSafeDraftMeta, isDraft, ROOT_CTX } from './meta';
import { extractFinalData, isInSameScope, recordVerScope } from './scope';
// 可直接返回的属性
// 避免 Cannot set property size of #<Map> which has only a getter
// 避免 Cannot set property size of #<Set> which has only a getter
const PROPERTIES_BLACK_LIST = ['length', 'constructor', 'asymmetricMatch', 'nodeType', 'size'];
const PBL_DICT = {}; // for perf
PROPERTIES_BLACK_LIST.forEach((item) => (PBL_DICT[item] = 1));
const TYPE_BLACK_DICT = { [ARRAY]: 1, [SET]: 1, [MAP]: 1 }; // for perf
export const FINISH_HANDLER_MAP = new Map();
export function buildLimuApis(options) {
var _a, _b, _c, _d, _e, _f, _g;
const opts = options || {};
const onOperate = opts.onOperate;
const hasOnOperate = !!onOperate;
const customKeys = opts.customKeys || [];
const fastModeRange = opts.fastModeRange || conf.fastModeRange;
// @ts-ignore
const immutBase = (_a = opts[IMMUT_BASE]) !== null && _a !== void 0 ? _a : false;
const readOnly = (_b = opts.readOnly) !== null && _b !== void 0 ? _b : false;
const disableWarn = opts.disableWarn;
const compareVer = (_c = opts.compareVer) !== null && _c !== void 0 ? _c : false;
const debug = (_d = opts.debug) !== null && _d !== void 0 ? _d : false;
// 调用那一刻起,确定 autoFreeze 值
// allow user overwrite autoFreeze setting in current call process
const autoFreeze = (_e = opts.autoFreeze) !== null && _e !== void 0 ? _e : conf.autoFreeze;
// 暂未实现 to be implemented in the future
const metaVer = genMetaVer();
const apiCtx = { metaMap: new Map(), newNodeMap: new Map(), debug, metaVer };
ROOT_CTX.set(metaVer, apiCtx);
const autoRevoke = (_f = opts.autoRevoke) !== null && _f !== void 0 ? _f : conf.autoRevoke;
const silenceSetTrapErr = (_g = opts.silenceSetTrapErr) !== null && _g !== void 0 ? _g : true;
const logChangeFailed = (op, key) => {
console.warn(`${op} ${key} failed, cuase draft root has been finised!`);
return silenceSetTrapErr;
};
let isDraftFinished = false;
const warnReadOnly = () => {
if (!disableWarn) {
console.warn('can not mutate state at readOnly mode!');
}
return true;
};
const execOnOperate = (op, key, options) => {
const { mayProxyVal, parentMeta: inputPMeta, value, isCustom = false } = options;
let isChanged = false;
if (!onOperate)
return { isChanged, mayProxyVal };
const parentMeta = (inputPMeta || {});
const { selfType = '', keyPath = [], copy, self, modified, proxyVal: parentProxy } = parentMeta || {};
let isBuiltInFnKey = false;
// 优先采用显式传递的 isChange
if (options.isChanged !== undefined) {
isChanged = options.isChanged;
}
else {
const fnKeys = CAREFUL_FNKEYS[selfType] || [];
if (fnKeys.includes(key)) {
isBuiltInFnKey = true;
const changeFnKeys = CHANGE_FNKEYS[selfType] || [];
isChanged = changeFnKeys.includes(key);
}
else if (op !== 'get') {
// 变化之后取 copy 比较
const node = modified ? copy : self;
isChanged = inputPMeta ? node[key] !== value : true;
}
}
let replacedValue = null;
let isReplaced = false;
const replaceValue = (value) => {
isReplaced = true;
replacedValue = value;
};
const getReplaced = () => ({ isReplaced, replacedValue });
onOperate({
immutBase,
parent: self,
parentType: selfType,
parentProxy,
op,
replaceValue,
getReplaced,
isBuiltInFnKey,
isChanged,
isCustom,
key,
keyPath,
fullKeyPath: keyPath.concat(key),
value,
proxyValue: mayProxyVal,
});
return {
mayProxyVal: isReplaced ? replacedValue : mayProxyVal,
isChanged,
};
};
const limuApis = (() => {
// let revoke: null | (() => void) = null;
/**
* 为了和下面这个 immer case 保持行为一致
* https://github.com/immerjs/immer/issues/960
* 如果数据节点上人工赋值了其他 draft 的话,当前 draft 结束后不能够被冻结( 见set逻辑 )
*/
let canFreezeDraft = true;
// >= 3.0+ ver, shadow copy on read, mark modified on write
const limuTraps = {
// parent指向的是代理之前的对象
get: (parent, key) => {
if (META_VER === key) {
return metaVer;
}
/** current child value, it may been replaced to a proxy value later */
const currentVal = parent[key];
if (JS_SYM_KEYS.includes(key)) {
// 避免报错 Method xx.yy called on incompatible receiver
// 例如 Array.from(draft)
if (isFn(currentVal)) {
// 执行 for(const item of list){ ... } 语句
if (Symbol.iterator === key && Array.isArray(parent)) {
let idx = 0;
// 模拟迭代器
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol
const iter = () => ({
next: () => {
const len = parent.length;
if (len === 0) {
return { done: true, value: undefined };
}
// 前一次迭代已到达最后一个元素时,idx === len
const done = idx === len;
// key 格式统一为字符串,故这里包一层 String
const value = done ? undefined : limuTraps.get(parent, String(idx));
idx++;
return { done, value };
},
[Symbol.iterator]: () => {
return iter;
},
});
return iter;
}
return currentVal.bind(parent);
}
return currentVal;
}
// 判断 toJSON 是为了兼容 JSON.stringify 调用, https://javascript.info/json#custom-tojson
if (key === '__proto__' || (key === 'toJSON' && !has(parent, key))) {
return currentVal;
}
let mayProxyVal = currentVal;
const parentMeta = getSafeDraftMeta(parent, apiCtx);
if (customKeys.includes(key)) {
const ret = execOnOperate('get', key, { parentMeta, mayProxyVal, value: currentVal, isChanged: false, isCustom: true });
return ret.mayProxyVal;
}
const parentType = parentMeta === null || parentMeta === void 0 ? void 0 : parentMeta.selfType;
// copyWithin、sort 、valueOf... will hit the keys of 'asymmetricMatch', 'nodeType',
// PROPERTIES_BLACK_LIST 里 'length', 'constructor', 'asymmetricMatch', 'nodeType'
if (TYPE_BLACK_DICT[parentType] && PBL_DICT[key]) {
if (key === 'length' || key === 'size') {
execOnOperate('get', key, { parentMeta, mayProxyVal, value: currentVal });
}
return parentMeta.copy[key];
}
// 可能会指向代理对象
mayProxyVal = getMayProxiedVal(currentVal, {
key,
compareVer,
parentMeta,
parentType,
ver: metaVer,
traps: limuTraps,
parent,
fastModeRange,
immutBase,
readOnly,
apiCtx,
hasOnOperate,
autoRevoke,
});
// 用下标取数组时,可直接返回
// 例如数组操作: arrDraft[0].xxx = 'new', 此时 arrDraft[0] 需要操作的是代理对象
if (parentType === ARRAY && canBeNum(key)) {
const ret = execOnOperate('get', key, { parentMeta, mayProxyVal, value: currentVal });
return ret.mayProxyVal;
}
// @ts-ignore
if (CAREFUL_TYPES[parentType]) {
mayProxyVal = handleDataNode(parent, {
op: key,
key,
value: currentVal,
metaVer,
calledBy: 'get',
parentType,
parentMeta,
apiCtx,
});
const ret = execOnOperate('get', key, { parentMeta, mayProxyVal, value: currentVal });
return ret.mayProxyVal;
}
const ret = execOnOperate('get', key, { parentMeta, mayProxyVal, value: currentVal });
return ret.mayProxyVal;
},
// parent 指向的是代理之前的对象
set: (parent, key, value) => {
if (isDraftFinished) {
return logChangeFailed('set', key);
}
const parentMeta = getSafeDraftMeta(parent, apiCtx);
// fix issue https://github.com/tnfe/limu/issues/12
let isValueDraft = false;
// is a draft proxy node
if (isDraft(value)) {
isValueDraft = true;
// see case debug/complex/set-draft-node
if (isInSameScope(value, metaVer)) {
const rawValue = getUnProxyValue(value, apiCtx);
if (rawValue === parent[key]) {
return true;
}
}
else {
// TODO: judge value must be a root draft node
// assign another scope draft node to current scope
canFreezeDraft = false;
}
}
if (readOnly) {
execOnOperate('set', key, { parentMeta, isChanged: false, value });
return warnReadOnly();
}
// speed up array operation
if (parentMeta && parentMeta.selfType === ARRAY) {
// @ts-ignore
if (parentMeta.copy && parentMeta.__callSet && canBeNum(key)) {
execOnOperate('set', key, { parentMeta, value });
parentMeta.copy[key] = value;
return true;
}
// @ts-ignore, mark is set called on parent node
parentMeta.__callSet = true;
}
let isChanged = false;
if (!onOperate) {
// 变化之后取 copy 比较
const node = parentMeta.modified ? parentMeta.copy : parentMeta.self;
isChanged = node[key] !== value;
}
else {
const ret = execOnOperate('set', key, { parentMeta, value });
isChanged = ret.isChanged;
}
if (isChanged) {
handleDataNode(parent, {
parentMeta,
key,
value,
metaVer,
calledBy: 'set',
apiCtx,
isValueDraft,
});
}
return true;
},
// delete or Reflect.deleteProperty will trigger this trap
deleteProperty: (parent, key) => {
if (isDraftFinished) {
return logChangeFailed('delete', key);
}
const parentMeta = getSafeDraftMeta(parent, apiCtx);
const value = parent[key];
if (readOnly) {
execOnOperate('del', key, { parentMeta, isChanged: false, value });
return warnReadOnly();
}
execOnOperate('del', key, { parentMeta, isChanged: true, value });
handleDataNode(parent, {
parentMeta,
op: 'del',
key,
value: '',
metaVer,
calledBy: 'deleteProperty',
apiCtx,
});
return true;
},
// trap function call
apply: function (target, thisArg, args) {
return target.apply(thisArg, args);
},
};
return {
createDraft: (mayDraft) => {
if (isPrimitive(mayDraft)) {
throw new Error('base state can not be primitive');
}
let oriBase = mayDraft;
const draftMeta = getSafeDraftMeta(mayDraft, apiCtx);
if (draftMeta) {
// 总是返回同一个 immutBase 代理对象
if (immutBase && draftMeta.isImmutBase) {
return draftMeta.proxyVal;
}
oriBase = draftMeta.self;
}
const meta = createScopedMeta('', oriBase, {
ver: metaVer,
traps: limuTraps,
immutBase,
readOnly,
compareVer,
apiCtx,
hasOnOperate,
autoRevoke,
});
recordVerScope(meta);
meta.execOnOperate = execOnOperate;
FINISH_HANDLER_MAP.set(meta.proxyVal, limuApis.finishDraft);
return meta.proxyVal;
},
finishDraft: (proxyDraft) => {
// attention: if pass a revoked proxyDraft
// it will throw: Cannot perform 'set' on a proxy that has been revoked
const rootMeta = getSafeDraftMeta(proxyDraft, apiCtx);
if (!rootMeta) {
throw new Error('rootMeta should not be null!');
}
if (rootMeta.level !== 0) {
throw new Error('can not finish sub draft node!');
}
// TODO support fastCopy
// immutBase 是一个一直可用的对象
// 对 immut() 返回的对象调用 finishDraft 则总是返回 immutBase 自身代理
// 将 immut() 返回结果传给 finishDraft 是无意义的
if (rootMeta.isImmutBase) {
return proxyDraft;
}
let final = extractFinalData(rootMeta, apiCtx);
if (autoFreeze && canFreezeDraft) {
// TODO deep pruning
// see https://github.com/immerjs/immer/issues/687
// let cachedFrozenOriginalBase = frozenOriginalBaseMap.get(rootMeta.originalSelf);
final = deepFreeze(final);
}
ROOT_CTX.delete(metaVer);
isDraftFinished = true;
return final;
},
};
})();
return limuApis;
}