标签:active forms 内容 事件机制 刷新 declare actual oss 输出
事件和平时所用的回调思想在与GUI(JavaScript,Swing)相关的技术中非常流行。而在Web应用程序的服务器端,我们很少去直接使用。但这并不意味着我们无法在服务端去实现一个面向事件的体系结构。在本文中,我们将重点介绍Spring框架中的事件处理。首先,会先介绍下事件驱动编程这个概念。接着,我们会将精力放在专门用于Spring框架中的事件处理之上。然后我们会看到实现事件调度和监听的主要方法。最后,我们将在Spring应用程序中展示如何使用基本的监听器。
事件驱动编程
在开始讨论事件驱动编程的编程方面之前,先来说一个场景,用来帮助大家更好地理解 event-driven 这个概念。在一个地方只有两个卖衣服的商店A和B.在A店中,我们的消费者需要一个一个的接受服务,即,同一时间只有一个客户可以购物。在B店里,可以允许几个客户同时进行购物,当有客户需要卖家的帮助时,他需要举起他的右手示意一下。卖家看到后会来找他,帮助其做出更好的选择。关于事件驱动( event-driven )编程这个概念通过上述场景描述总结后就是:通过做一些动作来作为对一些行为的回应。
如上所见,事件驱动的编程(也称为基于事件的编程)是基于对接收到的信号的反应的编程形式。这些信号必须以某种形式来传输信息。举一个简单例子: 点击按钮 。我们将这些信号称为事件。这些事件可以通过用户操作(鼠标点击,触摸)或程序条件执行触发(例如:一个元素的加载结束可以启动另一个操作)来产生。
为了更好地了解,请看以下示例,模仿用户在GUI界面中的操作:
publicclassEventBasedTest{ @Test publicvoidtest(){ Mouse mouse =newMouse(); mouse.addListener(newMouseListener() { @Override publicvoidonClick(Mouse mouse){ System.out.println("Listener#1 called"); mouse.addListenerCallback(); } }); mouse.addListener(newMouseListener() { @Override publicvoidonClick(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); } } classMouse{ privateList listeners =newArrayList(); privateintlistenerCallbacks =0; publicvoidaddListenerCallback(){ listenerCallbacks++; } publicintgetListenerCallbacks(){ returnlistenerCallbacks; } publicvoidaddListener(MouseListener listener){ listeners.add(listener); } publicvoidclick(){ System.out.println("Clicked !"); for(MouseListener listener : listeners) { listener.onClick(this); } } } interfaceMouseListener{ publicvoidonClick(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 {@codejava.util.EventListener} interface * for the Observer design pattern. * * 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. * *@authorRod Johnson *@authorJuergen Hoeller *@param the specific ApplicationEvent subclass to listen to *@seeorg.springframework.context.event.ApplicationEventMulticaster */ @FunctionalInterface publicinterfaceApplicationListenerextendsEventListener{ /** * Handle an application event. *@paramevent the event to respond to */ voidonApplicationEvent(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 {@codeApplicationContext}. * *@authorJuergen Hoeller *@since2.5 */ @SuppressWarnings("serial") publicabstractclassApplicationContextEventextendsApplicationEvent{ /** * Create a new ContextStartedEvent. *@paramsource the {@codeApplicationContext} that the event is raised for * (must not be {@codenull}) */ publicApplicationContextEvent(ApplicationContext source){ super(source); } /** * Get the {@codeApplicationContext} that the event was raised for. */ publicfinalApplicationContextgetApplicationContext(){ return(ApplicationContext) getSource(); } } /** * Event raised when an {@codeApplicationContext} gets started. * *@authorMark Fisher *@authorJuergen Hoeller *@since2.5 *@seeContextStoppedEvent */ @SuppressWarnings("serial") publicclassContextStartedEventextendsApplicationContextEvent{ /** * Create a new ContextStartedEvent. *@paramsource the {@codeApplicationContext} that has been started * (must not be {@codenull}) */ publicContextStartedEvent(ApplicationContext source){ super(source); } } /** * Event raised when an {@codeApplicationContext} gets stopped. * *@authorMark Fisher *@authorJuergen Hoeller *@since2.5 *@seeContextStartedEvent */ @SuppressWarnings("serial") publicclassContextStoppedEventextendsApplicationContextEvent{ /** * Create a new ContextStoppedEvent. *@paramsource the {@codeApplicationContext} that has been stopped * (must not be {@codenull}) */ publicContextStoppedEvent(ApplicationContext source){ super(source); } } /** * Event raised when an {@codeApplicationContext} gets initialized or refreshed. * *@authorJuergen Hoeller *@since04.03.2003 *@seeContextClosedEvent */ @SuppressWarnings("serial") publicclassContextRefreshedEventextendsApplicationContextEvent{ /** * Create a new ContextRefreshedEvent. *@paramsource the {@codeApplicationContext} that has been initialized * or refreshed (must not be {@codenull}) */ publicContextRefreshedEvent(ApplicationContext source){ super(source); } } /** * Event raised when an {@codeApplicationContext} gets closed. * *@authorJuergen Hoeller *@since12.08.2003 *@seeContextRefreshedEvent */ @SuppressWarnings("serial") publicclassContextClosedEventextendsApplicationContextEvent{ /** * Creates a new ContextClosedEvent. *@paramsource the {@codeApplicationContext} that has been closed * (must not be {@codenull}) */ publicContextClosedEvent(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 列表中。在这里顺便给大家推荐一个架构交流群:617434785,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源。相信对于已经工作和遇到技术瓶颈的码友,在这个群里会有你需要的内容。
删除监听器:添加方法一样,我们可以通过传递对象来删除一个监听器( removeApplicationListener(ApplicationListener listener) 或通过传递bean名称( removeApplicationListenerBean(String listenerBeanName)) , 第三种方法,removeAllListeners()用来删除所有已注册的监听器
将事件发送到已注册的监听器:由multicastEvent(ApplicationEvent event)源码注释可知,它用来向所有注册的监听器发送事件。实现可以从 org.springframework.context.event.SimpleApplicationEventMulticaster中 找到,如下所示:
@Override publicvoidmulticastEvent(ApplicationEvent event){ multicastEvent(event, resolveDefaultEventType(event)); } @Override publicvoidmulticastEvent(finalApplicationEvent event, @Nullable ResolvableType eventType){ ResolvableType type = (eventType !=null? eventType : resolveDefaultEventType(event)); for(finalApplicationListener listener : getApplicationListeners(event, type)) { Executor executor = getTaskExecutor(); if(executor !=null) { executor.execute(() -> invokeListener(listener, event)); } else{ invokeListener(listener, event); } } } privateResolvableTyperesolveDefaultEventType(ApplicationEvent event){ returnResolvableType.forInstance(event); } /** * Invoke the given listener with the given event. *@paramlistener the ApplicationListener to invoke *@paramevent the current event to propagate *@since4.1 */ @SuppressWarnings({"unchecked","rawtypes"}) protectedvoidinvokeListener(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{ throwex; } } } }
我们来看看 event multicaster 在应用程序上下文中所在的位置。在 AbstractApplicationContext 中定义的一些方法可以看到其中包含调用public void publishEvent方法。通过这种方法的注释可知,它负责向所有监听器发送给定的事件:
/** * Publish the given event to all listeners. * Note: Listeners get initialized after the MessageSource, to be able * to access it within listener implementations. Thus, MessageSource * implementations cannot publish events. *@paramevent the event to publish (may be application-specific or a * standard framework event) */ @Override publicvoidpublishEvent(ApplicationEvent event){ publishEvent(event,null); } /** * Publish the given event to all listeners. * Note: Listeners get initialized after the MessageSource, to be able * to access it within listener implementations. Thus, MessageSource * implementations cannot publish events. *@paramevent the event to publish (may be an {@linkApplicationEvent} * or a payload object to be turned into a {@linkPayloadApplicationEvent}) */ @Override publicvoidpublishEvent(Object event){ publishEvent(event,null); } /** * Publish the given event to all listeners. *@paramevent the event to publish (may be an {@linkApplicationEvent} * or a payload object to be turned into a {@linkPayloadApplicationEvent}) *@parameventType the resolved event type, if known *@since4.2 */ protectedvoidpublishEvent(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(eventinstanceofApplicationEvent) { applicationEvent = (ApplicationEvent) event; } else{ applicationEvent =newPayloadApplicationEvent<>(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.parentinstanceofAbstractApplicationContext) { ((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 * {@linkorg.springframework.context.event.ContextRefreshedEvent}. */ protectedvoidfinishRefresh(){ // 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(newContextRefreshedEvent(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. * Called by both {@codeclose()} and a JVM shutdown hook, if any. *@seeorg.springframework.context.event.ContextClosedEvent *@see#destroyBeans() *@see#close() *@see#registerShutdownHook() */ protectedvoiddoClose(){ 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(newContextClosedEvent(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 //--------------------------------------------------------------------- @Override publicvoidstart(){ getLifecycleProcessor().start(); publishEvent(newContextStartedEvent(this)); } @Override publicvoidstop(){ getLifecycleProcessor().stop(); publishEvent(newContextStoppedEvent(this)); }
使用Spring的Web应用程序也可以处理与请求相关联的另一种类型的事件(之前说到的 RequestHandledEvent )。它的处理方式和面向上下文的事件类似。首先,我们可以找到org.springframework.web.servlet.FrameworkServlet中处理请求的方法proce***equest。在这个方法结束的时候,调用了 private void publishRequestHandledEvent(HttpServletRequest request, HttpServletResponse response, long startTime, @Nullable Throwable failureCause) 方法。如其名称所表达的,此方法将向所有监听器发布给定的 RequestHandledEvent 。事件在传递给应用程序上下文的 publishEvent 方法后,将由 event multicaster 发送。这里没毛病,因为 RequestHandledEvent 扩展了与 ApplicationContextEvent 相同的类,即 ApplicationEvent 。来看看 publishRequestHandledEvent 方法的源码:
privatevoidpublishRequestHandledEvent(HttpServletRequest request, HttpServletResponse response, longstartTime, @Nullable Throwable failureCause) { //很多人问我Spring5和4的代码有什么区别,就在很多细微的地方,Spring一直在做不懈的改进和封装,不多说,没事可自行 //对比,能学到很多东西 if(this.publishEvents &&this.webApplicationContext !=null) { // Whether or not we succeeded, publish an event. longprocessingTime = System.currentTimeMillis() - startTime; this.webApplicationContext.publishEvent( newServletRequestHandledEvent(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? */ privatebooleanpublishEvents =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. *@seeorg.springframework.web.context.support.ServletRequestHandledEvent */ publicvoidsetPublishEvents(booleanpublishEvents){ this.publishEvents = publishEvents; }
假如有思考的话,从上面的代码中可以知道,事件在应用程序响应性上的表现会很差(大都是一个调用另一个)。这是因为默认情况下,它们是同步调用线程(即使用同一线程去处理事务,处理请求,以及准备视图的输出)。因此,如果一个监听器需要几秒钟的时间来响应,整个应用程序可能会受到慢的要死。幸运的是,我们可以指定事件处理的异步执行(参考上面的 multicastEvent 源码)。需要注意的是,所处理的事件将无法与调用者的上下文(类加载器或事务)进行交互。这里参考 multicastEvent 方法源码即可。默认情况下,org.springframework.core.task.SyncTaskExecutor用来调用相应监听器。
publicclassSyncTaskExecutorimplementsTaskExecutor,Serializable{ /** * Executes the given {@codetask} synchronously, through direct * invocation of it's {@linkRunnable#run() run()} method. *@throwsIllegalArgumentException if the given {@codetask} is {@codenull} */ @Override publicvoidexecute(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的定义:
<--ThisbeanwillcatchSampleCustomEventlaunchedintestedcontroller--> <--Thankstothisbeanwe'llabletogettheexecutiontimesoftestedcontrollerandlistener-->
事件和监听器的代码:
publicclassSampleCustomEventextendsApplicationContextEvent{ privatestaticfinallongserialVersionUID =4236181525834402987L; publicSampleCustomEvent(ApplicationContext source){ super(source); } } publicclassOtherCustomEventextendsApplicationContextEvent{ privatestaticfinallongserialVersionUID =5236181525834402987L; publicOtherCustomEvent(ApplicationContext source){ super(source); } } publicclassSampleCustomEventListenerimplementsApplicationListener{ @Override publicvoidonApplicationEvent(SampleCustomEvent event){ longstart = System.currentTimeMillis(); try{ Thread.sleep(5000); }catch(Exception e) { e.printStackTrace(); } longend = System.currentTimeMillis(); inttestTime = Math.round((end - start) /1000); ((TimeExecutorHolder) event.getApplicationContext().getBean("timeExecutorHolder")).addNewTime("sampleCustomEventListener",newInteger(testTime)); } }
没什么复杂的,事件只能被用来初始化。监听器通过获取当前时间(以毫秒为单位)来测试所执行时间,并在转换后保存(以秒为单位)。监听器使用的 TimeExecutorHolder 也不复杂:
publicclassTimeExecutorHolder{ privateMap testTimes =newHashMap(); publicvoidaddNewTime(String key, Integer value){ testTimes.put(key, value); } publicIntegergetTestTime(String key){ returntestTimes.get(key); } }
此对象只保留测试元素的执行时间一个Map。测试的controller实现看起来类似于监听器。唯一的区别是它发布一个事件(接着被已定义的监听器捕获)并返回一个名为“success”的视图:
@Controller publicclassTestController{ @Autowired privateApplicationContext context; @RequestMapping(value ="/testEvent") publicStringtestEvent(){ longstart = System.currentTimeMillis(); context.publishEvent(newSampleCustomEvent(context)); longend = System.currentTimeMillis(); inttestTime = (int)((end - start) /1000); ((TimeExecutorHolder) context.getBean("timeExecutorHolder")).addNewTime("testController",newInteger(testTime)); return"success"; } @RequestMapping(value ="/testOtherEvent") publicStringtestOtherEvent(){ context.publishEvent(newOtherCustomEvent(context)); return"success"; } }
最后,写一个测试用例,它调用/testEvent并在 TimeExecutorHolder bean 之后检查以验证两个部分的执行时间:
@RunWith(SpringJUnit4Cla***unner.class) @ContextConfiguration(locations={"file:applicationContext-test.xml"}) @WebAppConfiguration publicclassSpringEventsTest{ @Autowired privateWebApplicationContext wac; privateMockMvc mockMvc; @Before publicvoidsetUp(){ this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); } @Test publicvoidtest(){ 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"); intcontrollerSec = timeHolder.getTestTime("testController").intValue(); inteventSec = 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 publicvoidotherTest(){ 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),还是一次全实现,还是分几次派发出去,终究我们会在最后去将整个公式完全解答出来,这也就是所谓的事件机制,难么?
标签:active forms 内容 事件机制 刷新 declare actual oss 输出
原文地址:http://blog.51cto.com/13902811/2157431