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[$evtNameas $item )
            {
                
if ( is_null$item['scope'] ) )
                {
                    
//全局作用域
                     call_user_func_array$item['handler', $params );
                }
                
else if ( is_string$item['scope'] ))
                {
                    
//类上的静态调用
                    call_user_func_arrayarray$item['scope', $item['handler'] ) , $params );
                }
                
else
                {
                    
//在scope上调用
                    $strParams = '';
                    
$strCode = '$myobj->$fnName(';
                    
for ( $i &#61; 0 ; $i < count$params ) ; $i &#43;&#43; )
                    {
                        
$strParams .&#61; ( &#39;$params[&#39;.$i.&#39;]&#39; );
                        
if ( $i !&#61; count$params )-1 )
                        {
                            
$strParams .&#61; &#39;,&#39;;
                        }
                    }
                    
$strCode &#61; $strCode.$strParams.");";
                    
$anonymous &#61; create_function&#39;$fnName , $myobj , $params&#39; , $strCode);
                    
$anonymous$item[&#39;handler&#39;, $item[&#39;scope&#39;, $params );
                }
            }
        }
    }
    
    
/**
     * 获取一个监听者
     *
     * &#64;param string $evtName
     * &#64;param string $handler
     * &#64;param mixed $scope
     * &#64;param bool $isDelete    //获取监听者以后是否删除这个监听者&#xff0c;默认为false
     * &#64;return Array / null
     
*/
    
private function _getListener( $evtName , $handler , $scope , $isDelete &#61; false )
    {
        
$ret &#61; null;
        
if ( is_array$this->_handler[$evtName] ) )
        {
            
foreach ( $this->_handler[$evtNameas $key &#61;> $listeners )
            {
                
if ( $listeners[&#39;handler&#39;&#61;&#61; $handler && $listeners[&#39;scope&#39;&#61;&#61; $scope )
                {
                    
$ret &#61; $listeners;
                    
if ( $isDelete &#61;&#61; true )
                    {
                        
unset$this->_handler[$evtName][$key] );
                    }
                    
break;
                }
            }
            
if ( count($this->_handler[$evtName]) &#61;&#61; 0 )
            {
                
unset$this->_handler[$evtName] );
            }
        }
        
return $ret;
    }
}



下面是这个类的用法&#xff1a;

1&#xff1a;用简单函数响应事件

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

输出效果


2&#xff1a;用类静态函数和对象的方法响应事件

class MyClass
{
    
private $name &#61; &#39;abcde&#39;;
    
public function fnCallBack( $msg1 &#61; &#39;default msg1&#39; , $msg2 &#61; &#39;default msg2&#39; )
    {
        
echo &#39;object name:&#39;.$this->name;
        
echo "\n";
        
echo &#39;show msg1:&#39;.$msg1;
        
echo "\n";
        
echo &#39;show msg2:&#39;.$msg2;
        
echo "\n";
        
echo "\n";
    }
    
public static function fnCallBacks( $msg1 &#61; &#39;default msg1&#39; , $msg2 &#61; &#39;default msg2&#39; )
    {
        
echo &#39;show msg1 in static:&#39;.$msg1;
        
echo "\n";
        
echo &#39;show msg2 in static:&#39;.$msg2;
        
echo "\n";
        
echo "\n";
    }
}

$obj &#61; new MyClass();
$evt &#61; new Event();
$evt->addEventListener( &#39;test&#39; , &#39;fnCallBacks&#39; , &#39;MyClass&#39; );
$evt->addEventListener( &#39;test&#39; , &#39;fnCallBack&#39; , $obj );
$evt->fire(&#39;test&#39; , array(&#39;my first message&#39;) );

输出结果



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


Event类的包装

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

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

class NewsEvent extends Event 
{
    
public static $ADD_SUCCESS &#61; &#39;add_success&#39;;
    
public static $ADD_FAILED &#61; &#39;add_failed&#39;;
}

class News
{
    
public $title &#61; &#39;&#39;;
    
private $evt &#61; null;

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


以下是调用代码&#xff1a;

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


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

输出结果&#xff1a;






至此&#xff0c;类似Javascript&#xff08;或者是ActionScript3.0&#xff09;的事件模型&#xff0c;已经在PHP中实现了。


二次开发的注意事项


继承还是聚合

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

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


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

输出结果



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


事件描述文档

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

    编写要点

    1&#xff1a;事件列表
         也就是对象提供的所有事件响应列表&#xff0c;例如前面的News&#xff0c;事件列表就是&#xff1a;

          add_success
          add_failed

    2&#xff1a;触发事件的条件
          表示对象提供的事件在满足什么条件下触发&#xff0c;例如&#xff1a;

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

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

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

                 回调函数原型&#xff1a;
                 function ( $news )

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


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

                 回调函数原型&#xff1a;
                 function ( $news )

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


     其他参考

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


Ferris Xu
2009年04月23日