框架深入 - 事件应该怎么用

  • 作者:KK

  • 发表日期:2017.4.12


事件是什么

前端提到事件就很好理解吧,比如给一个按钮设置click事件函数,一旦按钮被点击的时候这个函数就被执行了

那如果说作为后端的PHP也有事件,这该怎么解释呢?——其实是一样的

比如说给一个对象设置click事件函数,一旦这个对象被click了那函数也被执行了

可是按照大家从前学习的PHP知识,哪个事件这回事,而且对象怎么可能会被click?——确实也是没有这种知识的

但我们可以通过自己封装代码来实现事件,而且事件不一定就是那些click mousemove mouseout这些动作,可以是我们自己定义的(参考文章《jQuery自定义事件》)


PHP实现事件的简单例子

class Button{
	public $onclick = null;
	
	public function click(){
		if($this->onclick){
			call_user_func($this->onclick);
		}
	}
}

$button = new Button();
$button->onclick = function(){
	echo 'button click';
};

$button->click(); //button click

你会不会有点愕然?实际上就是将一个函数赋值给一个属性,然后click方法执行了这个函数,写前端时都经常接触这样的写法

这种情况实际上你当然不是第一次接触,并不新鲜,换一个说法根本就是:设置一个回调函数,在某个时机触发这个回调

没错,事件的本质就是预先设置回调,在未来某一时机再触发回调


像Yii一样可以设置多个回调

其实上面的简单例子不是很好,因为只能设置一个事件回调,在前端和Yii里其实都能设置多个回调的:

class Button{
	public $id = '#btnLogin';

	private $_callbacks = [];
	
	//绑定事件,其实就是设定某一事件的回调嘛
	public function on($eventName, $callback){
		if(!isset($this->_callbacks[$eventName])){
			$this->_callbacks[$eventName] = [];
		}
		
		$this->_callbacks[$eventName][] = $callback;
	}
	
	//触发事件的所有回调
	public function trigger($eventName){
		if(isset($this->_callbacks[$eventName])){
			foreach($this->_callbacks[$eventName] as $callback){
				call_user_func($callback, $this);
			}
		}
	}
}

$button = new Button();
$button->on('click', function($sender){
	echo 'click1';
});

$button->on('click', function($sender){
	echo 'click2';
});

$button->on('show', function($sender){
	echo 'show' . $sender->id;
});


$button->trigger('click'); //click1click2

$button->trigger('show'); //show#btnLogin

以上示例中实现了绑定多个click事件函数,这样在A地方绑定一个函数,对象传到B地方的时候,B再绑一个函数就不会覆盖掉A的函数了,甚至事件名称也可以不冲突


事件的应用场景

  1. 当一个执行过程在中间部分 有可能动态地新增或减少执行逻辑的时候,这些逻辑可以通过事件动态增减

  2. 当一个业务逻辑代码太长,又在中间出现了非关键逻辑(并且可能添加更多或删除一些非关键逻辑)的时候,这些非关键可以通过事件剥离

第1种典型场景会用到事件,通常就出现在框架/组件级的逻辑代码中,少有需求是出现在业务代码中的

而第2种场景就是出现在业务代码中了,本质上第2种场景和第1种场景是同一种场景,只是我用更贴切常见情况的方式来讲而已,而在PHP中最常见就是在框架中用到

来举个需求例子:当APP运行完控制器的时候,如果此时我们要判断是否有错误日志文件,如果有就发邮件通知管理员有日志。

在这种情况下当然第一时间不会想着去改代码加判断吧?(比如去yii\base\Application::run这里改?)

但是谢天谢地,幸好框架提供了事件支持!它在yii\base\Application::run里面trigger了一个EVENT_AFTER_REQUEST事件!这就好办了,我们在入口文件这样写代码就行了:

use yii\web\Application;

//$config = ...
$app = new Application($config);

$app->on(Application::EVENT_AFTER_REQUEST, function(yii\base\Event $event){
	$logFileCount = count(scandir(Yii::getAlias('@runtime/logs')));
	if($logFileCount > 2){
		$app = $event->sender; //发送事件的源头对象,就是Yii::$app
		$email = $app->mailer->compose();
		$email->setTo('admin@xxx.com');
		$email->setSubject('发现了' . ($logFileCount - 2) . '个错误日志,请及时处理!');
		$email->setHtmlBody('邮件内容邮件内容……');
		$app->mailer->send($email);
	}
});

$app->run();

检测日志发邮件不是运行框架的非必须关键代码,只是我们外部自定义的逻辑,所以我们可以通过事件来动态增加运行后的事情

如果哪天不需要邮件报警了,就可以把这个$app->on(...)的事件监听代码去掉,这样就实现了与框架代码分离、解耦

这里只是拿框架来做例子而已,实际上我更想拿一个比较贴切的业务场景例子来打比方,不过目前暂时没有比较好的样本,以后再回来补充吧


为什么许多PHP程序员不容易理解Yii2的事件

  1. 事件主要是前端接触得多,那个很好理解,后端少有这样的应用场景

  2. 直到他接触Yii2的时候一般也就工作了1~2年,此前的工作经验都是在相对简单直接的层面写代码

  3. 在服务端使用事件对初级工程师而言是比较难以想像的事

  4. 扩展、解耦的软件设计的意识不够强烈,在应该使用事件的时候却直接枚举了当前的可能性,在未来增加可能性场景时还要继续增加if判断来枚举更多可能性