组件
组件可以扩展 HTML 元素,封装可重用的代码
在较高层面上,组件是自定义元素, Vue.js 的编译器为它添加特殊功能
在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。
使用组件
注册一个全局组件
<div id="example">
<!--web组件的定义脱离了一般的dom元素的写法,相当于自定义了元素-->
<my-component></my-component>
</div>
// 注册全局组件,指定之前设定的元素名,然后传入对象
Vue.component('my-component', {
template: '<div>A custom component!</div>'
})
// 创建根实例
new Vue({
el: '#example'
})
局部注册组件
不必在全局注册每个组件。通过使用组件实例选项注册,可以使组件仅在另一个实例/组件的作用域中可用
//将传入给组件的对象单独写
var Child = {
template: '<div>A custom component!</div>'
}
new Vue({
//通过components语法创建局部组件
//将组件仅仅放在这个vue实例里面使用
components: {
// <my-component> 将只在父模板可用
'my-component': Child
}
})
DOM模板解析说明
当使用 DOM 作为模版时(例如,将 el 选项挂载到一个已存在的元素上), 你会受到 HTML 的一些限制,
因为 Vue 只有在浏览器解析和标准化 HTML 后才能获取模版内容。
尤其像这些元素 <ul> , <ol>, <table> , <select> 限制了能被它包裹的元素, <option> 只能出现在其它元素内部。
<!--这种是不行的,会报错-->
<table>
<my-row>...</my-row>
</table>
<!--要通过is属性来处理-->
<table>
<tr is="my-row"></tr>
</table>
data必须是函数
使用组件时,大多数可以传入到 Vue 构造器中的选项可以在注册组件时使用,有一个例外: data 必须是函数。 实际上
//这样会报错,提示data必须是一个函数
Vue.component('my-component', {
template: '<span>{{ message }}</span>',
data: {
message: 'hello'
}
})
<div id="example-2">
<simple-counter></simple-counter>
<simple-counter></simple-counter>
<simple-counter></simple-counter>
</div>
var data = { counter: 0 }
Vue.component('simple-counter', {
template: '<button v-on:click="counter += 1">{{ counter }}</button>',
// data 是一个函数,因此 Vue 不会警告,
// 但是我们为每一个组件返回了同一个对象引用,所以改变其中一个会把其他都改变了
data: function () {
return data
}
})
new Vue({
el: '#example-2'
})
避免出现同时改变数据的情况
//返回一个新的对象,而不是返回同一个data对象引用
data: function () {
return { //字面量写法会创建新对象
counter: 0
}
}
构成组件
组件意味着协同工作,通常父子组件会是这样的关系:
组件 A 在它的模版中使用了组件 B 。它们之间必然需要相互通信
父组件要给子组件传递数据,子组件需要将它内部发生的事情告知给父组件
然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的。这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性。
在 Vue.js 中,父子组件的关系可以总结为 props down, events up 。
父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。看看它们是怎么工作的。
prop
使用prop传递数据
组件实例的作用域是孤立的。这意味着不能并且不应该在子组件的模板内直接引用父组件的数据。
使用 props 把数据传给子组件。
prop 是父组件用来传递数据的一个自定义属性
子组件需要显式地用 props 选项声明 “prop”
<div id="example-2">
<!--向这个组件传入一个字符串-->
<child message="hello!"></child>
</div>
Vue.component('child', {
// 声明 props,用数组形式的对象
props: ['message'],
// 就像 data 一样,prop 可以用在模板内
// 同样也可以在 vm 实例中像 “this.message” 这样使用
template: '<span>{{ message }}</span>'
});
new Vue({
el: '#example-2'
})
动态prop
用 v-bind 动态绑定 props 的值到父组件的数据中。每当父组件的数据变化时,该变化也会传导给子组件
<div id="example-2">
<!--使用v-modal实现双向绑定-->
<input v-model="parentMsg">
<br>
<!--需要注意这里使用短横线的变量,因为在html下是使用短横线变量的,但是在vue下使用驼峰变量-->
<!--将父组件的parentMsg和子组件的my-message进行绑定-->
<child v-bind:my-message="parentMsg"></child>
</div>
Vue.component('child', {
// 声明 props
props: ['my-message'],
template: '<span>{{ myMessage }}</span>' //如果写my-message会报错,需要转换为驼峰写法
});
new Vue({
el: '#example-2',
data: {
parentMsg: ''
}
})
短横线和驼峰写法
HTML 特性不区分大小写。当使用非字符串模版时,prop的名字形式会从 camelCase 转为 kebab-case(短横线隔开)
在javascript里面使用驼峰写法,但是在html里面需要转成短横线写法
反之亦然,vue会自动处理来自html的短横线写法转为驼峰写法
字面量语法和动态语法
<!-- 默认只传递了一个字符串"1" -->
<comp some-prop="1"></comp>
<!-- 用v-bind实现传递实际的数字 -->
<comp v-bind:some-prop="1"></comp>
单向数据流
prop 是单向绑定的
当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。
每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop 。如果你这么做了,Vue 会在控制台给出警告。
通常有两种改变 prop 的情况:
prop 作为初始值传入,子组件之后只是将它的初始值作为本地数据的初始值使用
定义一个局部 data 属性,并将 prop 的初始值作为局部数据的初始值。
<div id="example-2">
<!--这里用短横线写法-->
<child initial-counter="10"></child>
</div>
Vue.component('child', {
props: ['initialCounter'],//这里用驼峰写法
data: function () { //转为一个局部变量,写一个data对象给组件使用
return {counter: this.initialCounter}
},
template: '<span>{{ counter }}</span>'
});
new Vue({
el: '#example-2'
})
prop 作为需要被转变的原始值传入。
定义一个 computed 属性,此属性从 prop 的值计算得出。
//例子没有写完,但是根据第一个例子可以知道利用computed的手法原理其实跟写一个data差不多
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。
prop验证
组件可以为 props 指定验证要求,当组件给其他人使用时这很有用。
Vue.component('example', {
props: {
// 基础类型检测 (`null` 意思是任何类型都可以)
propA: Number,
// 多种类型
propB: [String, Number],
// 必传且是字符串
propC: {
type: String,
required: true
},
// 数字,有默认值
propD: {
type: Number,
default: 100
},
// 数组/对象的默认值应当由一个工厂函数返回
propE: {
type: Object,
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
return value > 10
}
}
}
})
自定义事件
每个 Vue 实例都实现了事件接口(Events interface)
使用 $on(eventName) 监听事件
使用 $emit(eventName) 触发事件
父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件。
<div id="counter-event-example">
<p>{{ total }}</p>
<!--监听子组件的事件触发,监听increment1事件,处理程序为incrementTotal事件-->
<button-counter v-on:increment1="incrementTotal"></button-counter>
<!--关键在于这里v-on绑定的是一个子组件的事件,并且赋值了一个父组件的方法给他,那么子组件里面就可以使用这个方法-->
<button-counter v-on:increment1="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
//监听click事件,处理程序为increment(子组件定义的方法)
template: '<button v-on:click="increment">{{ counter }}</button>',
//每一个counter都是独立的对象属性
data: function () {
return {
counter: 0
}
},
//子组件的方法
methods: {
increment: function () {
this.counter += 1;
//在子组件里面直接触发之前监听的increment1事件来执行父组件的方法
this.$emit('increment1');
}
},
})
new Vue({
el: '#counter-event-example',
data: {
total: 0
},
//父组件的方法
methods: {
incrementTotal: function () {
this.total += 1
}
}
})
1.组件之间因为作用域不同的关系,所以相互独立,所以子组件想要使用父组件的方法的话需要做一个新的监听映射
给组件绑定原生事件
<!--代替.on,这么就能够绑定原生js的事件了-->
<my-component v-on:click.native="doTheThing"></my-component>
使用自定义事件的表单输入组件
自定义事件也可以用来创建自定义的表单输入组件,使用 v-model 来进行数据双向绑定。
所以要让组件的 v-model 生效,它必须:
接受一个 value 属性
在有新的 value 时触发 input 事件
<!--直接使用v-model,v-modal默认处理input事件-->
<input v-model="something">
<!--v-modal是语法糖,翻译过来原理是这样:-->
<!--绑定一个value,然后监听input事件,通过获取input的输入来不断改变绑定的value的值,满足了v-modal的触发条件就可以实现v-modal了-->
<input v-bind:value="something" v-on:input="something = $event.target.value">
一个非常简单的货币输入:
<!--绑定一个v-model为price,其实是绑定了一个value-->
<currency-input v-model="price"></currency-input>
Vue.component('currency-input', {
template: '\
<span>\
$\
<input\
ref="input"\ //注册为input,是DOM的节点元素
v-bind:value="value"\ //v-model的value(也是prop)
v-on:input="updateValue($event.target.value)"\ //封装更新value的函数
>\
</span>\
',
props: ['value'], //父组件将绑定的value传给子组件
methods: {
// 不是直接更新值,而是使用此方法来对输入值进行格式化和位数限制
updateValue: function (value) {
var formattedValue = value //对值进行处理
// 删除两侧的空格符
.trim()
// 保留 2 小数位和2位数
.slice(0, value.indexOf('.') + 3)
// 如果值不统一,手动覆盖以保持一致,为了保持输入框显示内容跟格式化内容一致
if (formattedValue !== value) {
//因为注册是一个input元素,所以this.$refs 就是input元素
this.$refs.input.value = formattedValue
}
//手动触发input事件,将格式化后的值传过去,这是最终显示输入框的输出
this.$emit('input', Number(formattedValue))
}
}
})
//实例化vue实例的
new Vue({
el: '#aa', //要绑定一个vue实例,例如包裹一个id为aa的div
data:{
price:'' //v-model要有数据源
}
})
ref 被用来给元素或子组件注册引用信息。引用信息会根据父组件的 $refs 对象进行注册。如果在普通的DOM元素上使用,引用信息就是元素; 如果用在子组件上,引用信息就是组件实例 ref
这是一个比较完整的例子:
<div id="app">
<!--有3个组件,分别不同的v-model-->
<currency-input
label="Price"
v-model="price"
></currency-input>
<currency-input
label="Shipping"
v-model="shipping"
></currency-input>
<currency-input
label="Handling"
v-model="handling"
></currency-input>
<currency-input
label="Discount"
v-model="discount"
></currency-input>
<p>Total: ${{ total }}</p>
</div>
Vue.component('currency-input', {
template: '\
<div>\
<label v-if="label">{{ label }}</label>\
$\
<input\
ref="input"\ // 这些没什么特别,引用注册为input DOM元素
v-bind:value="value"\
v-on:input="updateValue($event.target.value)"\
v-on:focus="selectAll"\ //这里多了focus事件监听,焦点在的时候全选,也只是多了处理而已,对整体逻辑理解没啥影响
v-on:blur="formatValue"\ //这里多了blur事件监听,焦点离开的时候格式化
>\
</div>\
',
props: { //多个prop传递,因为prop是对象,只要是对象格式就行
value: {
type: Number,
default: 0
},
label: {
type: String,
default: ''
}
},
mounted: function () { //这是vue的过渡状态,暂时忽略不影响理解
this.formatValue()
},
methods: {
updateValue: function (value) {
var result = currencyValidator.parse(value, this.value)
if (result.warning) {
// 这里也使用了$refs获取引用注册信息
this.$refs.input.value = result.value
}
this.$emit('input', result.value)
},
formatValue: function () {
this.$refs.input.value = currencyValidator.format(this.value) //这里注意下,这个this是prop传递过来的,也相当于这个组件作用域
},
selectAll: function (event) { //event可以获取原生的js事件
// Workaround for Safari bug
// http://stackoverflow.com/questions/1269722/selecting-text-on-focus-using-jquery-not-working-in-safari-and-chrome
setTimeout(function () {
event.target.select()
}, 0)
}
}
})
new Vue({
el: '#app',
data: {
price: 0,
shipping: 0,
handling: 0,
discount: 0
},
computed: {
total: function () {
return ((
this.price * 100 +
this.shipping * 100 +
this.handling * 100 -
this.discount * 100
) / 100).toFixed(2)
}
}
})
非父子组件通信
在简单的场景下,使用一个空的 Vue 实例作为中央事件总线:
var bus = new Vue()
// 触发组件 A 中的事件
bus.$emit('id-selected', 1)
/*
通过on来监听子组件的事件来实现传递
*/
// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
// ...
})
使用Slot分发内容
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为 内容分发 (或 “transclusion” 如果你熟悉 Angular)
编译作用域
组件作用域简单地说是:父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。
假定 someChildProperty 是子组件的属性,上例不会如预期那样工作。父组件模板不应该知道子组件的状态。
<!-- 无效 -->
<child-component v-show="someChildProperty"></child-component>
如果要绑定子组件内的指令到一个组件的根节点,应当在它的模板内这么做:
Vue.component('child-component', {
// 有效,因为是在正确的作用域内
template: '<div v-show="someChildProperty">Child</div>',
data: function () {
return { //因为这个属性在当前组件内编译(创建了)
someChildProperty: true
}
}
})
类似地,分发内容是在父组件作用域内编译。
单个Slot
除非子组件模板包含至少一个 <slot> 插口,否则父组件的内容将会被丢弃。
当子组件模板只有一个没有属性的 slot 时,父组件整个内容片段将插入到 slot 所在的 DOM 位置,并替换掉 slot 标签本身。
备用内容在子组件的作用域内编译,并且只有在宿主元素为空,且没有要插入的内容时才显示备用内容。
<!--父组件模版:-->
<div id="aa">
<h1>我是父组件的标题</h1>
<!--子组件的作用域内编译,宿主元素为空,且没有要插入的内容-->
<my-component></my-component>
<my-component>
<p>这是一些初始内容</p>
<p>这是更多的初始内容</p>
</my-component>
</div>
Vue.component('my-component', {
//my-component 组件有下面模板
template: '\
<div>\
<h2>我是子组件的标题</h2> \
<slot> \ //有slot插口,所以没有被父组件丢弃
只有在没有要分发的内容时才会显示。\
</slot> \
</div> \
'
})
new Vue({
el: '#aa',
})
渲染结果:
<div id="aa"><h1>我是父组件的标题</h1>
<div>
<h2>我是子组件的标题</h2>
<!--这里是直接插入,没有使用DOM元素-->
只有在没有要分发的内容时才会显示。
</div>
<div>
<h2>我是子组件的标题</h2>
<p>这是一些初始内容</p>
<p>这是更多的初始内容</p>
</div>
</div>
有名字的Slot
<slot> 元素可以用一个特殊的属性 name 来配置如何分发内容。多个 slot 可以有不同的名字。具名 slot 将匹配内容片段中有对应 slot 特性的元素。
仍然可以有一个匿名 slot ,它是默认 slot ,作为找不到匹配的内容片段的备用插槽。如果没有默认的 slot ,这些找不到匹配的内容片段将被抛弃。
<div id="aa">
<app-layout>
<!--这是header-->
<h1 slot="header">这里可能是一个页面标题</h1>
<p>主要内容的一个段落。</p>
<p>另一个主要段落。</p>
<!--这是footer-->
<p slot="footer">这里有一些联系信息</p>
</app-layout>
</div>
Vue.component('app-layout', {
template: '\
<div class="container"> \
<header> \ //找到名字叫header的slot之后替换内容,这里替换的是整个DOM
<slot name="header"></slot> \
</header> \
<main> \ //因为slot没有属性,会将内容插入到slot的所在的DOM位置
<slot></slot> \
</main> \
<footer>\ //跟header类似
<slot name="footer"></slot> \
</footer> \
</div> \
'
});
new Vue({
el: '#aa',
})
渲染结果为:
<div class="container">
<header>
<h1>这里可能是一个页面标题</h1>
</header>
<main>
<p>主要内容的一个段落。</p>
<p>另一个主要段落。</p>
</main>
<footer>
<p>这里有一些联系信息</p>
</footer>
</div>
作用域插槽(vue2.1)
作用域插槽是一种特殊类型的插槽,用作使用一个(能够传递数据到)可重用模板替换已渲染元素。
在子组件中,只需将数据传递到插槽,就像你将 prop 传递给组件一样
在父级中,具有特殊属性 scope 的 <template> 元素,表示它是作用域插槽的模板。scope 的值对应一个临时变量名,此变量接收从子组件中传递的 prop 对象
<div id="parent" class="parent">
<child>
<!--接收从子组件中传递的prop对象(这个就是作用域插槽)-->
<template scope="props">
<span>hello from parent</span>
<!--使用这个prop对象-->
<span>{{ props.text }}</span>
</template>
</child>
</div>
Vue.component('child', {
props: ['props'], //这个写不写都可以,作用域插槽固定会接收prop对象,而且这个prop对象是肯定存在的
template: '\
<div class="child"> \
<slot text="hello from child"></slot> \ //在子组件里直接将数据传递给slot
</div> \
'
});
new Vue({
el: '#parent',
})
渲染结果:
<div class="parent">
<div class="child">
<span>hello from parent</span>
<!--子组件的东西出现在这里了-->
<span>hello from child</span>
</div>
</div>
另外一个例子,作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项
<div id="parent">
<!--绑定一个组件的prop ,位置1-->
<my-awesome-list :items="items">
<!-- 作用域插槽也可以在这里命名 -->
<!--这里props只代表确定接受prop对象的东西,不关注prop对象里面有什么,位置2-->
<template slot="item" scope="props">
<li class="my-fancy-item">{{ props.text }}</li>
</template>
</my-awesome-list>
</div>
Vue.component('my-awesome-list', {
props:['items'], //需要声明prop为items,需要是为下面的循环遍历的items的数据源做设定,位置3
template: '\
<ul> \
<slot name="item" v-for="item in items" :text="item.text"> \ //在slot中,循环遍历输出items的text,位置4
</slot> \
</ul> \
'
});
new Vue({
el: '#parent',
data : {
items:[ //初始化items数据
{text:"aa"},
{text:"bb"}
]
}
})
位置1,实现了一个组件的prop绑定,prop需要在组件里面声明,这里绑定的是items,这是要将父组件的items传递到子组件,所以在位置3里面需要声明,在vue实例要初始化
位置2,这里scope的props是代表作用域插槽接收来自prop对象的数据,props.text是代表每一个li要输出的是prop对象的text属性
位置3,在组件里声明props,为了接收父组件绑定的items属性,然后将其给位置4的循环使用
位置4,这里绑定了text属性,就是前呼位置2里面输出的prop对象的text属性
动态组件
多个组件可以使用同一个挂载点,然后动态地在它们之间切换。使用保留的 <component> 元素,动态地绑定到它的 is 特性
var vm = new Vue({
el: '#example',
data: {
currentView: 'home' //默认值
},
components: { //根据不同的值进行不同的组件切换,这里用components写法
home: { /* ... */ },
posts: { /* ... */ },
archive: { /* ... */ }
}
})
<!--这个is是一个字符串,根据返回值来给组件进行v-bind-->
<component v-bind:is="currentView">
<!-- 组件在 vm.currentview 变化时改变! -->
</component>
keep-alive
如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个 keep-alive 指令参数
<keep-alive>
<component :is="currentView">
<!-- 非活动组件将被缓存! -->
</component>
</keep-alive>
杂项
编写可复用组件
在编写组件时,记住是否要复用组件有好处。一次性组件跟其它组件紧密耦合没关系,但是可复用组件应当定义一个清晰的公开接口。
Vue 组件的 API 来自三部分 - props, events 和 slots :
Props 允许外部环境传递数据给组件
Events 允许组件触发外部环境的副作用
Slots 允许外部环境将额外的内容组合在组件中。
<!--v-bind,缩写:,绑定prop-->
<!--v-on,缩写@,监听事件-->
<!--slot插槽-->
<my-component
:foo="baz"
:bar="qux"
@event-a="doThis"
@event-b="doThat"
>
<img slot="icon" src="...">
<p slot="main-text">Hello!</p>
</my-component>
子组件索引
尽管有 props 和 events ,但是有时仍然需要在 JavaScript 中直接访问子组件。为此可以使用 ref 为子组件指定一个索引 ID 。
<div id="parent">
<user-profile ref="profile"></user-profile>
</div>
var parent = new Vue({ el: '#parent' })
// 访问子组件
var child = parent.$refs.profile
当 ref 和 v-for 一起使用时, ref 是一个数组或对象,包含相应的子组件。
$refs 只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案——应当避免在模版或计算属性中使用 $refs 。
ref 被用来给元素或子组件注册引用信息。引用信息会根据父组件的 $refs 对象进行注册。如果在普通的DOM元素上使用,引用信息就是元素; 如果用在子组件上,引用信息就是组件实例 ref
组件命名约定
当注册组件(或者 props)时,可以使用 kebab-case ,camelCase ,或 TitleCase 。Vue 不关心这个。
在 HTML 模版中,请使用 kebab-case 形式:
// 在组件定义中
components: {
// 使用 kebab-case 形式注册--横线写法
'kebab-cased-component': { /* ... */ },
// register using camelCase --驼峰写法
'camelCasedComponent': { /* ... */ },
// register using TitleCase --标题写法
'TitleCasedComponent': { /* ... */ }
}
<!-- 在HTML模版中始终使用 kebab-case--横线写法 -->
<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<title-cased-component></title-cased-component>
递归组件
组件在它的模板内可以递归地调用自己,不过,只有当它有 name 选项时才可以
当你利用Vue.component全局注册了一个组件, 全局的ID作为组件的 name 选项,被自动设置.
//组件可以用name来写名字
name: 'unique-name-of-my-component'
//也可以在创建的时候默认添加名字
Vue.component('unique-name-of-my-component', {
// ...
})
//如果同时使用的话,递归的时候就会不断递归自己,导致溢出
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
使用-v-once-的低级静态组件-Cheap-Static-Component
尽管在 Vue 中渲染 HTML 很快,不过当组件中包含大量静态内容时,可以考虑使用 v-once 将渲染结果缓存起来,就像这样:
Vue.component('terms-of-service', {
template: '\
<div v-once>\
<h1>Terms of Service</h1>\
... a lot of static content ...\
</div>\
'
})
v-once只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。