當前位置:
首頁 > 知識 > Spring5源碼解析-Spring框架中的事件和監聽器

Spring5源碼解析-Spring框架中的事件和監聽器

事件和平時所用的回調思想在與GUI(JavaScript,Swing)相關的技術中非常流行。而在Web應用程序的伺服器端,我們很少去直接使用。但這並不意味著我們無法在服務端去實現一個面向事件的體系結構。

在本文中,我們將重點介紹Spring框架中的事件處理。首先,會先介紹下事件驅動編程這個概念。接著,我們會將精力放在專門用於Spring框架中的事件處理之上。然後我們會看到實現事件調度和監聽的主要方法。最後,我們將在Spring應用程序中展示如何使用基本的監聽器。

事件驅動編程

在開始討論事件驅動編程的編程方面之前,先來說一個場景,用來幫助大家更好地理解event-driven這個概念。在一個地方只有兩個賣衣服的商店A和B.在A店中,我們的消費者需要一個一個的接受服務,即,同一時間只有一個客戶可以購物。在B店裡,可以允許幾個客戶同時進行購物,當有客戶需要賣家的幫助時,他需要舉起他的右手示意一下。賣家看到後會來找他,幫助其做出更好的選擇。關於事件驅動(event-driven)編程這個概念通過上述場景描述總結後就是:通過做一些動作來作為對一些行為的回應。

如上所見,事件驅動的編程(也稱為基於事件的編程)是基於對接收到的信號的反應的編程形式。這些信號必須以某種形式來傳輸信息。舉一個簡單例子:點擊按鈕。我們將這些信號稱為事件。這些事件可以通過用戶操作(滑鼠點擊,觸摸)或程序條件執行觸發(例如:一個元素的載入結束可以啟動另一個操作)來產生。

為了更好地了解,請看以下示例,模仿用戶在GUI界面中的操作:

public class EventBasedTest {
@Test
public void test() {
Mouse mouse = new Mouse();
mouse.addListener(new MouseListener() {
@Override
public void onClick(Mouse mouse) {
System.out.println("Listener#1 called");
mouse.addListenerCallback();
}
});
mouse.addListener(new MouseListener() {
@Override
public void onClick(Mouse mouse) {
System.out.println("Listener#2 called");
mouse.addListenerCallback();
}
});
mouse.click();
assertTrue("2 listeners should be invoked but only "+mouse.getListenerCallbacks()+" were", mouse.getListenerCallbacks() == 2);
}}
class Mouse {
private List<mouselistener> listeners = new ArrayList<mouselistener>();
private int listenerCallbacks = 0;
public void addListenerCallback() {
listenerCallbacks++;
}
public int getListenerCallbacks() {
return listenerCallbacks;
}
public void addListener(MouseListener listener) {
listeners.add(listener);
}
public void click() {
System.out.println("Clicked !");
for (MouseListener listener : listeners) {
listener.onClick(this);
}
}}
interface MouseListener {
public void onClick(Mouse source);}

列印輸出如下所示:

Clicked !
Listener#1 called
Listener#2 called

Spring中的Events

Spring基於實現org.springframework.context.ApplicationListener介面的bean來進行事件處理。這個介面中只有一個方法,onApplicationEvent用來當一個事件發送過來時這個方法來觸發相應的處理。該介面可以通過指定需要接收的事件來實現(不懂看源碼咯,源碼里方法接收一個event作為參數)。由此,Spring會自動過濾篩選可以用來接收給定事件的監聽器(listeners)。

/** * Interface to be implemented by application event listeners. * Based on the standard {@code java.util.EventListener} interface * for the Observer design pattern. * * <p>As of Spring 3.0, an ApplicationListener can generically declare the event type * that it is interested in. When registered with a Spring ApplicationContext, events * will be filtered accordingly, with the listener getting invoked for matching event * objects only. * * @author Rod Johnson * @author Juergen Hoeller * @param <E> the specific ApplicationEvent subclass to listen to * @see org.springframework.context.event.ApplicationEventMulticaster */@FunctionalInterfacepublic interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
/** * Handle an application event. * @param event the event to respond to */
void onApplicationEvent(E event);}

