解锁难题:调查vue.js中的多个事件听众
#javascript #网络开发人员 #前端 #vue

在本文中,我们将解决一个问题: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.jsindex.js中调用createApp()函数时,它会触发一系列事件,导致执行createRenderer()函数(查找createApp函数here)。此序列导致形成了app实例,并配有mount()方法。该方法与渲染器建立了关联(参见ensureRenderer() here)。该渲染器的主要任务是将我们的VNodes转换为我们与之互动的本机元素。

这是关键步骤的概述:

  1. 我们编译了我们的模板,导致了一系列_createElementVNode()调用。
  2. 这些调用构建了我们的虚拟dom,生成VNode
  3. 然后,渲染器遍历这些节点,将它们转换为本地DOM元素。

当渲染器将VNodes转换为本机DOM元素时,它使用VNodeprops对象通过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()方法建立本机事件绑定的底部。但是,在此步骤之前,还有其他操作。高级呼叫链如下:

  1. patchEvent()被调用。
  2. 它继续致电createInvoker()以生成调用函数。
  3. 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支持多个听众吗?答案取决于您对“支持”的解释。总结:

  1. 我们可以使用fn1(); fn2()调用多个功能,并且有一个test
  2. 我们还可以使用fn1(), fn2()调用它们。
  3. 如果存储在变量中,我们可以通过数组将其传递。

另外,鉴于新发现的知识,我们甚至可以这样称呼它们:

<template>
  <button @click="[fn1, fn2].forEach((fn) => fn($event))">
    Click!
  </button>
</template>