深入了解组件
一、组件注册
1、组件名 & 注册
- 组件名最好全部小写,并且用连字符隔开,比如:
test-component
- 关于全局注册和局部注册的内容参考基础部分
二、Prop属性
- prop 大小写
<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>
<!-- 组件中这么写 -->
props: ['postTitle']
- prop类型
//常规数组写法
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
//指定具体类型的对象写法
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // or any other constructor
}
传递静态或动态 prop
- 不用 v-bind 就只是一个字符串,用了就是一个变量或者一个表达式,一般都是要用 v-bind 的
- 传递 数字、布尔、数组、对象 都是遵从上面的规则的,传递一个对象的所有属性的写法比较特殊
<!-- 静态,就是只一个字符串 -->
<blog-post title="My journey with Vue"></blog-post>
<!-- 动态要么是一个变量值,要么就是一个 js 表达式 -->
<blog-post v-bind:title="post.title"></blog-post>
<!-- 传递一个对象的所有属性 -->
post: {
id: 1,
title: 'My Journey with Vue'
}
<blog-post v-bind="post"></blog-post>
<!-- 二者是等价的 -->
<blog-post
v-bind:id="post.id"
v-bind:title="post.title"
></blog-post>
单向数据流
- 所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行
- 注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态
Prop验证
- 注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的属性 (如 data、computed 等) 在 default 或 validator 函数中是不可用的
- 不满足验证的条件的话,控制台会输出警告信息
Vue.component('my-component', {
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
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 ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})
非Prop属性
<bootstrap-date-input data-date-picker="activated"></bootstrap-date-input>
- 这里的
bootstrap-date-input
是一个第三方的组件,data-date-picker
是一个属性,但是这个属性并没有在组件的 props 里面定义,data-date-picker="activated"
特性就会自动添加到<bootstrap-date-input>
的根元素上
合并替换已有属性
- 组件
<bootstrap-date-input
data-date-picker="activated"
class="date-picker-theme-dark"
></bootstrap-date-input>
- 组件的 template
<input type="date" class="form-control">
- 外部传入的属性大部分会替换掉内部属性,比如传入一个
type=text
的话,template里面的type属性就会被替换掉, - 但是style和class是比较特殊的,这两个属性,内部和外部的值会进行合并
禁用属性继承
- 有了
inheritAttrs: false
和$attrs
,可以手动决定这些特性会被赋予哪个元素,在使用基础组件的时候更像是使用原始的 HTML 元素
<h2>禁用属性继承</h2>
<div id="input-cus">
<base-input label="haha" value="default1" cus-sttr="cusattr" v-model="username">
</base-input>
<br>
<span>{{username}}</span>
</div>
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
`
});
var input_cus = new Vue({
el:"#input-cus",
data:{
username:"haha"
}
});
三、自定义事件
- 事件名推荐全部小写,否则可能出现无法监听的情况
1、自定义组件v-model
<div id="event1">
<base-checkbox v-model="lovingVue"></base-checkbox>
<br>
<span>{{lovingVue}}</span>
</div>
- 单选框、复选框等类型的输入控件需要使用 model 选项,因为他们的 value 有別的用途
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
});
new Vue({
el:"#event1",
data:{
lovingVue:false
}
});
2、将原生事件绑定到组件
使用 native 修饰符
<h2>原生事件绑定native</h2>
<div id="event1">
<base-input v-on:focus.native="onFocus"></base-input>
</div>
<script>
Vue.component('base-input', {
props:['value'],
template: `
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
`
});
new Vue({
el:"#event1",
methods:{
onFocus:function (){
alert("test onFocus");
}
}
});
</script>
使用 $listeners 属性
- 上面的例子中如果模板的最外层的元素不是input的话而是label的话,就只能这样来实现了
<div id="event1">
<base-input v-on:focus="onFocus" v-model="modelv"></base-input>
{{modelv}}
</div>
<script>
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
computed: {
inputListeners: function () {
var vm = this;
// `Object.assign` 将所有的对象合并为一个新对象
return Object.assign({},
this.$listeners,
{
input: function (event) {
vm.$emit('input', event.target.value)
}
}
)
}
},
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
>
</label>
`
});
new Vue({
el:"#event1",
data:{
modelv:"test-vmodel"
},
methods:{
onFocus:function () {
alert('focus me');
}
}
});
</script>
1、父组件可以使用 props 把数据传给子组件
2、子组件可以使用 $emit 触发父组件的自定义事件
3、sync 修饰符
- 正常情况组件的属性都是单向下行传递的
- 使用 sync 修饰符可以触发事件进行更新的方法来间接实现数据的双向绑定
<!--设置一个属性-->
<text-document v-bind:title.sync="doc.title"></text-document>
<!--设置多个属性-->
<text-document v-bind.sync="doc"></text-document>
四、插槽
- slot 插槽可以分发的内容:文本、html元素、组件
- 父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的
- slot插槽可以设置后备内容(类似于变量的默认值):父级组件中不提供内容的时候默认值就是这个了
<button type="submit">
<slot>Submit</slot>
</button>
- 要分发多个内容的时候可以使用有名字的 slot:v-slot 指令只能使用在 template 上
<div id="lay1">
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<template 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>
<script>
Vue.component('base-layout',{
template:`
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
`
});
new Vue({
el:'#lay1'
})
</script>
实例属性在父组件和子组件中都是可以访问,之所以组件注册的时候在根节点定义
1、作用域插槽
- 提供的组件带有一个可从子组件获取数据的可复用的插槽
- 在 2.5.0+,slot-scope 不再限制在
<template>
元素上使用,而可以用在插槽内的任何元素或组件上
<h2>作用域插槽</h2>
<div id="div1">
<!--<todo-list :todos="todos"></todo-list>-->
<todo-list v-bind:todos="todos">
<template slot-scope="{ todo }">
<span v-if="todo.isComplete">✓</span>
{{ todo.text }}
</template>
</todo-list>
</div>
<script>
Vue.component('todo-list', {
props: ['todos'],
template:
`
<ul>
<li
v-for="todo in todos"
v-bind:key="todo.id"
>
<slot v-bind:todo="todo">
{{ todo.text }}
</slot>
</li>
</ul>
`
});
new Vue({
el: "#div1",
data: {
todos: [
{id: 1, text: 'test1',isComplete:true},
{id: 2, text: 'test2',isComplete:true},
{id: 3, text: 'test3',isComplete:false},
{id: 4, text: 'test4',isComplete:true}
]
}
})
</script>
五、动态组件 & 异步组件
1、动态组件
- 就是前面讲到的使用
is
来进行组件切换的 - 这里只是用
keep-alive
的标签将component
包裹起来将失活的组件缓存起来,再次切换的时候进行激活,避免组件的重复渲染
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
2、异步组件
- 以一个工厂函数的方式定义组件,这个工厂函数会异步解析组件定义
- 在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染
- 工厂函数会收到一个 resolve 回调,这个回调函数会在你从服务器得到组件定义的时候被调用
- 可以调用 reject(reason) 来表示加载失败
- 这里的 setTimeout 是为了演示用的,实际使用如何获取组件需要自己定义
<h2>异步组件</h2>
<div id="div2">
<async-example></async-example>
</div>
<script>
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: '<div>I am async!</div>'
})
}, 5000)
});
new Vue({
el:'#div2'
});
</script>
六、处理边界情况
1、访问元素 & 组件
访问根实例
// Vue 根实例
new Vue({
data: {
foo: 1
},
computed: {
bar: function () { /* ... */ }
},
methods: {
baz: function () { /* ... */ }
}
})
// 获取根组件的数据
this.$root.foo
// 写入根组件的数据
this.$root.foo = 2
// 访问根组件的计算属性
this.$root.bar
// 调用根组件的方法
this.$root.baz()
访问父级组件实例
google-map-markers
访问google-map
的map
属性难以确定到底有多少层的包裹(要写几个$parent
)
<google-map>
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>
<google-map>
<google-map-region v-bind:shape="cityBoundaries">
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map-region>
</google-map>
var map = this.$parent.map || this.$parent.$parent.map
访问子组件实例或子元素
依赖注入
2、程序化的事件侦听器
侦听事件的三个方法
- 通过 $on(eventName, eventHandler) 侦听一个事件
- 通过 $once(eventName, eventHandler) 一次性侦听一个事件
- 通过 $off(eventName, eventHandler) 停止侦听一个事件
$emit、$on, 和 $off
和dispatchEvent、addEventListener 和 removeEventListener
并不是一回事
一个具体的示例
- 原来的代码
// 一次性将这个日期选择器附加到一个输入框上
// 它会被挂载到 DOM 上。
mounted: function () {
// Pikaday 是一个第三方日期选择器的库
this.picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
},
// 在组件被销毁之前,
// 也销毁这个日期选择器。
beforeDestroy: function () {
this.picker.destroy()
}
- 修改后的代码
<script src="https://unpkg.com/pickaday@1.7.0"></script>
<div id="app">
<input ref="dateInput" v-model="date" type="date">
</div>
<script>
new Vue({
el: '#app',
data: {
date: null
},
mounted: function () {
var picker = new Pikaday({
field: this.$refs.dateInput,
format: 'YYYY-MM-DD'
});
this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}
})
</script>
3、循环引用
递归组件
- 这样的递归调用要避免,递归一定要有结束条件
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
组件之间的循环引用
<div id="file">
<tree-folder :folder="folder"></tree-folder>
</div>
<script>
Vue.component('tree-folder',{
props:['folder'],
template:`
<p>
<span>{{ folder.name }}</span>
<tree-folder-contents :children="folder.children"/>
</p>
`
});
Vue.component('tree-folder-contents',{
props:['children'],
template:`
<ul>
<li v-for="child in children">
<tree-folder v-if="child.children" :folder="child"/>
<span v-else>{{ child.name }}</span>
</li>
</ul>
`
});
new Vue({
el:'#file',
data:{
folder:{
name:'folder1',
children:[
{name:'file1',children:null,content:'file1-content'},
{
name:'folder2',content:null,children:[
{name:'file2',children:null,content:'file2-content'},
{name:'file3',children:null,content:'file3-content'},
{name:'file4',children:null,content:'file4-content'}
]
}
]
}
}
})
</script>
4、模板定义的替代品
- 内联模板
<my-component inline-template>
<div>
<p>These are compiled as the component's own template.</p>
<p>Not parent's transclusion content.</p>
</div>
</my-component>
- X-Templates
<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>
<script >
Vue.component('hello-world', {
template: '#hello-world-template'
})
</script>
- 总结:最好还是使用 template 选项来定义模板,以上的二者都有一定的弊端
5、控制更新
强制更新:
- 得益于 vue 的响应式系统,一般情况是不需要强制更新的,
- 如果需要的话很可能是自己的代码有疏漏,比如没有留意到数组或对象的变更检测注意事项,或者依赖了一个未被 Vue 的响应式系统追踪的状态
- 非要强制更新的话就用
vm.$forceUpdate()
,这个方法是迫使 vue 实例重新渲染
通过 v-once 创建低开销的静态组件
- 组件包含了大量静态内容,可以在根元素上添加 v-once 特性以确保这些内容只计算一次然后缓存起来
- 这个东西也是有弊端的,不要随意滥用
Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`
})