事件通過org.springframework.context.ApplicationEvent實例來表示。這個抽象類繼承擴展了java.util.EventObject,可以使用EventObject中的getSource方法,我們可以很容易地獲得所發生的給定事件的對象。這裡,事件存在兩種類型:

與應用程序上下文相關聯:所有這種類型的事件都繼承自org.springframework.context.event.ApplicationContextEvent類。它們應用於由org.springframework.context.ApplicationContext引發的事件(其構造函數傳入的是ApplicationContext類型的參數)。這樣,我們就可以直接通過應用程序上下文的生命周期來得到所發生的事件:ContextStartedEvent在上下文啟動時被啟動,當它停止時啟動ContextStoppedEvent,當上下文被刷新時產生ContextRefreshedEvent,最後在上下文關閉時產生ContextClosedEvent。

/** * Base class for events raised for an {@code ApplicationContext}. * * @author Juergen Hoeller * @since 2.5 */@SuppressWarnings("serial")public abstract class ApplicationContextEvent extends ApplicationEvent {
/** * Create a new ContextStartedEvent. * @param source the {@code ApplicationContext} that the event is raised for * (must not be {@code null}) */
public ApplicationContextEvent(ApplicationContext source) {
super(source);
}
/** * Get the {@code ApplicationContext} that the event was raised for. */
public final ApplicationContext getApplicationContext() {
return (ApplicationContext) getSource();
}}/** * Event raised when an {@code ApplicationContext} gets started. * * @author Mark Fisher * @author Juergen Hoeller * @since 2.5 * @see ContextStoppedEvent */@SuppressWarnings("serial")public class ContextStartedEvent extends ApplicationContextEvent {
/** * Create a new ContextStartedEvent. * @param source the {@code ApplicationContext} that has been started * (must not be {@code null}) */
public ContextStartedEvent(ApplicationContext source) {
super(source);
}}/** * Event raised when an {@code ApplicationContext} gets stopped. * * @author Mark Fisher * @author Juergen Hoeller * @since 2.5 * @see ContextStartedEvent */@SuppressWarnings("serial")public class ContextStoppedEvent extends ApplicationContextEvent {
/** * Create a new ContextStoppedEvent. * @param source the {@code ApplicationContext} that has been stopped * (must not be {@code null}) */
public ContextStoppedEvent(ApplicationContext source) {
super(source);
}}/** * Event raised when an {@code ApplicationContext} gets initialized or refreshed. * * @author Juergen Hoeller * @since 04.03.2003 * @see ContextClosedEvent */@SuppressWarnings("serial")public class ContextRefreshedEvent extends ApplicationContextEvent {
/** * Create a new ContextRefreshedEvent. * @param source the {@code ApplicationContext} that has been initialized * or refreshed (must not be {@code null}) */
public ContextRefreshedEvent(ApplicationContext source) {
super(source);
}}/** * Event raised when an {@code ApplicationContext} gets closed. * * @author Juergen Hoeller * @since 12.08.2003 * @see ContextRefreshedEvent */@SuppressWarnings("serial")public class ContextClosedEvent extends ApplicationContextEvent {
/** * Creates a new ContextClosedEvent. * @param source the {@code ApplicationContext} that has been closed * (must not be {@code null}) */
public ContextClosedEvent(ApplicationContext source) {
super(source);
}}

  • 與request 請求相關聯:由org.springframework.web.context.support.RequestHandledEvent實例來表示,當在ApplicationContext中處理請求時,它們被引發。

