事件处理

#事件处理

事件方法

  1. <button @click="greet">Greet</button>
  2. methods: {
  3. greet: function (event) {# 此时默认函数第一个参数是原生的dom事件
  4. // `this` 在方法里指向当前 Vue 实例
  5. alert('Hello ' + this.name + '!')
  6. // `event` 是原生 DOM 事件
  7. if (event) {
  8. alert(event.target.tagName)
  9. }
  10. }
  11. }

在html里向方法传值

  1. <button @click="say('what')">Say what</button>
  2. methods: {
  3. say: function (message) {
  4. alert(message)
  5. }
  6. }

既传值又要接受event原生事件:用特殊变量 $event 把它传入方法

  1. <button @click="warn('Form cannot be submitted yet.', $event)"> # $event
  2. Submit
  3. </button>
  4. // ...
  5. methods: {
  6. warn: function (message, event) {
  7. // 现在我们可以访问原生事件对象
  8. if (event) event.preventDefault()
  9. alert(message)
  10. }
  11. }

事件修饰符

在事件处理程序中调用 event.preventDefault()event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。

  1. # before
  2. <button @click="warn('Form cannot be submitted yet.', $event)"> # $event
  3. Submit
  4. </button>
  5. // ...
  6. methods: {
  7. warn: function (message, event) {
  8. if (event) event.stopPropagation()
  9. }
  10. }
  11. # after
  12. <!-- 阻止单击事件继续传播 -->
  13. <a @click.stop="doThis"></a>

大大提升了开发体验,类似的事件修饰符有:

  • .stop #阻止单击事件继续传播
  • .prevent
  • .capture
  • .self
  • .once
  • .passive
  1. <!-- 阻止单击事件继续传播 -->
  2. <a v-on:click.stop="doThis"></a>
  3. <!-- 提交事件不再重载页面 -->
  4. <form v-on:submit.prevent="onSubmit"></form>
  5. <!-- 修饰符可以串联 -->
  6. <a v-on:click.stop.prevent="doThat"></a>
  7. <!-- 只有修饰符 -->
  8. <form v-on:submit.prevent></form>
  9. <!-- 添加事件监听器时使用事件捕获模式 -->
  10. <!-- 即元素自身触发的事件先在此处理,然后才交由内部元素进行处理 -->
  11. <div v-on:click.capture="doThis">...</div> ###!useful ####
  12. <!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
  13. <!-- 即事件不是从内部元素触发的 -->
  14. <div v-on:click.self="doThat">...</div> ###!useful ####

这部分建议 补充相关dom事件相关机制的知识

image.png

还有相关按键码

组件通信

父组件向子组件prop传值

#通过-Prop-向子组件传递数据

  1. 组件使用者 传递给组件title数据(此时为静态数据)
  2. <blog-post title="My journey with Vue"></blog-post>
  3. Vue.component('blog-post', {
  4. props: ['title'],// 子组件接受父组件的值
  5. template: '<h3>{{ title }}</h3>'// 子组件的template 定义了子组件的样式
  6. })
  1. // 组件使用者
  2. <blog-post
  3. v-for="post in posts"
  4. v-bind:key="post.id"
  5. v-bind:post="post" -- 传递给组件的数据 post
  6. />
  7. // 组件内部定义
  8. Vue.component('blog-post', {
  9. props: ['post'],
  10. template: `
  11. <div class="blog-post">
  12. <h3>{{ post.title }}</h3>
  13. <div v-html="post.content"></div>
  14. </div>
  15. `
  16. })

warnning ⚠️
不应该在子组件里直接修改prop的值,因为一般是prop在父组件里改变之后 父级 prop 的更新会向下流动到子组件中。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。

如果子组件修改了父组件的prop,那么数据流就是一个闭环了,很可能陷入无限循环中;

