说起事件(event),我们可是一点都不陌生。现实生活当中的事件无处不在,比如你发了一条微博,触发了一条事件,导致关注你的人收到了一条消息,看到你发的内容;比如你通过支付宝买东西,付了款,触发一个事件,导致你收到一条短信,告诉你刚刚扣款了,你账户余额还有多少...

我们将事件稍稍加以抽象,发现事件具有某些共同特点,比如事件其实不是孤立存在,它只是某个流程或者工序的一个特殊的“点”,可以理解为时间点,也可以理解为逻辑的点;其次,事件上可以绑定一些“动作”,比如发送一条短信或者发送一个邮件;第三,我可以绑定,当然也可以解绑,如果我反感频频的短信提醒,我可以选择不发短信,我自己去查看账户余额;第四,这些动作和主流程往往并没有直接的关系,往往是“附加”的:我已经付完款了,你发短信或者不发,发邮件或者直接客服通知我其实影响不大,并不影响我购物这个行为本身——反正我已经付完款,预期不久就会收到商品了。

其实,说到这里,已经有点入戏的感觉了。人有生老病死,一年有春夏秋冬四季演替,封建王朝有兴盛、停滞、衰亡的周期律——“其兴也勃焉,其亡也忽焉”。换句话说,人,季节,王朝等等这些世间万物都有自己的生命周期。

那么在软件行业,一个系统,一个组件,一个功能,一个类都是有自己的生命周期的——创建、运行、销毁。比如一个类(Class)都要经过__construct(),调用各个类方法,__destruct() 的过程。每个程序的运行,要理解为一个过程或者流程。那么这样来理解事件就有意义了:事件无非就是这个过程之中一些有意义的“点”。这些点是人为做的设定,比如插入数据库数据,那么校验前、后,插入前、后就可能是几个有意义的时间节点,把这些节点看成一个个的事件,就更加便于我们去理解这整个过程。复杂的东西理解起来困难,我们分成若干个阶段来理解岂不是就简单许多了吗?想想为啥计算机网络为啥要分成物理层、数据链路层、网络层,运输层、应用层这个简单道理就行了。除了我们更好的去理解程序的运行流程,更为重要的是还能够使我们能够“介入”这个流程,改变这个流程,从而实现我们的目的。这就是往事件上“附加”一些动作或者行为了,专业点说,就是事件处理器或者事件监听者。我们想要在某个特定的时间点做点什么,就事先在这个对应的事件上绑定事件处理器,当流程走到这一步时,相应的处理器就被执行,完成我们事先设定的目的。想象一下:软件的运行就是沿着自己设定的路线,走过一个又一个重要节点,同时触发这些节点事件,最终走到自己生命终点的一个过程。将事件理解为流程中的节点,不仅可以帮我更好的认识程序,也能更好的帮助我们改造程序。

事件的实现,其实是观察者模式的一种体现。可能有人会说,为啥是观察者模式?Yii的事件并不符合观察者模式的经典定义啊?的确,Yii的事件实现机制中确实不存在经典Observer和Subject即观察者和被观察者。但是,不要在意这些细节——观察者模式主要面临场景是一对多松耦合的对象之间的关系,要解决的问题就是对象状态改变,其他对象能得到通知。“一对多”和“松耦合”是其核心要义和功能。看看Yii的事件其实正好能实现这两个功能:Yii的每个事件(beforeSave,afterSave这些)都是一个被观察者,每个事件处理器都是它的观察者,被观察者被触发时,所有观察者都依次要执行。

小编心得:观察者模式的经典定义——观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会受到通知并自动更新。

下面,我们来谈谈Yii2本身是如何实现事件的。

事件相关的类

在Yii中,事件是Component引入的,BaseObject 是不支持事件的,BaseObject只支持属性。当你需要使用事件时,就需要继承Component或者它的子类,而不能直接继承BaseObject。好在我们之前反复提过,Yii2是基于Component的,就是说,Yii2中大部分类都是Component的子类,是天生带有事件功能的。和其他框架相比,我觉这是Yii2的一大优势——从“原子”层面都已经让类具备很强的扩展性了。面对具体业务场景,事件往往成为一把利剑,使得使用者得心应手。同时,\yii\base\Event是于Yii2事件功能紧密相连的一个类,他封装与事件相关的有关数据,并可以自定义一些功能函数作为辅助。

