#Vue,Browser,FE

前言

其实浏览器内置了很多的事件。

而许多事件会自动触发浏览器执行某些行为。

例如:

  • 点击一个链接 —— 触发导航(navigation)到该 URL。
  • 点击表单的提交按钮 —— 触发提交到服务器的行为。
  • 在文本上按下鼠标按钮并移动 —— 选中文本。

如果我们使用 JavaScript 处理一个事件,那么我们通常不希望发生相应的浏览器行为。而是想要实现其他行为进行替代。

阻止浏览器行为

有两种方式来告诉浏览器我们不希望它执行默认行为:

  • 主流的方式是使用 event 对象。有一个 event.preventDefault() 方法。
  • 如果处理程序是使用 on<event>(而不是 addEventListener)分配的,那返回 false 也同样有效。

在下面这个示例中,点击链接不会触发导航(navigation),浏览器不会执行任何操作:

<a href="/" onclick="return false">Click here</a>
or
<a href="/" onclick="event.preventDefault()">here</a>

Click here
or
here


请注意的是,处理程序是使用 on<event>分配的,并进行return返回false并不是一个好的实践。

从处理程序返回 false 是一个例外
事件处理程序返回的值通常会被忽略。

唯一的例外是从使用 on<event> 分配的处理程序中返回的 return false。

在所有其他情况下,return 值都会被忽略。并且,返回 true 没有意义。

处理程序选项 “passive”

addEventListener 的可选项 passive: true 向浏览器发出信号,表明处理程序将不会调用 preventDefault()。

为什么需要这样做?

移动设备上会发生一些事件,例如 touchmove(当用户在屏幕上移动手指时),默认情况下会导致滚动,但是可以使用处理程序的 preventDefault() 来阻止滚动。

因此,当浏览器检测到此类事件时,它必须首先处理所有处理程序,然后如果没有任何地方调用 preventDefault,则页面可以继续滚动。但这可能会导致 UI 中不必要的延迟和“抖动”。

passive: true 选项告诉浏览器,处理程序不会取消滚动。然后浏览器立即滚动页面以提供最大程度的流畅体验,并通过某种方式处理事件。

对于某些浏览器(Firefox,Chrome),默认情况下,touchstart 和 touchmove 事件的 passive 为 true。

event.defaultPrevented

如果默认行为被阻止,那么 event.defaultPrevented 属性为 true,否则为 false。

这儿有一个有趣的用例。

你还记得我们在 冒泡和捕获 一章中讨论过的 event.stopPropagation(),以及为什么停止冒泡是不好的吗?

有时我们可以使用 event.defaultPrevented 来代替,来通知其他事件处理程序,该事件已经被处理。

我们来看一个实际的例子。

默认情况下,浏览器在 contextmenu 事件(单击鼠标右键)时,显示带有标准选项的上下文菜单。我们可以阻止它并显示我们自定义的菜单,就像这样:

<button>Right-click shows browser context menu</button>

<button oncontextmenu="alert('Draw our menu'); return false">
  Right-click shows our context menu
</button>


现在,除了该上下文菜单外,我们还想实现文档范围的上下文菜单。

右键单击时,应该显示最近的上下文菜单:

<p>Right-click here for the document context menu</p>
<button id="elem">Right-click here for the button context menu</button>

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Document context menu");
  };
</script>

Right-click here for the document context menu


问题是,当我们点击 elem 时,我们会得到两个菜单:按钮级和文档级(事件冒泡)的菜单。

如何修复呢?其中一个解决方案是:“当我们在按钮处理程序中处理鼠标右键单击事件时,我们阻止其冒泡”,使用 event.stopPropagation():

<p>Right-click for the document menu</p>
<button id="elem">Right-click for the button menu (fixed with event.stopPropagation)</button>

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    event.stopPropagation();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Document context menu");
  };
</script>

Right-click for the document menu


现在按钮级菜单如期工作。但是代价太大,我们拒绝了任何外部代码对右键点击信息的访问,包括收集统计信息的计数器等。这是非常不明智的。

