组件是可复用的Vue实例,且带有一个名字,例如名字为my-cmp,那么我们则可以在一个通过new Vue创建的根实例中,把这个组件作为自定义元素来使用:
<div id="app">
<my-cmp>my-cmp>
div>
const vm = new Vue({
el: '#app'
})
因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 data、computed、watch、methods 以及生命周期钩子等。仅有的例外是像 el 这样根实例特有的选项。
Vue.component
利用Vue.component创建的组件组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中。
参数:
用法:
注册或获取全局组件。注册还会自动使用给定的id设置组件的名称。
示例:
<div id="app">
<button-counter>button-counter>
div>
Vue.component('button-counter', {
data () {
return {
count: 0,
}
},
template: `
`
})
const vm = new Vue({
el: '#app',
})
在components选项中定义要使用的组件。
对于 components 对象中的每一个属性来说,其属性名就是自定义元素的名字,其属性值就是这个组件的选项对象。
示例:
<div id="#app">
<button-counter>button-counter>
div>
const buttonCounter = {
data () {
return {
count: 0
}
},
template: `
`,
}
const vm = new Vue({
el: '#app',
components: {
'button-counter': buttonCounter
}
})
在注册一个组件的时候,我们始终需要给它一个名字。你给予组件的名字可能依赖于你打算拿它来做什么,所以命名要语义化。
组件名大小写
定义组件名的方式有两种:
使用kebab-case (短横线分隔命名)
Vue.component('my-component', {/***/});
当使用kebab-case定义一个组件时,你必须在引用这个自定义元素时使用kebab-case,例如:
。
使用PascalCase (大驼峰命名)
Vue.component('MyComponent', {/***/});
当使用PascalCase定义一个组件时,你在引用这个自定义元素时两种命名法都可以。也就是说
和
都是可接受的。注意,尽管如此,直接在 DOM (即字符串模板或单文件组件) 中使用时只有 kebab-case 是有效的。
另:我们强烈推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符
)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。
可以将组件进行任意次数的复用:
<div id="#app">
<button-counter>button-counter>
<button-counter>button-counter>
<button-counter>button-counter>
div>
在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的——但在 DOM 模板里永远不要这样做。
自闭合组件表示它们不仅没有内容,而且刻意没有内容。其不同之处就好像书上的一页白纸对比贴有“本页有意留白”标签的白纸。而且没有了额外的闭合标签,你的代码也更简洁。
不幸的是,HTML 并不支持自闭合的自定义元素——只有官方的“空”元素。所以上述策略仅适用于进入 DOM 之前 Vue 的模板编译器能够触达的地方,然后再产出符合 DOM 规范的 HTML。
当我们定义一个组件时,它的 data 并不是像这样直接提供一个对象:
data: {
count: 0
}
取而代之的是,一个组件的 data 选项必须是一个函数
,因此每个实例可以维护一份被返回对象的独立的拷贝:
data () {
return {
count: 0
}
}
如果 Vue 没有这条规则,点击一个按钮就可能会像下面一样影响到其它所有实例:
每个组件必须只有一个根元素,当模板的元素大于1时,可以将模板的内容包裹在一个父元素内。
组件默认只是写好结构、样式和行为,使用的数据应由外界传递给组件。
如何传递?注册需要接收的prop,将数据作为一个自定义特性传递给组件。
如:
<div id="app">
<video-item
title="羊村摇"
poster="https://developer.duyiedu.com/bz/video/955bac93ccb7f240d25a79b2ff6a9fdbda9537bc.jpg@320w_200h.webp"
play="638000"
rank="1207"
>video-item>
div>
Vue.component('video-item', {
props: ['title', 'poster', 'play', 'rank'],
})
在上述模板中,你会发现我们能够在组件实例中访问这个值,就像访问 data 中的值一样:
<div id="app">
<video-item
title="羊村摇"
poster="https://developer.duyiedu.com/bz/video/955bac93ccb7f240d25a79b2ff6a9fdbda9537bc.jpg@320w_200h.webp"
play="638000"
rank="1207"
>video-item>
div>
Vue.component('video-item', {
props: ['title', 'poster', 'play', 'rank'],
template: `{{ title }}`
})
HTML 中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。故:当传递的prop为 短横线分隔命名时,组件内的props 应为驼峰命名 。
如:
<div id="app">
<video-item sub-title="hello!">video-item>
div>
Vue.component('video-item', {
// 在 JavaScript 中是 subTitle
props: ['subTitle'],
template: '{{ subTitle }}
'
})
要注意的是:如果使用的是字符串模板,那么这个限制就不存在了。
像这样,我们已经知道了可以给 prop 传入一个静态的值:
<video-item title="羊村摇">video-item>
若想要传递一个动态的值,可以配合v-bind指令进行传递,如:
<video-item :title="title">video-item>
如果你想要将一个对象的所有属性都作为 prop 传入,你可以使用不带参数的 v-bind 。例如,对于一个给定的对象 person:
person: {
name: 'shanshan',
age: 18
}
传递全部属性:
<my-component v-bind="person">my-component>
上述代码等价于:
<my-component
:name="person.name"
:age="person.age"
>my-component>
我们可以为组件的 prop 指定验证要求,例如你可以要求一个 prop 的类型为什么。如果说需求没有被满足的话,那么Vue会在浏览器控制台中进行警告,这在开发一个会被别人用到的组件时非常的有帮助。
为了定制 prop 的验证方式,你可以为 props 中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:
Vue.component('my-component', {
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise
}
})
上述代码中,对prop进行了基础的类型检查,类型值可以为下列原生构造函数中的一种:String
、Number
、Boolean
、Array
、Object
、Date
、Function
、Symbol
、任何自定义构造函数、或上述内容组成的数组。
需要注意的是null
和 undefined
会通过任何类型验证。
除基础类型检查外,我们还可以配置高级选项,对prop进行其他验证,如:类型检测、自定义验证和设置默认值。
如:
Vue.component('my-component', {
props: {
title: {
type: String, // 检查 prop 是否为给定的类型
default: '杉杉最美', // 为该 prop 指定一个默认值,对象或数组的默认值必须从一个工厂函数返回,如:default () { return {a: 1, b: 10} },
required: true, // 定义该 prop 是否是必填项
validator (prop) { // 自定义验证函数,该prop的值回作为唯一的参数代入,若函数返回一个falsy的值,那么就代表验证失败
return prop.length < 140;
}
}
}
})
为了更好的团队合作,在提交的代码中,prop 的定义应该尽量详细,至少需要指定其类型。
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
这里有两种常见的试图改变一个 prop 的情形:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
非Prop特性指的是,一个未被组件注册的特性。当组件接收了一个非Prop特性时,该特性会被添加到这个组件的根元素上。
想象一下
的模板是这样的:
<input type="date" class="b">
为了给我们的日期选择器插件定制一个主题,我们可能需要像这样添加一个特别的类名:
<my-cmp
class="my-cmp"
>my-cmp>
在这种情况下,我们定义了两个不同的 class 的值:
对于绝大多数特性来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果在my-cmp元素上传入 type=“text” 就会替换掉 type=“date” 并把它破坏!庆幸的是,class 和 style 特性会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:my-cmp b。
如果不希望组件的根元素继承特性,那么可以在组件选项中设置 inheritAttrs: false
。如:
Vue.component('my-cmp', {
inheritAttrs: false,
// ...
})
在这种情况下,非常适合去配合实例的 $attrs 属性使用,这个属性是一个对象,键名为传递的特性名,键值为传递特性值。
{
required: true,
placeholder: 'Enter your username'
}
使用 inheritAttrs: false
和 $attrs
相互配合,我们就可以手动决定这些特性会被赋予哪个元素。如:
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
template: `
`,
})
注意:inheritAttrs: false 选项不会影响 style 和 class 的绑定。
首先,我们来写一个博文组件,如:
Vue.component('blog-post', {
props: {
post: {
type: Object,
}
},
template: `
{{ post.title }}
{{ post.content }}
`,
})
<div id="app">
<div :style="{fontSize: postFontSize + 'em'}">
<blog-post
v-for="post in posts"
:key="post.id"
:post="post"
>
blog-post>
div>
div>
const vm = new Vue({
el: '#app',
data: {
posts: [
{ title: '标题1', content: '正文内容', id: 0, },
{ title: '标题2', content: '正文内容', id: 1, },
{ title: '标题3', content: '正文内容', id: 2, },
],
postFontSize: 1
}
})
可以看到每一个博文组件中,都有一个按钮,可以去放大页面中字体的字号,也就是说,当点击这个按钮时,我们要告诉父组件改变postFontSize
数据去放大所有博文的文本。碰见这样的情况,该如何做呢?
Vue 实例提供了一个自定义事件来解决这个问题。父组件可以像处理原生DOM元素一样,通过 v-on
指令,监听子组件实例的任意事件,如:
<div id="app">
<div :style="{fontSize: postFontSize + 'em'}">
<blog-post
...
@enlarge-text="postFontSize += 0.1"
>
blog-post>
div>
div>
那么,怎么样能够去监听到一个 enlarge-text
这么奇怪的事件呢?这就需要在组件内,去主动触发一个自定义事件了。
如何触发?
通过调用 $emit 方法 并传入事件名称来触发一个事件,如:
Vue.component('blog-post', {
props: {
...
},
template: `
...
...
`,
})
这样,父组件就可以接收该事件,更新数据 pageFontSize
的值了。
在有些情况下,我们可能想让
组件决定它的文本要放大多少。这是可以使用 $emit 的第二个参数来提供这个值,如:
Vue.component('blog-post', {
props: {
...
},
template: `
...
...
`,
})
在父组件监听这个事件时,可以通过 $event 访问到被抛出的这个值:
<div id="app">
<div :style="{fontSize: postFontSize + 'em'}">
<blog-post
...
@enlarge-text="postFontSize += $event"
>
blog-post>
div>
div>
或者,将这个事件处理函数写成一个方法:
<div id="app">
<div :style="{fontSize: postFontSize + 'em'}">
<blog-post
...
@enlarge-text="onEnlargeText"
>
blog-post>
div>
div>
那么,这个值将会作为第一个参数,传入这个方法:
methods: {
onEnlargeText: function (enlargeAmount) {
this.postFontSize += enlargeAmount
}
}
不同于组件和prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所有的名称。如果触发一个camelCase名字的事件:
this.$emit('myEvent')
则监听这个名字的kabab-case版本是不会有任何效果的。
<my-component v-on:my-event="doSomething">my-component>
与组件和prop不同的是,事件名不会被当作一个 JS 变量名或者属性名,所以就没有理由使用camelCase 或 PascalCase 了。
并且 v-on 事件监听器在 DOM 模板中会被自动转换为全小写,所以 @myEvent 将会变成 @myevent,导致 myEvent 不可能被监听到。
因此,推荐始终使用 kebab-case 的事件名。
在组件上去监听事件时,我们监听的是组件的自动触发的自定义事件,但是在一些情況下,我们可能想要在一个组件的根元素上直接监听一个原生事件。这是,可以使用 v-on 指令的 .native 修饰符,如:
<base-input @focus.native="onFocus" @blur.native="onBlur">base-input>
Vue.component('base-input', {
template: `
`
})
这样处理,在有些时候是很有用的,不过在尝试监听一个类似元素时,这并不是一个好主意,例如
组件可能做了重构,如:
<label>
姓名:
<input type="text">
label>
可以看到,此时组件的根元素实际上是一个元素,那么父级的.native监听器将静默失败。它不会产生任何报错,但是onFocus
处理函数不会如预期被调用。
为了解决这个问题,Vue提供了一个$listeners属性,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:
{
focus: function (event) { /* ... */ }
blur: function (event) { /* ... */ },
}
有了这个 $listeners 属性,我们可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素,如:
Vue.component('base-input', {
template: `
`
})
由于自定义事件的出现,在组件上也可以使用v-model指令。
在 input 元素上使用v-mode指令时,相当于绑定了value特性以及监听了input事件:
<input v-model="searchText" />
等价于:
<input
:value="searchText"
@input="searchText = $event.target.value"
>
当把v-model指令用在组件上时:
<base-input v-model="searchText" />
则等价于:
<base-input
:value="searchText"
@input="searchText = $event"
/>
同 input 元素一样,在组件上使用v-model指令,也是绑定了value特性,监听了input事件。
所以,为了让 v-model 指令正常工作,这个组件内的必须:
Vue.component('base-input', {
props: ['value'],
template: `
`
})
这样操作后,v-model就可以在这个组件上工作起来了。
通过上面的学习,我们知道了,一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value 特性用于不同的目的。碰到这样的情况,我们可以利用 model 选项来避免冲突:
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
`
})
使用组件:
<base-checkbox v-model="lovingVue">base-checkbox>
这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的属性将会被更新。
除了使用 v-model 指令实现组件与外部数据的双向绑定外,我们还可以用 v-bind 指令的修饰符 .sync 来实现。
那么,该如何实现呢?
先回忆一下,不利用 v-model 指令来实现组件的双向数据绑定:
<base-input :value="searchText" @input="searchText = $event">base-input>
Vue.component('base-input', {
props: ['value'],
template: `
`
})
那么,我们也可以试着,将监听的事件名进行更改,如:
<base-input :value="searchText" @update:value="searchText = $event">base-input>
Vue.component('base-input', {
props: ['value'],
template: `
`
})
这样也是可以实现双向数据绑定的,那么和 .sync 修饰符 有什么关系呢?
此时,我们对代码进行修改:
<base-input :value.sync="searchText">base-input>
Vue.component('base-input', {
props: ['value'],
template: `
`
})
所以,.sync 修饰符 本质上也是一个语法糖,在组件上使用:
<base-input :value.sync="searchText">base-input>
等价于:
<base-input
:value="searchText"
@update:value="searchText = $event"
/>
当我们用一个对象同时设置多个prop时,也可以将.sync修饰符和 v-bind配合使用:
<base-input v-bind.sync="obj">base-input>
注意:
:title.sync="1+1"
,这样操作是无效的v-bind.sync
用在 一个字面量对象上,如 v-bind.sync="{ title: 'haha' }"
,是无法工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。先明确一件事情,在 vue 1.x 时,就已经支持 .sync 语法,但是此时的 .sync 可以完全在子组件中修改父组件的状态,造成整个状态的变换很难追溯,所以官方在2.0时移除了这个特性。然后在 vue2.3时,.sync又回归了,跟以往不同的是,现在的.sync完完全全就是一个语法糖的作用,跟v-model的实现原理是一样的,也不容易破环院有的数据模型,所以使用上更安全也更方便。
prop
+ 事件
来达成目的。和 HTML 元素一样,我们经常需要向一个组件传递内容,像这样:
<my-cmp>
Something bad happened.
my-cmp>
如果有这样的需求,我们就可以通过插槽来做。
通过插槽,我们可以这样合成组件:
<my-cmp>
写在组件标签结构中的内容
my-cmp>
组件模板中可以写成:
<div>
<slot>slot>
div>
当组件渲染时,
将会被替换为“写在组件标签结构中的内容”。
插槽内可以包含任何模板代码,包括HTML和其他组件。
如果
没有包含
元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。
当在插槽中使用数据时:
<my-cmp>
这是插槽中使用的数据:{{ user }}
my-cmp>
该插槽跟模板的其他地方一样可以访问相同的实例属性,也就是相同的“作用域”,而不能访问
的作用域。
请记住:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
我们可以设置默认插槽,它会在没有提供内容时被渲染,如,在
组件中:
Vue.compopnent('my-cmp', {
template: `
`
})
我们希望这个内绝大多数情况下都渲染文本“Submit”,此时就可以将“Submit”作为后备内容,如:
Vue.compopnent('my-cmp', {
template: `
`
})
当使用组件未提供插槽时,后备内容将会被渲染。如果提供插槽,则后备内容将会被取代。
有时我们需要多个插槽,如
组件:
Vue.compopnent('my-cmp', {
template: `
`
})
此时,可以在
元素上使用一个特殊的特性:name。利用这个特性定义额外的插槽:
Vue.compopnent('my-cmp', {
template: `
`
})
一个不带 name 的
出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
<my-cmp>
<template v-slot:header>
<h1>头部h1>
template>
<p>内容p>
<p>内容p>
<template v-slot:footer>
<p>底部p>
template>
my-cmp>
现在元素中的所有内容都会被传入相应的插槽。任何没有被包裹在带有
v-slot
的中的内容都会被视为默认插槽的内容。
为了模板更清晰,也可以写成以下这样:
<my-cmp>
<template v-slot:header>
<h1>头部h1>
template>
<template v-slot:default>
<p>内容p>
<p>内容p>
template>
<template v-slot:footer>
<p>底部p>
template>
my-cmp>
注意:v-slot只能添加在上,只有一种例外情况。
为了能够让插槽内容访问子组件的数据,我们可以将子组件的数据作为
元素的一个特性绑定上去:
Vue.component('my-cmp', {
data () {
return {
user: {
name: '杉杉',
age: 18,
}
}
},
template: `
`,
})
绑定在
元素上的特性被称为插槽 prop。
那么在父级作用域中,我们可以给v-slot
带一个值来定义我们提供的插槽prop的名字:
<div id="app">
<my-cmp>
<template v-slot:default="slotProps">
{{ slotProps.user.name }}
template>
my-cmp>
div>
当被提供的内容只有默认插槽时,组件的标签可以被当作插槽的模板来使用,此时,可以将v-slot
直接用在组件上:
<my-cmp v-slot:default="slotProps">
{{ slotProps.user.name }}
my-cmp>
也可以更简单:
<my-cmp v-slot="slotProps">
{{ slotProps.user.name }}
my-cmp>
注意:默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确。
<my-cmp v-slot="slotProps">
{{ slotProps.user.name }}
<template v-slot:other="otherSlotProps">
slotProps 在这里是不合法的
template>
my-cmp>
只要出现多个插槽,就需要为所有的插槽使用完整的基于的语法。
我们可以使用解构传入具体的插槽prop,如:
<my-cmp v-slot="{ user }">
{{ user.name }}
my-cmp>
这样模板会更简洁,尤其是在为插槽提供了多个prop时。
此外还可以有其他可能,如prop重命名:
<my-cmp v-slot="{ user: person }">
{{ person.name }}
my-cmp>
以及自定义后备内容,当插槽prop是undefined时生效:
<my-cmp v-slot="{ user = { name: 'Guest' } }">
{{ user.name }}
my-cmp>
Vue 2.6.0新增
<my-cmp>
<template v-slot:[dynamicSlotName]>
...
template>
my-cmp>
Vue 2.6.0新增
跟v-on
和v-bind
一样,v-slot
也有缩写,将v-slot:
替换为#
。
<my-cmp>
<template #header>
<h1>头部h1>
template>
<template #default>
<p>内容p>
<p>内容p>
template>
<template #footer>
<p>底部p>
template>
my-cmp>
当然,和其它指令一样,该缩写只在其有参数的时候才可用。
自 2.6.0 起被废弃
<my-cmp>
<template slot="header">
<h1>头部h1>
template>
<template>
<p>内容p>
<p>内容p>
template>
<template slot="footer">
<p>底部p>
template>
my-cmp>
自 2.6.0 起被废弃
<my-cmp>
<template slot="default" slot-scope="slotProps">
{{ slotProps.user.name }}
template>
my-cmp>
当我们在一个多标签的界面中,在不同组件之间进行动态切换是非常有用的。
<div id="app">
<button
v-for="page in pages"
@click="pageCmp = page.cmp"
:key="page.id"
>{{ page.name }}button>
<component :is="pageCmp">component>
div>
Vue.component('base-post', {
data () {
return {
postCmp: '',
posts: [
{ title: "标题1", content: { template: `内容1`}, id: 11},
{ title: "标题2", content: { template: `内容2`}, id: 12},
{ title: "标题3", content: { template: `内容3`}, id: 13},
],
}
},
mounted () {
this.postCmp = this.posts[0].content;
},
template: `
`
})
Vue.component('base-more', {
template: `更多内容`
})
const vm = new Vue({
el: '#app',
data: {
pages: [
{ name: '博客', cmp: 'base-post', id: 0},
{ name: '更多', cmp: 'base-more', id: 1}
],
pageCmp: 'base-post'
},
})
通过上面方法,我们可以实现组件间的切换,能够注意到的是:每一次切换标签时,都会创建一个新的组件实例,重新创建动态组件在更多情况下是非常有用的,但是在这个案例中,我们会更希望哪些标签的组件实例能够在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个
元素将动态组件包裹起来。如:
<keep-alive>
<component v-bind:is="pageCmp">component>
keep-alive>
注意:
要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册。
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
当组件在
内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。
activated:keep-alive 组件激活时调用。
deactivated: keep-alive 组件停用时调用。
接下来我们要学习的都是和处理边界情况有关的功能,即一些需要对 Vue 的规则做一些小调整的特殊情况。需要注意的是,这些功能都是有劣势或危险场景的。
在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 DOM 元素。不过也确实在一些情况下做这些事情是合适的。
在每个子组件中,可以通过 $root 访问根实例。
// Vue 根实例
new Vue({
data: {
foo: 1
},
computed: {
bar () { /* ... */ }
},
methods: {
baz () { /* ... */ }
}
})
所有的子组件都可以将这个实例作为一个全局 store 来访问或使用。
// 获取根组件的数据
this.$root.foo
// 写入根组件的数据
this.$root.foo = 2
// 访问根组件的计算属性
this.$root.bar
// 调用根组件的方法
this.$root.baz()
在demo或在有少量组件的小型应用中使用是非常方便的。但是在大型应用里使用就会很复杂了。所以,我们还是要用Vuex(后面会学)来管理应用的状态。
在子组件中,可以通过 $parent 访问 父组件实例。这可以替代将数据以prop的方式传入子组件的方式。
如:
<cmp-parent>
<cmp-a>cmp-a>
cmp-parent>
若 cmp-parent 需要共享一个属性 share,它的所有子元素都需要访问 share 属性,在这种情况下 cmp-a 可以通过 this.$parent.share的方式访问share。
但是,通过这种模式构建出来的组件内部仍然容易出现问题。比如,我们在cmp-a 中嵌套一个一个子组件 cmp-b,如:
<cmp-parent>
<cmp-a>
<cmp-b>cmp-b>
cmp-a>
cmp-parent>
那么,在cmp-b组件中去访问share时,需要先查看一下,其父组件中是否存在share,如果不存在,则在向上一级查找,落实到代码上为:
var share = this.$parent.share || this.$parent.$parent.share;
这样做,很快组件就会失控:触达父级组件会使应用更难调试和理解,尤其是当变更父组件数据时,过一段时间后,很难找出变更是从哪里发起的。
碰到上述情况,可以使用依赖注入解决。
在上面的例子中,利用 $parent 属性,没有办法很好的扩展到更深层级的嵌套组件上。这也是依赖注入的用武之地,它用到了两个新的实例选项:provide 和 inject。
provide 选项允许我们指定想要提供给后代组件的数据/方法,例如:
Vue.component('cmp-parent', {
provide () {
return {
share: this.share,
}
},
data () {
return {
share: 'share',
}
},
template: `cmp-parent`
})
然后再任何后代组件中,我们都可以使用 inject 选项来接受指定想要添加在实例上的属性。
Vue.component('cmp-a', {
inject: ['share'],
template: `cmp-a`
})
相比 $parent 来说,这个用法可以让我们在任意后代组件中访问share,而不需要暴露整个 cmp-parent 实例。这允许我们更好的持续研发该组件,而不需要担心我们可能会改变/移除一些子组件依赖的东西。同时这些组件之间的接口是始终明确定义的,就和 props 一样。
实际上,你可以把依赖注入看作一部分“大范围有效的 prop”,除了:
然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 Vuex 这样真正的状态管理方案了。
尽管存在prop和事件,但是有时候我们仍可能需要在JS里直接访问一个子组件,那么此时,我们可以通过 ref 特性为子组件赋予一个ID引用:
<my-cmp ref="cmp">my-cmp>
这样就可以通过this.$refs.cmp 来访问
实例。
ref 也可以 对指定DOM元素进行访问,如:
<input ref="input" />
那么,我们可以通过 this.$refs.input 来访问到该DOM元素。
当ref 和 v-for 一起使用时,得到的引用将会是一个包含了对应数据源的这些子组件的数组。
注意:$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。应该避免在模板或计算属性中访问 $refs。
除了 v-on 和 $emit 外, Vue 实例在其事件接口中还提供了其它的方法。我们可以:
这几个方法一般不会被用到,但是,当需要在一个组件实例上手动侦听事件时,他们是可以派的上用场的。
例如,有时我们会在组件中集成第三方库:
Vue.component('my-cmp', {
// 一次性将这个日期选择器附加到一个输入框上
// 它会被挂载到 DOM 上。
mounted () {
// Pikaday 是一个第三方日期选择器的库
this.picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD',
})
},
// 在组件被销毁之前,
// 也销毁这个日期选择器。
beforeDestroy () {
this.picked.destroy();
},
template: `
`,
})
使用上面的方法,有两个潜在的问题:
所有,我们可以通过程序化的侦听器解决这两个问题:
Vue.component('my-cmp', {
mounted () {
var picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD',
})
this.$once('hook:beforeDestroy', () => {
picker.destroy();
})
},
template: `
`
})
使用了这个策略,我们还可以让多个输入框元素使用不同的pikaday:
Vue.component('my-cmp', {
mounted () {
this.datePicker('inputA');
this.datePicker('inputB');
},
methods: {
datePicker (refName) {
var picker = new Pikaday({
field: this.$refs[refName],
format: 'YYYY-MM-DD',
})
this.$once('hook:beforeDestroy', () => {
picker.destroy();
})
},
},
template: `
`
})
注意,即便如此,如果你发现自己不得不在单个组件里做很多建立和清理的工作,最好的方式通常还是创建更多的模块化组件,在这个例子中,我们推荐创建一个可复用的
组件。
组件是可以在它们自己的模板中调用自身的,不过它们只能通过name选项来做这件事:
name: 'my-cmp'
不过当使用 Vue.component 全局注册一个组件时,全局的ID会自动设置为该组件的 name 选项。
Vue.component('my-cmp', { /** */});
稍有不慎,递归组件就可能导致无限循环:
name: 'my-cmp',
template: ` `
类似上述的组件将会导致“max stack size exceeded”错误,所以要确保递归调用是条件性的 (例如使用一个最终会得到 false 的 v-if)。
有时,在去构建一些组件时,会出现组件互为对方的后代/祖先:
Vue.component('cmp-a', {
template: `
`
})
Vue.component('cmp-b', {
template: `
`
})
此时,我们使用的是全局注册组件,并不会出现悖论,但是如果使用的为局部组件就会出现悖论。
但是即使用了全局注册组件,在使用webpack去导入组件时,也会出现一个错误:Failed to mount component: template or render function not defined。
模块系统发现它需要 A,但是首先 A 依赖 B,但是 B 又依赖 A,但是 A 又依赖 B,如此往复。这变成了一个循环,不知道如何不经过其中一个组件而完全解析出另一个组件。为了解决这个问题,我们需要给模块系统一个点:“A 反正是需要 B 的,但是我们不需要先解析 B。”
beforeCreate () {
this.$options.components.CmpB = require('./tree-folder-contents.vue').default;
}
或者,在本地注册组件的时候,你可以使用 webpack 的异步 import:
components: {
CmpB: () => import('./tree-folder-contents.vue')
}
在使用组件时,写上特殊的特性:inline-template,就可以直接将里面的内容作为模板而不是被分发的内容(插槽)。
<my-cmp inline-template>
<div>
<p>These are compiled as the component's own template.p>
<p>Not parent's transclusion content.p>
div>
my-cmp>
不过,inline-template 会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 元素来定义模板。
另一个定义模板的方式是在一个 元素中,并为其带上 text/x-template 的类型,然后通过一个 id 将模板引用过去。例如:
<script
type="text/x-template"
id="hello-world-template"
>
<p>Hello hello hello</p>
script>
Vue.component('hello-world', {
template: '#hello-world-template'
})
这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。
当 更改了某个数据,页面未重新渲染时,可以调用 $forceUpdate 来做一次强制更新。
但是在做强制更新前,需要留意数组或对象的变更检测注意事项,99.9%的情况,都是在某个地方做错了事,如果做了上述检查,仍未发现问题,那么可以通过 $forceUpdate来更新。
渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once 特性以确保这些内容只计算一次然后缓存起来,就像这样:
Vue.component('terms-of-service', {
template: `
Terms of Service
... a lot of static content ...
`
})
试着不要过度使用这个模式。当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。
父组件传递数据给子组件时,可以通过特性传递。
推荐使用这种方式进行父->子通信。
子组件传递数据给父组件时,触发事件,从而抛出数据。
推荐使用这种方式进行子->父通信。
祖先组件传递数据给子孙组件时,可以利用$attrs传递。
demo或小型项目可以使用$attrs进行数据传递,中大型项目不推荐,数据流会变的难于理解。
$attrs的真正目的是撰写基础组件,将非Prop特性赋予某些DOM元素。
可以在子孙组件中执行祖先组件的函数,从而实现数据传递。
demo或小型项目可以使用$listeners进行数据传递,中大型项目不推荐,数据流会变的难于理解。
$listeners的真正目的是将所有的事件监听器指向这个组件的某个特定的子元素。
可以在子组件中访问根实例的数据。
对于 demo 或非常小型的有少量组件的应用来说这是很方便的。中大型项目不适用。会使应用难于调试和理解。
可以在子组件中访问父实例的数据。
对于 demo 或非常小型的有少量组件的应用来说这是很方便的。中大型项目不适用。会使应用难于调试和理解。
可以在父组件中访问子实例的数据。
对于 demo 或非常小型的有少量组件的应用来说这是很方便的。中大型项目不适用。会使应用难于调试和理解。
可以在父组件中访问子实例的数据。
$refs 只会在组件渲染完成之后生效,并且它们不是响应式的,适用于demo或小型项目。
祖先组件提供数据(provide),子孙组件按需注入(inject)。
会将组件的阻止方式,耦合在一起,从而使组件重构困难,难以维护。不推荐在中大型项目中使用,适用于一些小组件的编写。
Vue.prototype.$bus = new Vue();
Vue.component('cmp-a', {
data () {
return {
a: 'a'
}
},
methods: {
onClick () {
this.$bus.$on('click', this.a)
}
},
template: `
`,
})
Vue.component('cmp-a', {
mounted () {
this.$bus.$on('click', data => {
console.log(data);
})
},
template: `
b
`,
})
非父子组件通信时,可以使用这种方法,但仅针对于小型项目。中大型项目使用时,会造成代码混乱不易维护。
状态管理,中大型项目时强烈推荐使用此种方式,日后再学~
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
var minxin = {
created () {
this.hello();
},
methods: {
hello () {
console.log('hello,我是混入中的函数');
},
}
}
Vue.component('my-cmp', {
mixins: [mixin],
template: `
xx
`
})
当组件和混入对象含有同名选项时,这些选项会以恰当的方式进行“合并”。
合并数据,以组件数据优先:
var mixin = {
data () {
return {
msg: 'hello',
}
}
}
new Vue({
mixins: [mixin],
data: {
msg: 'goodbye',
},
created: function () {
console.log(this.msg)
})
合并钩子函数,将合并为一个数组。先调用混入对象的钩子,再调用组件自身钩子。
var mixin = {
created () {
console.log('混入对象钩子')
}
}
new Vue({
el: '#app',
mixins: [mixin],
created () {
console.log('组件钩子')
}
})
合并值为对象的选项,如 methods、components 等,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。使用恰当时,这可以用来为自定义选项注入处理逻辑。
// 为自定义的选项 'myOption' 注入一个处理器。
Vue.mixin({
created () {
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
new Vue({
myOption: 'hello!'
})
谨慎使用全局混入,因为它会影响每个单独创建的 Vue 实例 (包括第三方组件)。大多数情况下,只应当应用于自定义选项。
我们可以自己写一个自定义指令去操作DOM元素,以达到代码复用的目的。注意,在 Vue 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
全局注册指令:
Vue.directive('focus', {/** */})
局部注册指令
const vm = new Vue({
el: '#app',
directives: {
focus: {/** */}
}
})
使用:
<input v-focus></input>
例如,写一个自动聚焦的输入框:
Vue.directive('focus', {
// 当被绑定的元素插入到DOM时执行
inserted: function (el) {
el.focus();
}
})
此时,在input元素上使用 v-focus 指令就可以实现自动聚焦了。
自定义指令对象提供了钩子函数供我们使用,这些钩子函数都为可选。
只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。
指令所在组件的 VNode 及其子 VNode 全部更新后调用。
只调用一次,指令与元素解绑时调用(被绑定的Dom元素被Vue移除)。
// 绑定的值为false,display为none,值为true,display为""
Vue.directive('myshow', {
bind (el, binding, vnode, oldVnode) {
var display = binding.value ? '' : 'none';
el.style.display = display;
},
update (el, binding, vnode, oldVnode) {
var display = binding.value ? '' : 'none';
el.style.display = display;
}
})
// 1. 通过绑定的数据,给元素设置value
// 2. 当触发input事件时,去更改数据的值
// 3. 更改数据后,同步input的value值
Vue.directive('mymodel', {
bind (el, binding, vnode) {
const vm = vnode.context;
const { value, expression } = binding;
el.value = value;
el.oninput = function (e) {
const inputVal = el.value;
vm[expression] = inputVal;
}
},
update (el, binding) {
const { value } = binding;
el.value = value;
},
})
Vue.directive('slice', {
bind (el, binding, vnode) {
const vm = vnode.context;
let { value, expression, arg, modifiers } = binding;
if(modifiers.number) {
value = value.replace(/[^0-9]/g, '');
}
el.value = value.slice(0, arg);
vm[expression] = value.slice(0, arg);
el.oninput = function (e) {
let inputVal = el.value;
if(modifiers.number) {
inputVal = inputVal.replace(/[^0-9]/g, '');
}
el.value = inputVal.slice(0, arg);
vm[expression] = inputVal.slice(0, arg);
}
},
update (el, binding, vnode) {
const vm = vnode.context;
let { value, arg, expression, modifiers } = binding;
if(modifiers.number) {
value = value.replace(/[^0-9]/g, '');
}
el.value = value.slice(0, arg);
vm[expression] = value.slice(0, arg);
},
})
指令的参数可以是动态的。如:v-directive:[arguments]="value
,argument
参数可以根据组件实例数据进行更新。
重写 v-slice
Vue.directive('slice', {
bind (el, binding, vnode) {
const vm = vnode.context;
let { value, expression, arg, modifiers } = binding;
if(modifiers.number) {
value = value.replace(/[^0-9]/g, '');
}
el.value = value.slice(0, arg);
vm[expression] = value.slice(0, arg);
el.oninput = function (e) {
let inputVal = el.value;
if(modifiers.number) {
inputVal = inputVal.replace(/[^0-9]/g, '');
}
el.value = inputVal.slice(0, arg);
vm[expression] = inputVal.slice(0, arg);
}
},
update (el, binding, vnode) {
const vm = vnode.context;
let { value, arg, expression, modifiers } = binding;
if(modifiers.number) {
value = value.replace(/[^0-9]/g, '');
}
el.value = value.slice(0, arg);
vm[expression] = value.slice(0, arg);
el.oninput = function (e) {
let inputVal = el.value;
if(modifiers.number) {
inputVal = inputVal.replace(/[^0-9]/g, '');
}
el.value = inputVal.slice(0, arg);
vm[expression] = inputVal.slice(0, arg);
}
},
})
当想在 bind 和 update 中触发相同行为,而不关心其他钩子时,可以写成函数的形式:
Vue.directive('myshow', (el, binding) => {
const { value } = binding;
const display = value ? '' : 'none';
el.style.display = display;
})
Vue.directive('slice', (el, binding, vnode) => {
const vm = vnode.context;
let { value, expression, arg, modifiers } = binding;
if(modifiers.number) {
value = value.replace(/[^0-9]/g, '');
}
el.value = value.slice(0, arg);
vm[expression] = value.slice(0, arg);
el.oninput = function (e) {
let inputVal = el.value;
if(modifiers.number) {
inputVal = inputVal.replace(/[^0-9]/g, '');
}
el.value = inputVal.slice(0, arg);
vm[expression] = inputVal.slice(0, arg);
}
})
如果自定义指令需要多个值,可以传入一个 JS 对象字面量。指令函数能够接受所有合法的 JS 表达式。
<div v-demo="{ color: 'white', text: 'hello!' }">div>
Vue.directive('demo', function (el, binding) {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
——后续还会更新。