Spring如何將事件分配給專門的監聽器?這個過程由事件廣播器(event multicaster)來實現,由org.springframework.context.event.ApplicationEventMulticaster介面的實現表示。此介面定義了3種方法,用於:

  • 添加新的監聽器:定義了兩種方法來添加新的監聽器:addApplicationListener(ApplicationListener<?> listener)和addApplicationListenerBean(String listenerBeanName)。當監聽器對象已知時,可以應用第一個。如果使用第二個,我們需要將bean name 得到listener對象(依賴查找DL),然後再將其添加到listener列表中。

  • 刪除監聽器:添加方法一樣,我們可以通過傳遞對象來刪除一個監聽器(removeApplicationListener(ApplicationListener<?> listener)或通過傳遞bean名稱(removeApplicationListenerBean(String listenerBeanName)), 第三種方法,removeAllListeners()用來刪除所有已註冊的監聽器

  • 將事件發送到已註冊的監聽器:由multicastEvent(ApplicationEvent event)源碼注釋可知,它用來向所有註冊的監聽器發送事件。實現可以從org.springframework.context.event.SimpleApplicationEventMulticaster中找到,如下所示:

@Overridepublic void multicastEvent(ApplicationEvent event) {
multicastEvent(event, resolveDefaultEventType(event));}@Overridepublic void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
Executor executor = getTaskExecutor();
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}}private ResolvableType resolveDefaultEventType(ApplicationEvent event) {
return ResolvableType.forInstance(event);}/** * Invoke the given listener with the given event. * @param listener the ApplicationListener to invoke * @param event the current event to propagate * @since 4.1 */@SuppressWarnings({"unchecked", "rawtypes"})protected void invokeListener(ApplicationListener listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
listener.onApplicationEvent(event);
}
catch (Throwable err) {
errorHandler.handleError(err);
}
}
else {
try {
listener.onApplicationEvent(event);
}
catch (ClassCastException ex) {
String msg = ex.getMessage();
if (msg == null || msg.startsWith(event.getClass().getName())) {
// Possibly a lambda-defined listener which we could not resolve the generic event type for
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Non-matching event type for listener: " + listener, ex);
}
}
else {
throw ex;
}
}
}}

我們來看看event multicaster在應用程序上下文中所在的位置。在AbstractApplicationContext中定義的一些方法可以看到其中包含調用public void publishEvent方法。通過這種方法的注釋可知,它負責向所有監聽器發送給定的事件:

/** * Publish the given event to all listeners. * <p>Note: Listeners get initialized after the MessageSource, to be able * to access it within listener implementations. Thus, MessageSource * implementations cannot publish events. * @param event the event to publish (may be application-specific or a * standard framework event) */
@Override
public void publishEvent(ApplicationEvent event) {
publishEvent(event, null);
}
/** * Publish the given event to all listeners. * <p>Note: Listeners get initialized after the MessageSource, to be able * to access it within listener implementations. Thus, MessageSource * implementations cannot publish events. * @param event the event to publish (may be an {@link ApplicationEvent} * or a payload object to be turned into a {@link PayloadApplicationEvent}) */
@Override
public void publishEvent(Object event) {
publishEvent(event, null);
}
/** * Publish the given event to all listeners. * @param event the event to publish (may be an {@link ApplicationEvent} * or a payload object to be turned into a {@link PayloadApplicationEvent}) * @param eventType the resolved event type, if known * @since 4.2 */
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
Assert.notNull(event, "Event must not be null");
if (logger.isTraceEnabled()) {
logger.trace("Publishing event in " + getDisplayName() + ": " + event);
}
// Decorate event as an ApplicationEvent if necessary
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent) event;
}
else {
applicationEvent = new PayloadApplicationEvent<>(this, event);
if (eventType == null) {
eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType();
}
}
// Multicast right now if possible - or lazily once the multicaster is initialized
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
}
else {
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}
// Publish event via parent context as well...
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext) {
((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
}
else {
this.parent.publishEvent(event);
}
}
}

該方法由以下方法調用:啟動上下文(啟動後發布ContextStartedEvent),停止上下文(停止後發布ContextStoppedEvent),刷新上下文(刷新後發布ContextRefreshedEvent)並關閉上下文(關閉後發布ContextClosedEvent):