class Event extends BaseObject
{// 事件的名称public $name;// 事件发布者,通常是调用了 trigger() 的对象或类,也可传递别的对象。public $sender;// 此事件是否被处理了,若为true,那事件的处理到此为止public $handled = false;// 事件的相关数据public $data;// 保存全局事件的处理器private static $_events = [];// 绑定类级别事件handlerpublic static function on($class, $name, $handler, $data = null, $append = true){// ...}// 取消类级别事件handler的绑定public static function off($class, $name, $handler = null){// ...}// 解绑所有类级别事件的handlerpublic static function offAll(){self::$_events = [];}// 判断一个类和其所有父类上是否绑定有事件处理器public static function hasHandlers($class, $name){// ...}// 触发类级别的事件,执行所有这个事件上绑定的handlerpublic static function trigger($class, $name, $event = null){// ...}
}

事件处理器

所谓事件处理器(事件handler)就是事件处理程序。“触发事件”和执行“事件处理器”其实是一个意思。说到底,事件处理器到底是什么呢?其实仅仅只是一个callable——或者是全局函数,或者是类的方法等。本质上说,事件处理器就是一段PHP代码,一个PHP函数。

具体说来,handler 有这么几种形式:

  • 匿名函数 function ($event) { ... }

  • 对象的方法 $object->handleClick() 即 [$object, 'handleClick']

  • 类的静态方法 Page::handleClick()

  • PHP系统函数或者自定义全局函数 trim,array_filter,handleClick等,注意只是函数名,不需要带任何参数

无论是哪种形式的handler,它们最终都必须要有如下的形式:

function ($event){...}

这里的$event正是yii\base\Event类或者其子类的实例。

事件的绑定和解绑

有了事件处理器,就可以将其绑定到特定的事件上了。绑定事件处理器是通过yii\base\Component::on() 来进行,相应的解绑是yii\base\Component::off()。

绑定

// 绑定的过程就是将handler写入$_event[]的过程
public function on($name, $handler, $data = null, $append = true)
{$this->ensureBehaviors();//$append为true时$handler置于数组最后,触发时最后被执行if ($append || empty($this->_events[$name])) {$this->_events[$name][] = [$handler, $data];} else { //$append 为false时$handler置于数组最前,触发时最先被执行array_unshift($this->_events[$name], [$handler, $data]);}
}

例如:

$order = new Order();
$order->on(Order::EVENT_CREATING, [$obj, 'verifyOrder',false]) ;
$order->on(Order::EVENT_CREATED, 'checkOrder') ;
$order->on(Order::EVENT_PAYED, ['\common\lib\Email', 'send']) ;
$order->on(Order::EVENT_PAYED, function ($event) {echo '订单已经支付';
});

绑定事件处理器时可以提供额外数据作为 \yii\base\Component::on() 方法的第三个参数,数据在事件被触发时能被处理器使用。如:

// 第三个参数传递数据,当订单被支付时输出'订单已支付'
$order->on(Order::EVENT_PAYED, function ($event) {echo $event->data;
}, '订单已支付');

事件处理器的绑定可以如上在运行的时候进行,也可以在配置的进行绑定。无论哪种方式,都必须是先绑定,后触发。

解绑

public function off($name, $handler = null)
{$this->ensureBehaviors();if (empty($this->_events[$name])) {return false;}// 如果没有传递$handler参数,那么解绑事件$name名下的所有handlerif ($handler === null) {unset($this->_events[$name]);return true;}$removed = false;// 如果传递参数$name,那么解绑$name下特定的handler:$handlerforeach ($this->_events[$name] as $i => $event) {// 注意$event的第一个元素是hanlder,第二个是$dataif ($event[0] === $handler) { unset($this->_events[$name][$i]);$removed = true;}}if ($removed) {$this->_events[$name] = array_values($this->_events[$name]);}return $removed;
}

解绑的例子:

// 解绑事件下所有的事件处理器
$order->off(Order::EVENT_CREATING) ;
// 解绑事件下checkOrder处理器
$order->off(Order::EVENT_CREATED, 'checkOrder') ;

一般来说,可以通过on绑定的handler,都可以通过off来解绑。但是匿名函数是除外的,你不能解绑某个特定的匿名函数,除非你事先将其保存为变量,而后通过这个变量来指定其解绑。

