/*--------------------------------------------------------------------------------------------- * Licensed under the MIT License. * * @Author: fantasticsoul *--------------------------------------------------------------------------------------------*/ import { AnyObject, DraftMeta, IApiCtx } from '../inner-types'; import { ARRAY, IS_RAW, MAP, PRIVATE_META, PROXYITEM_FNKEYS, SET } from '../support/consts'; import { isFn, isObject, isPrimitive, noop } from '../support/util'; import { mayMakeCopy } from './copy'; import { getDraftMetaByCtx, getSafeDraftMeta, markModified, newMeta } from './meta'; import { recordVerScope } from './scope'; export function createScopedMeta(key: any, baseData: any, options: any) { 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 = { ...traps, get: (parent: any, key: any) => { 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; } export function shouldGenerateProxyItems(parentType: any, key: any) { // !!! 对于 Array,直接生成 proxyItems if (parentType === ARRAY) return true; const fnKeys = PROXYITEM_FNKEYS[parentType] || []; return fnKeys.includes(key); } export function getMayProxiedVal(val: any, options: { parentMeta: DraftMeta } & AnyObject) { const { key, parentMeta, parent, parentType, apiCtx } = options; const mayCreateProxyVal = (val: any, inputKey?: string) => { 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: any = []; if (parentType === SET) { const tmp = new Set(); (parent as Set).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 as Map).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); } export function getUnProxyValue(value: any, apiCtx: IApiCtx) { 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: any, meta: DraftMeta, options: { dataType: 'Map' | 'Set'; apiCtx: any; }, ) { 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: any[]) { markModified(meta); return oriDel(...args); }; mapOrSet.clear = function limuClear(...args: any[]) { markModified(meta); return oriClear(...args); }; if (dataType === SET) { const oriAdd = mapOrSet.add.bind(mapOrSet); mapOrSet.add = function limuAdd(...args: any[]) { 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: any[]) { 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: any[]) { 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; }; } }