另一个替代方案是,检查 document 处理程序是否阻止了浏览器的默认行为?如果阻止了,那么该事件已经得到了处理,我们无需再对此事件做出反应。

<p>Right-click for the document menu (added a check for event.defaultPrevented)</p>
<button id="elem">Right-click for the button menu</button>

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    if (event.defaultPrevented) return;

    event.preventDefault();
    alert("Document context menu");
  };
</script>

Right-click for the document menu (added a check for event.defaultPrevented)

现在一切都可以正常工作了。如果我们有嵌套的元素,并且每个元素都有自己的上下文菜单,那么这也是可以运行的。只需确保检查每个 contextmenu 处理程序中的 event.defaultPrevented。

event.stopPropagation() 和 event.preventDefault()
正如我们所看到的,event.stopPropagation() 和 event.preventDefault()(也被认为是 return false)是两个不同的东西。它们之间毫无关联。

嵌套的上下文菜单结构
还有其他实现嵌套上下文菜单的方式。其中之一是拥有一个具有 document.oncontextmenu 处理程序的全局对象,以及使我们能够在其中存储其他处理程序的方法。
该对象将捕获任何右键单击,浏览存储的处理程序并运行适当的处理程序。

但是,每段需要上下文菜单的代码都应该了解该对象,并使用它的帮助,而不是使用自己的 contextmenu 处理程序。

总结

有很多默认的浏览器行为:

  • mousedown —— 开始选择(移动鼠标进行选择)。
  • <input type="checkbox"> 上的 click —— 选中/取消选中的 input。
  • submit —— 点击<input type="submit">或者在表单字段中按下 Enter 键会触发该事件,之后浏览器将提交表单。
  • keydown —— 按下一个按键会导致将字符添加到字段,或者触发其他行为。
  • contextmenu —— 事件发生在鼠标右键单击时,触发的行为是显示浏览器上下文菜单。
  • ……还有更多……

如果我们只想通过 JavaScript 来处理事件,那么所有默认行为都是可以被阻止的。

想要阻止默认行为 —— 可以使用 event.preventDefault() return false。第二个方法只适用于通过 on<event> 分配的处理程序。

addEventListener passive: true 选项告诉浏览器该行为不会被阻止。这对于某些移动端的事件(像 touchstart 和 touchmove)很有用,用以告诉浏览器在滚动之前不应等待所有处理程序完成。

如果默认行为被阻止,event.defaultPrevented 的值会变成 true,否则为 false。

前言

让我们从一个示例开始。

处理程序(handler)被分配给了 <div>,但是如果你点击任何嵌套的标签(例如 <em><code>),该处理程序也会运行:

<div onclick="alert('The handler!')">
  <em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em>
</div>

So, if you click on EM, the handler on DIV runs.

冒泡

冒泡(bubbling)原理很简单。

当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。

假设我们有 3 层嵌套 FORM > DIV > P ,它们各自拥有一个处理程序:

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form onclick="alert('form')">FORM
  <div onclick="alert('div')">DIV
    <p onclick="alert('p')">P</p>
  </div>
</form>

现在,如果你点击 <p>,那么会发生什么?

  1. 首先,处理程序在 <p> 上运行。

  2. 然后,它在 <div> 上运行。

  3. 然后,它在 <form> 上运行。

  4. 最后,它在 document 上运行。

这种行为被称为“事件冒泡”,因为它像气泡一样从元素冒出来。

因此,如果我们点击 <p>,那么我们将看到 3 个 alert:p → div → form。

event.target

父元素上的处理程序始终可以获取事件实际发生位置的详细信息。

引发事件的那个嵌套层级最深的元素被称为目标元素,可以通过 event.target 访问。

注意与 this(=event.currentTarget)之间的区别:

  • this 是在处理程序运行时的“当前”元素,它始终相同。

  • event.target 是在事件发生时的“目标”元素,它可以是任何元素,它在冒泡过程中改变。

