limu
Version:
A fast js lib of immutable data, based on shallow copy on read and mark modified on write mechanism
155 lines (140 loc) • 5.46 kB
text/typescript
/*---------------------------------------------------------------------------------------------
* Licensed under the MIT License.
*
* @Author: fantasticsoul
*--------------------------------------------------------------------------------------------*/
import type { AnyObject, DraftMeta } from '../inner-types';
import {
ARRAY,
CAREFUL_FNKEYS,
CHANGE_ARR_ORDER_METHODS,
MAP,
OP_DEL,
OP_SET,
SET,
SHOULD_REASSIGN_ARR_METHODS,
SHOULD_REASSIGN_MAP_METHODS,
SHOULD_REASSIGN_SET_METHODS,
} from '../support/consts';
import { isFn, isPrimitive } from '../support/util';
import { getUnProxyValue } from './helper';
import { getDraftMetaByCtx, markModified } from './meta';
import { delKeyPath } from './path-util';
function mayMarkModified(options: { calledBy: string; parentMeta: DraftMeta; op: string; parentType: string; key: string | number }) {
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: DraftMeta, key: string) {
const pathCopy = parentMeta.keyPath.slice();
pathCopy.push(key);
const valPathKey = pathCopy.join('|');
return valPathKey;
}
export function handleDataNode(parentDataNode: any, copyCtx: { parentMeta: DraftMeta } & AnyObject) {
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();
}