Javascript在处理事件的时候,使用了观察者模式,使得发生事件的对象和响应事件的对象完全的解耦,提高了系统的可扩展性。例如:

var  myListener  =   function ( e )
{
    
// TODO ...
}
myButton.addEventListener(
" click " , myListener);

    上面的代码为myButton添加了一个响应click事件的函数myListener。myButton对象不需要知道myListener函数是在何处以何种方式实现的,也不需要关心究竟有多少个响应函数会响应click事件,这样就可以极大的提高系统的扩展性和健壮性。

    要实现这个功能,最核心的技术就是函数的传递。在Javascript中,由于函数本身也是一个对象,因此可以很方便的当参数传递,但是,在PHP中如何传递函数呢?具体的方法可以参考我的另外一篇文章:
    PHP回调函数的实现方法
     http://www.cnitblog.com/CoffeeCat/archive/2009/04/21/56541.html


    我们已经知道如何在PHP中实现回调函数,因此就可以实现事件模型了,下面给出Event类的代码,这个类维护一个事件的响应队列,并提供注册事件、卸载事件和触发事件的方法。


class  Event {
    
protected   $_handler   =   array ();     // 事件处理器列表,是一个key=>value的数组,其中,key是字符串,表示事件名,value是数组,保存了响应事件的函数句柄
    
    
/* *
     * 添加一个事件处理器
     * 如果已经添加过了,就不重复添加了
     *
     * @param string                        要响应的事件名
     * @param string                         函数名
     * @param Object / string / null        作用域,也就是对象,如果为空,则表示全局作用域,如果为字符串,则表示指定的是类名,可以指定静态方法
     
*/
    
public   function  addEventListener(  $evtName   ,   $handler   ,   $scope   =   null  )
    {
        
$item   =   $this -> _getListener(  $evtName   ,   $handler   ,   $scope );
        
if  (  is_null $item  ) )
        {
            
$item   =   array ();
            
$item [ ' handler ' =   $handler ;
            
$item [ ' scope ' =   $scope ;
            
$this -> _handler[ $evtName ][]  =   $item ;    
        }
        
else
        {
            
echo   ' listener ignore ' ;
        }
    }
    
/* *
     * 删除一个事件处理器
     *
     * @param string                        要响应的事件名
     * @param string                         函数名
     * @param Object / string / null        作用域,也就是对象,如果为空,则表示全局作用域,如果为字符串,则表示指定的是类名,可以指定静态方法
     
*/
    
public   function  removeEventListener(  $evtName   ,   $handler   ,   $scope   =   null  )
    {
        
$item   =   $this -> _getListener(  $evtName   ,   $handler   ,   $scope   ,   true  );
    }
    
    
/* *
     * 触发函数事件
     *
     * @param string $evtName        事件名
     * @param Array $params        调用时的参数
     
*/
    
public   function  fire(  $evtName   ,   $params   =   null  )
    {
        
if  (  is_array $this -> _handler[ $evtName ] ) )
        {
            
foreach  (  $this -> _handler[ $evtName as   $item  )
            {
                
if  (  is_null $item [ ' scope ' ] ) )
                {
                    
// 全局作用域
                      call_user_func_array $item [ ' handler ' ,   $params  );
                }
                
else   if  (  is_string $item [ ' scope ' ] ))
                {
                    
// 类上的静态调用
                     call_user_func_array array $item [ ' scope ' ,   $item [ ' handler ' ] )  ,   $params  );
                }
                
else
                {
                    
// 在scope上调用
                     $strParams   =   '' ;
                    
$strCode   =   ' $myobj->$fnName( ' ;
                    
for  (  $i   =   0  ;  $i   <   count $params  ) ;  $i   ++  )
                    {
                        
$strParams   .=  (  ' $params[ ' . $i . ' ] '  );
                        
if  (  $i   !=   count $params  ) - 1  )
                        {
                            
$strParams   .=   ' , ' ;
                        }
                    }
                    
$strCode   =   $strCode . $strParams . " ); " ;
                    
$anonymous   =   create_function ' $fnName , $myobj , $params '   ,   $strCode );
                    
$anonymous $item [ ' handler ' ,   $item [ ' scope ' ,   $params  );
                }
            }
        }
    }
    
    
/* *
     * 获取一个监听者
     *
     * @param string $evtName
     * @param string $handler
     * @param mixed $scope
     * @param bool $isDelete    //获取监听者以后是否删除这个监听者,默认为false
     * @return Array / null
     
*/
    
