组件
1.1组件与复用
1.2 组件用法
组件需要注册后方能使用,有下面两种注册方式:
- 全局注册:注册后任何 Vue 实例都可以使用
- 局部注册:使用 components 选项,注册后的组件只有在该实例作用域下有效。组件中也可使用 components 选项来注册组件,使得组件可以嵌套。
全局注册组件示例代码
Vue.component('my-component', {
//选项
})
局部注册组件示例代码
components: {
'my-component': Child
}
在某些情况下,Vue 组件的模块会受到 HTML 的限制,比如 table 标签内规定只有 tr 、td、th 等表格元素,因此在 table 内直接使用组件是无效的。此情况下,可以使用特殊的 is 属性 来挂载组件,示例代码如下:
<div id="app">
<table>
<tbody is="my-component"></tbody>
</table>
</div>
<script>
Vue.component('my-component', {
template: '<div>这里是组件内容</div>'
});
Var app = new Vue({
el: '#app'
})
</script>
tbody 在渲染时,会被替换为组件的内容。常见的限制元素还有 ul 、ol 、select.
除了 template 选项外,组件中还可以像 Vue 实例那样使用其他的选项,比如 data、computed、methods 等。但是在使用 data 时,和实例稍有分别, data 必须是函数,然后将数据return 出去。
例如:
Vue.component('my-component', {
template: '<div>message</div>',
data: function () {
return {
message: '组件内容'
}
}
});
注意:
- 如果使用的是字符串模板,是不受限制的,比如 .vue 单文件用法等。
JavaScript 对象是引用关系,所以如果 return 出的对象引用了外部的一个对象,则该对象是共享的,任何一方的修改都会同步。例如
1.2 使用 props传递数据
1.2.1 基本用法
组件的功能在于:
- 复用模板的内容
- 进行组件之间的通信
父组件向子组件传递数据或参数,子组件接收到后根据参数的不同来渲染不同的内容或执行操作。这个正向传递数据的过程是由 props 来实现的
在组件中,通过选项 props 来声明需要从父组件接收的数据,props的值可以是下面两种:
- 字符串数组
- 对象
props 中声明的数据与 组件 data 函数 return 的数据主要区别在于:
- props 的数据来自父级,data 中的数据是组件自己的数据,作用域是组件本身;
- 都可以在模板 template 以及 计算属性 computed 和方法 methods 中使用
由于 HTML 特性不区分大小写,当使用 DOM 模板时,驼峰命名(camelCase)的props 名称要转为短横分隔命名(kebab-case).
<div id="app">
<my-component warning-text="提示信息"></my-component>
</div>
<script>
vue.component('my-component', {
props: ['warningText'],
template: '<div>{{warningText}}</div>'
});
var app = new Vue({
el: '#app'
})
</script>
注意:
- 如果使用的是 字符串模板,仍然可以忽略这些限制。
有时,传递的数据并不是直接写死的,而是来自父级的动态数据,此时可以使用指令 v-bind(:) 来动态绑定 props 的值,当父组件的数据变化时,也会传递给子组件。
<div id="app">
<input type="text" v-model="parentMessge">
<my-component :message="parentMessge"></my-component>
</div>
<script>
vue.component('my-component', {
props: ['message'],
template: '<div>{{message}}</div>'
});
var app = new Vue({
el: '#app',
data: {
parentMessage: ''
}
})
</script>
注意:
- 如果你要直接传递数字、布尔值、数组、对象,而且不使用 v-bind,传递的仅仅是字符串。
1.2.2 单向数据流
Vue 2.x 与 Vue 1.x 比较大的一个改变是:
Vue 2.x 通过 props 传递的数据是单向的,即父组件数据变化时会传递给子组件,但是反过来不行。
Vue 1.x 中提供了 .sync 修饰符来支持双向绑定。
设计成单向传递数据的原因是:尽可能将父子组件解耦,避免子组件无意中修改了父组件的状态
实际业务中,会经常遇到两种需要改变 prop 的情况:
- 父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件 data 内再声明一个数据,引用父组件的 prop
- prop 作为需要被转变的原始值传入。这种情况使用计算属性就可以了。
第一种情况示例代码;
<div id="app">
<my-component :init-count="1"></my-component>
</div>
<script>
vue.component('my-component', {
props: ['initCount'],
template: '<div :style="style">{{count}}</div>',
data: function () {
return {
count: this.initCount
}
}
});
var app = new Vue({
el: '#app'
})
</script>
组件中声明了数据 count,它在组件初始化时会获得来自父组件的 initCount,之后就与之无关了,只用维护 count, 这样就可以避免直接操作 initCount。
第二种情况示例代码:
<div id="app">
<my-component :width="100"></my-component>
</div>
<script>
vue.component('my-component', {
props: ['width'],
template: '<div :style="style">组件内容</div>',
computed: {
style: function () {
return {
width: this.width + 'px'
}
}
}
});
var app = new Vue({
el: '#app'
})
</script>
因为用 CSS 传递宽度要带单位(px),但是每次都写太麻烦,而且数值计算一般是不带单位的,所以统一在组件内使用计算属性就可以了。
1.2.3 数据验证
前面介绍的 props 选项的值都是一个数组,除了数组外,还可以是对象,当 prop 需要验证时,就需要对象写法。
一般当你的组件需要提供给别人使用,都推荐进行数据验证
比如某个数据必须是数字类型,如果传入字符串,就会在控制台弹出警告。
下面是几个 prop 的示例:
vue.component('my-component', {
props: {
//必须是数字类型
propA: Number,
//必须是字符串或数字类型
propB: [String, Number],
//布尔值,如果没有定义,默认值就是true
propC: {
type: Boolean,
default: true
},
//数字,而且是必转
propD: {
type: Number,
requuired: true
},
//如果是数组或对象,默认值必须是一个函数来返回
propE: {
type: array,
default: function () {
return [];
}
},
//自定义一个验证函数
propF: {
validator: function (value) {
return value > 10;
}
}
}
});
验证的 type 类型可以是:
- String
- Number
- Boolean
- Object
- Array
- Function
type 也可以是一个自定义构造器,使用 instanceof 检测。
当 prop 验证失败时,在开发版本下会在控制台抛出一条警告。
1.3 组件通信
父组件向子组件通信,通过 props 传递数据即可;
子组件和父组件通信,通过 $emit 即可。
但是Vue 组件通信的场景很多,归纳起来,组件关系分为:
- 父子组件通信
- 兄弟组件通信
- 跨级组件通信
1.3.1 自定义事件
指令 v-on 的作用:
- 监听 DOM 事件
- 用于组件之间的自定义事件
当子组件需要向父组件传递数据时,就要用自定义事件。
若你了解过 JavaScript 的设计模式—观察者模式,一定知道 dispatchEvent 和 addEventListener 这两个方法。 Vue 组件也有与之类似的一套模式,
子组件用 $emit() 来触发事件,父组件用 $on() 来监听子组件的事件。
注: emit 含义是 发出、发射,在这里即触发事件
父组件也可以直接在子组件的自定义标签上使用 v-on 来监听子组件触发的自定义事件。
示例代码如下:
<div id="app">
<p>总数: {{ total }}</p>
<my-component
@increase="handleGetTotal"
@reduce="handleGetTotal"></my-component>
</div>
<script>
// 全局注册组件
vue.component('my-component', {
template: '\
<div>\
<button @click="handleIncrease">+1</button>
<button @click="handleReduce">11</button>\
</div>',
data: function () {
return {
counter: 0
}
},
methods: {
handleIncrease: function () {
this.counter++;
this.$emit('increase', this.counter);
},
handleReduce: function () {
this.counter--;
this.$emit('reduce', this.counter);
}
}
});
var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleGetTotal: function (total) {
this.total = total;
}
}
})
</script>
分析:
- 子组件有两个按钮,分别实现加1 和减一的效果,在改变组件的 data “counter” 后,通过 $emit() 再把它传递给父组件。
- 父组件用 v-on: increase 和 v-on: reduce(示例使用的是语法糖)
- $emit() 方法的第一个参数是自定义事件的名称,例如示例的 increase 和 reduce 后面的参数都是要传递的数据,可以不填或填写多个。
除了用 v-on 在组件上监听自定义事件外,也可以监听 DOM 事件,这时可以用 .native 修饰符 表示监听的是一个原生事件,监听的是该组件的根元素,示例代码如下:
<my-component v-on:click.native="handleClick"></my-component>
1.3.2 使用 v-model
Vue 2.x 可以在自定义组件上使用 v-model 指令
<div id="app">
<p>总数: {{ total }}</p>
<my-component v-model="total"></my-component>
</div>
<script>
vue.component('my-component', {
template: '<button @click="handleClick">+1</button>',
data: function () {
return {
counter: 0
}
},
methods: {
handleClick: function () {
this.counter++;
this.$emit('input', this.counter);
}
}
});
var app = new Vue({
el: '#app',
data: {
total: 0
}
})
</script>
这里组件 $emit() 的事件名是特殊的 input, 在使用组件的父级,并没有在my-component上使用 @input=“handler”,而是直接用了 v-model 绑定的一个数据 total. 这也可以称作是一个语法糖,因为上面的示例可以间接地用自定义事件来实现:
<div id="app">
<p>总数: {{ total }}</p>
<my-component @input="handleGetTotal"></my-component>
</div>
<script>
//组件代码
vue.component('my-component', {
template: '<button @click="handleClick">+1</button>',
data: function () {
return {
counter: 0
}
},
methods: {
handleClick: function () {
this.counter++;
this.$emit('input', this.counter);
}
}
});
var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleGetTotal: function (total) {
this.total = total;
}
}
})
</script>
v-model 还可用来创建自定义的表单输入组件,进行数据双向绑定。
实现一个具有双向绑定的 v-model 组件要满足下面两个要求:
- 接收一个 value 属性
- 在有新的 value 时触发 input 事件
1.3.3 非父子组件通信
实际业务中,除了父子组件通信外,还有很多非父子组件通信的场景,非父子组件一般有两种:兄弟组件和跨多级组件。
vue.js 1.x 中,除了 $emit() 方法外,还提供了 $dispatch() 和 $broadcast() 这两个方法。
- $dispatch() 用于向上级派发事件,只要是它的父级(一级或多级以上),都可以在 Vue 实例的 events 选项内接收,示例代码如下:
<!-- 注意:该示例需使用 Vue.js 1.x 版本 -->
<div id="app">
{{ message }}
<my-component></my-component>
</div>
<script>
//组件代码
vue.component('my-component', {
template: '<button @click="handleDispatch">派发事件</button>',
methods: {
handleDispatch: function () {
this.$dispatch('on-message', '来自内部组件的数据');
}
}
});
var app = new Vue({
el: '#app',
data: {
message: ''
},
events: {
'on-message': function (msg) {
this.message = msg;
}
}
})
</script>
同理,$broadcast() 是由上级向下级广播事件的,用法完全一致,只是方向相反。
这两种方法一旦发出事件后,任何组件都是可以接收到的,就近原则,而且会在第一次接收到后停止冒泡,除非返回true.
虽然上面两个方法看起来很好用,但是 Vue.js 2.x 中都废弃了,原因是:
- 基于组件树结构的事件流的方式让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱
- 无法解决兄弟组件通信的问题。
在vue.js 2.x 中,推荐使用一个空的 Vue 实例 作为中央事件总线(bus),也就是一个中介。为更好理解它,举一个生活中的例子。
比如你需要租房子,你可能会找房产中介来登记你的需求,然后中介把你的信息发给满足要求的出租者,出租者再把报价和看房时间告诉中介,由中介再转达给你,整个过程中,买家和卖家没有任何交流,都是通过中间人传话的。
或者你最近可能要换房了,你会找中介登记你的信息,订阅与你找房需求相关的资讯,一旦有符合你的房子出现时,中介会通知你,并传达你房子的具体信息。
这两个例子,你和出租者就是两个跨级的组件,而房地产中介就是这个 中央事件总线(bus)。
示例代码如下:
<div id="app">
{{ message }}
<component-a></component-a>
</div>
<script>
var bus = new Vue();
Vue.component('component-a', {
template: '<button @click="handleEvent">传递事件</button>',
methods: {
handleEvent: function () {
bus.$emit('on-message', '来自组件 component-a 的内容');
}
}
});
var app = new Vue({
el: '#app',
data: {
message: ''
},
mounted: function () {
var _this = this;
//在实例初始化时,监听来自bus实例的事件
bus.$on('on-message', function (msg) {
_this.message = msg;
});
}
})
</script>
分析:
- 首先创建了一个名为 bus 的空 Vue 实例,里面没有任何内容;
- 然后全局定义了组件 component-a;
- 最后创建 Vue 实例app, 在 app 初始化时,也就是在声明周期 mounted 钩子函数里监听了来自 bus 的事件 on-message, 而在组件 component-a 中,点击按钮会通过 bus 把事件 on-message 发出去,此时 app 就会接收到来自 bus 的事件,进而在回调里完成自己的业务逻辑
这种方法巧妙而轻量地实现了任何组件间的通信,包含父子、兄弟、跨级,而且 Vue 1.x 和 Vue 2.x 都适用。如果深入使用,可以扩展 bus 实例,给它添加 data、methods、computed等选项,这些都是可以公用的。
在业务中,尤其是协同开发时非常有用,因为经常需要共享一些通用的信息,比如用户登录的昵称、性别、邮箱等,还有用户的授权 token 等。
只需要在初始化时让 bus 获取一次,任何时间、任何组件就可以从中直接使用了,在单页面富应用(SPA)中会很实用,进阶篇会介绍。
当你的项目比较大,有更多的人参与开发时,也可以选择更好的状态管理解决方案 vuex。
父链
在子组件中,使用 this.parent(中间少了一个符号,输入无法识别,见下面代码) 可以直接访问该组件的父实例或组件,父组件也可以通过 this.children(中间少了一个符号,输入无法识别,见下面代码) 访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件。
示例代码如下:
<div id="app">
{{ message }}
<component-a></component-a>
</div>
<script>
Vue.component('component-a', {
template: '<button @click="handleEvent">通过父链直接修改数据</button>',
methods: {
handleEvent: function () {
//访问到父链后,可以做任何操作,比如直接修改数据
this.$parent.message = '来自组件 component-a 的内容';
}
}
});
var app = new Vue({
el: '#app',
data: {
message: ''
}
})
</script>
实际业务中,子组件应尽可能避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只有组件自己能修改它的状态。
父子组件最好还是通过 props 和 $emit 来通信。
子组件索引
当子组件较多时,通过 this.$children 来遍历出我们需要的一个组件实例是比较困难的,尤其是组件动态渲染时,它们的序列是不固定的。
Vue提供了子组件索引的方法,用特殊的属性 ref 来为子组件指定一个索引名称,示例代码如下:
<div id="app">
<button @click="handleRef"></button>
<component-a ref="comA"></component-a>
</div>
<script>
Vue.component('component-a', {
template: '<div>子组件</div>',
data: function () {
return {
message: '子组件内容'
}
}
});
var app = new Vue({
el: '#app',
message: {
handleRef: function () {
//通过 $refs 来访问指定的实例
var msg = this.$refs.comA.message;
console.log(msg);
}
}
})
</script>
在父组件模板中,子组件标签上使用 ref 指定一个名称,并在父类组件内通过 this.$refs 来访问指定名称的子组件。
提示:
- $refs 只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用 $refs。
1.4 使用 slot 分发内容
1.4.1 什么是 slot
当需要让组件混合使用,混合父组件的内容与子组件的模板时,就会用到 slot,这个过程叫做内容分发。以 app组件 为例,它有两个特点:
- app 组件不知道它的挂载点会有什么内容。挂载点的内容是由 app 的父组件决定的。
- app 组件很可能有它自己的模板。
props 传递数据、events 触发事件 和 slot 内容分发 构成了 Vue 组件的 3 个 API 来源,再复杂的组件也是由这 3 部分构成的。
1.4.2 作用域
正式介绍slot 前,需先知道一个概念:编译的作用域。比如父组件中有如下模板:
<child-component>
{{ message }}
</child-component>
此处的 message 就是一个 slot,但是它绑定的是父组件的数据,而不是组件 child-component 的数据。
父组件模板的内容是在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。
例如下面的示例代码:
<div id="app">
<child-component v-show="showChild"></child-component>
</div>
<script>
Vue.component('child-component', {
template: '<div>子组件</div>'
});
var app = new Vue(
el: '#app',
data: {
showChild: true
}
)
</script>
这里的状态 showChild 绑定的是 父组件的数据,如果想在子组件上绑定,那应该是:
<div id="app">
<child-component ></child-component>
</div>
<script>
Vue.component('child-component', {
template: '<div v-show="showChild">子组件</div>',
data: {
showChild: true
}
});
var app = new Vue(
el: '#app'
)
</script>
因此,slot 分发的内容,作用域是在父组件上的。
1.4.3 slot 用法
单个slot
在子组件内使用特殊的 slot 元素就可以为这个子组件开启一个 slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的slot标签 及它的内容。示例代码如下:
<div id="app">
<child-component>
<p>分发的内容</p>
<p>更多分发的内容</p>
</child-component>
</div>
<script>
Vue.component('child-component', {
template: '\
<div>\
<slot>\
<p>如果父组件没有插入内容,我将作为默认出现</p>\
</sot>\
</div>',
});
var app = new Vue(
el: '#app'
)
</script>
注意:
- 子组件 slot 内的备用内容,它的作用域是子组件本身。
具名slot
给 slot 元素指定一个 name 后可以分发多个内容,具名 Slot 可以与单个 Slot 共存,示例如下:
<div id="app">
<child-component>
<h2 slot="header">标题</h2>
<p>正文内容</p>
<p>更多的正文内容</p>
<div slot="footer">底部信息</div>
</child-component>
</div>
<script>
Vue.component('child-component', {
template: '\
<div class="container">\
<div class="header">\
<slot name="header"></slot>\
</div>\
<div class="main">\
<slot></slot>\
</div>\
<div class="footer">\
<slot name="footer"></slot>\
</div>\
</div>',
});
var app = new Vue(
el: '#app'
)
</script>
子组件内声明了 3 slot 元素,其中在 div class=“main” 内的 slot 没有使用 name 特性,它将作为默认 slot 出现,父组件没有使用 slot 特性的元素与内容都将出现在这里。
如果没有指定默认的匿名 slot,父组件内多余的内容片段都将抛弃。
上面例子最终渲染后的结果为:
<div id="app">
<div class="header">
<h2>标题</h2>
</div>
<div class="main">
<p>正文内容</p>
<p>更多的正文内容</p>
</div>
<div class="footer">
<div>底部信息</div>
</div>
</div>
在组合使用组件时,内容分发 API 至关重要。
1.4.4 作用域插槽
作用域插槽是一种特殊的 slot, 使用一个可以复用的模板 替换 已渲染元素。这个概念比较难理解,先放着。
1.4.5 访问 slot
在 Vue.js 1.x 中,想要获取某个 slot 是比较麻烦的,需要用 v-el 间接获取。而在 2.x 提供了用来访问被 slot 分发的内容的方法 $slots.
通过 $slots 可以访问某个具名 slot, a(含义见下) 包括了所有没有被包含在具名 slot 中的节点。
a 表示 this.$slots.default
$slots 在业务中几乎用不到,在用 render 函数创建组件时会比较有用,但主要还是用于独立组件开发中。
参考资料:vue.js 实战