/** * Finish the refresh of this context, invoking the LifecycleProcessor"s * onRefresh() method and publishing the * {@link org.springframework.context.event.ContextRefreshedEvent}. */protected void finishRefresh() {
// Clear context-level resource caches (such as ASM metadata from scanning).
clearResourceCaches();
// Initialize lifecycle processor for this context.
initLifecycleProcessor();
// Propagate refresh to lifecycle processor first.
getLifecycleProcessor().onRefresh();
// Publish the final event.生命周期Refreshed事件
publishEvent(new ContextRefreshedEvent(this));
// Participate in LiveBeansView MBean, if active.
LiveBeansView.registerApedplicationContext(this);}/** * Actually performs context closing: publishes a ContextClosedEvent and * destroys the singletons in the bean factory of this application context. * <p>Called by both {@code close()} and a JVM shutdown hook, if any. * @see org.springframework.context.event.ContextClosedEvent * @see #destroyBeans() * @see #close() * @see #registerShutdownHook() */protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
if (logger.isInfoEnabled()) {
logger.info("Closing " + this);
}
LiveBeansView.unregisterApplicationContext(this);
try {
// Publish shutdown event. ContextClosed事件
publishEvent(new ContextClosedEvent(this));
}
catch (Throwable ex) {
logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
}
// Stop all Lifecycle beans, to avoid delays during individual destruction.
try {
getLifecycleProcessor().onClose();
}
...
}
//---------------------------------------------------------------------
// Implementation of Lifecycle interface
//---------------------------------------------------------------------@Overridepublic void start() {
getLifecycleProcessor().start();
publishEvent(new ContextStartedEvent(this));}@Overridepublic void stop() {
getLifecycleProcessor().stop();
publishEvent(new ContextStoppedEvent(this));}

使用Spring的Web應用程序也可以處理與請求相關聯的另一種類型的事件(之前說到的RequestHandledEvent)。它的處理方式和面向上下文的事件類似。首先,我們可以找到org.springframework.web.servlet.FrameworkServlet中處理請求的方法processRequest。在這個方法結束的時候,調用了private void publishRequestHandledEvent(HttpServletRequest request, HttpServletResponse response, long startTime, @Nullable Throwable failureCause)方法。如其名稱所表達的,此方法將向所有監聽器發布給定的RequestHandledEvent。事件在傳遞給應用程序上下文的publishEvent方法後,將由event multicaster發送。這裡沒毛病,因為RequestHandledEvent擴展了與ApplicationContextEvent相同的類,即ApplicationEvent。來看看publishRequestHandledEvent方法的源碼:

private void publishRequestHandledEvent(HttpServletRequest request, HttpServletResponse response,
long startTime, @Nullable Throwable failureCause) {
//很多人問我Spring5和4的代碼有什麼區別,就在很多細微的地方,Spring一直在做不懈的改進和封裝,不多說,沒事可自行 //對比,能學到很多東西
if (this.publishEvents && this.webApplicationContext != null) {
// Whether or not we succeeded, publish an event.
long processingTime = System.currentTimeMillis() - startTime;
this.webApplicationContext.publishEvent(
new ServletRequestHandledEvent(this,
request.getRequestURI(), request.getRemoteAddr(),
request.getMethod(), getServletConfig().getServletName(),
WebUtils.getSessionId(request), getUsernameForRequest(request),
processingTime, failureCause, response.getStatus()));
}
}

需要注意的是,你可以關閉基於請求的事件的調度。FrameworkServlet的setPublishEvents(boolean publishEvents)允許禁用事件分派,例如改進應用程序性能(看代碼注釋,當沒有監聽器來管理相應事件的時候,幹嘛要浪費性能)。默認情況下,事件調度被激活(默認為true)。

/** Should we publish a ServletRequestHandledEvent at the end of each request? */private boolean publishEvents = true; /** * Set whether this servlet should publish a ServletRequestHandledEvent at the end * of each request. Default is "true"; can be turned off for a slight performance * improvement, provided that no ApplicationListeners rely on such events. * @see org.springframework.web.context.support.ServletRequestHandledEvent */public void setPublishEvents(boolean publishEvents) {
this.publishEvents = publishEvents;}