那我们来举个例子吧!

例如,如果我们有一个处理程序 form.onclick,那么它可以“捕获”表单内的所有点击。无论点击发生在哪里,它都会冒泡到 <form> 并运行处理程序。

注意哈!这边是将所有的事件都绑定到了 form 上,而不是每个元素上。

在 form.onclick 处理程序中:

this(=event.currentTarget)是 <form> 元素,因为处理程序在它上面运行。
event.target 是表单中实际被点击的元素。

A click shows both event.target and this to compare:

FORM
DIV

P

停止冒泡

冒泡事件从目标元素开始向上冒泡。通常,它会一直上升到 <html>,然后再到 document 对象,有些事件甚至会到达 window,它们会调用路径上所有的处理程序。

但是任意处理程序都可以决定事件已经被完全处理,并停止冒泡。

用于停止冒泡的方法是 event.stopPropagation()。

例如,如果你点击 <button>,这里的 body.onclick 不会工作:

<body onclick="alert(`the bubbling doesn't reach here`)">
  <button onclick="event.stopPropagation()">Click me</button>
</body>

捕获

事件处理的另一个阶段被称为“捕获(capturing)”。

DOM 事件标准描述了事件传播的 3 个阶段:

  1. 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
  2. 目标阶段(Target phase)—— 事件到达目标元素。
  3. 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。

下面是在表格中点击 <td> 的图片,摘自规范:

Event-Bubbling

也就是说:点击 <td>,事件首先通过祖先链向下到达元素(捕获阶段),然后到达目标(目标阶段),最后上升(冒泡阶段),在途中调用处理程序。

之前,我们只讨论了冒泡,因为捕获阶段很少被使用。通常我们看不到它。

使用 on<event> 属性或使用 HTML 特性(attribute)或使用两个参数的 addEventListener(event, handler) 添加的处理程序,对捕获一无所知,它们仅在第二阶段和第三阶段运行。

总结

当一个事件发生时 —— 发生该事件的嵌套最深的元素被标记为“目标元素”(event.target)。

  • 然后,事件从文档根节点向下移动到 event.target,并在途中调用分配了 addEventListener(…, true) 的处理程序(true 是 {capture: true} 的一个简写形式)。
  • 然后,在目标元素自身上调用处理程序。
  • 然后,事件从 event.target 冒泡到根,调用使用 on<event>、HTML 特性(attribute)和没有第三个参数的,或者第三个参数为 false/{capture:false} 的addEventListener 分配的处理程序。

每个处理程序都可以访问 event 对象的属性:

  • event.target —— 引发事件的层级最深的元素。
  • event.currentTarget(=this)—— 处理事件的当前元素(具有处理程序的元素)
  • event.eventPhase —— 当前阶段(capturing=1,target=2,bubbling=3)。

任何事件处理程序都可以通过调用 event.stopPropagation() 来停止事件,但不建议这样做,因为我们不确定是否确实不需要冒泡上来的事件,也许是用于完全不同的事情。

捕获阶段很少使用,通常我们会在冒泡时处理事件。这背后有一个逻辑。

事件处理程序也是如此。在特定元素上设置处理程序的代码,了解有关该元素最详尽的信息。特定于 的处理程序可能恰好适合于该 ,这个处理程序知道关于该元素的所有信息。所以该处理程序应该首先获得机会。然后,它的直接父元素也了解相关上下文,但了解的内容会少一些,以此类推,直到处理一般性概念并运行最后一个处理程序的最顶部的元素为止。

前言

Web 组件是一组 Web 原生 API 的总称,允许开发人员创建可重用的自定义元素。

Vue 和 Web Components 主要是互补技术。无论是将自定义元素集成到现有的 Vue 应用程序中,还是使用 Vue 构建和分发自定义元素,Vue 对使用和创建自定义元素都有出色的支持。

什么是Custom Elements

Web 组件的一个关键特性是创建自定义元素:即由 Web 开发人员定义行为的 HTML 元素,扩展了浏览器中可用的元素集。