(啥?子组件还可以修改父组件传来的值,从而父组件里的数据就改变了?
=-==》 因为,是引用传递。无关vue,是js基础

在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态。

有哪些场景子组件可能会有修改父组件传下的prop的值呢?

  1. prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的数据来使用
  2. prop 以一种原始的值传入且需要进行转换(加工)后显示在子组件里

不推荐直接修改prop然后展示修改后的prop,我们针对以上两个场景分别有如下解决方案

  1. #1 子组件本地需要维护一个随时更新的变量,其中父组件的prop为初始值
  2. --> 子组件内部维护一个初始值为propdata
  3. props: ['initialCounter'],
  4. data: function () {
  5. return {
  6. counter: this.initialCounter
  7. }
  8. }
  9. #2 prop只是数据加工=> computed
  10. props: ['size'],
  11. computed: {
  12. normalizedSize: function () {
  13. return this.size.trim().toLowerCase()
  14. }
  15. }

插槽传值

插槽

父组件监听子组件事件

有些时候我们需要在父组件里监听子组件的事件,然后让父组件统一修改子组件里共享的父组件的数据

即子组件里有共同的属性,我们就将这个属性提到父组件里统一管理,
一般情况是 父组件接受事件改变父组件里的属性,从而使得子组件里承接的父组件里的数据自动更新;
但是现在的情况是 子组件要接受事件 通知父组件 让父组件改变
===>
1.子组件监听事件,然后往上手动通知父组件,事件发生了===》 原生的事件系统里 有冒泡本可以解决
2.父组件监听子组件的事件==》子组件调用父组件方法

在vue里,子组件通知父组件事件:
$emit(‘xxxname’) xxxname是父组件的方法

  1. # 子组件 触发父级别的自定义事件enlarge-text
  2. <button @click="$emit('enlarge-text')">
  3. Enlarge text
  4. </button>
  5. # 父组件
  6. <blog-post
  7. ...
  8. @enlarge-text="changeFontfunc"
  9. ></blog-post>

除了自定义事件有两种写法,比如子组件click触发通知父组件;
1.父组件 @on-click
2.父组件

子组件都是通过emit触发通知父组件

  1. <template>
  2. <button @click="handleClick">
  3. <slot></slot>
  4. </button>
  5. </template>
  6. <script>
  7. export default {
  8. methods: {
  9. handleClick (event) {
  10. this.$emit('on-click', event);
  11. }
  12. }
  13. }
  14. </script>

子组件不仅是调用父组件方法,还要向父组件传值

  1. <button v-on:click="$emit('enlarge-text', 0.1)">
  2. Enlarge text
  3. </button>
  4. #
  5. <blog-post
  6. ...
  7. v-on:enlarge-text="postFontSize += $event" # 直接$event接受这个值
  8. ></blog-post>
  9. 或者 作为第一个数传入函数方法
  10. <blog-post
  11. ...
  12. v-on:enlarge-text="onEnlargeText"
  13. ></blog-post>
  14. methods: {
  15. onEnlargeText: function (enlargeAmount) {
  16. this.postFontSize += enlargeAmount
  17. }
  18. }

等等。如果子组件是input非受控组件,那么能否在父组件上使用v-model使得 父组件的改变 子组件的input也改变呢?

即子组件的input的值的管理能力 是来自 父组件,但子组件作为用户的第一接触者,需要响应用户的改变从而更新父组件的值.
即 子组件v-model下的数据双向绑定

  1. # 父组件
  2. <custom-input v-model="searchText"></custom-input>
  3. # 子组件内部
  4. <input >
  5. 想要子组件接受父组件的值,同时响应用户的交互,从而更新父组件的值。
  6. 此时应该怎么写子组件?
  7. ===》
  8. Vue.component('custom-input', {
  9. props: ['value'],
  10. template: `
  11. <input
  12. :value="value"
  13. @input="$emit('input', $event.target.value)"
  14. >
  15. `
  16. })

因为v-model其实是两个数据处理结合的语法糖

  1. <input v-model="searchText">
  2. ==> 等价于
  3. <input
  4. :value="searchText"
  5. @input="searchText = $event.target.value"
  6. >
  7. 换在组件上就是:
  8. <custom-input
  9. :value="searchText"
  10. @input="searchText = $event" $event接受子组件的值 即子组件向父组件传$event.target.value
  11. ></custom-input>
  12. # 此时的value是传给子组件的prop 也可以换成 :searchText
  13. # 同样 input也可以换成其他事件方法
  14. <custom-input
  15. :searchText="searchText"
  16. @inputClick="searchText = $event" $event接受子组件的值 即子组件向父组件传$event.target.value
  17. ></custom-input>
  18. ===> 子组件
  19. Vue.component('custom-input', {
  20. props: ['searchText'],
  21. template: `
  22. <input
  23. :value="searchText"
  24. @input="$emit('inputClick', $event.target.value)"
  25. >
  26. `
  27. })
  28. 但是如果要保持父组件是用v-model的方式使用 就必须传值为 value input。不能改变名字

:sync

之前针对input事件有个v-model的兼容
那么对于子组件的 非input事件就不能在通知父级改变同时兼有v-model的方式里吗?

索性,vue提供了 update:myPropName 的模式触发事件取而代之

  1. # 子组件
  2. this.$emit('update:title', newTitle)
  3. # 父组件
  4. <text-document
  5. v-bind:title="doc.title"
  6. v-on:update:title="doc.title = $event"
  7. ></text-document>
  8. # :sync 简写 ===> 类似 v-model。这里:title是传给子组件的propdoc.title是值
  9. <text-document :title.sync="doc.title"></text-document>

.sync 修饰符的 v-bind 不能和表达式一起使用v-bind:title.sync=”doc.title + ‘!’”
(怎么可能定义两个数据修改源~)
当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

这样会把 doc 对象中的每一个属性 (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。

插槽

插槽就是 开放了一个空间给子组件渲染东西,但是父组件仍具有控制子组件渲染的插槽内容大致在哪里的权力;

插槽于父组件而言就是一个 子组件的占位符,
于子组件就是 渲染东西在父组件里的出口

  1. # 组件使用者 决定了组件渲染的内容
  2. <alert-box>
  3. Something bad happened. # 这个内容会被释放到父组件的插槽内(如组件定义的有插槽的话
  4. </alert-box>
  5. # 组件定义
  6. Vue.component('alert-box', {
  7. template: `
  8. <div class="demo-alert-box">
  9. <strong>Error!</strong>
  10. <slot></slot> # 定义的slot决定了组件使用者渲染内容的出口
  11. </div>
  12. `
  13. })
  14. # 同时组件定义的相互配合 决定了slot渲染在组件的哪个部分

插槽提供了一个 除了prop以外 另一种向组件内部传数据的方式。即将数据写在组件开闭之间;然后组件内部使用slot来接住这个数据

  1. <navigation-link url="/profile">
  2. <!-- 添加一个 Font Awesome 图标 -->
  3. <span class="fa fa-user"></span>
  4. Your Profile
  5. </navigation-link>
  6. # 组件内部
  7. <a
  8. v-bind:href="url"
  9. class="nav-link"
  10. >
  11. <slot></slot>
  12. </a>
  13. # 如果组件内部没有包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。

插槽的默认值

为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染

  1. <button type="submit">
  2. <slot>Submit</slot> # submit是默认会被渲染的内容。如果组件使用者并没有在组件开闭中间传入值
  3. </button>

具名插槽

父:
子:

父组件

  1. <base-layout>
  2. <template v-slot:header>
  3. <h1>Here might be a page title</h1>
  4. </template>
  5. <template v-slot:default> v-slot:default 可以不写
  6. <p>A paragraph for the main content.</p>
  7. <p>And another one.</p>
  8. </template>
  9. <template v-slot:footer>
  10. <p>Here's some contact info</p>
  11. </template>
  12. </base-layout>

子组件

  1. <div class="container">
  2. <header>
  3. <slot name="header"></slot>
  4. </header>
  5. <main>
  6. <slot></slot> # 默认对应 default
  7. </main>
  8. <footer>
  9. <slot name="footer"></slot>
  10. </footer>
  11. </div>

插槽作用域

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的


  1. <navigation-link url="/profile">
  2. # 这个插槽里的内容是读不到父组件的prop 如此时的url
  3. Logged in as {{url}}
  4. </navigation-link>

组件使用者在插槽里读取子组件的数据

  1. # 绑定 插槽prop
  2. <current-user>
  3. <template v-slot:default="slotProps">
  4. {{ slotProps.user.firstName }}
  5. </template>
  6. </current-user>
  7. # 子组件传值给父组件
  8. <span>
  9. <slot v-bind:user="user">
  10. {{ user.lastName }}
  11. </slot>
  12. </span>

当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上:

  1. <current-user v-slot:default="slotProps">
  2. {{ slotProps.user.firstName }}
  3. </current-user>

但要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法

解构插槽

  1. <current-user v-slot="{ user }">
  2. {{ user.firstName }}
  3. </current-user>
  4. <current-user v-slot="{ user: person }">
  5. {{ person.firstName }}
  6. </current-user>
  7. 娄底写法
  8. <current-user v-slot="{ user = { firstName: 'Guest' } }">
  9. {{ user.firstName }}
  10. </current-user>

动态与异步组件

:is

在一个多标签的界面里,tab下面渲染界面的切换可以使用动态切换组件来实现
(特别是当tab里的内容过于复杂我们将每个内容都单独封装成了组件的时候)
此时,动态切换组件就十分有用了
image.png

  1. <!-- 组件会在 `currentTabComponent` 改变时改变 -->
  2. <component v-bind:is="currentTabComponent"></component>

currentTabComponent 可以包括 已注册组件的名字 or 一个组件的选项对象

is的坏处是 每次都是重新实例化组件。
实例化意味着会丢失组件本来的内部数据。比如tab下的一些浏览位置等

  1. <!-- 失活的组件将会被缓存!-->
  2. <keep-alive>
  3. <component v-bind:is="currentTabComponent"></component>
  4. </keep-alive>

异步组件

  1. Vue.component('async-example', function (resolve, reject) {
  2. setTimeout(function () {
  3. // `resolve` 回调传递组件定义
  4. resolve({
  5. template: '<div>I am async!</div>'
  6. })
  7. }, 1000)
  8. })
  9. # 将异步组件和 webpack code-splitting 功能一起配合使用
  10. Vue.component('async-webpack-example', function (resolve) {
  11. // 这个特殊的 `require` 语法将会告诉 webpack
  12. // 自动将你的构建代码切割成多个包,这些包
  13. // 会通过 Ajax 请求加载
  14. require(['./my-async-component'], resolve)
  15. })
  16. 简写
  17. Vue.component(
  18. 'async-webpack-example',
  19. // 这个 `import` 函数会返回一个 `Promise` 对象。
  20. () => import('./my-async-component')
  21. )
  22. 局部注册
  23. new Vue({
  24. // ...
  25. components: {
  26. 'my-component': () => import('./my-async-component')
  27. }
  28. })

深入细节

prop

如果在组件的prop里使用驼峰命名,那么在组件里传值就需要将驼峰命名传换为 连字;
(因为HTML 中的特性名是大小写不敏感的,浏览器会把所有大写字符解释为小写字符

  1. Vue.component('blog-post', {
  2. // 在 JavaScript 中是 camelCase 的
  3. props: ['postTitle'], #驼峰
  4. template: '<h3>{{ postTitle }}</h3>'
  5. })
  6. <!-- HTML 中是 kebab-case -->
  7. <blog-post post-title="hello!"></blog-post>

但 如果你使用字符串模板,那么这个限制就不存在了。

prop也可以接受类型校验,此时就不是数组传入数据了,而是对象

  1. props: {
  2. title: String,
  3. likes: Number,
  4. isPublished: Boolean,
  5. commentIds: Array,
  6. author: Object,
  7. callback: Function,
  8. contactsPromise: Promise // or any other constructor
  9. }
  10. # prop验证
  11. props: {
  12. // 基础的类型检查 (`null` `undefined` 会通过任何类型验证)
  13. propA: Number,
  14. // 多个可能的类型
  15. propB: [String, Number],
  16. // 必填的字符串
  17. propC: {
  18. type: String,
  19. required: true
  20. },
  21. // 带有默认值的数字
  22. propD: {
  23. type: Number,
  24. default: 100
  25. },
  26. }

注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的属性 (如 datacomputed 等) 在 defaultvalidator 函数中是不可用的。

类prop属性具有穿透性:

  1. <bootstrap-date-input data-date-picker="activated"></bootstrap-date-input>

比如实例组件上挂载里data-xxx属性,但是子组件并没有显式用prop去接,那么这个属性data-date-picker="activated" 特性就会自动添加到 <bootstrap-date-input> 的根元素上。

组件的循环引用

递归组件

组件可以自己调用自己,常用于 递归生成组件上,比如 tree组件。
前提是组件本身有 name属性

  1. Vue.component('todo-item', {
  2. // ...
  3. })
  4. export default {
  5. name: 'TodoItem',
  6. // ...
  7. }

组件之间的循环引用

如果组件之间存在相互循环引用的情况,比如 资源管理器;
类比 ul li 下面 又各自有 ul li
我们推荐的解决方式是:

  1. # 在本地注册组件的时候,你可以使用 webpack 的异步 import
  2. components: {
  3. TreeFolderContents: () => import('./tree-folder-contents.vue')
  4. }