假如有思考的話,從上面的代碼中可以知道,事件在應用程序響應性上的表現會很差(大都是一個調用另一個)。這是因為默認情況下,它們是同步調用線程(即使用同一線程去處理事務,處理請求,以及準備視圖的輸出)。因此,如果一個監聽器需要幾秒鐘的時間來響應,整個應用程序可能會受到慢的要死。幸運的是,我們可以指定事件處理的非同步執行(參考上面的multicastEvent源碼)。需要注意的是,所處理的事件將無法與調用者的上下文(類載入器或事務)進行交互。這裡參考multicastEvent方法源碼即可。默認情況下,org.springframework.core.task.SyncTaskExecutor用來調用相應監聽器。

public class SyncTaskExecutor implements TaskExecutor, Serializable {
/** * Executes the given {@code task} synchronously, through direct * invocation of it"s {@link Runnable#run() run()} method. * @throws IllegalArgumentException if the given {@code task} is {@code null} */
@Override
public void execute(Runnable task) {
Assert.notNull(task, "Runnable must not be null");
task.run();
}}

在Spring中實現一個簡單的監聽器

為了更好的理解事件監聽器,我們來寫一個小的測試用例。通過這個例子,我們要證明默認情況下,監聽器listeners在其調用者線程中執行了分發的事件。所以,為了不立即得到結果,我們在監聽器中休眠5秒(調用Thread.sleep(5000))。測試檢查是否達到3個目的:如果controller 的返回結果和所預期的視圖名稱相匹配,如果事件監聽器花了5秒鐘的時間才響應(Thread.sleep執行沒有任何問題),並且如果controller 的同樣花了5秒鐘來生成視圖(因為監聽器的休眠)。

第二個定義的測試將驗證我們的監聽器是否在另一個事件中被捕獲(和之前的類繼承同一個類型)。首先,在配置文件中對bean的定義:

< -- This bean will catch SampleCustomEvent launched in tested controller -->
<bean class="com.migo.event.SampleCustomEventListener">
< -- Thanks to this bean we"ll able to get the execution times of tested controller and listener -->
<bean class="com.migo.event.TimeExecutorHolder" id="timeExecutorHolder">
</bean></bean>

事件和監聽器的代碼:

public class SampleCustomEvent extends ApplicationContextEvent {
private static final long serialVersionUID = 4236181525834402987L;
public SampleCustomEvent(ApplicationContext source) {
super(source);
}}
public class OtherCustomEvent extends ApplicationContextEvent {
private static final long serialVersionUID = 5236181525834402987L;
public OtherCustomEvent(ApplicationContext source) {
super(source);
}}
public class SampleCustomEventListener implements ApplicationListener<samplecustomevent> {
@Override
public void onApplicationEvent(SampleCustomEvent event) {
long start = System.currentTimeMillis();
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
int testTime = Math.round((end - start) / 1000);
((TimeExecutorHolder) event.getApplicationContext().getBean("timeExecutorHolder")).addNewTime("sampleCustomEventListener", new Integer(testTime));
}}

沒什麼複雜的,事件只能被用來初始化。監聽器通過獲取當前時間(以毫秒為單位)來測試所執行時間,並在轉換後保存(以秒為單位)。監聽器使用的TimeExecutorHolder也不複雜:

public class TimeExecutorHolder {
private Map<String, Integer> testTimes = new HashMap();
public void addNewTime(String key, Integer value) {
testTimes.put(key, value);
}
public Integer getTestTime(String key) {
return testTimes.get(key);
}}

此對象只保留測試元素的執行時間一個Map。測試的controller實現看起來類似於監聽器。唯一的區別是它發布一個事件(接著被已定義的監聽器捕獲)並返回一個名為「success」的視圖:

@Controllerpublic class TestController {
@Autowired
private ApplicationContext context;
@RequestMapping(value = "/testEvent")
public String testEvent() {
long start = System.currentTimeMillis();
context.publishEvent(new SampleCustomEvent(context));
long end = System.currentTimeMillis();
int testTime = (int)((end - start) / 1000);
((TimeExecutorHolder) context.getBean("timeExecutorHolder")).addNewTime("testController", new Integer(testTime));
return "success";
}
@RequestMapping(value = "/testOtherEvent")
public String testOtherEvent() {
context.publishEvent(new OtherCustomEvent(context));
return "success";
}}

