應用程式請求註冊對Input事件的監聽
一、應用程式在繪製View時註冊監聽事件
[/frameworks/base/core/java/android/view/ViewRootImpl.java]
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
...
requestLayout();
//建立InputChannel例項,建立後的InputChannel是未初始化的,可以通過從Parcel物件中讀取資訊初始化
//或者呼叫transferTo(InputChannel outParameter)從另一個InputChannel例項中獲取初始化資訊。
if ((mWindowAttributes.inputFeatures &
WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
try {
...
//addToDisplay()方法會呼叫WindowManagerService的addWindow()方法。
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(),mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
...
mView = null;
mInputChannel = null;
...
throw new RuntimeException("Adding window failed", e);
} finally {
...
}
if (mInputChannel != null) {
if (mInputQueueCallback != null) {
mInputQueue = new InputQueue();
mInputQueueCallback.onInputQueueCreated(mInputQueue);
}
//建立WindowInputEventReceiver例項,使用前面建立好的InputChannel例項作引數。
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
}
}
}
}
Session類的addToDisplay()方法會呼叫WindowManagerService的addWindow()方法,這個方法非常重要。
[/frameworks/base/services/java/com/android/server/wm/WindowManagerService.java]
public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
...
synchronized(mWindowMap) {
...
if (outInputChannel != null && (attrs.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
String name = win.makeInputChannelName();
//建立名為name的socketpair例項,將socket[0](服務端)註冊到InputManagerService中,
//將socket[1](客戶端)傳遞給應用程序。
InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
win.setInputChannel(inputChannels[0]);
inputChannels[1].transferTo(outInputChannel);
mInputManager.registerInputChannel(win.mInputChannel, win.mInputWindowHandle);
}
}//synchronized
return res;
}
二、InputManagerService對socketpair服務端的處理
2.1 InputManagerService的初始化和啟動
public InputManagerService(Context context) {
this.mContext = context;
//InputManagerService的Handler使用的是DisplayThread的Looper。
this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper());
mUseDevInputEventForAudioJack =
context.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
//在native層完成初始化, nativeInit()返回的是JNI層建立的NativeInputManager物件的指標。Java層儲存Native的物件指標,這個是Android中非常常見的用法。
mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
LocalServices.addService(InputManagerInternal.class, new LocalService());
}
[com_android_server_input_InputManagerService.cpp]
static jlong nativeInit(JNIEnv* env, jclass /* clazz */,
jobject serviceObj, jobject contextObj, jobject messageQueueObj) {
sp messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
if (messageQueue == NULL) {
jniThrowRuntimeException(env, “MessageQueue is not initialized.”);
return 0;
}
//new一個NativeInputManager例項,
NativeInputManager* im = new NativeInputManager(contextObj, serviceObj,messageQueue->getLooper());
im->incStrong(0);
return reinterpret_cast<jlong>(im);
}
NativeInputManager類
NativeInputManager類的設計
class NativeInputManager : public virtual RefBase,
public virtual InputReaderPolicyInterface,
public virtual InputDispatcherPolicyInterface,
public virtual PointerControllerPolicyInterface {
protected:
virtual ~NativeInputManager();
public:
NativeInputManager(jobject contextObj, jobject serviceObj, const sp<Looper>& looper);
inline sp<InputManager> getInputManager() const { return mInputManager; }
status_t registerInputChannel(JNIEnv* env, const sp<InputChannel>& inputChannel,
const sp<InputWindowHandle>& inputWindowHandle, bool monitor);
status_t unregisterInputChannel(JNIEnv* env, const sp<InputChannel>& inputChannel);
...
private:
sp<InputManager> mInputManager;
jobject mContextObj;
jobject mServiceObj;
sp<Looper> mLooper;
}
NativeInputManager的建構函式
NativeInputManager::NativeInputManager(jobject contextObj,
jobject serviceObj, const sp<Looper>& looper) :
mLooper(looper), mInteractive(true) {
JNIEnv* env = jniEnv();
mContextObj = env->NewGlobalRef(contextObj);
mServiceObj = env->NewGlobalRef(serviceObj);
{
AutoMutex _l(mLock);
mLocked.systemUiVisibility = ASYSTEM_UI_VISIBILITY_STATUS_BAR_VISIBLE;
mLocked.pointerSpeed = 0;
mLocked.pointerGesturesEnabled = true;
mLocked.showTouches = false;
}
mInteractive = true;
sp<EventHub> eventHub = new EventHub();
mInputManager = new InputManager(eventHub, this, this);
}
“`
“`####InputManagerService的啟動####
主要的工作是把Native層的InputReaderThread和InputDispatcherThread兩個執行緒啟動起來。
public void start() {
//在native層啟動
nativeStart(mPtr);
// Add ourself to the Watchdog monitors.
Watchdog.getInstance().addMonitor(this);
registerPointerSpeedSettingObserver();
registerShowTouchesSettingObserver();
mContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updatePointerSpeedFromSettings();
updateShowTouchesFromSettings();
}
}, new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mHandler);
updatePointerSpeedFromSettings();
updateShowTouchesFromSettings();
}
com_android_server_input_InputManagerService.cpp
static void nativeStart(JNIEnv* env, jclass /* clazz */, jlong ptr) {
NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
status_t result = im->getInputManager()->start();
if (result) {
jniThrowRuntimeException(env, "Input manager could not be started.");
}
}
InputManager的設計
介面:
class InputManagerInterface : public virtual RefBase {
protected:
InputManagerInterface() { }
virtual ~InputManagerInterface() { }
public:
/* Starts the input manager threads. */
virtual status_t start() = 0;
/* Stops the input manager threads and waits for them to exit. */
virtual status_t stop() = 0;
/* Gets the input reader. */
virtual sp<InputReaderInterface> getReader() = 0;
/* Gets the input dispatcher. */
virtual sp<InputDispatcherInterface> getDispatcher() = 0;
};
具體類:
class InputManager : public InputManagerInterface {
protected:
virtual ~InputManager();
public:
InputManager(
const sp<EventHubInterface>& eventHub,
const sp<InputReaderPolicyInterface>& readerPolicy,
const sp<InputDispatcherPolicyInterface>& dispatcherPolicy);
// (used for testing purposes)
InputManager(
const sp<InputReaderInterface>& reader,
const sp<InputDispatcherInterface>& dispatcher);
virtual status_t start();
virtual status_t stop();
virtual sp<InputReaderInterface> getReader();
virtual sp<InputDispatcherInterface> getDispatcher();
private:
sp<InputReaderInterface> mReader;
sp<InputReaderThread> mReaderThread;
sp<InputDispatcherInterface> mDispatcher;
sp<InputDispatcherThread> mDispatcherThread;
void initialize();
};
socketpair服務端的註冊
最終呼叫的是InputDispatcher的registerInputChannel()函式。
status_t InputDispatcher::registerInputChannel(const sp<InputChannel>& inputChannel,
const sp<InputWindowHandle>& inputWindowHandle, bool monitor);
- 以socketFd作為索引,將Connection例項新增到:KeyedVector
InputDispatcher的分發執行緒
分發流程:
1. 輪詢mCommandQueue;
2. 輪詢mLooper。
void InputDispatcher::dispatchOnce() {
nsecs_t nextWakeupTime = LONG_LONG_MAX;
{ // acquire lock
AutoMutex _l(mLock);
mDispatcherIsAliveCondition.broadcast();
//如果佇列mCommandQueue不為空,則呼叫dispatchOnceInnerLocked()函式進行分發。
if (!haveCommandsLocked()) {
dispatchOnceInnerLocked(&nextWakeupTime);
}
if (runCommandsLockedInterruptible()) {
nextWakeupTime = LONG_LONG_MIN;
}
} // release lock
nsecs_t currentTime = now();
int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);
mLooper->pollOnce(timeoutMillis);
}
bool InputDispatcher::runCommandsLockedInterruptible() {
//如果佇列mCommandQueue為空,直接返回flase。
if (mCommandQueue.isEmpty()) {
return false;
}
do {
//依次從佇列mCommandQueue中出列一個元素,呼叫command()函式,直到佇列為空為止,返回true。
CommandEntry* commandEntry = mCommandQueue.dequeueAtHead();
Command command = commandEntry->command;
(this->*command)(commandEntry);
commandEntry->connection.clear();
delete commandEntry;
} while (! mCommandQueue.isEmpty());
return true;
}
三、應用程式對SocketPair客戶端事件的處理
3.1 Native層的InputEventReceiver的設計
[/frameworks/base/core/jni/android_view_InputEventReceiver.cpp]
class NativeInputEventReceiver : public LooperCallback {
public:
NativeInputEventReceiver(JNIEnv* env,
jobject receiverWeak, const sp<InputChannel>& inputChannel,
const sp<MessageQueue>& messageQueue);
status_t initialize();
void dispose();
status_t finishInputEvent(uint32_t seq, bool handled);
status_t consumeEvents(JNIEnv* env, bool consumeBatches, nsecs_t frameTime,
bool* outConsumedBatch);
protected:
virtual ~NativeInputEventReceiver();
private:
struct Finish {
uint32_t seq;
bool handled;
};
jobject mReceiverWeakGlobal;
InputConsumer mInputConsumer;
sp<MessageQueue> mMessageQueue;
PreallocatedInputEventFactory mInputEventFactory;
bool mBatchedInputEventPending;
int mFdEvents;
Vector<Finish> mFinishQueue;
void setFdEvents(int events);
const char* getInputChannelName() {
return mInputConsumer.getChannel()->getName().string();
}
virtual int handleEvent(int receiveFd, int events, void* data);
};
status_t NativeInputEventReceiver::initialize() {
//監聽型別為ALOOPER_EVENT_INPUT
setFdEvents(ALOOPER_EVENT_INPUT);
return OK;
}
//NativeInputEventReceiver繼承自LooperCallback,有事件發生時會回撥handleEvent()函式。
void NativeInputEventReceiver::setFdEvents(int events) {
if (mFdEvents != events) {
mFdEvents = events;
int fd = mInputConsumer.getChannel()->getFd();
if (events) {
mMessageQueue->getLooper()->addFd(fd, 0, events, this, NULL);
} else {
mMessageQueue->getLooper()->removeFd(fd);
}
}
}
int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {
if (events & (ALOOPER_EVENT_ERROR | ALOOPER_EVENT_HANGUP)) {
return 0; // remove the callback
}
if (events & ALOOPER_EVENT_INPUT) {
JNIEnv* env = AndroidRuntime::getJNIEnv();
status_t status = consumeEvents(env, false /*consumeBatches*/, -1, NULL);
mMessageQueue->raiseAndClearException(env, "handleReceiveCallback");
return status == OK || status == NO_MEMORY ? 1 : 0;
}
if (events & ALOOPER_EVENT_OUTPUT) {
for (size_t i = 0; i < mFinishQueue.size(); i++) {
const Finish& finish = mFinishQueue.itemAt(i);
status_t status = mInputConsumer.sendFinishedSignal(finish.seq, finish.handled);
if (status) {
mFinishQueue.removeItemsAt(0, i);
if (status == WOULD_BLOCK) {
return 1; // keep the callback, try again later
}
ALOGW("Failed to send finished signal on channel '%s'. status=%d",
getInputChannelName(), status);
if (status != DEAD_OBJECT) {
JNIEnv* env = AndroidRuntime::getJNIEnv();
String8 message;
message.appendFormat("Failed to finish input event. status=%d", status);
jniThrowRuntimeException(env, message.string());
mMessageQueue->raiseAndClearException(env, "finishInputEvent");
}
return 0; // remove the callback
}
}
mFinishQueue.clear();
setFdEvents(ALOOPER_EVENT_INPUT);
return 1;
}
return 1;
}
status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {
if (consumeBatches) {
mBatchedInputEventPending = false;
}
if (outConsumedBatch) {
*outConsumedBatch = false;
}
ScopedLocalRef<jobject> receiverObj(env, NULL);
bool skipCallbacks = false;
for (;;) {
uint32_t seq;
InputEvent* inputEvent;
status_t status = mInputConsumer.consume(&mInputEventFactory,
consumeBatches, frameTime, &seq, &inputEvent);
if (status) {
if (status == WOULD_BLOCK) {
...
}
ALOGE("channel '%s' ~ Failed to consume input event. status=%d",
getInputChannelName(), status);
return status;
}
assert(inputEvent);
if (!skipCallbacks) {
...
jobject inputEventObj;
switch (inputEvent->getType()) {
case AINPUT_EVENT_TYPE_KEY:
inputEventObj = android_view_KeyEvent_fromNative(env,
static_cast<KeyEvent*>(inputEvent));
break;
case AINPUT_EVENT_TYPE_MOTION: {
MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);
if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) {
*outConsumedBatch = true;
}
inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
break;
}
default:
assert(false); // InputConsumer should prevent this from ever happening
inputEventObj = NULL;
}
if (inputEventObj) {
//呼叫Java層的dispatchInputEvent()方法。
env->CallVoidMethod(receiverObj.get(),
gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
if (env->ExceptionCheck()) {
ALOGE("Exception dispatching input event.");
skipCallbacks = true;
}
env->DeleteLocalRef(inputEventObj);
} else {
ALOGW("channel '%s' ~ Failed to obtain event object.", getInputChannelName());
skipCallbacks = true;
}
}
/* 前面執行成功時,skipCallbacks為false, 最終會呼叫sendFinishedSignal()函式,
它會構建InputMessage,格式:
InputMessage msg;
msg.header.type = InputMessage::TYPE_FINISHED;
msg.body.finished.seq = seq;
msg.body.finished.handled = handled;
mChannel->sendMessage(&msg);
socketpair的服務端會收到該訊息後,就知道該訊息已經成功分發了。
*/
if (skipCallbacks) {
mInputConsumer.sendFinishedSignal(seq, false);
}
}
}
四、InputDispatcher事件傳送成功後的處理
int InputDispatcher::handleReceiveCallback(int fd, int events, void* data) {
InputDispatcher* d = static_cast<InputDispatcher*>(data);
{ // acquire lock
AutoMutex _l(d->mLock);
ssize_t connectionIndex = d->mConnectionsByFd.indexOfKey(fd);
if (connectionIndex < 0) {
ALOGE("Received spurious receive callback for unknown input channel. "
"fd=%d, events=0x%x", fd, events);
return 0; // remove the callback
}
bool notify;
sp<Connection> connection = d->mConnectionsByFd.valueAt(connectionIndex);
if (!(events & (ALOOPER_EVENT_ERROR | ALOOPER_EVENT_HANGUP))) {
if (!(events & ALOOPER_EVENT_INPUT)) {
ALOGW("channel '%s' ~ Received spurious callback for unhandled poll event. "
"events=0x%x", connection->getInputChannelName(), events);
return 1;
}
nsecs_t currentTime = now();
bool gotOne = false;
status_t status;
for (;;) {
uint32_t seq;
bool handled;
status = connection->inputPublisher.receiveFinishedSignal(&seq, &handled);
if (status) {
break;
}
d->finishDispatchCycleLocked(currentTime, connection, seq, handled);
gotOne = true;
}
if (gotOne) {
d->runCommandsLockedInterruptible();
if (status == WOULD_BLOCK) {
return 1;
}
}
notify = status != DEAD_OBJECT || !connection->monitor;
if (notify) {
ALOGE("channel '%s' ~ Failed to receive finished signal. status=%d",
connection->getInputChannelName(), status);
}
} else {
// Monitor channels are never explicitly unregistered.
// We do it automatically when the remote endpoint is closed so don't warn
// about them.
notify = !connection->monitor;
if (notify) {
ALOGW("channel '%s' ~ Consumer closed input channel or an error occurred. "
"events=0x%x", connection->getInputChannelName(), events);
}
}
// Unregister the channel.
d->unregisterInputChannelLocked(connection->inputChannel, notify);
return 0; // remove the callback
} // release lock
}
status_t InputDispatcher::unregisterInputChannelLocked(const sp<InputChannel>& inputChannel,
bool notify) {
ssize_t connectionIndex = getConnectionIndexLocked(inputChannel);
if (connectionIndex < 0) {
ALOGW("Attempted to unregister already unregistered input channel '%s'",
inputChannel->getName().string());
return BAD_VALUE;
}
//移除mConnectionsByFd中與connIndex對應的鍵值對。
sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);
mConnectionsByFd.removeItemsAt(connectionIndex);
if (connection->monitor) {
removeMonitorChannelLocked(inputChannel);
}
//移除該InputChannel包含的socketFd。
mLooper->removeFd(inputChannel->getFd());
nsecs_t currentTime = now();
abortBrokenDispatchCycleLocked(currentTime, connection, notify);
connection->status = Connection::STATUS_ZOMBIE;
return OK;
}
相關推薦
應用程式請求註冊對Input事件的監聽
一、應用程式在繪製View時註冊監聽事件 [/frameworks/base/core/java/android/view/ViewRootImpl.java] public void setView(View view, WindowManager.La
Spring的事件監聽及應用
最近公司在重構廣告系統,其中核心的打包功由廣告系統呼叫,即對apk打包的呼叫和打包完成之後的回撥,需要提供相應的介面給廣告系統。因此,為了將apk打包的核心流程和對接廣告系統的業務解耦,利用了spring的事件監聽特性來滿足需求。以下說明spring的事件機制的相關內容。 1.觀察
如何對vue中的元件進行點選事件監聽
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-default/index.css"> </head> <body> <div id="
如何判斷一個HTTP請求是瀏覽器請求還是應用程式請求?
1、獲取請求的request HttpServletRequest request=ServletActionContext.getRequest(); 2、攔截器中判斷請求頭 通常判斷來自手機端的請求還是PC端的請求只需要判斷: request.getHea
觀察者模式的程式例項C++ 以及觀察者模式與事件監聽的區別
一、什麼是觀察者模式 Observer模式也叫觀察者模式,它的作用是當一個物件的狀態發生變化時,可以自己主動通知其它關聯物件,自己主動重新整理物件狀態。 舉個樣例,使用者介面能夠作為一個觀察者,業務資料是被觀察者,使用者介面觀察業務資料的變化,發現數據變化後,
事件監聽註冊事件的相容性問題
關於時間監聽問題: 標準: 元素.addEventListener(‘事件型別’,事件處理程式); 在IE低版本中,該鍵的值是undefined,所以無法當做方法或函式呼叫。所以會報錯。 IE低版本: 元素.attachEvent(‘事件型別’,事件處理程式);
Android應用程式請求SurfaceFlinger服務渲染Surface的過程分析
在前面一篇文章中,我們分析了Android應用程式請求SurfaceFlinger服務建立Surface的過程。有了Surface之後,Android應用程式就可以在上面繪製自己的UI了,接著再請求SurfaceFlinger服務將這個已經繪製好了UI的Surf
input標籤監聽事件總結
監聽事件對於文字標籤元素很常見,如input、select這些,監聽其實就是當他們的值發生改變時去觸發相應事件。input常見的事件如下: 1.onfocus 當input 獲取到焦點時觸發 2.onblur 當input失去焦點時觸發 3.onchange 當i
如何將應用程式exe註冊成服務,直接從後臺執行
方法一:使用windows自帶的命令sc 使用sc create 方法建立。 如:sc create CaptureScreen binpath= "F:\zwmei-project\decklink-learning\OutputBitmap\Deb
Spring中的事件監聽機制在專案中的應用
最經在做專案的時候,呼叫某個介面的時候為了呼叫地圖,而不希望因為呼叫超時影響到主執行緒,使用了spring的時間監聽機制。 Spring中提供一些Aware相關的介面,BeanFactoryAware、 ApplicationContextAware、Reso
利用C#開發web應用程式時,對登錄檔進行操作提示沒有許可權的解決辦法
因為公司專案需要對web程式新增一套限制客戶惡意傳播的方案。沒辦法,東西放在客戶的伺服器或者電腦裡面。鑑於本人菜鳥一個,也就能想到利用兩種方案,具體的實現的方式,將會在之後的博文中寫出。 我寫這篇文章
輸入框事件監聽(一):keydown、keyup、input
當輸入框的值發生變化時,我們可以通過keydown、keyup、input、onchange、blur事件觀察到其值的變化,但它們的應用時機與應用場景存在顯著的差異。 1. 實時觀察 需要觀察到使用者每次鍵盤輸入的變化,必須要用keydown、keyup
jquery 自定義input輸入監聽事件
網上一段JS,考來的,自定義監聽: $.event.special.valuechange = { teardown: function (namespaces) { $(this).unbind('.valuechange')
addEventListener註冊事件(事件監聽)
註冊事件的簡單方式:: bth.on事件 = function{ }; 如果重複註冊相同的事件,後面的事件會把前面的事件覆蓋掉。 例子: 輸入一下程式碼,點選事件,控制檯輸出 document.onclick = function () { console.log("厲害"
事件處理程式(事件繫結、事件監聽、事件偵聽器)
相應某個事件的函式叫做事件處理程式(或事件偵聽器)。 1、TTML事件處理程式 <script type="text/javascript"> function showMessage() { alert('hello world!')
25 API-GUI(事件監聽機制,介面卡模式),Netbeans的概述和使用(模擬登陸註冊GUI版)
1:GUI(瞭解) (1)使用者圖形介面GUI:方便直觀CLI:需要記憶一下命令,麻煩(2)兩個包:java.awt:和系統關聯較強 ,屬重量級控制元件javax.swing:純Java編寫,增強了移植性,屬輕量級控制元件。(3)GUI的繼承體系元件:元件就是物件容器元
mfc 如何捕獲應用程式視窗以外的滑鼠事件
一般應用程式當中的滑鼠事件只能爭對應用程式視窗內部有效,如果點選應用程式以外的視窗,例如點選其它應用程式視窗等,這個時候,滑鼠訊息是不會發送給我們的應用程式視窗,更不會激發事件。這樣怎麼處理呢。我們可以利用mfc視窗中的windows訊息處理函式,來處理我們的滑鼠或鍵盤事件操作如下: 首先在我們需要開啟捕獲
Spring事件監聽模式應用場景和思路
什麼是事件 程式中的事件其實和現實差不多,例如:Js中的事件有很多 如滑鼠的單擊事件onclick。 當點選某個按鈕時--觸發某個方法。當你不去觸發這個事件、這個事件就永遠的在等待 喚醒事件的人; 事件三要素 1、定義一個事件(火災事件、碰撞事件、收到資訊事件。。。
springBoot事件監聽 在專案實際業務中的非同步應用
第一步 :在啟動類添加註解@EnableAsync,自定義執行緒池 第二步 : 編寫實體繼承ApplicationEvent 第三步:編寫事件處理 注入spring容器 方法名上添加註解@Async和@EventListener(非同步處理和事件監聽) 第四步 :
jquery實現級聯遇到的ajax同步請求、動態DOM元素監聽事件
記錄一次實現級聯選單選項遇到的一系列問題 實現動態生成select下拉選項 json資料格式example: [ { "eventTyp