undo和redo是在做一次笔试题的时候遇到的,最近在用redux重构todos的时候,又出现了,这就写一下吧。
在应用中构建撤销和重做功能不是很简单,因为,面对这两个问题,你需要克隆所有相关的model来追踪每一个历史状态,还需要考虑整个撤销堆栈,用户的初始更改也是可以撤销的。
在redux中可以轻松的实现撤销,因为:
- 我们在redux应用中,只需要关心
state
的子树;- state也是不可变数据,所有修改被描述成独立的
action,
action与预期的撤销堆栈模型很接近了;- reducer的签名
(state,action)=>state
可以很自然的实现reducer enhancers
或者higher order reducers
.。它们会在我们为reducer添加额外的功能的时候保持着这个签名。
(以上粘自官网,hhh)
如果我们希望在一个应用在中实现撤销和重做,我们应该保存更多的state来解决问题:
- 撤销或重做之后的状态?
- 当前状态?
- 撤销堆栈中过去和未来的状态是什么?
根据这些,我们设计它们的算法:
{
past:Array<T>,
present:T,
future:Array<T>
}
undo:撤销
- 移除past中的最后一个元素
- 将上一步移除的元素赋予present.
- 将原来的present插入到future的最前面。
redo:重做
- 移除future中的第一个元素。
- 将上一步移除的元素赋予present.
- 将原来present追加到past的最后面。
处理其他Action
- 将当前present追加到past的最后面。
- 处理完action所产生的新的state赋予present.
- 清空future.
Reducer Enhancer
function undoable() {
//用一个空的action来调用reducer来产生初始的state;
const initialState = {
past: [],
present: reducer(undefined, {}),
future: []
};
//返回一个可以执行撤销和重做的新的reducer
return function (state = initialState, action) {
const {past, present, future} = state;
switch (action.type) {
case "UNDO":
const previous = past[past.length - 1];
const newPast = past.slice(0, past.length - 1);
return {
past: newPast,
present: previous,
future: [present, ...future]
};
case"REDO":
const next = future[0];
const newFuture = future.slice(1);
return {
past: [...past, present],
present: next,
future: newFuture
};
default:
//将其他action委托给原始的reducer处理
const newPresent = reducer(present, action);
if (present === newPresent) {
return state;
}
return {
past: [...past, present],
present: newPresent,
future: []
};
}
}
}
现在我们就可以将任意的reducer通过这个undoable()
进行封装,从而让它们可以处理undo和redo这两个action.
redux undo
使用:npm install --save redux-undo
下面将以todo为例,编写reducer.
扫描二维码关注公众号,回复:
2999745 查看本文章
import undoable,{includeAction} from 'redux-undo';
const todo = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
};
case "TOGGLE_TODO":
if (state.id !== action.id) {
return state;
}
return {
...state,
completed: !state.completed
};
default:
return state
}
};
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [...state, todo(undefined, action)];
case 'TOGGLE_TODO':
return state.map(t => todo(t, action));
default:
return state;
}
};
const undoableTodos = undoable(todos, {filter: includeAction(['ADD_TODO', 'TOGGLE_TODO'])})
export default undoableTodos;
注1: distinctState()过滤器会过滤掉没有引起state变化的action.
注2:当你要恢复一个state时,必须把present追加到它上面,并且需要通过检查.past.length
和.future.length
才能确定撤销和重做是否被使用。
//判断可否进行UndoRedo操作
const mapStateToProps = (state) => ({
canUndo: state.todos.past.length > 0,
canRedo: state.todos.future.length > 0
});
然后就是一系列渲染等等的操作,代码Github上,可以看看。