Custom Elements有两种类型的自定义元素:

  • 自定义内置元素(Customized built-in element) 继承自标准的 HTML 元素,例如 HTMLImageElement 或 HTMLParagraphElement。它们的实现定义了标准元素的行为。
  • 独立自定义元素(Autonomous custom element) 继承自 HTML 元素基类 HTMLElement。你必须从头开始实现它们的行为。

自定义元素生命周期回调

当然Custom Elements也有自定义元素生命周期回调

一旦你的自定义元素被注册,当页面中的代码以特定方式与你的自定义元素交互时,浏览器将调用你的类的某些方法。 通过提供这些方法的实现,规范称之为生命周期回调,你可以运行代码来响应这些事件。

自定义元素生命周期回调包括:

  • connectedCallback():每当元素添加到文档中时调用。规范建议开发人员尽可能在此回调中实现自定义元素的设定,而不是在构造函数中实现。
  • disconnectedCallback():每当元素从文档中移除时调用。
  • adoptedCallback():每当元素被移动到新文档中时调用。
  • attributeChangedCallback():在属性更改、添加、移除或替换时调用。有关此回调的更多详细信息,请参见响应属性变化。

以下是一个记录这些生命周期事件的最小自定义元素示例:

// 为这个元素创建类
class MyCustomElement extends HTMLElement {
  static observedAttributes = ["color", "size"];

  constructor() {
    // 必须首先调用 super 方法
    super();
  }

  connectedCallback() {
    console.log("自定义元素添加至页面。");
  }

  disconnectedCallback() {
    console.log("自定义元素从页面中移除。");
  }

  adoptedCallback() {
    console.log("自定义元素移动至新页面。");
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`属性 ${name} 已变更。`);
  }
}

customElements.define("my-custom-element", MyCustomElement);

在 Vue 中使用自定义元素​

在 Vue 应用程序中使用自定义元素在很大程度上与使用原生 HTML 元素相同,但需要记住以下几点:

跳过组件解析​

默认情况下,Vue 会尝试将非原生 HTML 标签解析为已注册的 Vue 组件,然后再将其渲染为自定义元素。 这将导致 Vue 在开发过程中发出“无法解析组件”警告。为了让 Vue 知道某些元素应该被视为自定义元素并跳过组件解析,我们可以指定compilerOptions.isCustomElement选项。

如果您使用 Vue 进行构建设置,则该选项应通过构建配置传递,因为它是编译时选项。

浏览器内配置示例:

// Only works if using in-browser compilation.
// If using build tools, see config examples below.
app.config.compilerOptions.isCustomElement = (tag) => tag.includes('-')

Vite 配置示例​

// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
  plugins: [
    vue({
      template: {
        compilerOptions: {
          // treat all tags with a dash as custom elements
          isCustomElement: (tag) => tag.includes('-')
        }
      }
    })
  ]
}

Vue CLI 配置示例​

// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => ({
        ...options,
        compilerOptions: {
          // treat any tag that starts with ion- as custom elements
          isCustomElement: tag => tag.startsWith('ion-')
        }
      }))
  }
}

使用 Vue 构建自定义元素​

自定义元素的主要好处是它们可以与任何框架一起使用,甚至可以在没有框架的情况下使用。这使得它们非常适合分发最终消费者可能不使用相同前端堆栈的组件,或者当您希望将最终应用程序与其使用的组件的实现细节隔离时。

定义自定义元素​
Vue 支持通过该方法使用完全相同的 Vue 组件 API 创建自定义元素defineCustomElement。该方法接受与 相同的参数defineComponent,但返回一个扩展的自定义元素构造函数HTMLElement:

模板

<my-vue-element></my-vue-element>
import { defineCustomElement } from 'vue'

const MyVueElement = defineCustomElement({
  // normal Vue component options here
  props: {},
  emits: {},
  template: `...`,

  // defineCustomElement only: CSS to be injected into shadow root
  styles: [`/* inlined css */`]
})

