In this article, we will tackle a question: Does Vue.js support multiple event listeners? Our journey will take us deep into the mechanics of Vue.js, unraveling some intriguing undocumented behaviors along the way. Let's begin with a closer look at the official documentation on "Event Handling" in Vue.js. The primary method of attaching an event listener is through the syntax, which can also be simplified as . v-on:click="handler" @click="handler" In this syntax, the refers to a reference to a function. Additionally, in the "Inline Handlers" section, it's highlighted that you can employ arbitrary JavaScript code directly within the attribute. handler For instance, you can use to increment a variable. An important note is provided in the "Method vs. Inline Detection" section, which indicates that: @click="count++" The template compiler detects method handlers by checking whether the value string is a valid JavaScript identifier or a property access path. v-on So, does Vue support multiple listeners? The answer seems to lean towards no, but it's not entirely clear-cut. Let recap it with the code: <script setup> import { ref } from 'vue'; const count = ref(0); function inc() { count.value += 1; } </script> <template> <h3>{{ count }}</h3> <button @click="count++">Incremenet by count++</button> <button @click="inc">Incremenet by ref</button> <button @click="inc()">Incremenet by call</button> <button @click="() => inc()">Incremenet by lambda</button> </template> Now, let's within the Vue SFC Playground to closely examine how the Vue.js compiler has compiled these listeners. take a plunge into the JS tab We will encounter the following code snippet (I've omitted the portions for the sake of readability): _cache[0] || (_cache[0] = $event => (count.value++)) _createElementVNode("h3", null, _toDisplayString(count.value), 1 /* TEXT */); // @click="count++" will be compiled to... _createElementVNode( "button", { onClick: ($event) => count.value++, }, "Incremenet by count++", ); // @click="inc" will be compiled to... _createElementVNode("button", { onClick: inc }, "Incremenet by ref"); // @click="inc()" will be compiled to... _createElementVNode( "button", { onClick: ($event) => inc(), }, "Incremenet by call", ); // @click="() => inc()" will be compiled to... _createElementVNode( "button", { onClick: () => inc(), }, "Incremenet by lambda", ); The behavior is indeed intriguing. When a reference to is passed, the compiler simplifies it to . However, for , and , the compiler follows a distinct route. It encapsulates the code enclosed within the template's into a lambda function and then proceeds to execute it exactly as it's written. inc { onClick: inc } count++ inc() () => inc() " This observation offers valuable insight: if the compiler wraps code within a lambda, we can leverage native JavaScript capabilities to call multiple functions within a single expression using or . Let's try it. fn1(); fn2() fn1(), fn2() We will introduce another function, , which will invoke the native function and pass into it. You can access the updated Playground . showAlert() alert() count.value here Here is the code: // @click="count++, showAlert()" will be compiled to... _createElementVNode( "button", { onClick: ($event) => (count.value++, showAlert()), }, "Increment by count++", ); // How to pass multiple refs? _createElementVNode("button", { onClick: inc }, "Increment by ref"); // @click="inc(); showAlert()" will be compiled to... _createElementVNode( "button", { onClick: ($event) => { inc(); showAlert(); }, }, "Increment by call", ); // @click="() => (inc(), showAlert())" will be compiled to... _createElementVNode( "button", { onClick: () => (inc(), showAlert()), }, "Increment by lambda", ); For , , and , everything works fine, allowing us to call multiple functions for a single event. @click="count++, showAlert()" @click="inc(); showAlert()" @click="() => (inc(), showAlert())" The issue arises when dealing with the case. How can we pass multiple into the handler? The official documentation is notably silent on the topic of passing multiple references. ref refs @click="..." It appears that this feature might not be supported, leaving us unable to achieve this behavior directly. To explore this further, let's experiment with the two initial approaches that come to mind: and , and observe how they are compiled by Vue.js. fn1, fn2 [fn1, f2] // @click="inc, showAlert" will be complied to... _createElementVNode("button", { onClick: $event => (inc, showAlert) }, "Multiple refs 1"); // @click="[inc, showAlert]" will be complied to... _createElementVNode("button", { onClick: $event => ([inc, showAlert]) }, "Multiple refs 2") Unfortunately, both attempts did not yield success. Vue.js compiles these expressions in a manner that involves encapsulating the code enclosed within the template's . This approach is consistent with the behavior we previously uncovered. " Let's take a step back and examine the scenario where we simply pass a function identifier without any accompanying braces in the event handler. () // @click="inc" will be compiled to... _createElementVNode( "button", { onClick: inc }, "Incremenet by ref", ); Vue simply maps to . Now, let's recap the rule we extracted from the documentation. inc onClick The template compiler detects method handlers by checking whether the value string is a valid JavaScript identifier or a property access path. v-on Incorporating the insights gained from the above, we can rephrase this rule as follows: If the string within the template's or , is recognized as a valid JavaScript identifier, Vue.js compiler will directly map it to . v-on @event { onEvent: <Valid JS Identifier> } Or like this: Using only the name of a variable or a function will result in direct mapping. Our revised definition omits any reference to a "method" handler; it purely states that when a valid identifier is used, it is directly passed as is. This implies that you can even pass a numeric value like to an handler, provided you first create an identifier (in other words, a variable) that's bound to the value. 1337 onClick Passing a number as a handler clearly won't yield the desired results. However, as we recall, our aim is to pass multiple handlers as an array of refs. Given our newly established understanding, this is achievable. However, the prerequisite is to create a "valid JS Identifier (variable)" to store the reference to the array. Let's put this into action and see the results. . An interesting observation unfolds. Take a look here Firstly, using a "valid JS Identifier (variable)" named , we successfully pass an array to , and it gets mapped accordingly. multiple onClick However, TypeScript expresses its discontent. It raises an error stating: Type '(() => void)[]' is not assignable to type '(payload: MouseEvent) => void'. Type '(() => void)[]' provides no match for the signature '(payload: MouseEvent): void'.ts(2322) In essence, the types within Vue.js prevent us from passing an array of functions as a click listener. Let's set this aside for now, and simply click on the button to observe whether both listeners will be invoked. And yes, they are. We witness the counter value incrementing, followed by the appearance of the alert. But hold on, there's a puzzle to solve. Why is this functioning? What's going on behind the scenes? To comprehend this, we must delve deeper and grasp the mechanics underlying the translation of Vue's , created by the function, into a native DOM element. The key lies in exploring the source code of Vue.js itself! VNode _createElementVNode When we call the function in our main or , it triggers a chain of events that leads to the execution of the function (look for function ). createApp() app.js index.js createRenderer() createApp here This sequence results in the formation of an instance, complete with a method. This method establishes an association with the renderer (see ). This renderer's primary task is to convert our s into the native DOM elements we interact with. app mount() ensureRenderer() here VNode Here's an overview of the key steps: We compile our template, resulting in a series of calls. _createElementVNode() These calls build our Virtual DOM, generating s. VNode The renderer then traverses these nodes, converting them into native DOM elements. As the renderer transforms s into native DOM elements, it performs additional tasks using the object of a through the method. VNode props VNode patchProp Additionally, note that the function is invoked with , encompassing a "patched" . Let's delve into this for further understanding. createRenderer(rendererOptions) extended rendererOptions patchProp method export const patchProp: DOMRendererOptions['patchProp'] = ( // Omitted params... ) => { if (key === 'class') { patchClass(el, nextValue, isSVG) } else if (key === 'style') { patchStyle(el, prevValue, nextValue) // Keep in mind that we provide an object containing on<EventName> keys. // `isOn(key)` will return true for these keys. } else if (isOn(key)) { if (!isModelListener(key)) { // If the listener isn't intended for `v-model`, we utilize the `patchEvent` mechanism. patchEvent(el, key, prevValue, nextValue, parentComponent) } } // ... We can interpret the code as follows: "If a prop is , apply special handling based on values. If a prop is , implement special handling based on values. And if a prop begins with , perform specific actions using ." class class style style on patchEvent Let's direct our attention to the . We've reached the bottom where Vue establishes native event bindings through the browser's method. However, prior to this step, there are additional operations in play. patchEvent method addEventListener() The high-level call chain is as follows: is invoked. patchEvent() It proceeds to call in order to generate an invoker function. createInvoker() Inside the , we invoke , passing a wrapped version (altered by ) of the value provided in the event handler. invoker callWithAsyncErrorHandling patchStopImmediatePropagation @click="..." Now, let's examine to uncover the answer to the question: "Why does passing multiple refs to a function work?" patchStopImmediatePropagation function patchStopImmediatePropagation( e: Event, value: EventValue ): EventValue { // If the value is an array, there's even more to explore! // We can call $event.stopImmediatePropagation() // and other functions within the array won't be invoked. if (isArray(value)) { const originalStop = e.stopImmediatePropagation e.stopImmediatePropagation = () => { originalStop.call(e) ;(e as any)._stopped = true } // This is where the actual function calls occur. return value.map(fn => (e: Event) => !(e as any)._stopped && fn && fn(e)) } else { return value } } And here we are, fully informed. Even though the official documentation and TypeScript might not explicitly endorse it, we've found a code segment that effectively allows us to pass event listeners using an array of function references. There is the that introduced this functionality. It appears that at some point in the past, there might have been an intention to enable the capability of passing multiple listeners. However, as it stands now, this remains an undocumented feature. commit Lastly, let's address the question we initially posed: Does Vue support multiple listeners? The answer hinges on your interpretation of "supports". To summarize: We can invoke multiple functions using , and there's a for that. fn1(); fn2() test We can also invoke them using . fn1(), fn2() We can pass it through an array if stored in a variable. Alternatively, given the newfound knowledge, we can even call them like so: <template> <button @click="[fn1, fn2].forEach((fn) => fn($event))"> Click! </button> </template> Also published here