MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue事件处理 原生事件与自定义事件的对比与选择

2023-05-307.7k 阅读

Vue 事件处理概述

在 Vue 开发中,事件处理是构建交互性用户界面的关键部分。Vue 提供了强大的事件处理机制,允许开发者轻松地捕获和响应各种用户操作,例如点击按钮、输入文本、提交表单等。其中,原生事件和自定义事件是两种重要的事件类型,它们在不同的场景下有着各自的用途和特点。

原生事件

什么是原生事件

原生事件是指浏览器 DOM 元素本身所具有的事件,比如 click(点击事件)、input(输入事件)、submit(表单提交事件)等。这些事件是浏览器为了响应用户与页面元素的交互而提供的基础功能。在 Vue 中,我们可以很方便地为模板中的 DOM 元素绑定原生事件。

绑定原生事件的语法

在 Vue 模板中,使用 v - on: 指令(也可以使用缩写 @)来绑定原生事件。例如,要为一个按钮绑定点击事件,可以这样写:

<template>
  <div>
    <button @click="handleClick">点击我</button>
  </div>
</template>

<script>
export default {
  methods: {
    handleClick() {
      console.log('按钮被点击了');
    }
  }
}
</script>

在上述代码中,@click 表示绑定 click 原生事件,handleClick 是在 Vue 实例的 methods 选项中定义的方法,当按钮被点击时,该方法会被调用。

常见原生事件及其应用场景

  1. click 事件 这是最常用的原生事件之一,用于捕获用户对元素的点击操作。除了按钮,还可以用于链接、图片等元素。例如,在一个图片库应用中,点击图片可以放大显示:
<template>
  <div>
    <img :src="imageUrl" @click="zoomImage" alt="示例图片">
  </div>
</template>

<script>
export default {
  data() {
    return {
      imageUrl: 'https://example.com/image.jpg'
    };
  },
  methods: {
    zoomImage() {
      // 实现图片放大逻辑
      console.log('图片放大');
    }
  }
}
</script>
  1. input 事件 该事件在用户输入文本时触发,常用于处理输入框的值变化。比如,在一个搜索框中,实时获取用户输入并进行搜索:
<template>
  <div>
    <input type="text" @input="search" placeholder="搜索">
  </div>
</template>

<script>
export default {
  methods: {
    search(e) {
      const searchText = e.target.value;
      // 执行搜索逻辑
      console.log('搜索内容:', searchText);
    }
  }
}
</script>
  1. submit 事件 用于表单提交场景。当用户点击提交按钮或在表单输入框中按下回车键时触发。例如,在一个登录表单中:
<template>
  <form @submit="submitForm">
    <input type="text" placeholder="用户名">
    <input type="password" placeholder="密码">
    <button type="submit">登录</button>
  </form>
</template>

<script>
export default {
  methods: {
    submitForm(e) {
      e.preventDefault();
      // 处理登录逻辑
      console.log('表单提交');
    }
  }
}
</script>

submitForm 方法中,调用 e.preventDefault() 可以阻止表单的默认提交行为,避免页面刷新。

自定义事件

什么是自定义事件

自定义事件是 Vue 组件间通信的一种重要方式,它允许开发者在组件内部定义和触发特定的事件,然后在父组件中监听并处理这些事件。自定义事件可以用于解耦组件之间的关系,使组件的复用性更强。

定义和触发自定义事件

在 Vue 组件中,使用 this.$emit 方法来触发自定义事件。例如,我们创建一个子组件 ChildComponent,并在其中触发一个自定义事件 custom - event

<template>
  <div>
    <button @click="sendCustomEvent">触发自定义事件</button>
  </div>
</template>

<script>
export default {
  methods: {
    sendCustomEvent() {
      this.$emit('custom - event', '传递的数据');
    }
  }
}
</script>

在上述代码中,当按钮被点击时,sendCustomEvent 方法会通过 this.$emit 触发 custom - event 事件,并传递一个字符串数据。

在父组件中监听自定义事件

在父组件中,通过在子组件标签上使用 v - on: 指令(或 @ 缩写)来监听子组件触发的自定义事件。例如:

<template>
  <div>
    <ChildComponent @custom - event="handleCustomEvent"></ChildComponent>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  methods: {
    handleCustomEvent(data) {
      console.log('接收到自定义事件的数据:', data);
    }
  }
}
</script>

在父组件中,当子组件触发 custom - event 事件时,父组件的 handleCustomEvent 方法会被调用,并接收到子组件传递的数据。

原生事件与自定义事件的对比

