事件处理
事件方法
<button @click="greet">Greet</button>
methods: {
greet: function (event) {# 此时默认函数第一个参数是原生的dom事件
// `this` 在方法里指向当前 Vue 实例
alert('Hello ' + this.name + '!')
// `event` 是原生 DOM 事件
if (event) {
alert(event.target.tagName)
}
}
}
在html里向方法传值
<button @click="say('what')">Say what</button>
methods: {
say: function (message) {
alert(message)
}
}
既传值又要接受event原生事件:用特殊变量 $event
把它传入方法
<button @click="warn('Form cannot be submitted yet.', $event)"> # $event
Submit
</button>
// ...
methods: {
warn: function (message, event) {
// 现在我们可以访问原生事件对象
if (event) event.preventDefault()
alert(message)
}
}
事件修饰符
在事件处理程序中调用 event.preventDefault()
或 event.stopPropagation()
是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
# before
<button @click="warn('Form cannot be submitted yet.', $event)"> # $event
Submit
</button>
// ...
methods: {
warn: function (message, event) {
if (event) event.stopPropagation()
}
}
# after
<!-- 阻止单击事件继续传播 -->
<a @click.stop="doThis"></a>
大大提升了开发体验,类似的事件修饰符有:
- .stop #阻止单击事件继续传播
- .prevent
- .capture
- .self
- .once
- .passive
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即元素自身触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div> ###!useful ####
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div> ###!useful ####
这部分建议 补充相关dom事件相关机制的知识
还有相关按键码
组件通信
父组件向子组件prop传值
组件使用者 传递给组件title数据(此时为静态数据)
<blog-post title="My journey with Vue"></blog-post>
Vue.component('blog-post', {
props: ['title'],// 子组件接受父组件的值
template: '<h3>{{ title }}</h3>'// 子组件的template 定义了子组件的样式
})
// 组件使用者
<blog-post
v-for="post in posts"
v-bind:key="post.id"
v-bind:post="post" -- 传递给组件的数据 post
/>
// 组件内部定义
Vue.component('blog-post', {
props: ['post'],
template: `
<div class="blog-post">
<h3>{{ post.title }}</h3>
<div v-html="post.content"></div>
</div>
`
})
warnning ⚠️
不应该在子组件里直接修改prop的值,因为一般是prop在父组件里改变之后 父级 prop 的更新会向下流动到子组件中。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。
如果子组件修改了父组件的prop,那么数据流就是一个闭环了,很可能陷入无限循环中;
(啥?子组件还可以修改父组件传来的值,从而父组件里的数据就改变了?
=-==》 因为,是引用传递。无关vue,是js基础
在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态。
有哪些场景子组件可能会有修改父组件传下的prop的值呢?
- prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的数据来使用
- prop 以一种原始的值传入且需要进行转换(加工)后显示在子组件里
不推荐直接修改prop然后展示修改后的prop,我们针对以上两个场景分别有如下解决方案
#1 子组件本地需要维护一个随时更新的变量,其中父组件的prop为初始值
--> 子组件内部维护一个初始值为prop的data
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
#2 prop只是数据加工=> computed
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
插槽传值
见 插槽
父组件监听子组件事件
有些时候我们需要在父组件里监听子组件的事件,然后让父组件统一修改子组件里共享的父组件的数据
即子组件里有共同的属性,我们就将这个属性提到父组件里统一管理,
一般情况是 父组件接受事件改变父组件里的属性,从而使得子组件里承接的父组件里的数据自动更新;
但是现在的情况是 子组件要接受事件 通知父组件 让父组件改变
===>
1.子组件监听事件,然后往上手动通知父组件,事件发生了===》 原生的事件系统里 有冒泡本可以解决
2.父组件监听子组件的事件==》子组件调用父组件方法
在vue里,子组件通知父组件事件:
$emit(‘xxxname’) xxxname是父组件的方法
# 子组件 触发父级别的自定义事件enlarge-text
<button @click="$emit('enlarge-text')">
Enlarge text
</button>
# 父组件
<blog-post
...
@enlarge-text="changeFontfunc"
></blog-post>
除了自定义事件有两种写法,比如子组件click触发通知父组件;
1.父组件 @on-click
2.父组件
子组件都是通过emit触发通知父组件
<template>
<button @click="handleClick">
<slot></slot>
</button>
</template>
<script>
export default {
methods: {
handleClick (event) {
this.$emit('on-click', event);
}
}
}
</script>
子组件不仅是调用父组件方法,还要向父组件传值
<button v-on:click="$emit('enlarge-text', 0.1)">
Enlarge text
</button>
# 父
<blog-post
...
v-on:enlarge-text="postFontSize += $event" # 直接$event接受这个值
></blog-post>
或者 作为第一个数传入函数方法
<blog-post
...
v-on:enlarge-text="onEnlargeText"
></blog-post>
methods: {
onEnlargeText: function (enlargeAmount) {
this.postFontSize += enlargeAmount
}
}
等等。如果子组件是input非受控组件,那么能否在父组件上使用v-model使得 父组件的改变 子组件的input也改变呢?
即子组件的input的值的管理能力 是来自 父组件,但子组件作为用户的第一接触者,需要响应用户的改变从而更新父组件的值.
即 子组件v-model下的数据双向绑定
# 父组件
<custom-input v-model="searchText"></custom-input>
# 子组件内部
<input >
想要子组件接受父组件的值,同时响应用户的交互,从而更新父组件的值。
此时应该怎么写子组件?
===》
Vue.component('custom-input', {
props: ['value'],
template: `
<input
:value="value"
@input="$emit('input', $event.target.value)"
>
`
})
因为v-model其实是两个数据处理结合的语法糖
<input v-model="searchText">
==> 等价于
<input
:value="searchText"
@input="searchText = $event.target.value"
>
换在组件上就是:
<custom-input
:value="searchText"
@input="searchText = $event" $event接受子组件的值 即子组件向父组件传$event.target.value
></custom-input>
# 此时的value是传给子组件的prop 也可以换成 :searchText
# 同样 input也可以换成其他事件方法
<custom-input
:searchText="searchText"
@inputClick="searchText = $event" $event接受子组件的值 即子组件向父组件传$event.target.value
></custom-input>
===> 子组件
Vue.component('custom-input', {
props: ['searchText'],
template: `
<input
:value="searchText"
@input="$emit('inputClick', $event.target.value)"
>
`
})
但是如果要保持父组件是用v-model的方式使用 就必须传值为 value 和 input。不能改变名字
:sync
之前针对input事件有个v-model的兼容
那么对于子组件的 非input事件就不能在通知父级改变同时兼有v-model的方式里吗?
索性,vue提供了 update:myPropName
的模式触发事件取而代之
# 子组件
this.$emit('update:title', newTitle)
# 父组件
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
# :sync 简写 ===> 类似 v-model。这里:title是传给子组件的prop。doc.title是值
<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
监听器。
插槽
插槽就是 开放了一个空间给子组件渲染东西,但是父组件仍具有控制子组件渲染的插槽内容大致在哪里的权力;
插槽于父组件而言就是一个 子组件的占位符,
于子组件就是 渲染东西在父组件里的出口
# 组件使用者 决定了组件渲染的内容
<alert-box>
Something bad happened. # 这个内容会被释放到父组件的插槽内(如组件定义的有插槽的话
</alert-box>
# 组件定义
Vue.component('alert-box', {
template: `
<div class="demo-alert-box">
<strong>Error!</strong>
<slot></slot> # 定义的slot决定了组件使用者渲染内容的出口
</div>
`
})
# 同时组件定义的相互配合 决定了slot渲染在组件的哪个部分
插槽提供了一个 除了prop以外 另一种向组件内部传数据的方式。即将数据写在组件开闭之间;然后组件内部使用slot来接住这个数据
<navigation-link url="/profile">
<!-- 添加一个 Font Awesome 图标 -->
<span class="fa fa-user"></span>
Your Profile
</navigation-link>
# 组件内部
<a
v-bind:href="url"
class="nav-link"
>
<slot></slot>
</a>
# 如果组件内部没有包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。
插槽的默认值
为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染
<button type="submit">
<slot>Submit</slot> # submit是默认会被渲染的内容。如果组件使用者并没有在组件开闭中间传入值
</button>
具名插槽
父: xxxx
子:
父组件
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<template v-slot:default> v-slot:default 可以不写
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
子组件
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> # 默认对应 default
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
插槽作用域
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的
<navigation-link url="/profile">
# 这个插槽里的内容是读不到父组件的prop 如此时的url
Logged in as {{url}}
</navigation-link>
组件使用者在插槽里读取子组件的数据
# 绑定 插槽prop
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
# 子组件传值给父组件
<span>
<slot v-bind:user="user">
{{ user.lastName }}
</slot>
</span>
当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot
直接用在组件上:
<current-user v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</current-user>
但要出现多个插槽,请始终为所有的插槽使用完整的基于 <template>
的语法
解构插槽
<current-user v-slot="{ user }">
{{ user.firstName }}
</current-user>
<current-user v-slot="{ user: person }">
{{ person.firstName }}
</current-user>
娄底写法
<current-user v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
</current-user>
动态与异步组件
:is
在一个多标签的界面里,tab下面渲染界面的切换可以使用动态切换组件来实现
(特别是当tab里的内容过于复杂我们将每个内容都单独封装成了组件的时候)
此时,动态切换组件就十分有用了
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>
currentTabComponent
可以包括 已注册组件的名字 or 一个组件的选项对象
is的坏处是 每次都是重新实例化组件。
实例化意味着会丢失组件本来的内部数据。比如tab下的一些浏览位置等
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
异步组件
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
# 将异步组件和 webpack 的 code-splitting 功能一起配合使用
Vue.component('async-webpack-example', function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(['./my-async-component'], resolve)
})
简写
Vue.component(
'async-webpack-example',
// 这个 `import` 函数会返回一个 `Promise` 对象。
() => import('./my-async-component')
)
局部注册
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component')
}
})
深入细节
prop
如果在组件的prop里使用驼峰命名,那么在组件里传值就需要将驼峰命名传换为 连字;
(因为HTML 中的特性名是大小写不敏感的,浏览器会把所有大写字符解释为小写字符
Vue.component('blog-post', {
// 在 JavaScript 中是 camelCase 的
props: ['postTitle'], #驼峰
template: '<h3>{{ postTitle }}</h3>'
})
<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>
但 如果你使用字符串模板,那么这个限制就不存在了。
prop也可以接受类型校验,此时就不是数组传入数据了,而是对象
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // or any other constructor
}
# prop验证
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
}
注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的属性 (如
data
、computed
等) 在default
或validator
函数中是不可用的。
类prop属性具有穿透性:
<bootstrap-date-input data-date-picker="activated"></bootstrap-date-input>
比如实例组件上挂载里data-xxx属性,但是子组件并没有显式用prop去接,那么这个属性data-date-picker="activated"
特性就会自动添加到 <bootstrap-date-input>
的根元素上。
组件的循环引用
递归组件
组件可以自己调用自己,常用于 递归生成组件上,比如 tree组件。
前提是组件本身有 name属性
Vue.component('todo-item', {
// ...
})
export default {
name: 'TodoItem',
// ...
}
组件之间的循环引用
如果组件之间存在相互循环引用的情况,比如 资源管理器;
类比 ul li 下面 又各自有 ul li
我们推荐的解决方式是:
# 在本地注册组件的时候,你可以使用 webpack 的异步 import:
components: {
TreeFolderContents: () => import('./tree-folder-contents.vue')
}