9.组件的奥秘
9.1 什么是组件?
组件(Component)是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。
9.2 使用组件
9.2.1 注册
之前说过,我们可以用 Vue.extend() 创建一个组件构造器:
var MyComponent = Vue.extend({
// 选项...
})
要把这个构造器用作组件,需要用 Vue.component(tag, constructor) 注册 :
// 全局注册组件,tag 为 my-component
Vue.component('my-component', MyComponent)
对于自定义标签名字,Vue.js 不强制要求遵循 W3C 规则(小写,并且包含一个短杠),尽管遵循这个规则比较好。
组件在注册之后,便可以在父实例的模块中以自定义元素 <my-component> 的形式使用。要确保在初始化根实例之前注册了组件:
<div id="example">
<my-component></my-component>
</div>
// 定义
var MyComponent = Vue.extend({
template: '<div>A custom component!</div>'
})
// 注册
Vue.component('my-component', MyComponent)
// 创建根实例
new Vue({
el: '#example'
})
渲染为:
<div id="example">
<div>A custom component!</div>
</div>
注意组件的模板替换了自定义元素,自定义元素的作用只是作为一个挂载点。可以用实例选项 replace 决定是否替换。
9.2.2 局部注册
不需要全局注册每个组件。可以让组件只能用在其它组件内,用实例选项 components 注册:
var Child = Vue.extend({ /* ... */ })
var Parent = Vue.extend({
template: '...',
components: {
// <my-component> 只能用在父组件模板内
'my-component': Child
}
})
这种封装也适用于其它资源,如指令、过滤器和过渡。
9.2.3 组件选项问题
传入 Vue 构造器的多数选项也可以用在 Vue.extend() 中,不过有两个特例: data 和 el。试想如果我们简单地把一个对象作为 data 选项传给 Vue.extend():
var data = { a: 1 }
var MyComponent = Vue.extend({
data: data
})
这么做的问题是 MyComponent 所有的实例将共享同一个 data 对象!这基本不是我们想要的,因此我们应当使用一个函数作为 data 选项,让这个函数返回一个新对象:
var MyComponent = Vue.extend({
data: function () {
return { a: 1 }
}
})
同理,el 选项用在 Vue.extend() 中时也须是一个函数。
9.3 Props
9.3.1 使用 Props 传递数据
组件实例的作用域是孤立的。这意味着不能并且不应该在子组件的模板内直接引用父组件的数据。可以使用 props 把数据传给子组件。
“prop” 是组件数据的一个字段,期望从父组件传下来。子组件需要显式地用 props 选项 声明 props:
Vue.component('child', {
// 声明 props
props: ['msg'],
// prop 可以用在模板内
// 可以用 `this.msg` 设置
template: '<span>{{ msg }}</span>'
})
然后向它传入一个普通字符串:
<child msg="hello!"></child>
props的两种写法如下:
简单语法
// 简单语法
Vue.component('props-demo-simple', {
props: ['size', 'myMessage']
})
对象语法
// 对象语法,指定验证要求
Vue.component('props-demo-advanced', {
props: {
// 只检测类型
size: Number,
// 检测类型 + 其它验证
name: {
type: String,
required: true,
// 双向绑定
twoWay: true
}
}
})
9.3.2 camelCase vs. kebab-case
HTML 特性不区分大小写。名字形式为 camelCase 的 prop 用作特性时,需要转为 kebab-case(短横线隔开):
Vue.component('child', {
// camelCase in JavaScript
props: ['myMessage'],
template: '<span>{{ myMessage }}</span>'
})
<!-- kebab-case in HTML -->
<child my-message="hello!"></child>
9.3.3 动态 Props
类似于用 v-bind 绑定 HTML 特性到一个表达式,也可以用 v-bind 绑定动态 Props 到父组件的数据。每当父组件的数据变化时,也会传导给子组件:
<div>
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child>
</div>
使用 v-bind 的缩写语法通常更简单:
<child :my-message="parentMsg"></child>
9.3.3 字面量语法 vs. 动态语法
初学者常犯的一个错误是使用字面量语法传递数值:
<!-- 传递了一个字符串 "1" -->
<comp some-prop="1"></comp>
因为它是一个字面 prop,它的值以字符串 "1" 而不是以实际的数字传下去。如果想传递一个实际的 JavaScript 数字,需要使用动态语法,从而让它的值被当作 JavaScript 表达式计算:
<!-- 传递实际的数字 -->
<comp :some-prop="1"></comp>
9.3.4 Prop 绑定类型
prop 默认是单向绑定:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。不过,也可以使用 .sync 或 .once 绑定修饰符显式地强制双向或单次绑定:
比较语法:
<!-- 默认为单向绑定 -->
<child :msg="parentMsg"></child>
<!-- 双向绑定 在VUE2中已经废弃 -->
<child :msg.sync="parentMsg"></child>
<!-- 单次绑定 -->
<child :msg.once="parentMsg"></child>
双向绑定会把子组件的 msg 属性同步回父组件的 parentMsg 属性。单次绑定在建立之后不会同步之后的变化。
Vue 2.x相比较Vue 1.x而言,升级变化除了实现了Virtual-Dom以外,给使用者最大不适就是移除的组件的props的双向绑定功能。以往在Vue1.x中利用props的twoWay和.sync绑定修饰符就可以实现props的双向绑定功能,但是在Vue2中彻底废弃了此功能,如果需要双向绑定需要自己来实现。
注意如果 prop 是一个对象或数组,是按引用传递。在子组件内修改它会影响父组件的状态,不管是使用哪种绑定类型。
上述这几句话是非常重要的。例如array、object都属于引用类型的数据,那么单向绑定对其则不起作用,但是为了单向数据流的纯净化,也要尊重单向数据流的理念。尽量避免方向的修改从而影响父组件的变化。
9.3.5 Prop 验证
组件可以为 props 指定验证要求。当组件给其他人使用时这很有用,因为这些验证要求构成了组件的 API,确保其他人正确地使用组件。此时 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
}
}
}
})
type 可以是下面原生构造器:
- String
- Number
- Boolean
- Function
- Object
- Array
type 也可以是一个自定义构造器,使用 instanceof 检测。
当 prop 验证失败了,Vue 将拒绝在子组件上设置此值,如果使用的是开发版本会抛出一条警告。
9.4 父子组件通信
9.4.1 父链
子组件可以用 this.$parent 访问它的父组件。根实例的后代可以用 this.$root 访问它。父组件有一个数组 this.$children,包含它所有的子元素。
尽管可以访问父链上任意的实例,不过子组件应当避免直接依赖父组件的数据,尽量显式地使用 props 传递数据。另外,在子组件中修改父组件的状态是非常糟糕的做法,因为:
-
这让父组件与子组件紧密地耦合;
-
只看父组件,很难理解父组件的状态。因为它可能被任意子组件修改!理想情况下,只有组件自己能修改它的状态。
9.4.2 自定义事件
Vue 实例实现了一个自定义事件接口,用于在组件树中通信。这个事件系统独立于原生 DOM 事件,用法也不同。
每个 Vue 实例都是一个事件触发器:
-
使用 $on() 监听事件;
-
使用 $emit() 在它上面触发事件;
-
使用 $dispatch() 派发事件,事件沿着父链冒泡;
-
使用 $broadcast() 广播事件,事件向下传导给所有的后代。
不同于 DOM 事件,Vue 事件在冒泡过程中第一次触发回调之后自动停止冒泡,除非回调明确返回 true。
简单例子:
<!-- 子组件模板 -->
<template id="child-template">
<input v-model="msg">
<button v-on:click="notify">Dispatch Event</button>
</template>
<!-- 父组件模板 -->
<div id="events-example">
<p>Messages: {{ messages | json }}</p>
<child></child>
</div>
// 注册子组件
// 将当前消息派发出去
Vue.component('child', {
template: '#child-template',
data: function () {
return { msg: 'hello' }
},
methods: {
notify: function () {
if (this.msg.trim()) {
this.$dispatch('child-msg', this.msg)
this.msg = ''
}
}
}
})
// 初始化父组件
// 将收到消息时将事件推入一个数组
var parent = new Vue({
el: '#events-example',
data: {
messages: []
},
// 在创建实例时 `events` 选项简单地调用 `$on`
events: {
'child-msg': function (msg) {
// 事件回调内的 `this` 自动绑定到注册它的实例上
this.messages.push(msg)
}
}
})
9.4.3 使用 v-on 绑定自定义事件
上例非常好,不过从父组件的代码中不能直观的看到 "child-msg" 事件来自哪里。如果我们在模板中子组件用到的地方声明事件处理器会更好。为此子组件可以用 v-on 监听自定义事件:
<child v-on:child-msg="handleIt"></child>
这样就很清楚了:当子组件触发了 "child-msg" 事件,父组件的 handleIt 方法将被调用。所有影响父组件状态的代码放到父组件的 handleIt 方法中;子组件只关注触发事件。
9.4.4 子组件索引 ref
尽管有 props 和 events,但是有时仍然需要在 JavaScript 中直接访问子组件。为此可以使用 v-ref 为子组件指定一个索引 ID。例如:
<div id="parent">
<user-profile v-ref:profile></user-profile>
</div>
var parent = new Vue({ el: '#parent' })
// 访问子组件
var child = parent.$refs.profile
v-ref 和 v-for 一起用时,ref 是一个数组或对象,包含相应的子组件。
9.4.5 父子通信的完整例子
第一步:在组件内的data对象中创建一个props属性的副本
因为result不可写,所以需要在data中创建一个副本myResult变量,初始值为props属性result的值,同时在组件内所有需要调用props的地方调用这个data对象myResult。
Vue.component("switchbtn", {
template: "<div @click='change'>{{myResult?'开':'关'}}</div>",
props: ["result"],
data: function () {
return {
myResult: this.result//data中新增字段
};
},
......
});
创建针对props属性的watch来同步组件外对props的修改
此时在组件外(父组件)修改了组件的props,会同步到组件内对应的props上,但是不会同步到你刚刚在data对象中创建的那个副本上,所以需要再创建一个针对props属性result的watch(监听),当props修改后对应data中的副本myResult也要同步数据。
Vue.component("switchbtn", {
template: "<div @click='change'>{{myResult?'开':'关'}}</div>",
props: ["result"],
data: function () {
return {
myResult: this.result
};
},
watch: {
result(val) {
this.myResult = val;//新增result的watch,监听变更并同步到myResult上
}
},
......
创建针对props副本的watch,通知到组件外
此时在组件内修改了props的副本myResult,组件外不知道组件内的props状态,所以需要再创建一个针对props副本myResult,即对应data属性的watch。
在组件内向外层(父组件)发送通知,通知组件内属性变更,然后由外层(父组件)自己来变更他的数据。
最终全部代码:
<div id="app">
<switchbtn :result="result" @on-result-change="onResultChange"></switchbtn>
<input type="button" value="change" @click="change">
</div>
Vue.component("switchbtn", {
template: "<div @click='change'>{{myResult?'开':'关'}}</div>",
props: ["result"],
data: function () {
return {
myResult: this.result//①创建props属性result的副本--myResult
};
},
watch: {
result(val) {
this.myResult = val;//②监听外部对props属性result的变更,并同步到组件内的data属性myResult中
},
myResult(val){
//xxcanghai 小小沧海 博客园
this.$emit("on-result-change",val);//③组件内对myResult变更后向外部发送事件通知
}
},
methods: {
change() {
this.myResult = !this.myResult;
}
}
});
new Vue({
el: "#app",
data: {
result: true
},
methods: {
change() {
this.result = !this.result;
},
onResultChange(val){
this.result=val;//④外层调用组件方注册变更方法,将组件内的数据变更,同步到组件外的数据状态中
}
}
});
9.5 非父子组件通信
有时候非父子关系的组件也需要通信。在简单的场景下,使用一个空的 Vue 实例作为中央事件总线:
var bus = new Vue()
// 触发组件 A 中的事件
bus.$emit('id-selected', 1)
// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
// ...
})
在更多复杂的情况下,你应该考虑使用专门的 状态管理模式(VUEX).
9.6 使用 Slot 分发内容
在使用组件时,常常要像这样组合它们:
<app>
<app-header></app-header>
<app-footer></app-footer>
</app>
注意两点:
- <app> 组件不知道它的挂载点会有什么内容。挂载点的内容是由<app>的父组件决定的。
- <app> 组件很可能有它自己的模版。
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为 内容分发 (或 “transclusion” 如果你熟悉 Angular)。Vue.js 实现了一个内容分发 API ,参照了当前 Web 组件规范草案,使用特殊的 <slot> 元素作为原始内容的插槽。
编译作用域
在深入内容分发 API 之前,我们先明确内容的编译作用域。假定模板为:
<child-component>
{{ message }}
</child-component>
message 应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件。组件作用域简单地说是:
父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。
一个常见错误是试图在父组件模板内将一个指令绑定到子组件的属性/方法:
<!-- 无效 -->
<child-component v-show="someChildProperty"></child-component>
假定 someChildProperty 是子组件的属性,上例不会如预期那样工作。父组件模板不应该知道子组件的状态。
如果要绑定子组件内的指令到一个组件的根节点,应当在它的模板内这么做:
Vue.component('child-component', {
// 有效,因为是在正确的作用域内
template: '<div v-show="someChildProperty">Child</div>',
data: function () {
return {
someChildProperty: true
}
}
})
类似地,分发内容是在父组件作用域内编译。
9.6.1 单个 Slot
除非子组件模板包含至少一个 <slot> 插口,否则父组件的内容将会被丢弃。当子组件模板只有一个没有属性的 slot 时,父组件整个内容片段将插入到 slot 所在的 DOM 位置,并替换掉 slot 标签本身。
最初在 <slot> 标签中的任何内容都被视为备用内容。备用内容在子组件的作用域内编译,并且只有在宿主元素为空,且没有要插入的内容时才显示备用内容。
假定 my-component 组件有下面模板:
<div>
<h2>我是子组件的标题</h2>
<slot>
只有在没有要分发的内容时才会显示。
</slot>
</div>
父组件模版:
<div>
<h1>我是父组件的标题</h1>
<my-component>
<p>这是一些初始内容</p>
<p>这是更多的初始内容</p>
</my-component>
</div>
渲染结果:
<div>
<h1>我是父组件的标题</h1>
<div>
<h2>我是子组件的标题</h2>
<p>这是一些初始内容</p>
<p>这是更多的初始内容</p>
</div>
</div>
9.6.2 具名 Slot
<slot> 元素可以用一个特殊的属性 name 来配置如何分发内容。多个 slot 可以有不同的名字。具名 slot 将匹配内容片段中有对应 slot 特性的元素。
仍然可以有一个匿名 slot ,它是默认 slot ,作为找不到匹配的内容片段的备用插槽。如果没有默认的 slot ,这些找不到匹配的内容片段将被抛弃。
例如,假定我们有一个 app-layout 组件,它的模板为:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
父组件模版:
<app-layout>
<h1 slot="header">这里可能是一个页面标题</h1>
<p>主要内容的一个段落。</p>
<p>另一个主要段落。</p>
<p slot="footer">这里有一些联系信息</p>
</app-layout>
渲染结果为:
<div class="container">
<header>
<h1>这里可能是一个页面标题</h1>
</header>
<main>
<p>主要内容的一个段落。</p>
<p>另一个主要段落。</p>
</main>
<footer>
<p>这里有一些联系信息</p>
</footer>
</div>
在组合组件时,内容分发 API 是非常有用的机制。
9.6.3 作用域插槽
2.1.0 新增
作用域插槽是一种特殊类型的插槽,用作使用一个(能够传递数据到)可重用模板替换已渲染元素。
在子组件中,只需将数据传递到插槽,就像你将 prop 传递给组件一样:
<div class="child">
<slot text="hello from child"></slot>
</div>
在父级中,具有特殊属性 scope 的 <template> 元素,表示它是作用域插槽的模板。scope 的值对应一个临时变量名,此变量接收从子组件中传递的 prop 对象:
<div class="parent">
<child>
<template scope="props">
<span>hello from parent</span>
<span>{{ props.text }}</span>
</template>
</child>
</div>
如果我们渲染以上结果,得到的输出会是:
<div class="parent">
<div class="child">
<span>hello from parent</span>
<span>hello from child</span>
</div>
</div>
作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项:
<my-awesome-list :items="items">
<!-- 作用域插槽也可以在这里命名 -->
<template slot="item" scope="props">
<li class="my-fancy-item">{{ props.text }}</li>
</template>
</my-awesome-list>
列表组件的模板:
<ul>
<slot name="item"
v-for="item in items"
:text="item.text">
<!-- fallback content here -->
</slot>
</ul>
9.7 动态组件
多个组件可以使用同一个挂载点,然后动态地在它们之间切换。使用保留的 <component> 元素,动态地绑定到它的 is 特性:
var vm = new Vue({
el: '#example',
data: {
currentView: 'home'
},
components: {
home: { /* ... */ },
posts: { /* ... */ },
archive: { /* ... */ }
}
})
<component v-bind:is="currentView">
<!-- 组件在 vm.currentview 变化时改变! -->
</component>
也可以直接绑定到组件对象上:
var Home = {
template: '<p>Welcome home!</p>'
}
var vm = new Vue({
el: '#example',
data: {
currentView: Home
}
})
9.7.1 keep-alive
如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个 keep-alive 指令参数:
<keep-alive>
<component :is="currentView">
<!-- 非活动组件将被缓存! -->
</component>
</keep-alive>
在API 参考查看更多 <keep-alive> 的细节。
9.7.2 动态组件完整例子
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>组件功能 动态组件</title>
<script type="text/javascript" src="js/vue.2.1.0.js"></script>
</head>
<body>
<div id="app">
<keep-alive>
<component v-bind:is="currentView">
<!--
1.组件在 vm.currentview 变化时改变! 可以通过v-bind:is指令绑定组件
2.如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个 keep-alive 指令
添加keep-alive和不加keep-alive最大的区别在于,如果不加,每次切换都属于重新生成组件实例,都会出发created事件,
反之则不会
-->
</component>
</keep-alive>
<button type="button" name="button" v-on:click="componentChangeEvent('home')">home组件</button>
<button type="button" name="button" @click="componentChangeEvent('user')">user组件</button>
<button type="button" name="button" @click="componentChangeEvent('list')">list组件</button>
</div>
<script type="text/javascript">
var myMixin = {
created: function () {
//这里的this指向组件的自身
console.log("created")
}
}
var Home = Vue.extend({
template: '<div>home组件内容</div>'
,mixins: [myMixin]
});
var User = Vue.extend({
template: '<div>User组件内容</div>'
,mixins: [myMixin]
});
var List = Vue.extend({
template: '<div>List组件内容</div>'
,mixins: [myMixin]
});
var vm = new Vue({
el:'#app'
,components:{
"home":Home
,"user":User
,"list":List
}
,methods:{
componentChangeEvent:function(name){
this.currentView = name;
}
}
,data: {
currentView: 'home'
}
});
</script>
</body>
</html>