作用范围

  1. 原生事件 原生事件主要作用于 DOM 元素,它们是浏览器提供的用于响应用户与页面元素直接交互的机制。原生事件的触发范围局限于单个 DOM 元素及其子元素(在事件冒泡的情况下)。例如,一个按钮的 click 事件只会在按钮本身及其内部元素被点击时触发。
  2. 自定义事件 自定义事件主要用于 Vue 组件之间的通信,其作用范围跨越组件边界。自定义事件可以在子组件内部触发,然后由父组件或其他相关组件监听和处理,实现组件间的数据传递和行为协调。例如,一个复杂表单组件内部的子组件可以通过触发自定义事件告知父组件表单数据的变化。

触发方式

  1. 原生事件 原生事件由用户在浏览器中的操作(如点击、输入等)自动触发,开发者只需在模板中绑定相应的事件处理函数即可。例如,当用户点击按钮时,浏览器会自动触发 click 原生事件,进而调用绑定的方法。
  2. 自定义事件 自定义事件需要在组件的 JavaScript 代码中通过 this.$emit 方法手动触发。开发者可以根据组件内部的业务逻辑,在合适的时机触发自定义事件。比如,在一个购物车组件中,当商品数量发生变化时,通过 this.$emit 触发一个自定义事件通知父组件更新购物车总价。

事件参数

  1. 原生事件 原生事件触发时,传递给事件处理函数的参数通常是与事件相关的 DOM 事件对象(如 MouseEventKeyboardEvent 等)。这个事件对象包含了事件发生时的各种信息,如鼠标位置、按键信息等。例如,在 click 事件处理函数中,可以通过 event.target 获取被点击的 DOM 元素。
<template>
  <div>
    <button @click="handleClick">点击我</button>
  </div>
</template>

<script>
export default {
  methods: {
    handleClick(event) {
      console.log('被点击的元素:', event.target);
    }
  }
}
</script>
  1. 自定义事件 自定义事件触发时传递的参数由开发者在 this.$emit 方法中指定。这些参数可以是任何类型的数据,如字符串、对象、数组等,方便在组件间传递有意义的业务数据。例如,在前面的购物车组件中,触发自定义事件时可以传递商品的数量、价格等信息给父组件。
<template>
  <div>
    <button @click="updateCart">更新购物车</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      product: {
        name: '商品 1',
        price: 100,
        quantity: 1
      }
    };
  },
  methods: {
    updateCart() {
      this.$emit('cart - updated', this.product);
    }
  }
}
</script>

在父组件中监听 cart - updated 事件时,就可以接收到 product 对象。

事件绑定位置

  1. 原生事件 原生事件在 Vue 模板中直接绑定到 DOM 元素上,通过 v - on:@ 指令进行绑定。这种绑定方式直观,与传统的 HTML 事件绑定有一定的相似性。例如:
<input type="text" @input="handleInput">
  1. 自定义事件 自定义事件在父组件的模板中绑定到子组件标签上,通过监听子组件触发的特定自定义事件来进行处理。例如:
<ChildComponent @custom - event="handleCustomEvent"></ChildComponent>

而在子组件内部,通过 this.$emit 触发自定义事件,不需要在模板中直接绑定。

如何选择原生事件与自定义事件

根据事件源和目标判断

  1. 如果事件源是 DOM 元素且处理逻辑仅涉及该元素本身或其 DOM 子树 此时应优先选择原生事件。比如,一个按钮的点击效果,如显示隐藏某个菜单,直接使用 click 原生事件绑定处理函数即可。
<template>
  <div>
    <button @click="toggleMenu">切换菜单</button>
    <div v - if="isMenuVisible">菜单内容</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isMenuVisible: false
    };
  },
  methods: {
    toggleMenu() {
      this.isMenuVisible =!this.isMenuVisible;
    }
  }
}
</script>
  1. 如果事件源是一个 Vue 组件,且需要与父组件或其他组件进行通信 则应使用自定义事件。例如,一个分页组件,当用户点击下一页或上一页按钮时,需要通知父组件更新数据,这时在分页组件内部触发自定义事件,父组件监听该事件来处理数据更新。
<template>
  <div>
    <button @click="prevPage">上一页</button>
    <button @click="nextPage">下一页</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      currentPage: 1
    };
  },
  methods: {
    prevPage() {
      if (this.currentPage > 1) {
        this.currentPage--;
        this.$emit('page - changed', this.currentPage);
      }
    },
    nextPage() {
      this.currentPage++;
      this.$emit('page - changed', this.currentPage);
    }
  }
}
</script>

在父组件中:

<template>
  <div>
    <PaginationComponent @page - changed="fetchData"></PaginationComponent>
    <div v - for="item in dataList" :key="item.id">{{item.name}}</div>
  </div>
</template>

<script>
import PaginationComponent from './PaginationComponent.vue';

export default {
  components: {
    PaginationComponent
  },
  data() {
    return {
      dataList: []
    };
  },
  methods: {
    fetchData(page) {
      // 根据 page 从服务器获取数据并更新 dataList
      console.log('获取第', page, '页数据');
    }
  }
}
</script>