$myHandler = function($event) {....}
$order->on(Order::EVENT_CREATED, $myHandler);
$order->off(Order::EVENT_CREATED, $myHandler);

保存handler的数据结构

在Component中$_events专门用来维护这些handler:

private $_events = [];

事件的绑定逻辑是这样的:

  • 参数 $append 是否为 true , 表示所要绑定的事件handler要放在 $_event[] 数组的最后面。这也是默认的绑定方式。
  • 参数 $append 是否为 false ,表示handler要放在数组的最前面。
  • 如果事件还没有绑定任何handler,那么无论$append为true还是false,$_event[EVENT_NAME][]只有这么一个元素,既是第一个也是最后一个

handler在 $event[] 数组中的位置就代表执行的先后顺序,在现实生活中往往意义重大。

事件的触发

事件通过Component::trigger()来触发,触发本质就是执行事件handler的过程。

$order->trigger(Order::EVENT_CREATED);

源码如下:

public function trigger($name, Event $event = null)
{$this->ensureBehaviors();if (!empty($this->_events[$name])) {// 执行handler必须传递一个Event实例,用来传递数据if ($event === null) {$event = new Event();}// 指定是谁触发的这个事件,默认就是trigger调用者自身if ($event->sender === null) {$event->sender = $this;}$event->handled = false; // 默认事件没有被处理$event->name = $name; // 事件名称foreach ($this->_events[$name] as $handler) {$event->data = $handler[1];// 最关键的地方:所有handler都是通过call_user_func来执行的call_user_func($handler[0], $event);// 如果在$handler[0]中,$event->handled被置为true,表示事件已经被处理好了,后续handler不用再执行了if ($event->handled) {return;}}}// 执行类级别的事件处理器Event::trigger($this, $name, $event);
}

在触发事件时,我们经常会通过Event对象来传递数据:
假如有个场景是在文章下面评论送积分,那么评论后就会触发送积分的事件user_after_publish,另外我们知道送积分还有很多别的场合,不同场合送的分数不一样。因此我们积分需要有个类PointEvent来表示:

use yii\base\Event;
class PointEvent extends Event
{//要赠送的积分数量public $points = 0;// 事件处理结果消息public $msg = '';// 其他方法}

发表评论之前,先绑定“送积分”事件handler:

// 绑定handler
$user = Yii::$app->user->identity;
$user->on(User::EVENT_AFTER_PUBLISH, [$obj, 'afterPublish']);....// 在某个时间点触发
// 实例化一个PointEvent,points 指定为 10
$event = new PointEvent();
$event->points = 10;
$user->trigger(User::EVENT_AFTER_PUBLISH, $event);

事件处理器要为发表评论的用户积分+10:

public function afterPublish($event)
{$user = $event->sender;$points = $event->points;$user->points += $points;$user->save();
}

说到这里,可能有的小伙伴会问,用户发表评论获得积分这么个功能为啥要通过这种方式来实现,为啥不用下面“简单”的方式来实现呢:

public function publishComment()
{$param = Yii::$app->request->post();$user = Yii::$app->user->identity;// 新建评论$comment = new Comment();$comment->load($param, 'data');$comment->save();// 更新用户积分$user->points += intval($param['points']);$user->save();}

这样做,就是把新建评论和更新用户放在一个方法里面。但是这样做将会有个隐形的问题,如果用户评论之后,不光要积分+10,还要告知文章作者怎么办?这里只能在publishComment()后面继续添加代码了,如果哪天又要发送邮件啥的,还得继续往里面添加代码。这违反了“面对扩展开放,面对修改关闭”的编程原则,久而久之这块代码就会变得非常臃肿,很难再维护,分出去再写几个方法也无济于事。

分析这原因,就是从功能上说,评论和用户获得积分其实是松耦合的关系——你可以给积分也可以不给积分。但是在publishComment()方法你把这两个功能捆绑的死死的,丝毫分不开——这就是一种糟糕的设计。

相反,用事件就能很好解决这个问题。我们说,事件其实是一个流程上的某个特定的点,这里是流程就是用户发表评论,EVENT_AFTER_PUBLISH是发表成功后的一个节点,程序走到这里,触发一下。有处理器就执行,没有就继续往后执行。我们绑定了一个送积分的处理器 ,也可以绑定推送消息的处理器。这么看待问题,就已经解耦了两种功能。在实现上,将发表评论看做流程本身,而送积分,推送提醒看做是“附加”的,在需要的时候绑定下,不需要就不绑定。

下面是主流程:

public function publishComment()
{$user = Yii::$app->user->identity;// 新建之前的时间点:触发EVENT_BEFORE_PUBLISH$user->trigger(User::EVENT_BEFORE_PUBLISH);// 新建评论$comment = new Comment();$comment->load($param, 'data');$comment->save();// 新建之后的时间点:触发下 EVENT_AFTER_PUBLISH$user->trigger(User::EVENT_AFTER_PUBLISH, $event);}

有的场合是需要送积分:

$user->on(User::EVENT_AFTER_PUBLISH, [Points, 'givePoints']);

有的场合是要发邮件提醒:

$user->on(User::EVENT_AFTER_PUBLISH, [Email, 'notifyPoserOwner']);

还有的场合是需要判断用户有没有发表评论的权限:

$user->on(User::EVENT_BEFORE_PUBLISH, [Authorization, 'checkAuth']);
...
public function checkAuth($event)
{$user = $event->sender;if (!$user->isAdmin) {throw new AccessDeniedException('你没有权限发表评论');}
}

事件处理器Points::givePoints,Email::notifyPoserOwner,Authorization::checkAuth 分布在不同的类中,并没有出现在上面publishComment里面,而且可增可减,完全视需要而定。因此真正满足了“开闭原则”的要求。

小编心得:开闭原则——对扩展开放,对修改关闭。这是代码组织当中的一条非常重要的原则。

事件的级别

前面的事件,都是针对类的实例而言的,也就是事件的触发、处理全部都在实例范围内。这种级别的事件用情专一,不与类的其他实例发生关系,也不与其他类、其他实例发生关系。除了实例级别的事件外,还有类级别的事件。

这就好比是公司里的不同阶层。底层的码农们只能自己发发牢骚,个人的喜怒哀乐只发生在自己身上,影响不了其他人。而团队负责人如果心情不好,整个团队的所有成员今天都要战战兢兢,慎言慎行。到了公司老总那里,他今天不爽,那么公司所有员工都可能跟着遭殃。事件也是这样的,不同层次的事件,决定了他影响到的范围。

类级别的事件是由\yii\base\Event中的方法来实现的,和实例级别的事件在Component中的实现原理大同小异,只不过作用范围不同:

  • 实例级别的事件只能影响自身
  • 类级别的事件影响所有它的实例,类本身,所有子类,以及所有子类的实例

类级别事件

比如,我打算所有人发表评论后,都要发送给文章作者了。那么每个用户都去绑定一下那是多么糟糕的事情,那我们可以将发送提醒绑定在User类上,那么任何$user实例在发表评论后的节点上都绑定了发送提醒的处理器了。

如果某个场合又给某个实例$user绑定了送积分的事件,那么等评论一发表,用户首先获得积分,然后收到邮件——实例级别的事件总是先触发,类级别的事件总是后触发。别的$user没有绑定送积分的话当然就没有积分送了。

类级别事件handler的绑定通过Event::on()来实现的:

public static function on($class, $name, $handler, $data = null, $append = true)
{$class = ltrim($class, '\\');// 如果$append为true,那么$handler置于handler列表最后面if ($append || empty(self::$_events[$name][$class])) {self::$_events[$name][$class][] = [$handler, $data];} else { // 如果$append为true,那么$handler置于handler列表最前面array_unshift(self::$_events[$name][$class], [$handler, $data]);}
}

相比Component::on(),Event::on()还需要传入类(名) $class。Event维护的$_event[]要比Component下的$_event[]多一个维度:$class。绑定和解绑事件handler其实相当于向$_events[$name][$class]数组插入/删除handler的过程。当$append为true,$handler将被最后执行(默认情况),当为false,$handler将被第一个执行,除非有后面的handler取代其位置。

Event::on(User::class, Comment::EVENT_AFTER_PUBLISH, [$this, 'afterPublish'], $data);
Event::on(User::class, Comment::EVENT_BEFORE_PUBLISH, [$this, 'beforePublish'], [], false);

类级别事件解绑通过Event::off(),和实例级别事件解绑过程类似:

public static function off($class, $name, $handler = null)
{$class = ltrim($class, '\\');if (empty(self::$_events[$name][$class])) {return false;}if ($handler === null) {unset(self::$_events[$name][$class]);return true;}$removed = false;foreach (self::$_events[$name][$class] as $i => $event) {if ($event[0] === $handler) {unset(self::$_events[$name][$class][$i]);$removed = true;}}// ...
}

如果传递参数$handler,那么解绑类$class下事件$name中的$handler,否则就将$class下的事件$name的处理器清空。

事件的触发通过Event::trigger()来实现:

public static function trigger($class, $name, $event = null)
{if (empty(self::$_events[$name])) {return;}if ($event === null) {$event = new static();}$event->handled = false;$event->name = $name;if (is_object($class)) {// 当$class为一个对象时,sender默认便指定为这个对象if ($event->sender === null) {$event->sender = $class;}$class = get_class($class);} else {$class = ltrim($class, '\\');}// 将$class,$class所有的父类,$class所有实现的接口放入待检查的数组$classes$classes = array_merge([$class],class_parents($class, true),class_implements($class, true));foreach ($classes as $class) {if (empty(self::$_events[$name][$class])) {continue;}// 如果$classes中绑定有事件$name,那么就执行其名下所有handlerforeach (self::$_events[$name][$class] as $handler) {$event->data = $handler[1];// 类级别的事件handler最终也是通过call_user_func来执行,并传递事件$eventcall_user_func($handler[0], $event);if ($event->handled) {return;}}}
}

通过上面的源码和注释,可知类级别的事件可以向所有父类传递,子类事件handler执行,父类同名事件的handler也要执行。由于类级别事件会被类自身、类的实例、后代类、后代类实例所触发,所以,对于越底层的类而言,其类事件的影响范围就越大。因此,在使用类事件上要注意,尽可能往后代类安排,以控制好影响范围,尽可能不在基础类上安排类事件。

接口事件

还有一种更为抽象方式来处理事件,你可以为特殊的事件创建一个独立的接口, 然后在你需要的类中实现它。比如:

namespace app\interfaces;interface DanceEventInterface
{const EVENT_DANCE = 'dance';
}

然后在两个类中实现:

class Dog extends Component implements DanceEventInterface
{public function meetBuddy(){echo "Woof!";$this->trigger(DanceEventInterface::EVENT_DANCE);}
}class Developer extends Component implements DanceEventInterface
{public function testsPassed(){echo "Yay!";$this->trigger(DanceEventInterface::EVENT_DANCE);}
}

要处理由这些类触发的 EVENT_DANCE ,调用 Event::on()来绑定,并将接口类名作为第一个参数:
你可以在这些类中触发这个事件:

// trigger event for Dog class
Event::trigger(Dog::class, DanceEventInterface::EVENT_DANCE);// trigger event for Developer class
Event::trigger(Developer::class, DanceEventInterface::EVENT_DANCE);

其实这没有什么神秘的,看看Event::trigger()中有这么一段你就明白了:

    $classes = array_merge([$class],class_parents($class, true),// $class 实现的接口事件也触发下class_implements($class, true));

全局事件

全局事件本质上也是实例事件的一种,只不过是可以在任何地方进行绑定,解绑,触发。比如Application和Yii::$app所管理的任何组件。全局事件无非是因为这些实例是可全局访问的罢了。因为这些实例都是继承Component的,所以用法和Component的事件基本相同:

use app\components\Foo;Yii::$app->on('beforeRequest', function ($event) {echo get_class($event->sender);  // 显示 "app\components\Foo"
});Yii::$app->trigger('beforeRequest', new Event(['sender' => new Foo]));

转载于:https://www.cnblogs.com/minirice/p/yii2_event.html

Yii2基本概念之——事件(Event)相关推荐

  1. C#事件(event)解析

    原文:http://www.cnblogs.com/michaelxu/archive/2008/04/02/1134217.html 事件(event),这个词儿对于初学者来说,往往总是显得有些神秘 ...

  2. python 同步 事件 event 简介

    目录 1. Event 1.1 set() 1.2 wait() 1.3 clear() 1.4 is_set() 2. 协调线程同步 3. 一个更复杂的例子 事件 Event是另一种python多线 ...

  3. 秒杀多线程第六篇 经典线程同步 事件Event

    阅读本篇之前推荐阅读以下姊妹篇: <秒杀多线程第四篇 一个经典的多线程同步问题> <秒杀多线程第五篇 经典线程同步关键段CS> 上一篇中使用关键段来解决经典的多线程同步互斥问题 ...

  4. [数据库] Navicat for MySQL事件Event实现数据每日定期操作

    在我们操作数据库过程中,通常会遇到一些某个时间点操作数据库的问题,例如:         (1).每天凌晨12点对数据库进行定时备份,结算和汇总:         (2).每天凌晨2点删除数据库前三天 ...

  5. C#总结(二)事件Event 介绍总结

    最近在总结一些基础的东西,主要是学起来很难懂,但是在日常又有可能会经常用到的东西.前面介绍了 C# 的 AutoResetEvent的使用介绍, 这次介绍事件(event). 事件(event),对于 ...

  6. 线程queue、事件event及协程

    线程queue.事件event及协程 线程queue 多线程抢占资源,让其保持串行的两种方式: ​ 1.互斥锁 ​ 2.队列 线程队列分为以下三种: 1.Queue(先进先出) import queu ...

  7. 事件EVENT,WaitForSingleObject(),WaitForMultipleObjecct()和SignalObjectAndWait() 的使用(上)

    用户模式的线程同步机制效率高,如果需要考虑线程同步问题,应该首先考虑用户模式的线程同步方法.但是,用户模式的线程同步有限制,对于多个进程之间的线程同步,用户模式的线程同步方法无能为力.这时,只能考虑使 ...

  8. Python 线程事件 Event - Python零基础入门教程

    目录 一.Python 线程事件 Event 函数介绍 二.Python 线程事件 Event 原理 三.Python 线程事件 Event 使用 四.重点总结 五.猜你喜欢 零基础 Python 学 ...

  9. 事件EVENT与waitforsingleobject的使用以及Mutex与Event的区别

    Mutex与Event控制互斥事件的使用详解最近写一程序,误用了Mutex的功能,错把Mutex当Event用了.[Mutex]使用Mutex的主要函数:CreateMutex.ReleaseMute ...

  10. JS-图片其他事件-Event对象-事件委托-DOM对象

    JS-图片其他事件-Event对象-事件委托-DOM对象 1 回顾 1. 事件回调函数中 this 的指向2. 鼠标事件click dblclick contextmenumouseenter mou ...

最新文章

  1. python控制台输出到文件_Python print 立即打印内容到重定向的文件
  2. CodeForces - 1553E Permutation Shift(暴力+置换群求环)
  3. Android 获取短信验证码,自动填充
  4. php header 文件大小,php获取远程文件大小及信息的函数(head_php
  5. 视频的播放的用例设计点
  6. 002-软件质量模型
  7. 多个需要验证的输入框思路问题
  8. 如何设置win7计算机不更新,win7系统不要自动更新的设置步骤(图文)
  9. APF有源滤波器仿真,三相三线制 谐波电流检测模块基于p-q方法,ip-iq等方法
  10. 使用web-play开发web应用
  11. 哈雷监控设备的操作及升级NSG9k6G
  12. iOS GPUImage研究六:为视频添加图片水印
  13. 2017第17届CBME中国孕婴童展、童装展会刊(参展商名录)
  14. 打破边界,边缘计算有何应用场景?
  15. ceph---luminous版的安装
  16. html鼠标四种,鼠标有哪些种类?四种鼠标类型的优缺点
  17. PHP rsa私钥pkcs8加密,Openssl rsa私钥的PKCS#1和PKCS#8格式以及加密和转化
  18. LTspice XVII > Transformer 变压器仿真
  19. Java后端学习路线,零基础这样学
  20. x轴z轴代表的方向图片_数控机床的X,Y,Z轴分别指什么方向的运动

热门文章

  1. windows7系统的“ .exe”图标显示不正常解决办法
  2. 高效能人士的第三个习惯——要事第一
  3. sklearn 5.18.3 SGD - Maximum margin separating hyperplane
  4. JavaScript(6):回调函数
  5. 远程控制计算机显示为什么不能满屏,win7远程桌面不能全屏显示怎么办-处理win7远程桌面不能全屏显示的方法 - 河东软件园...
  6. 计算机无法启动bios,笔记本进不了bios的解决方法
  7. 微信小程序-腾讯地图报错:鉴权失败,请传入正确的key
  8. 3.对于python的一个非正式导言 编译之三
  9. 推荐系统-推荐模型总结
  10. ps换脸教程:ps换脸教程步骤具体,怎样选择复制层