UNPKG

limu

Version:

A fast js lib of immutable data, based on shallow copy on read and mark modified on write mechanism

1,460 lines (1,449 loc) 76.3 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.limu = {})); })(this, (function (exports) { 'use strict'; /** * 4.0 开始,新增 keyPaths 支持多引用记录 * 4.1 开始,支持 draft 操作过程中产生新引用 */ const VER$1 = '4.1.1'; /** 版本号key */ const META_VER = Symbol('V'); /** 标识这是一个 immut 创建的根对象 */ const IMMUT_BASE = Symbol('IMMUT_BASE'); /** markRaw 调用会给对象标记 IS_RAW 为 true */ const IS_RAW = Symbol('IS_RAW'); /** 数据节点私有数据 */ const PRIVATE_META = Symbol('P'); const MAP = 'Map'; const SET = 'Set'; const ARRAY = 'Array'; const OBJECT = 'Object'; /** * limu 需要关心的 symbol 读取 key 列表 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol */ const JS_SYM_KEYS = [Symbol.iterator, Symbol.toStringTag, IS_RAW]; const CAREFUL_TYPES = { Map: MAP, Set: SET, Array: ARRAY }; const OBJ_DESC = '[object Object]'; const MAP_DESC = '[object Map]'; const SET_DESC = '[object Set]'; const ARR_DESC = '[object Array]'; const FN_DESC = '[object Function]'; const desc2dataType = { [MAP_DESC]: MAP, [SET_DESC]: SET, [ARR_DESC]: ARRAY, [OBJ_DESC]: OBJECT, }; const SHOULD_REASSIGN_ARR_METHODS = ['push', 'pop', 'shift', 'splice', 'unshift', 'reverse', 'copyWithin', 'delete', 'fill']; const SHOULD_REASSIGN_MAP_METHODS = ['set', 'clear', 'delete']; const SHOULD_REASSIGN_SET_METHODS = ['add', 'clear', 'delete']; const CHANGE_ARR_ORDER_METHODS = ['splice', 'sort', 'unshift', 'shift']; const arrFnKeys = [ 'concat', 'copyWithin', 'entries', 'every', 'fill', 'filter', 'find', 'findIndex', 'flat', 'flatMap', 'forEach', 'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', 'map', 'pop', 'push', 'reduce', 'reduceRight', 'reverse', 'shift', 'unshift', 'slice', 'some', 'sort', 'splice', 'values', 'valueOf', ]; const mapFnKeys = ['clear', 'delete', 'entries', 'forEach', 'get', 'has', 'keys', 'set', 'values']; const setFnKeys = ['add', 'clear', 'delete', 'entries', 'forEach', 'has', 'keys', 'values']; const CAREFUL_FNKEYS = { [MAP]: mapFnKeys, [SET]: setFnKeys, [ARRAY]: arrFnKeys, }; const CHANGE_FNKEYS = { [MAP]: ['clear', 'set', 'delete'], [SET]: ['clear', 'add', 'delete'], [ARRAY]: ['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'copyWithin'], }; const PROXYITEM_FNKEYS = { [MAP]: ['forEach', 'get'], [SET]: ['forEach'], [ARRAY]: ['forEach', 'map'], }; const OP_GET = 'get'; const OP_SET = 'set'; const OP_DEL = 'del'; /*--------------------------------------------------------------------------------------------- * Licensed under the MIT License. * * @Author: fantasticsoul *--------------------------------------------------------------------------------------------*/ function getId(idWrap, customPrefix = '') { if (idWrap.value >= Number.MAX_SAFE_INTEGER) { idWrap.value = 1; idWrap.prefixSeed += 1; } else { idWrap.value += 1; } const { value, prefixSeed } = idWrap; const metaVer = `${customPrefix}${prefixSeed}_${value}`; return metaVer; } const verWrap = { value: 0, prefixSeed: 1 }; const idWrap = { value: 0, prefixSeed: 1 }; const symbolIdWrap = { value: 0, prefixSeed: 1 }; const sourceIdWrap = { value: 0, prefixSeed: 1 }; const symbolStrDict = {}; const strSymbolDict = {}; function genMetaId() { return getId(idWrap, 'MID_'); } function genMetaVer() { return getId(verWrap, 'MV_'); } function genSymbolId() { return getId(symbolIdWrap, 'SI_'); } function genSourceId() { return getId(sourceIdWrap, 'SR_'); } const conf = { autoFreeze: false, autoRevoke: true, }; const toString = Object.prototype.toString; const canUseReflect = !!Reflect; const hasProp = Object.prototype.hasOwnProperty; function has(obj, key) { if (canUseReflect) { return Reflect.has(obj, key); } return hasProp.call(obj, key); } function deepDrill(obj, parentObj, key, subObjCb) { const list = []; const innerDeep = (obj, parentObj, key) => { if (isPrimitive(obj)) { return; } if (list.includes(obj)) { // 出现循环引用,及时跳出来,避免出现错误:Maximum call stack size exceeded return; } list.push(obj); subObjCb(obj, parentObj, key); // const drillThenCb = (obj: any, parentObj: any, key: any) => { // innerDeep(obj, parentObj, key); // subObjCb(obj, parentObj, key); // }; if (Array.isArray(obj)) { obj.forEach((item, idx) => { innerDeep(item, obj, idx); }); } // TODO 处理 set // if (isSet(obj)) { // obj.forEach((item: any) => { // subObjCb(item, obj, idx); // }); // } if (isMap(obj)) { obj.forEach((value, key) => { innerDeep(value, obj, key); }); } if (isObject(obj)) { Object.keys(obj).forEach((key) => { innerDeep(obj[key], obj, key); }); } }; innerDeep(obj, parentObj, key); } function getValStrDesc(val) { // return Array.isArray(val) ? ARR_DESC : toString.call(val); return toString.call(val); } function noop(...args) { return args; } function isObject(val) { // attention,null desc is '[object Null]' return getValStrDesc(val) === OBJ_DESC; } function isMap(val) { return getValStrDesc(val) === MAP_DESC; } function isSet(val) { return getValStrDesc(val) === SET_DESC; } function isFn(val) { return getValStrDesc(val) === FN_DESC; } function getDataType(dataNode) { var strDesc = getValStrDesc(dataNode); const dataType = desc2dataType[strDesc]; return dataType; } function isPrimitive(val) { const desc = getValStrDesc(val); return ![OBJ_DESC, ARR_DESC, MAP_DESC, SET_DESC, FN_DESC].includes(desc); } function isPromiseFn(obj) { return obj.constructor.name === 'AsyncFunction' || 'function' === typeof obj.then; } function isPromiseResult(result) { return typeof Promise !== 'undefined' && result instanceof Promise; } function canBeNum(val) { var valType = typeof val; if (valType === 'number') return true; if (valType === 'string') return /^[0-9]*$/.test(val); return false; } function isSymbol(maySymbol) { return typeof maySymbol === 'symbol'; } /** * 是否已被 markRaw 标记 */ function isMardedRaw(val) { var _a; if (!val) return false; return (_a = val[IS_RAW]) !== null && _a !== void 0 ? _a : false; } function getProxyVer(mayProxy) { if (!mayProxy) { return ''; } return mayProxy[META_VER] || ''; } function isPrevVerDraft(mayProxy, curVer) { const ver = getProxyVer(mayProxy); if (!ver) { return false; } return ver !== curVer; } function deepCopy$1(obj) { const innerDeep = (obj) => { if (isPrimitive(obj)) { return obj; } // TODO: 或许这里通过 metaVer 能够解决多引用问题 ( see 3.4.2 ) // TODO: 传入参数 reuse,确定是否走逻辑查看 meta.modified,并复用 meta.copy or meta.self let newNode = obj; if (Array.isArray(obj)) { newNode = obj.slice(); newNode.forEach((item, idx) => { newNode[idx] = innerDeep(item); }); } if (isSet(obj)) { const tmpArr = Array.from(obj); tmpArr.forEach((item, idx) => { tmpArr[idx] = innerDeep(item); }); newNode = new Set(tmpArr); } if (isMap(obj)) { newNode = new Map(obj); newNode.forEach((value, key) => { newNode.set(key, innerDeep(value)); }); } if (isObject(obj)) { newNode = {}; Object.keys(obj).forEach((key) => { newNode[key] = innerDeep(obj[key]); }); } return newNode; }; return innerDeep(obj); } /** * 尝试生成copy * @param val * @returns */ function tryMakeCopy(val, readOnly) { if (readOnly) { return val; } if (Array.isArray(val)) { return val.slice(); } let copy = val; if (val && isObject(val)) { copy = Object.assign({}, val); } if (isMap(val)) { copy = new Map(val); } if (isSet(val)) { copy = new Set(val); } return copy; } // 调用处已保证 meta 不为空 function mayMakeCopy(ori, options) { // LABEL: SEE IF IMMUT HAS MEM LEAK if (options.immutBase) { return ori; } const copy = tryMakeCopy(ori, options.readOnly); return copy; } function ensureStrKey(maySymbolKey) { const key = maySymbolKey; if (!isSymbol(maySymbolKey)) { return maySymbolKey; } let symbolStr = symbolStrDict[key]; if (!symbolStr) { symbolStr = genSymbolId(); symbolStrDict[key] = symbolStr; } return symbolStr; } function getKeyPathIdx(keyPaths, keyPath) { const keyPathStrs = keyPaths.map((v) => v.join('|')); const keyPathStr = keyPath.join('|'); return keyPathStrs.indexOf(keyPathStr); } function getKeyStrByPath(keyPath, ensureStr) { let target = keyPath; if (ensureStr) { target = toKeyStrPath(keyPath, true); } return target.join('|'); } /** * in: ['a', 'b', Symbol] * out: ['a', 'b', 'SID_1'] */ function toKeyStrPath(keyPath, traversal) { let keyStrPath = []; if (traversal) { keyPath.forEach((v) => { const keyStr = ensureStrKey(v); keyStrPath.push(keyStr); }); return keyStrPath; } keyStrPath = keyPath.slice(); const lastIdx = keyPath.length - 1; const lastKey = keyPath[lastIdx]; const keyStr = ensureStrKey(lastKey); keyStrPath[lastIdx] = keyStr; return keyStrPath; } /** * out: ['a', 'b', 'SID_1'] * in: ['a', 'b', Symbol] */ function toKeyPath(keyStrPath) { return keyStrPath.map((str) => strSymbolDict[str] || str); } function pushKeyPath(meta, keyPath, inputKeyStrPath) { const { keyPaths, keyStrPaths } = meta; const keyStrPath = inputKeyStrPath || toKeyStrPath(keyPath); const idx = getKeyPathIdx(keyStrPaths, keyStrPath); if (idx < 0) { keyPaths.push(keyPath); keyStrPaths.push(keyStrPath); } } /** * 删一条路径,并将 keyPath, keyStrPath 重新指向一个存在的路径 */ function delKeyPath(meta) { const { keyPaths, keyStrPaths, keyStrPath } = meta; const idx = getKeyPathIdx(keyStrPaths, keyStrPath); keyStrPaths.splice(idx, 1); keyPaths.splice(idx, 1); meta.keyPath = keyPaths[0]; meta.keyStrPath = keyStrPaths[0]; } /** * string 获取不到,尝试转为 number 获取 */ function getMapVal(map, key) { const strKeyVal = map.get(key); if (strKeyVal !== undefined) { return strKeyVal; } const numKeyVal = map.get(Number(key) || key); if (numKeyVal !== undefined) { return numKeyVal; } return undefined; } function getVal(obj, keyPath) { let val; let parent = obj; const lastIdx = keyPath.length - 1; let isGetted = true; for (let i = 0; i <= lastIdx; i++) { const key = keyPath[i]; if (!parent && i < lastIdx) { isGetted = false; break; } val = isMap(parent) ? getMapVal(parent, key) : parent[key]; parent = val; } return { val, isGetted }; } function getValByKeyPaths(obj, keyPaths) { let targetVal; let isValGetted = false; const lastIdx = keyPaths.length - 1; for (let i = 0; i <= lastIdx; i++) { const keyPath = keyPaths[i]; const { isGetted, val } = getVal(obj, keyPath); if (isGetted) { targetVal = val; isValGetted = true; break; } } return { isGetted: isValGetted, val: targetVal }; } function setVal(obj, keyPath, val) { let parent = obj; const lastIdx = keyPath.length - 1; for (let i = 0; i <= lastIdx; i++) { if (!parent) { break; } const key = keyPath[i]; if (i === lastIdx) { parent[key] = val; break; } parent = isMap(parent) ? getMapVal(parent, key) : parent[key]; } } function setValByKeyPaths(obj, keyPaths, val) { const lastIdx = keyPaths.length - 1; for (let i = 0; i <= lastIdx; i++) { const keyPath = keyPaths[i]; setVal(obj, keyPath, val); } } /** * in: keyPath [a,b,0,d,0] [[a,b,0], [info,0]] * out: [[a,b,0,d,0], [info,0,d,0]] */ function intersectPath(keyPath, commonKeyPaths) { const inputKeyStr = getKeyStrByPath(keyPath); let restStr = ''; for (const tmpKeyPath of commonKeyPaths) { const tmpKeyStr = getKeyStrByPath(tmpKeyPath, true); const prefix = `${tmpKeyStr}|`; if (inputKeyStr.startsWith(prefix)) { restStr = inputKeyStr.substring(prefix.length); break; } } const newKeyPaths = []; if (restStr) { const subStrPaths = restStr.split('|'); const subPaths = toKeyPath(subStrPaths); commonKeyPaths.forEach((keyPath) => { newKeyPaths.push(keyPath.concat(subPaths)); }); } return newKeyPaths; } /** multi ref data, sourceId 2 key */ /** * multi ref data: sourceId 2 key dict * key: srouceId, value: { fullKeyStr: [ [path1Arr], [path2Arr] ] } */ const MRDSid2KeyDict = new Map(); /** * multi ref data: sourceId 2 all multi ref keys list * key: sourceId, value: [ [ keyPath1, keyPath2 ], [ anotherKeyPath1, anotherKeyPath2 ] ], * 数组里每个元素代表一个相同对象的多个引用路径 */ const MRDSid2PathsList = new Map(); const finalDataSourceId = new WeakMap(); const ROOT_CTX = new Map(); function markModified(meta) { meta.rootMeta.modified = true; const doMark = (meta) => { if (meta && !meta.modified) { meta.modified = true; doMark(meta.parentMeta); } }; doMark(meta); } function getKeyPath(draftNode, curKey, apiCtx) { const pathArr = [curKey]; const meta = getSafeDraftMeta(draftNode, apiCtx); if (meta && meta.level > 0) { const { keyPath } = meta; return [...keyPath, curKey]; } return pathArr; } function newMeta(key, baseData, options) { const { ver, parentMeta = null, immutBase, compareVer, apiCtx, hasOnOperate } = options; const dataType = getDataType(baseData); let sourceId = options.sourceId; let keyPath = []; let keyStrPath = []; let arrKeyPath = []; let arrKeyPaths = []; let keyStrPaths = []; let keyPaths = []; const keyStr = ensureStrKey(key); let level = 0; let copy = null; if (parentMeta) { sourceId = parentMeta.sourceId; copy = parentMeta.copy; level = getNextMetaLevel(copy, apiCtx); const isParentArr = parentMeta.selfType === ARRAY; arrKeyPath = isParentArr ? parentMeta.keyPath.concat(key) : parentMeta.arrKeyPath; keyPath = getKeyPath(copy, key, apiCtx); keyStrPath = toKeyStrPath(keyPath); // 尝试查出记录的多引用关系 let paths = []; // 当前节点本身也是可由数组下标访问到的 if (parentMeta.arrKeyPath.length) { const keyStr = getKeyStrByPath(parentMeta.arrKeyPath, true); const parentPaths = getMultiRefPathsByKey(sourceId, keyStr); paths = intersectPath(keyPath, parentPaths); } if (!paths.length) { const { keyStrPathStr } = parentMeta; const fullKeyStr = keyStrPathStr ? `${keyStrPathStr}|${keyStr}` : keyStr; paths = getMultiRefPathsByKey(sourceId, fullKeyStr); } if (paths.length > 1) { // draft 结束后再次创建草稿,发现了多引用路径 const { copy: rootRaw } = parentMeta.rootMeta; const { val: curNode } = getVal(rootRaw, keyPath); const toDelIdxList = []; let isNodeParentArr = false; const mayArrKeyPaths = []; paths.forEach((keyPath, idx) => { const { val: tmpNode } = getVal(rootRaw, keyPath); if (!isNodeParentArr) { const parentKeyPath = keyPath.slice(0, keyPath.length - 1); const { val: parentNode } = getVal(rootRaw, parentKeyPath); if (Array.isArray(parentNode)) { // 只有有一条路径上确认了该节点是数组下标索引到节点,即可标识 isNodeParentArr 为 true isNodeParentArr = true; } } // 当前节点还是上一个版本的节点 if (tmpNode === curNode) { keyPaths.push(keyPath); keyStrPaths.push(toKeyStrPath(keyPath)); mayArrKeyPaths.push(keyPath); } else { toDelIdxList.push(idx); } }); if (isNodeParentArr) { arrKeyPaths = mayArrKeyPaths; } // 移除已不再是共同引用的路径记录 toDelIdxList.forEach((idx) => paths.splice(idx, 1)); } else if (parentMeta.keyPaths.length > 0) { // 父节点上可能有多路径,转移到子节点的 keyPaths keyStrPaths 里 parentMeta.keyPaths.forEach((keyPath) => { const curKeyPath = keyPath.concat(key); keyPaths.push(curKeyPath); keyStrPaths.push(toKeyStrPath(curKeyPath)); }); } else { keyPaths = [keyPath]; keyStrPaths = [keyStrPath]; } } if (!arrKeyPath.length && arrKeyPaths.length) { arrKeyPath = arrKeyPaths[0]; } if (arrKeyPath.length && !arrKeyPaths.length) { arrKeyPaths.push(arrKeyPath); } const keyStrPathStr = parentMeta ? `${parentMeta.keyStrPathStr}|${keyStr}` : keyStr; const meta = { id: genMetaId(), sourceId, // @ts-ignore add later rootMeta: null, parentMeta, parent: copy, selfType: dataType, self: baseData, // @ts-ignore add later copy: null, key, keyStr, keyPath, keyStrPath, keyStrPathStr, keyPaths, keyStrPaths, arrKeyPath, arrKeyPaths, level, // @ts-ignore add later /** @type any */ proxyVal: null, proxyItems: null, modified: false, scopes: [], /** * 当前对象 是否是一个一直可用的代理对象(不会被revoke), * 服务于 immut 接口 */ isImmutBase: immutBase, isDel: false, isArrOrderChanged: false, newNodeStats: {}, newNodeMap: new Map(), newNodes: [], ver, compareVer, revoke: noop, hasOnOperate, execOnOperate: noop, }; if (level === 0) { meta.rootMeta = meta; } else { // @ts-ignore 这里 parentMeta 一定有值 meta.rootMeta = parentMeta.rootMeta; } return meta; } /** * 是否是一个当前版本对应的草稿对象代理节点 */ function isDraft$1(mayDraft) { if (!mayDraft) { return false; } const meta = getDraftProxyMeta(mayDraft); if (!meta) { return false; } return !meta.isImmutBase; } function getNextMetaLevel(mayContainMetaObj, apiCtx) { const meta = getDraftMetaByCtx(mayContainMetaObj, apiCtx); return meta ? meta.level + 1 : 1; } function getSafeDraftMeta(proxyDraft, apiCtx) { // @ts-ignore return apiCtx.metaMap.get(proxyDraft); } function getDraftMetaByCtx(mayProxyDraft, apiCtx) { if (!mayProxyDraft) { return null; } if (apiCtx) { return apiCtx.metaMap.get(mayProxyDraft) || null; } return getPrivateMeta(mayProxyDraft) || null; } function getDraftMeta(mayProxyDraft) { if (!mayProxyDraft) { return null; } return getPrivateMeta(mayProxyDraft) || null; } function getMetaVer(mayDraftProxy) { return mayDraftProxy ? mayDraftProxy[META_VER] || '' : ''; } function getDraftProxyMeta(mayDraftProxy) { return getPrivateMeta(mayDraftProxy) || null; } /** * 判断两个值是否相同,true 表示不相等,false 表示相等 */ function isDiff$1(val1, val2) { const meta1 = getDraftProxyMeta(val1); const meta2 = getDraftProxyMeta(val2); if (!meta1 && !meta2) { return !Object.is(val1, val2); } const { self: self1, modified: modified1, compareVer: cv1, ver: ver1, level: level1, } = meta1 || { self: val1, modified: false, compareVer: false, ver: '0', level: 0 }; const { self: self2, modified: modified2, compareVer: cv2, ver: ver2, level: level2, } = meta2 || { self: val2, modified: false, compareVer: false, ver: '0', level: 0 }; if (self1 !== self2) { // self 是内部维护的值,可不用 Object.is 判断 return true; } if ((cv1 || cv2) && (level1 === 0 || level2 === 0) && ver1 !== ver2) { return true; } return modified1 || modified2; } /** * 浅比较两个对象,除了专用于比较 helux 生成的代理对象,此函数还可以比较普通对象 * ```txt * true:两个对象一样 * false:两个对象不一样 * ``` */ function shallowCompare$1(prevObj, nextObj, compareLimuProxyRaw = true) { const diffFn = compareLimuProxyRaw ? isDiff$1 : Object.is; const isObjDiff = (a, b) => { for (let i in a) if (!(i in b)) return true; for (let i in b) if (diffFn(a[i], b[i])) return true; return false; }; const isEqual = !isObjDiff(prevObj, nextObj); return isEqual; } /** * 内部调用,相信 proxyData 是 limu 产生的代理数据 */ function getPrivateMeta(proxyData) { return proxyData[PRIVATE_META]; } function replaceMetaPartial(oldMeta, newMeta, key) { newMeta.copy = oldMeta.copy; newMeta.self = oldMeta.self; newMeta.parentMeta[key] = oldMeta.self; } function getSourceId(rawData) { return finalDataSourceId.get(rawData) || genSourceId(); } function setSourceId(rawData, sourceId) { return finalDataSourceId.set(rawData, sourceId); } function getMultiRefPathsDict(sourceId) { // get 时不创建,避免额外性能损耗 return MRDSid2KeyDict.get(sourceId); } function setMultiRefPaths(sourceId, key, paths) { let dict = MRDSid2KeyDict.get(sourceId); if (!dict) { dict = {}; MRDSid2KeyDict.set(sourceId, dict); } dict[key] = paths; } function getMultiRefPathsByKey(sourceId, key) { const dict = getMultiRefPathsDict(sourceId); // 不写为 (dict || {})[key] || [] , 因为此写法性能较差一些 if (!dict) { return []; } return dict[key] || []; } function getMultiRefPaths(sourceId) { const paths = MRDSid2PathsList.get(sourceId) || []; return paths; } function clearMultiRefData(sourceId, toClearIdxList, toClearKeyStrList) { const dict = MRDSid2KeyDict.get(sourceId); if (dict) { toClearKeyStrList.forEach((keyStr) => Reflect.deleteProperty(dict, keyStr)); } const pathsList = MRDSid2PathsList.get(sourceId) || []; // @ts-ignore const newPathsList = pathsList.filter((v, idx) => !toClearIdxList.includes(idx)); MRDSid2PathsList.set(sourceId, newPathsList); } function recordMultiRefData(meta, keyStrs) { const { sourceId, keyPaths } = meta; keyStrs.forEach((keyStr) => setMultiRefPaths(sourceId, keyStr, keyPaths)); // [[1,2,3],[4,5,6]], [[a,b],[x]], ... const pathsList = MRDSid2PathsList.get(sourceId) || []; const keyStrsOfKP = keyPaths.map((keyPath) => getKeyStrByPath(keyPath, true)); let matched = false; out: for (const paths of pathsList) { for (const keyPath of paths) { const curKeyStr = getKeyStrByPath(keyPath, true); if (keyStrsOfKP.includes(curKeyStr)) { const keyStrsOfPaths = paths.map((keyPath) => getKeyStrByPath(keyPath, true)); keyPaths.forEach((keyPath, idx) => { if (!keyStrsOfPaths.includes(keyStrsOfKP[idx])) { paths.push(keyPath); } }); matched = true; break out; } } } if (!matched) { pathsList.push(keyPaths); } MRDSid2PathsList.set(sourceId, pathsList); } /** * 将某个草稿对象(代理对象)赋值到另一个地方,例如: draft.current = draft.list[0]; * 将重建路径连接关系 */ function mayRelinkPath(key, parentMeta, valueMeta) { let proxyVal = null; const shouldRelink = valueMeta && valueMeta.parentMeta !== parentMeta; if (!shouldRelink) { return proxyVal; } const prevKeyPath = valueMeta.keyPath; const newKeyPath = parentMeta.keyPath.concat(key); const prevKeyStrPath = toKeyStrPath(prevKeyPath); const newKeyStrPath = toKeyStrPath(newKeyPath); const prevKeyStr = prevKeyStrPath.join('|'); const newKeyStr = newKeyStrPath.join('|'); if (prevKeyStr !== newKeyStr) { // 发现一条新的路径指向当前 value,说明存在多引用 pushKeyPath(valueMeta, newKeyPath, newKeyStrPath); recordMultiRefData(valueMeta, [prevKeyStr, newKeyStr]); // if (valueMeta.modified) { // let curKey = key; // let curMeta = valueMeta; // let curPMeta = parentMeta; // do { // curPMeta.copy[curKey] = curMeta.copy; // curPMeta.modified = true; // curKey = curPMeta.key; // curMeta = curPMeta; // // @ts-ignore // curPMeta = curPMeta.parentMeta; // } while (curPMeta); // } const modified = valueMeta.modified; let curKey = key; let curMeta = valueMeta; let curPMeta = parentMeta; do { curPMeta.copy[curKey] = curMeta.copy; curPMeta.modified = modified; curKey = curPMeta.key; curMeta = curPMeta; // @ts-ignore curPMeta = curPMeta.parentMeta; } while (curPMeta); proxyVal = valueMeta.proxyVal; } return proxyVal; } function ressignArrayItem(listMeta, itemMeta, ctx) { 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) => item === itemMeta.copy || item === itemMeta.proxyVal); if (index >= 0) { copy[index] = targetNode; } return; } copy[key] = targetNode; } function isInSameScope(mayDraftProxy, callerScopeVer) { if (!isObject(mayDraftProxy)) { return true; } return getMetaVer(mayDraftProxy) === callerScopeVer; } function clearScopes(rootMeta, apiCtx) { const { metaMap } = apiCtx; // TODO 下钻有一定的性能损耗,允许用户关闭此逻辑 findDraftNodeNewRef=false const drilledMap = 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, parentObj, key) => { 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; 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; } function handleMultiRef(rootMeta, final) { const keyPathsList = getMultiRefPaths(rootMeta.sourceId); let idx = -1; const toClearIdxList = []; const toClearKeyStrList = []; for (const keyPaths of keyPathsList) { idx += 1; let changedMeta = null; let fixedMeta = null; const results = []; 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); } } function extractFinalData(rootMeta, apiCtx) { 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; } function recordVerScope(meta) { meta.rootMeta.scopes.push(meta); } function createScopedMeta(key, baseData, options) { const { traps, immutBase, apiCtx, autoRevoke } = options; // new meta data for current data node const meta = newMeta(key, baseData, options); const copy = mayMakeCopy(baseData, options); meta.copy = copy; const dataNodeTraps = Object.assign(Object.assign({}, traps), { get: (parent, key) => { if (PRIVATE_META === key) { return meta; } return traps.get(parent, key); } }); if (immutBase) { const ret = new Proxy(copy, dataNodeTraps); meta.proxyVal = ret; meta.revoke = noop; } else { const ret = Proxy.revocable(copy, dataNodeTraps); meta.proxyVal = ret.proxy; meta.revoke = autoRevoke ? ret.revoke : noop; } apiCtx.metaMap.set(copy, meta); apiCtx.metaMap.set(meta.proxyVal, meta); apiCtx.metaMap.set(meta.self, meta); return meta; } function shouldGenerateProxyItems(parentType, key) { // !!! 对于 Array,直接生成 proxyItems if (parentType === ARRAY) return true; const fnKeys = PROXYITEM_FNKEYS[parentType] || []; return fnKeys.includes(key); } function getMayProxiedVal(val, options) { const { key, parentMeta, parent, parentType, apiCtx } = options; 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 = tmp; // 区别于 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 = tmp; // 区别于 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(val, key); } 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 使用 */ 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 = getDraftMetaByCtx(mayProxyVal, apiCtx); const value = draftMeta ? draftMeta.copy || draftMeta.self : mayProxyVal; meta.rootMeta.execOnOperate('get', args[0], { mayProxyVal, value, parentMeta: meta, isChanged: false }); } return mayProxyVal; }; } } function mayMarkModified(options) { const { calledBy, parentMeta, op, parentType } = options; // 对于由 set 陷阱触发的 handleDataNode 调用,需要替换掉爷爷数据节点 key 指向的 value if (['deleteProperty', 'set'].includes(calledBy) || (calledBy === 'get' && ((parentType === SET && SHOULD_REASSIGN_SET_METHODS.includes(op)) || // 针对 Set.add (parentType === ARRAY && SHOULD_REASSIGN_ARR_METHODS.includes(op)) || // 针对 Array 一系列的改变操作 (parentType === MAP && SHOULD_REASSIGN_MAP_METHODS.includes(op)))) // 针对 Map 一系列的改变操作 ) { markModified(parentMeta); } } function getValPathKey(parentMeta, key) { const pathCopy = parentMeta.keyPath.slice(); pathCopy.push(key); const valPathKey = pathCopy.join('|'); return valPathKey; } function handleDataNode(parentDataNode, copyCtx) { const { op, key, value: mayProxyValue, calledBy, parentType, parentMeta, apiCtx, isValueDraft, mayNewNode } = copyCtx; /** * 防止 value 本身就是一个 Proxy * var draft_a1_b = draft.a1.b; * draft.a2 = draft_a1_b; */ const value = getUnProxyValue(mayProxyValue, apiCtx); /** * 链路断裂,此对象未被代理 * // draft = { a: { b: { c: 1 } }}; * const newData = { n1: { n2: 2 } }; * draft.a = newData; * draft.a.n1.n2 = 888; // 此时 n2_DataNode 是未代理对象 */ if (!parentMeta) { parentDataNode[key] = value; return; } const { self, copy: parentCopy } = parentMeta; mayMarkModified({ calledBy, parentMeta, op, key, parentType }); // 是 Map, Set, Array 类型的方法操作或者值获取 const fnKeys = CAREFUL_FNKEYS[parentType] || []; // 是函数调用 if (isFn(mayProxyValue) && fnKeys.includes(op)) { // slice 操作无需使用 copy,返回自身即可 if ('slice' === op) { return self.slice; } if (CHANGE_ARR_ORDER_METHODS.includes(op)) { parentMeta.isArrOrderChanged = true; } if (parentCopy) { // 因为 Map 和 Set 里的对象不能直接操作修改,是通过 set 调用来修改的 // 所以无需 bind(parentDataNodeMeta.proxyVal), 否则会以下情况出现, // Method Map.prototype.forEach called on incompatible receiver // Method Set.prototype.forEach called on incompatible receiver // see https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Errors/Called_on_incompatible_type if (parentType === SET || parentType === MAP) { // 注意 forEach 等方法已提前生成了 proxyItems,这里 bind 的目标优先取 proxyItems return parentCopy[op].bind(parentCopy); } return parentCopy[op]; } return self[op].bind(self); } if (!parentCopy) { return value; } const oldValue = parentCopy[key]; const tryMarkDel = () => { const oldValueMeta = getDraftMetaByCtx(oldValue, apiCtx); oldValueMeta && (oldValueMeta.isDel = true); }; const tryMarkUndel = () => { const valueMeta = getDraftMetaByCtx(mayProxyValue, apiCtx); if (valueMeta && valueMeta.isDel) { valueMeta.isDel = false; valueMeta.key = key; valueMeta.keyPath = parentMeta.keyPath.concat([key]); valueMeta.level = parentMeta.level + 1; valueMeta.parent = parentMeta.copy; valueMeta.parentMeta = parentMeta; } }; if (OP_DEL === op) { const valueMeta = getDraftMetaByCtx(mayProxyValue, apiCtx); // for test/complex/data-node-change case3 if (valueMeta) { const { keyPaths } = valueMeta; if (keyPaths.length === 1) { valueMeta.isDel = true; } else { // 存在多个路径指到当前对象时,调用 delKeyPath 删一条路径,并将 keyPath, keyStrPath 重新指向一个存在的路径 delKeyPath(valueMeta); } } else { // for test/complex/data-node-change (node-change 2) tryMarkDel(); } const val = parentCopy[key]; if (!isPrimitive(val)) { apiCtx.newNodeMap.delete(getValPathKey(parentMeta, key)); } delete parentCopy[key]; return; } // set 时非原始值都当做新节点记录下来 if (OP_SET === op && mayNewNode) { if (!isValueDraft && !isPrimitive(value)) { parentMeta.newNodeStats[key] = true; apiCtx.newNodeMap.set(getValPathKey(parentMeta, key), { parent: parentCopy, node: value, key, target: null }); } } parentCopy[key] = value; // 谨防是 a.b = { ... } ---> a.b = 1 的变异赋值方式 tryMarkDel(); tryMarkUndel(); } function deepFreeze$1(obj) { if (isPrimitive(obj)) { return obj; } // @ts-ignore if (Array.isArray(obj) && obj.length > 0) { obj.forEach(deepFreeze$1); return Object.freeze(obj); } if (isSet(obj)) { const set = obj; // TODO: throw error 'do not mutate' ? set.add = () => set; set.delete = () => false; set.clear = noop; // @ts-ignore for (const item of set.values()) { Object.freeze(item); } return Object.freeze(obj); } if (isMap(obj)) { const map = obj; // TODO: throw error 'do not mutate' ? map.set = () => map; map.delete = () => false; map.clear = noop; // @ts-ignore for (const item of map.values()) { Object.freeze(item); } return Object.freeze(obj); } // get all properties const propertyNames = Object.getOwnPropertyNames(obj); propertyNames.forEach((name) => { // @ts-ignore const value = obj[name]; deepFreeze$1(value); }); return Object.freeze(obj); } // 可直接返回的属性 // 避免 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 const FINISH_HANDLER_MAP = new Map(); 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 || []; // @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; // 调用那一刻起,确定 autoFreeze 值 // allow user overwrite autoFreeze setting in current call process const autoFreeze = (_d = opts.autoFreeze) !== null && _d !== void 0 ? _d : conf.autoFreeze; const disableProxy = (_e = opts.disableProxy) !== null && _e !== void 0 ? _e : false; let metaVer = ''; let isDraftFinished = false; const apiCtx = { metaMap: new Map(), newNodeMap: new Map(), metaVer }; if (!disableProxy) { metaVer = genMetaVer(); apiCtx.metaVer = 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} failed, cuase draft root has been finised! key:`, key); return silenceSetTrapErr; }; const logSetExpiredValFailed = (op, key) => { console.warn(`${op} failed, cuase the value is an expired limu proxy data! key:`, key); return silenceSetTrapErr; }; 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; const isNotGet = op !== OP_GET; // set del 时,replacedValue 初始值指向 value // get 时,replacedValue 初始值指向 mayProxyVal let replacedValue = isNotGet ? value : mayProxyVal; if (!onOperate) return { isChanged, replacedValue }; const parentMeta = (inputPMeta || {}); const { selfType = '', keyPath = [], copy, self, modified, proxyVal: parentProxy, arrKeyPath = [], keyPaths = [], keyStrPaths = [], arrKeyPaths = [], } = parentMeta; // console.log('execOnOperate parentMeta', 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 (isNotGet) { // 变化之后取 copy 比较 const node = modified ? copy : self; isChanged = inputPMeta ? node[key] !== value : true; } } let isReplaced = false; const replaceValue = (newValue) => { isReplaced = true; replacedValue = newValue; }; const getReplaced = () => ({ isReplaced, replacedValue }); onOperate({ immutBase, parent: self, parentType: selfType, parentProxy, op, replaceValue, getReplaced, isBuiltInFnKey, isChanged, isCustom, key, keyPath, keyPaths, keyStrPaths, fullKeyPath: keyPath.concat(key), arrKeyPath, arrKeyPaths, value, // 写操作时,proxyValue 是 undefined proxyValue: mayProxyVal, }); return { replacedValue, isChanged, }; }; const limuApis = (() => { // let revoke: null | (() => void) = null; /** * 为了和下面这个 immer case 保持行为一致 * https://github.com/immerjs/immer/issues/960 * 如果数据节点上人工赋值了其他 draft 的话,当前 draft 结束后不能够被冻结( 见se