根据业务逻辑复杂度判断

  1. 对于简单的交互逻辑 如果只是对 DOM 元素的基本操作,如改变样式、显示隐藏等,使用原生事件更为直接和简单。例如,在一个图片展示组件中,点击图片切换图片的显示状态(如从缩略图切换到原图):
<template>
  <div>
    <img :src="thumbnailUrl" @click="toggleImage" alt="缩略图">
    <img v - if="isFullImage" :src="fullImageUrl" alt="原图">
  </div>
</template>

<script>
export default {
  data() {
    return {
      thumbnailUrl: 'https://example.com/thumbnail.jpg',
      fullImageUrl: 'https://example.com/full.jpg',
      isFullImage: false
    };
  },
  methods: {
    toggleImage() {
      this.isFullImage =!this.isFullImage;
    }
  }
}
</script>
  1. 对于复杂的业务逻辑,涉及多个组件间的协作和数据传递 自定义事件能更好地实现组件间的解耦和通信。例如,在一个电商订单流程中,订单确认组件、支付组件、物流组件等需要相互协作。当支付成功后,支付组件可以触发一个自定义事件,通知订单确认组件更新订单状态,同时通知物流组件准备发货。
<template>
  <div>
    <PaymentComponent @payment - success="handlePaymentSuccess"></PaymentComponent>
  </div>
</template>

<script>
import PaymentComponent from './PaymentComponent.vue';

export default {
  components: {
    PaymentComponent
  },
  methods: {
    handlePaymentSuccess() {
      // 通知订单确认组件更新订单状态
      this.$emit('update - order - status', '已支付');
      // 通知物流组件准备发货
      this.$emit('prepare - shipping');
    }
  }
}
</script>

根据组件复用性考虑

  1. 如果组件的功能较为独立,不依赖外部组件的状态变化 使用原生事件可以使组件更加内聚,复用性更高。例如,一个简单的倒计时组件,只需要根据自身的逻辑进行倒计时操作,不需要与其他组件进行复杂的通信,使用原生事件处理用户操作(如暂停、重置倒计时)即可。
<template>
  <div>
    <span>{{countdown}}</span>
    <button @click="pauseCountdown">暂停</button>
    <button @click="resetCountdown">重置</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      countdown: 60,
      timer: null
    };
  },
  mounted() {
    this.startCountdown();
  },
  methods: {
    startCountdown() {
      this.timer = setInterval(() => {
        this.countdown--;
        if (this.countdown <= 0) {
          clearInterval(this.timer);
        }
      }, 1000);
    },
    pauseCountdown() {
      clearInterval(this.timer);
    },
    resetCountdown() {
      clearInterval(this.timer);
      this.countdown = 60;
      this.startCountdown();
    }
  },
  beforeDestroy() {
    clearInterval(this.timer);
  }
}
</script>
  1. 如果组件需要与其他组件进行灵活的交互,并且在不同场景下可能有不同的响应方式 自定义事件可以提高组件的灵活性和复用性。例如,一个通用的弹窗组件,在不同的页面中可能需要在关闭弹窗时执行不同的操作,通过触发自定义事件 close,父组件可以根据自身需求监听该事件并执行相应的逻辑。
<template>
  <div v - if="isVisible" class="popup">
    <div class="popup - content">
      <slot></slot>
      <button @click="closePopup">关闭</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isVisible: false
    };
  },
  methods: {
    openPopup() {
      this.isVisible = true;
    },
    closePopup() {
      this.isVisible = false;
      this.$emit('close');
    }
  }
}
</script>

在父组件中:

<template>
  <div>
    <button @click="openPopup">打开弹窗</button>
    <PopupComponent @close="handlePopupClose"></PopupComponent>
  </div>
</template>

<script>
import PopupComponent from './PopupComponent.vue';

export default {
  components: {
    PopupComponent
  },
  methods: {
    openPopup() {
      this.$refs.popup.openPopup();
    },
    handlePopupClose() {
      // 执行父组件特定的逻辑,如保存数据、刷新页面等
      console.log('弹窗关闭,执行父组件逻辑');
    }
  }
}
</script>

原生事件与自定义事件结合使用的场景

在实际开发中,很多时候原生事件和自定义事件会结合使用。例如,在一个表单组件中,当用户输入内容时,通过原生 input 事件实时获取输入值,并触发自定义事件通知父组件数据发生了变化。

<template>
  <div>
    <input type="text" @input="handleInput" placeholder="输入内容">
  </div>
</template>

<script>
export default {
  methods: {
    handleInput(e) {
      const value = e.target.value;
      this.$emit('input - changed', value);
    }
  }
}
</script>