private   function  _getListener(  $evtName   ,   $handler   ,   $scope   ,   $isDelete   =   false  )
    {
        
$ret   =   null ;
        
if  (  is_array $this -> _handler[ $evtName ] ) )
        {
            
foreach  (  $this -> _handler[ $evtName as   $key   =>   $listeners  )
            {
                
if  (  $listeners [ ' handler ' ==   $handler   &&   $listeners [ ' scope ' ==   $scope  )
                {
                    
$ret   =   $listeners ;
                    
if  (  $isDelete   ==   true  )
                    {
                        
unset $this -> _handler[ $evtName ][ $key ] );
                    }
                    
break ;
                }
            }
            
if  (  count ( $this -> _handler[ $evtName ])  ==   0  )
            {
                
unset $this -> _handler[ $evtName ] );
            }
        }
        
return   $ret ;
    }
}



下面是这个类的用法:

1:用简单函数响应事件

// 简单回调函数
function  fnCallBack(  $msg1   =   ' default msg1 '   ,   $msg2   =   ' default msg2 '  )
{
    
echo   ' show msg1: ' . $msg1 ;
    
echo   " <br />\n " ;
    
echo   ' show msg2: ' . $msg2 ;
    
echo   " <br />\n " ;
    
echo   " <br />\n " ;
}
$evt   =   new  Event();
$evt -> addEventListener(  ' test '   ,   ' fnCallBack '  );
$evt -> fire( ' test '   ,   array ( ' my first message ' ) );

输出效果


2:用类静态函数和对象的方法响应事件

class  MyClass
{
    
private   $name   =   ' abcde ' ;
    
public   function  fnCallBack(  $msg1   =   ' default msg1 '   ,   $msg2   =   ' default msg2 '  )
    {
        
echo   ' object name: ' . $this -> name;
        
echo   " <br />\n " ;
        
echo   ' show msg1: ' . $msg1 ;
        
echo   " <br />\n " ;
        
echo   ' show msg2: ' . $msg2 ;
        
echo   " <br />\n " ;
        
echo   " <br />\n " ;
    }
    
public   static   function  fnCallBacks(  $msg1   =   ' default msg1 '   ,   $msg2   =   ' default msg2 '  )
    {
        
echo   ' show msg1 in static: ' . $msg1 ;
        
echo   " <br />\n " ;
        
echo   ' show msg2 in static: ' . $msg2 ;
        
echo   " <br />\n " ;
        
echo   " <br />\n " ;
    }
}

$obj   =   new  MyClass();
$evt   =   new  Event();
$evt -> addEventListener(  ' test '   ,   ' fnCallBacks '   ,   ' MyClass '  );
$evt -> addEventListener(  ' test '   ,   ' fnCallBack '   ,   $obj  );
$evt -> fire( ' test '   ,   array ( ' my first message ' ) );

输出结果



可以看到,我们可以将响应事件的函数放置在任何位置,事件对象都能够正确的进行回调。


Event类的包装

    Event类提供了基本的事件模型,不过在实际应用的时候,我们应该对这个类进行进一步包装,因为不同的对象有不同的事件。例如,我们之前的PHP代码是通过fire来触发相关事件的,而在实际应用的时候,我们是通过一些操作的发生来触发事件的。

    举个例子,我们要开发一个新闻系统,并使新闻在添加操作的时候具有事件响应的能力,我们就需要把Event类包装在News中,如:

class  NewsEvent  extends  Event 
{
    
public   static   $ADD_SUCCESS   =   ' add_success ' ;
    
public   static   $ADD_FAILED   =   ' add_failed ' ;
}

class  News
{
    
public   $title   =   '' ;
    
private   $evt   =   null ;

    
public   function  __construct( )
    {
        
$this -> evt  =   new  NewsEvent();
    }
    
// 添加一篇新闻到数据库
     public   function  add()
    {
        
try
        {
            
// 在这里进行添加新闻的操作
             echo   ' adding news into Databasedot.gif ' ;
            
echo   " <br />\n " ;
            
// 添加完成,触发成功事件
             $this -> evt -> fire(NewsEvent :: $ADD_SUCCESS   ,   array $this  ));
        }
        
catch Exception   $e  )
        {
            
// 添加失败,触发失败事件
             $this -> evt -> fire(NewsEvent :: $ADD_FAILED   ,   array $this  ));
        }
    }
    
    
// 提供了事件注册和卸载的接口
     public   function  addEventListener(  $evtName   ,   $handler   ,   $scope   =   null  )
    {
        
$this -> evt -> addEventListener( $evtName   ,   $handler   ,   $scope  );
    }
    