最後,寫一個測試用例,它調用/testEvent並在TimeExecutorHolder bean之後檢查以驗證兩個部分的執行時間:

@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations={"file:applicationContext-test.xml"})@WebAppConfigurationpublic class SpringEventsTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
@Test
public void test() {
try {
MvcResult result = mockMvc.perform(get("/testEvent")).andReturn();
ModelAndView view = result.getModelAndView();
String expectedView = "success";
assertTrue("View name from /testEvent should be ""+expectedView+"" but was ""+view.getViewName()+""", view.getViewName().equals(expectedView));
} catch (Exception e) {
e.printStackTrace();
}
TimeExecutorHolder timeHolder = (TimeExecutorHolder) this.wac.getBean("timeExecutorHolder");
int controllerSec = timeHolder.getTestTime("testController").intValue();
int eventSec = timeHolder.getTestTime("sampleCustomEventListener").intValue();
assertTrue("Listener for SampleCustomEvent should take 5 seconds before treating the request but it took "+eventSec+" instead", eventSec == 5);
assertTrue("Because listener took 5 seconds to response, controller should also take 5 seconds before generating the view, but it took "+controllerSec+ " instead", controllerSec == 5);
}
@Test
public void otherTest() {
TimeExecutorHolder timeHolder = (TimeExecutorHolder) this.wac.getBean("timeExecutorHolder");
timeHolder.addNewTime("sampleCustomEventListener", -34);
try {
MvcResult result = mockMvc.perform(get("/testOtherEvent")).andReturn();
ModelAndView view = result.getModelAndView();
String expectedView = "success";
assertTrue("View name from /testEvent should be ""+expectedView+"" but was ""+view.getViewName()+""", view.getViewName().equals(expectedView));
} catch (Exception e) {
e.printStackTrace();
}
Integer eventSecObject = timeHolder.getTestTime("sampleCustomEventListener");
assertTrue("SampleCustomEventListener shouldn"t be trigerred on OtherEvent but it was", eventSecObject.intValue() == -34);
}}

測試通過沒有任何問題。它證明了我們所設定的許多假設。

首先,我們看到事件編程包括在信號發送到應用程序時觸發並執行某些操作。這個信號必須有一個監聽器在監聽。在Spring中,由於監聽器中的泛型定義(void onApplicationEvent(E event);),事件可以很容易地被listeners所捕獲。通過它,如果所觸發的事件對應於監聽器所預期的事件,我們無須多餘的檢查(說的啰嗦了,就是符合所需求的類型即可,省去很多麻煩,我們可以直接根據泛型就可以實現很多不同的處理)。我們還發現,默認情況下,監聽器是以同步方式執行的。所以在調用線程同時執行比如視圖生成或資料庫處理的操作是不行的。

最後,要說的是,算是一個前後端通用的思想吧,所謂的事件,其實想來,不過是一個介面而已,把這個介面派發出去(event multicaster),由誰來實現,這是他們的事情,這裡就有一個裝飾類(這麼理解就好),其名字叫listener,拿到這個派發的事件介面,然後調用相應的實現,這裡為了程序的更加靈活和高可用,我們會調用相應的adapter適配器,最後調用其相應的Handler實現,然後Handler會調用相應的service,service調用dao。

同樣這個思想用在前端就是組件對外派發一個事件,這個事件由其父組件或者實現,或者繼續向外派發,最後用一個具體的方法將之實現即可

其實對應於我們的數學來講就是,我們定義一個數學公式f(x)*p(y)一樣,這個派發出去,無論我們先實現了f(x)還是先實現了p(y),還是一次全實現,還是分幾次派發出去,終究我們會在最後去將整個公式完全解答出來,這也就是所謂的事件機制,難么?

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 青峰科技 的精彩文章:

巧用Unity 2D功能:只需六步開發簡單的2D UFO遊戲
Go語言:成長的十年
Cplusplus,學習多態總結,編程學習
Spring5源碼解析-Spring中的bean工廠後置處理器
C++—const volatile mutable的用法

TAG:青峰科技 |