// Register the custom element.
// After registration, all `<my-vue-element>` tags
// on the page will be upgraded.
customElements.define('my-vue-element', MyVueElement)

// You can also programmatically instantiate the element:
// (can only be done after registration)
document.body.appendChild(
  new MyVueElement({
    // initial props (optional)
  })
)

当我们谈论自定义元素和Vue组件时,实际上在讨论构建网页应用程序时使用的两种不同方式。自定义元素是一种Web标准,就像HTML元素一样,而Vue组件是Vue.js框架提供的一种更高级的构建方式。

有人认为只使用自定义元素是更“未来”的方式,但这段文字指出这种看法过于简单。它列举了一些原因,说明为什么Vue组件模型更为实用。其中一些关键点包括:

Vue组件提供了更多功能,如方便的模板系统、管理状态的方法,以及在服务器上渲染组件的高效方式。这些功能对于构建复杂的应用程序是必要的。

Vue组件支持强大的组合机制,而自定义元素在这方面有一些局限。这意味着使用Vue,你更容易构建灵活而强大的组件结构。使用Vue,你能够借助一个成熟的框架和庞大的社区,而不必自己构建和维护一套内部框架。

自定义元素和 Vue 组件之间确实存在一定程度的功能重叠:它们都允许我们定义具有数据传递、事件发出和生命周期管理的可重用组件。然而,Web 组件 API 的级别相对较低且简单。要构建实际的应用程序,我们需要一些该平台未涵盖的附加功能:

  • 声明性且高效的模板系统;

  • 反应式状态管理系统,有利于跨组件逻辑提取和重用;

  • 一种在服务器上渲染组件并在客户端 (SSR) 上进行组合的高性能方法,这对于 SEO 和LCP 等 Web Vitals 指标非常重要。原生自定义元素 SSR 通常涉及在 Node.js 中模拟 DOM,然后序列化变异的 DOM,而 Vue SSR 会尽可能编译为字符串连接,这更加高效。

defineCustomElement API Vue 组件转化

使用 defineCustomElement API 将 Vue 组件转化为可以注册的自定义元素类有一些好处:

  1. 跨框架集成: 通过将 Vue 组件转化为自定义元素类,你可以在不同的前端框架和库中使用这个组件。这种方式使得你的组件更具通用性,可以与其他技术栈集成。

  2. 独立使用: 将 Vue 组件注册为自定义元素后,它可以独立于 Vue 应用使用。这意味着你可以在没有整个 Vue 应用的情况下使用该组件,以及在不同的构建系统和模块系统中引入它。

  3. 逐步迁移: 如果你的应用是逐步迁移到 Vue 的,你可以通过将某些组件转化为自定义元素来实现渐进式迁移。这使得你可以逐步地将 Vue 组件引入到一个已经存在的项目中,而无需一次性重写整个应用。

  4. Web Components 标准兼容性: 将 Vue 组件注册为自定义元素使其与 Web Components 标准兼容。这意味着你可以利用 Web Components 生态系统的其他工具和库,使你的组件更具互操作性。

也就是说defineCustomElement API 的作用是将 Vue 组件编译为可以在浏览器中使用的自定义元素(Custom Element)。这意味着你不需要依赖 Vue 编译器在浏览器端实时编译 Vue 组件。

在使用 defineCustomElement API 时,Vue 组件会被提前编译成原生的自定义元素,这样就可以在浏览器中直接使用,而无需在运行时进行编译。

总体而言,通过使用 defineCustomElement API,你可以将 Vue 组件与自定义元素相结合,从而在更广泛的上下文中使用和共享这些组件,提高了组件的可复用性和灵活性。这在跨端组件开发集成上有很大的好处,你大可以先将组件开发成自定义元素,然后再在不同的端中使用。一个很典型的例子是我们之前提到的vue2和vue3的集成实现,这意味着你只需要将vue3的组件编译成自定义元素,然后在vue2中使用即可。