智慧 + 毅力 = 无所不能

正确性、健壮性、可靠性、效率、易用性、可读性、可复用性、兼容性、可移植性...

导航

undo机制分析

Posted on 2009-12-01 15:58  Bill Yuan  阅读(583)  评论(0编辑  收藏  举报

相信有不少RIA应用都有undo/redo功能。这里我就拿自己做过的画图板为例子说明一下它的实现原理(没有啥有用的代码,理解原理就行)。

分析

undo是什么?在用word的时候,写了一行字后悔了,执行一下undo那行字就消失了。undo就这么简单,将做过的事情再倒退回去。说专业一点,就是执行一个逆向动作。拿画图板里最简单的画直线来讲,画了一条直线,对应的undo就是擦掉这个线。很多的命令也都是类似的。所以我们看到,一个命令可以被“undo”,它至少包含了两种动作,一个是do,一个是undo,他们是相互逆向的动作。下面的事情就明朗了,如果一个命令被认为是可以undo的,那么它至少要实现两个方法,do和undo(大多数时候redo就是do)。但是命令千变万化,比如画直线和画曲线就不同,这里就需要用到设计模式中的Command。
代码:

代码
package com.drawing
{
        
public interface ICommand
        {
                
public function doIt():void //do
                public function undoIt():void //undo
        }
}


我定义了一个ICommand接口,它按照我分析的定义了两个方法do和undo,所以认为它是可以被undo的。Command代表的不是一个实实在在的组件,而是一个动作。所有的具体动作都是来自ICommand。比如画直线,它就可以这么定义。
代码:

代码


如果需要额外参数,从构造函数或者public方法注入都可以。当实际使用的时候,只要直接调用doIt或者undoIt就行了,根本不必管它是啥方法。
undoManager一统天下

分析完了undo原理,下面要做的就是把很多可以undo的命令组合起来思考。o(∩_∩)o…如果很多undo,redo混在一起就会发现事情也不是那么简单。这里有必要搞个undoManager来管理一下。还是先从原理开始分析,观察一下word,如果你分别写了三行字,undo两回后再写了一行字,然后你又想把原来的第二行恢复,发现无论如何也不能恢复!是不是有点乱?这里你只要明白一点就可以了:如果有一个新动作执行,那么原来已经被undo的动作就不能再redo了。用下面的图可以说明。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

如果把command看成一条链的话,先执行ABCD,undo一次就退回倒C,再undo就退回到B,如果有新的command加进来而不是继续undo或redo,新的command链ABE便会形成,CD就会被抛弃,再也不能redo了。从AS3角度,准备两个堆栈结构(Array),分别代表现存的已经执行过的command(如AB),和已经被undo过的command(以后可以被redo也可以被抛弃,如CD)。看代码。
代码:

 

代码
package com.drawing
{
        
public class UndoManager implements ICommand
        {
                
public function UndoManager()
                {
                }

                
// 一条队列存放已经执行过的command
                private var queue:Array = new Array();
                
// 一条队列存放已经undo过的command
                private var undoQueue:Array = new Array();

                
public function addNewCmd(cmd:ICommand):void
                {
                        undoQueue 
= new Array();//新command加入的时候,undo队列便被清空
                        queue.push(cmd);
                }

                
/////////////////////////////////
                
//redo
                public function doIt():void
                {
                        
//TODO: implement function
                        if(undoQueue.length>0)
                        {
                                var ic:ICommand 
= undoQueue.pop() as ICommand;
                                ic.doIt();
//redo

                                queue.push(ic);
                        }
                }

                
//undo
                public function undoIt():void
                {
                        
//TODO: implement function
                        if(queue.length>0)
                        {
                                
//把队列尾部的command拿出来执行undo
                                var ic:ICommand = queue.pop() as ICommand;
                                ic.undoIt();

                                
//执行完后插入undoQueue
                                undoQueue.push(ic);
                        }
                }

        }
}


实际用的时候很简单了,执行完一个command就addNewCmd到UndoManager,如果想undo或redo就直接操作UndoManager,根本不必管现在是啥状态,是不是很简单。