Open cisen opened 5 years ago
immer的撤销重做比较复杂,因为每一步都是记录变更,而不是变更后的结果,而且每一次操作都是一个数组,需要做两次数组翻转,一个实现的store例子:
import produce, { applyPatches } from 'immer';
import { genRandomKey } from './until';
// 注意:由于一开始对immer产生的变更记录不熟,导致历史记录用数组+index记录,建议后期
// 用类effect-list的链表重构
export default function createStore(initialState) {
let state = initialState;
const listeners= [];
// 通过key来保证是否将多次修改保存为一次
const changesMap = new Map();
const inverseChangesMap = new Map();
// 存一份key好倒叙遍历Map,节省性能
let changeKeyArr = [];
let inverseChangesKeyArr = [];
// 已经回退了多少步
let undoSteps = 0;
state.changeKeyArr = changeKeyArr;
state.inverseChangesKeyArr = inverseChangesKeyArr;
// isRecord: 是否需要记录为一次历史记录,否则将放到上一次历史记录的map里面
// key:相同key的将记录为一次记录
function setState(cb, options = {isRecord: false, key: null, inverseKey: null}) {
state = produce(state, cb, (patches, inversePatches) => {
if (undoSteps) {
changeKeyArr = deleHistoryItem(undoSteps, changeKeyArr, changesMap);
inverseChangesKeyArr = deleHistoryItem(undoSteps, inverseChangesKeyArr, inverseChangesMap);
}
console.log('resizing', patches);
let inverOption = Object.assign({}, options, {key: options.inverseKey});
buildHistoryItem(options, patches, changeKeyArr, changesMap);
buildInverseHistoryItem(inverOption, inversePatches, inverseChangesKeyArr, inverseChangesMap);
undoSteps = 0;
});
state = produce(state, (state) => {
state.undoSteps = 0;
state.doSteps = changeKeyArr.length - 0;
});
// state = assign({}, state, partial);
for (let i = 0; i < listeners.length; i++) {
listeners[i](state);
}
}
function getState() {
return state;
}
function subscribe(listener) {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
// undo
function undo() {
console.log('undo data', changesMap, inverseChangesMap);
let key = inverseChangesKeyArr[inverseChangesKeyArr.length - (undoSteps + 1)];
if (!key) {
return false;
}
let inverseChanges = inverseChangesMap.get(key);
if (!inverseChanges || !inverseChanges.length) {
console.log('undo fail', inverseChanges);
return;
}
state = applyPatches(state, inverseChanges);
undoSteps += 1;
state = produce(state, (state) => {
state.undoSteps = undoSteps;
state.doSteps = changeKeyArr.length - undoSteps;
});
for (let i = 0; i < listeners.length; i++) {
listeners[i](state);
}
return true;
}
// redo
function redo() {
console.log('undo data', changesMap, inverseChangesMap);
let key = changeKeyArr[changeKeyArr.length - undoSteps];
if (!key) {
return false;
}
let changes = changesMap.get(key);
if (!changes || !changes.length) {
console.log('redo fail', changes);
return;
}
state = applyPatches(state, changes);
undoSteps -= 1;
state = produce(state, (state) => {
state.undoSteps = undoSteps;
state.doSteps = changeKeyArr.length - undoSteps;
});
for (let i = 0; i < listeners.length; i++) {
listeners[i](state);
}
return true;
}
// 建立一条历史记录
function buildHistoryItem(options, patches, keyArray, historyMap) {
const { isRecord, key } = options;
let patchesEnable = patches && patches.length;
if (key) {
if (historyMap.has(key)) {
if (patchesEnable) {
historyMap.get(key).push(...patches);
}
} else {
if (patchesEnable) {
historyMap.set(key, []);
historyMap.get(key).push(...patches);
// historyMap.set(key, [...patches]);
keyArray.push(key);
}
}
} else {
// isRecord: 是否需要记录为一次历史记录,否则将放到上一次历史记录的map里面
if (isRecord) {
// 撤销重做不能用同样的key,否则map去重会有问题
let newKey = genRandomKey();
if (patchesEnable) {
historyMap.set(newKey, []);
historyMap.get(newKey).push(...patches);
// historyMap.set(newKey, [...patches]);
keyArray.push(newKey);
}
// 没key又没isRecord的直接放到上一次的map里面
} else {
let lastChangesKey = keyArray[keyArray.length - 1];
if (patchesEnable) {
if (lastChangesKey) {
historyMap.get(lastChangesKey).push(...patches);
} else {
// 如果已经没有撤退则产生新的记录
let nextKey = genRandomKey();
keyArray.push(nextKey);
historyMap.set(nextKey, []);
historyMap.get(nextKey).push(...patches);
// historyMap.set(nextKey, [...patches]);
}
}
}
}
}
// 建立一条倒序历史记录
function buildInverseHistoryItem(options, patches, keyArray, historyMap) {
const { isRecord, key } = options;
let patchesEnable = patches && patches.length;
if (key) {
if (historyMap.has(key)&& historyMap.get(key) && historyMap.get(key).length) {
if (patchesEnable) {
historyMap.get(key).unshift(...patches.reverse());
}
} else {
if (patchesEnable) {
historyMap.set(key, []);
historyMap.get(key).push(...patches.reverse());
// historyMap.set(key, [...patches]);
keyArray.push(key);
}
}
} else {
// isRecord: 是否需要记录为一次历史记录,否则将放到上一次历史记录的map里面
if (isRecord) {
// 撤销重做不能用同样的key,否则map去重会有问题
let newKey = genRandomKey();
if (patchesEnable) {
historyMap.set(newKey, []);
historyMap.get(newKey).push(...patches.reverse());
// historyMap.set(newKey, [...patches]);
keyArray.push(newKey);
}
// 没key又没isRecord的直接放到上一次的map里面
} else {
let lastChangesKey = keyArray[keyArray.length - 1];
if (patchesEnable) {
if (lastChangesKey && historyMap.get(lastChangesKey) && historyMap.get(lastChangesKey).length) {
historyMap.get(lastChangesKey).unshift(...patches.reverse());
} else {
// 如果已经没有撤退则产生新的记录
let nextKey = genRandomKey();
keyArray.push(nextKey);
historyMap.set(nextKey, []);
historyMap.get(nextKey).push(...patches);
// historyMap.set(nextKey, [...patches]);
}
}
}
}
}
// 删除没用的历史记录
function deleHistoryItem(undoStepIdex, keyArray, historyMap) {
let newKeyArray = keyArray.slice(0, keyArray.length - undoStepIdex);
let deleKeyArray = keyArray.slice(keyArray.length - undoStepIdex);
deleKeyArray.forEach((item) => {
if (item) historyMap.delete(item);
});
return newKeyArray;
}
return {
setState,
getState,
undo,
redo,
subscribe
};
}
用法
produce
由于setState接受一个函数,所有又可以这么写
produce可深层比较,甚至深层嵌套的数组,只修改其中一个元素,整个数组都不===,但是里面的其他元素都是===的,如
Currying
produce还可以作为一个回调函数接受上一个函数传递下来的参数作为参数,比如下面的draft和index分别对象map的item和index
// example usage console.dir([{}, {}, {}].map(mapper)) //[{index: 0}, {index: 1}, {index: 2}])
如果你想初始化没有初始化的state,你可以传递produce第二个参数
applyPatches
applyPatches主要用于撤销/重做 在处理producer的过程中,Immer可以记录所有reducer改变生成的patches,这能让你使用一份fork数据实现撤销重做功能。执行patches需要使用applyPatches函数 例子:
Map和Set
Map和Set都需要将旧map copy到新map中,但是这里有个问题,比如一个map中的width改变了,在不可变判断的时候,判断不出height是否有改变,它是这个map的所有属性都改变了
map:
其他
这样会导致撤销重做死循环爆内存