在父组件中:

<template>
  <div>
    <FormComponent @input - changed="updateData"></FormComponent>
    <div>最新数据: {{data}}</div>
  </div>
</template>

<script>
import FormComponent from './FormComponent.vue';

export default {
  components: {
    FormComponent
  },
  data() {
    return {
      data: ''
    };
  },
  methods: {
    updateData(value) {
      this.data = value;
    }
  }
}
</script>

这样,既利用了原生事件获取 DOM 元素的输入值,又通过自定义事件实现了组件间的数据传递和通信。

再比如,在一个树形结构组件中,当用户点击树节点时,通过原生 click 事件获取节点信息,然后触发自定义事件通知父组件进行节点展开、收缩或其他相关操作。

<template>
  <div>
    <ul>
      <li v - for="node in treeData" :key="node.id" @click="handleNodeClick(node)">
        {{node.label}}
        <ul v - if="node.children">
          <TreeNode :treeData="node.children"></TreeNode>
        </ul>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  props: {
    treeData: {
      type: Array,
      default: () => []
    }
  },
  methods: {
    handleNodeClick(node) {
      // 处理节点点击逻辑,如展开收缩
      if (node.children) {
        node.isExpanded =!node.isExpanded;
      }
      this.$emit('node - clicked', node);
    }
  }
}
</script>

在父组件中:

<template>
  <div>
    <TreeNode :treeData="tree" @node - clicked="handleNodeClicked"></TreeNode>
  </div>
</template>

<script>
import TreeNode from './TreeNode.vue';

export default {
  components: {
    TreeNode
  },
  data() {
    return {
      tree: [
        {
          id: 1,
          label: '节点 1',
          children: [
            {
              id: 11,
              label: '子节点 11'
            }
          ]
        }
      ]
    };
  },
  methods: {
    handleNodeClicked(node) {
      // 父组件根据节点信息执行其他操作,如加载子节点数据
      console.log('点击的节点:', node);
    }
  }
}
</script>

通过这种原生事件与自定义事件的结合,实现了树形结构组件内部的交互处理以及与父组件的通信。

原生事件与自定义事件的注意事项

原生事件

  1. 事件修饰符的使用 Vue 提供了一些事件修饰符来简化原生事件的处理,如 .prevent(阻止默认行为)、.stop(阻止事件冒泡)等。使用这些修饰符时要注意其作用和适用场景。例如,在表单提交时,使用 .prevent 修饰符可以避免页面刷新:
<form @submit.prevent="submitForm">
  <!-- 表单内容 -->
</form>
  1. 事件绑定的性能优化 当在模板中绑定大量原生事件时,可能会对性能产生一定影响。特别是对于频繁触发的事件,如 scrollresize 等,应尽量减少不必要的事件处理逻辑,或者使用防抖(debounce)、节流(throttle)等技术来优化性能。例如,使用 Lodash 的 debounce 函数来处理 scroll 事件:
<template>
  <div @scroll="debouncedHandleScroll">
    <!-- 页面内容 -->
  </div>
</template>

<script>
import { debounce } from 'lodash';

export default {
  methods: {
    handleScroll() {
      // 处理滚动逻辑
      console.log('页面滚动');
    },
    debouncedHandleScroll: debounce(function () {
      this.handleScroll();
    }, 300)
  }
}
</script>

自定义事件

  1. 事件命名规范 自定义事件的命名应遵循一定的规范,通常采用小写字母加 - 的形式,以提高代码的可读性和可维护性。避免使用与原生事件相同的名称,防止混淆。例如,使用 item - added 而不是 add 作为自定义事件名称。
  2. 组件间通信的层级问题 当组件嵌套层次较深时,使用自定义事件进行通信可能会变得繁琐。此时可以考虑使用 Vuex 等状态管理工具来统一管理组件间的数据共享和通信,避免过多的事件传递。例如,在一个多层嵌套的组件结构中,最内层组件需要通知最外层组件数据变化,通过层层传递自定义事件会使代码复杂,而 Vuex 可以更方便地实现这种跨层级的数据交互。

总结原生事件与自定义事件的选择要点

在 Vue 开发中,原生事件和自定义事件各有其特点和适用场景。原生事件适用于直接操作 DOM 元素的交互,具有简单直接的特点;自定义事件则主要用于组件间的通信和复杂业务逻辑的处理,能有效解耦组件关系。在选择时,要综合考虑事件源和目标、业务逻辑复杂度、组件复用性等因素,合理使用原生事件和自定义事件,以构建高效、可维护的前端应用程序。同时,要注意原生事件的性能优化和自定义事件的命名规范等问题,确保代码质量和开发效率。通过灵活运用这两种事件类型,开发者可以充分发挥 Vue 的事件处理机制的优势,打造出优秀的用户界面。