public   function  removeEventListener(  $evtName   ,   $handler   ,   $scope   =   null  )
    {
        
$this -> evt -> removeEventListener( $evtName   ,   $handler   ,   $scope  );
    }
}


以下是调用代码:

function  fnAddSuccess(  $news  )
{
    
echo   ' news: ' . $news -> title . '  has been added ' ;
}
function  fnAddFailed(  $news  )
{
    
echo   ' news: ' . $news -> title . '  is NOT added ' ;
}
$myNews   =   new  News();
$myNews -> title  =   ' my news title ' ;
$myNews ->addEventListener(NewsEvent:: $ADD_SUCCESS  , fnAddSuccess );
$myNews ->addEventListener(NewsEvent:: $ADD_FAILED  , fnAddFailed );
$myNews ->add();


观察粗体的代码,可以看到,这个实现方式已经和我们的Javascript的方式非常相似了。

输出结果:






至此,类似Javascript(或者是ActionScript3.0)的事件模型,已经在PHP中实现了。


二次开发的注意事项


继承还是聚合

您可以通过包装Event类来使您自己的对象具有事件处理的能力,一般来说,包装Event类有2种方法,继承和聚合,“Event类的包装”中就是使用聚合来进行包装的,下面还是以新闻系统为例子,提供继承的示例:

None.gif class  News  extends  Event
None.gif{
None.gif    
public static $ADD_SUCCESS = 'add_success' ;
None.gif    
public static $ADD_FAILED = 'add_failed'
;
None.gif    
None.gif    
public   $title   =   '' ;
None.gif
None.gif    
// 添加一篇新闻到数据库
None.gif
     public   function  add()
None.gif    {
None.gif        
try
None.gif        {
None.gif            
// 在这里进行添加新闻的操作
None.gif
             echo   ' adding news into Databasedot.gif ' ;
None.gif            
echo   " <br />\n " ;
None.gif            
// 添加完成,触发成功事件
None.gif
             $this->fire(self::$ADD_SUCCESS , array$this  ));
None.gif        }
None.gif        
catch Exception   $e  )
None.gif        {
None.gif            
// 添加失败,触发失败事件
None.gif
             $this->fire(self::$ADD_FAILED , array$this  ));
None.gif        }
None.gif    }
None.gif}


调用示例
None.gif function  fnAddSuccess(  $news  )
None.gif{
None.gif    
echo   ' news: ' . $news -> title . '  has been added ' ;
None.gif}
None.gif
function  fnAddFailed(  $news  )
None.gif{
None.gif    
echo   ' news: ' . $news -> title . '  is NOT added ' ;
None.gif}
None.gif
$myNews   =   new  News();
None.gif
$myNews -> title  =   ' my news title ' ;
None.gif
$myNews -> addEventListener(News :: $ADD_SUCCESS   ,  fnAddSuccess );
None.gif
$myNews -> addEventListener(News :: $ADD_FAILED   ,  fnAddFailed );
None.gif
$myNews -> add();

输出结果



    继承的代码比聚合更简洁,但是我还是建议使用聚合。因为PHP是单继承模式,所以如果使用了继承来包装Event类,那么您的类就不能再继承其他类了。除非您确定您的类不会继承别的类,否则就不要使用继承。利用聚合将使代码的灵活性更大。


事件描述文档

    如果您的类提供了事件机制,这就意味着您的类可以被别人扩展。例如,前面我的News提供了事件机制,那么别人就可以通过响应onAddSuccess来做一些其他操作(比如发邮件通知管理员)。所以,提供清晰的事件描述文档十分重要。

     编写要点

    1:事件列表
         也就是对象提供的所有事件响应列表,例如前面的News,事件列表就是:

          add_success
          add_failed

    2:触发事件的条件
          表示对象提供的事件在满足什么条件下触发,例如:

          add_success
                  成功添加一篇新闻以后触发
          add_failed
                  一篇新闻添加失败后触发

    3:回调函数原型
           表示用户应该如何来定义用于响应的回调函数,同时要说明函数的参数是什么含义,例如:

          add_success
                  成功添加一篇新闻以后触发

                 回调函数原型:
                 function ( $news )

                 news表示刚刚添加成功的News类的对象


          add_failed
                  一篇新闻添加失败后触发

                 回调函数原型:
                 function ( $news )

                news表示刚刚添加失败的News类的对象


     其他参考

          大多数有文档的Javascript代码都有事件描述文档,可以参考JQuery的一个Widget:DatePicker的文档描述进行编写:
          http://docs.jquery.com/UI/Datepicker#events


Ferris Xu
2009年04月23日