在本文中,我们将解决一个问题:vue.js是否支持多个事件听众?我们的旅程将使我们深入vue.js的机制,并在此过程中揭示了一些有趣的无证行为。
让我们从vue.js中的“事件处理”的正式文档仔细研究开始。附加事件侦听器的主要方法是通过v-on:click="handler"
语法,也可以简化为@click="handler"
。在此语法中,handler
是指对函数的引用。此外,在“直列处理程序”部分中,强调您可以在属性中直接采用任意JavaScript代码。例如,您可以使用@click="count++"
来增加变量。在“方法与内联检测”部分中提供了一个重要的注释,该部分表明
模板编译器通过检查
v-on
值字符串是有效的JavaScript标识符还是属性访问路径来检测方法处理程序。
那么,Vue支持多个听众吗?答案似乎倾向于否,但这并不是完全明确的。
让它用代码回顾:
<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>
现在,让我们在VUE SFC操场中的take a plunge into the JS tab仔细研究VUE.JS编译器如何编译这些听众。
我们将遇到以下代码段(为了可读性,我省略了_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",
);
这种行为确实很有趣。当传递对inc
的引用时,编译器将其简化为{ onClick: inc }
。但是,对于count++
,inc()
和() => inc()
,编译器遵循了一条不同的路线。它将包含在模板的"
中的代码封装到lambda函数中,然后按照书面方式准确地执行它。该观察结果提供了有价值的见解:如果编译器将代码包装在lambda中,我们可以利用本机JavaScript功能使用fn1(); fn2()
或fn1(), fn2()
在单个表达式中调用多个功能。让我们尝试一下。
我们将介绍另一个功能showAlert()
,该功能将调用本机alert()
函数并将count.value
传递到其中。您可以访问更新的操场here。这是代码:
// @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",
);
对于@click="count++, showAlert()"
,@click="inc(); showAlert()"
和@click="() => (inc(), showAlert())"
,一切都很好,允许我们调用单个事件的多个功能。
处理ref
案件时出现问题。我们如何将多个refs
传递到@click="..."
处理程序中?官方文档对传递多个参考的主题显着保持沉默。看来可能不支持此功能,使我们无法直接实现此行为。
要进一步探讨这一点,让我们尝试一下想到的两种初始方法:fn1, fn2
和[fn1, f2]
,并观察vue.js.
的如何编译它们。
// @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")
不幸的是,这两种尝试都不会取得成功。 vue.js以涉及封装模板"
中包含的代码的方式编译这些表达式。这种方法与我们先前发现的行为一致。
让我们退后一步,检查场景,在事件处理程序中,我们只是通过函数标识符而没有任何随附的()
括号。
// @click="inc" will be compiled to...
_createElementVNode(
"button",
{ onClick: inc },
"Incremenet by ref",
);
vue只需将inc
映射到onClick
。现在,让我们回顾一下我们从文档中提取的规则。
模板编译器通过检查
v-on
值字符串是有效的JavaScript标识符还是属性访问路径来检测方法处理程序。
整合从上面获得的见解,我们可以按以下方式重述此规则:
如果模板的
v-on
或@event
中的字符串被认为是有效的JavaScript标识符,则VUE.JS编译器将将其直接映射到{ onEvent: <Valid JS Identifier> }
。
或喜欢:
仅使用变量或函数的名称将导致直接映射。
我们的修订定义省略了对“方法”处理程序的任何引用;它纯粹指出,当使用有效的标识符时,它会直接传递。这意味着您甚至可以将像1337
这样的数字值传递给onClick
处理程序,前提
将数字作为处理程序显然不会产生所需的结果。但是,正如我们所记得的那样,我们的目标是将多个处理人员作为一系列裁判传递。鉴于我们新建立的理解,这是可以实现的。但是,先决条件是创建一个“有效的JS标识符(变量)”来存储对数组的引用。让我们将其付诸实践并查看结果。
Take a look here。一个有趣的观察结果。
首先,使用名为multiple
的“有效JS标识符(变量)”,我们成功地将数组传递给onClick
,并得到相应的映射。
但是,打字稿表达了不满。它提出了一个错误,说明:
type'(()=> void)[]''不能分配给type'(有效载荷:mouseevent)=> void'。
type'(()=> void)[]''不提供签名的匹配'(有效载荷:mouseevent):void'.ts(2322)
本质上,vue.js中的类型阻止了我们作为单击“侦听器”的函数。
让我们暂时搁置一旁,只需单击按钮即可观察是否将调用两个侦听器。是的,他们是。我们目睹了计数器价值的增加,然后出现警报。但是请坚持,有一个难题可以解决。为什么这个功能?幕后发生了什么?
为了理解这一点,我们必须更深入地研究并掌握由_createElementVNode
函数创建的Vue的VNode
翻译的机制,并将其转化为本机DOM元素。关键在于探索vue.js本身的源代码!
当我们在主app.js
或index.js
中调用createApp()
函数时,它会触发一系列事件,导致执行createRenderer()
函数(查找createApp
函数here)。此序列导致形成了app
实例,并配有mount()
方法。该方法与渲染器建立了关联(参见ensureRenderer()
here)。该渲染器的主要任务是将我们的VNode
s转换为我们与之互动的本机元素。
这是关键步骤的概述:
- 我们编译了我们的模板,导致了一系列
_createElementVNode()
调用。 - 这些调用构建了我们的虚拟dom,生成
VNode
。 - 然后,渲染器遍历这些节点,将它们转换为本地DOM元素。
当渲染器将VNode
s转换为本机DOM元素时,它使用VNode
的props
对象通过patchProp
方法执行其他任务。
此外,请注意,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)
}
} // ...
我们可以如下解释代码:“如果道具是class
,请根据class
值进行特殊处理。如果道具是style
,请根据style
值实现特殊处理。使用patchEvent
的动作。”
让我们将注意力转移到patchEvent
method上。我们已经到达了Vue通过浏览器的addEventListener()
方法建立本机事件绑定的底部。但是,在此步骤之前,还有其他操作。高级呼叫链如下:
-
patchEvent()
被调用。 - 它继续致电
createInvoker()
以生成调用函数。 - 在
invoker
中,我们调用callWithAsyncErrorHandling
,传递了@click="..."
事件处理程序中提供的值的包装版本(由patchStopImmediatePropagation
更改)。
现在,让我们检查patchStopImmediatePropagation
以揭示问题的答案:“为什么将多个refs转到函数工作?”
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
}
}
,我们在这里充分了解。即使官方文档和打字稿可能无法明确认可它,我们已经找到了一个代码段,该段允许我们使用一系列函数参考来传递事件侦听器。
有一个引入此功能的commit。看来,在过去的某个时刻,可能有目的是使能够传递多个听众的能力。但是,就目前而言,这仍然是一个无证件的功能。
最后,让我们解决我们最初提出的问题:vue支持多个听众吗?答案取决于您对“支持”的解释。总结:
- 我们可以使用
fn1(); fn2()
调用多个功能,并且有一个test。 - 我们还可以使用
fn1(), fn2()
调用它们。 - 如果存储在变量中,我们可以通过数组将其传递。
另外,鉴于新发现的知识,我们甚至可以这样称呼它们:
<template>
<button @click="[fn1, fn2].forEach((fn) => fn($event))">
Click!
</button>
</template>