深入了解组件
传递静态或动态Prop
传入静态的值:
<blog-post title="My journey with Vue"></blog-post>
这时候值是一个字符串
你也可以通过v-bind动态赋值:
<blog-post v-bind:title="post.title"></blog-post>
这时候值是一个js表达式
传入一个布尔值
// 包含该prop没有值的情况在内,都意味着true
<blog-post is-pushlished></blog-post>
<blog-post :is-pushlished="true"></blog-post>
传入一个对象的所有属性
如果你想要将一个对象的所有属性都作为prop传入,你可以使用不带参数的v-bind。例如,对于一个给定的对象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的更新会向下流动到子组件中,但是反过来不行。这样会防止从子组件意外改变父组件的状态,从而导致你的应用数据流向难以理解。额外的,每次父级组件发生更新时,子组件中所有prop都将会刷新为最新的值。
总结:prop会在子组件创建之前传递,父组件通过prop向子组件传递基本数据类型和引用数据类型,如果是基本数据类型,这时候改变props的数据就会报错,如果是引用数据类型如果改变原始数据不会报错,但是重新赋值或改变某个属性值就会报错
prop验证
Vuecomponent('my-component', {
props: {
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 是否必传,如果为true,则父组件必须要传这个值,否则报错
propC: {
type: String,
required: true
},
// 带有默认值的对象
propD: {
type: Object,
default: () => { message: 'hello' }
},
// 自定义验证函数,如果不符合条件就会报错
propE: {
validator: (value) => {
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})
注意:prop会在一个组件实例创建之前进行验证,所以实例的属性(如data、computed等)在default或validator函数中是不可用的
非prop的特性
一个非prop特性是指传向一个组件,但是该组件并没有相应prop定义的特性。
<bootstrap data-date-picker="activated"></bootstrap>
data-date-picker="activated"这个特性就会自动添加到<bootstrap>的根元素上。
替换/合并已有的特性
假如子组件<bootstrap-date-input>的模板是这样的:
<input type="date" class="form-control">
这时候我们在子组件上添加一个type和一个class:
<bootstrap-date-input type="text" class="date-picker"></bootstrap-date-input>
子组件的模版上已经有了这两个属性了,这时候我们发现子组件内部设置的type被外部的type替换成text,但是class合并成form-control和date-picker。
总结:除了class和style特性外部和内部的根元素会合并起来,其他的值会被外部替换掉
禁用特性继承
如果你不希望组件的根元素继承特性,你可以在组件的选项中设置inheritAttrs:false。例如:
Vue.component('my-component', {
inheritAttrs:false,
....
})
这尤其适合配合实例的$attrs属性使用,该属性包含了传递给一个组件的特性名和特性值。
自定义事件
model属性
一个组件上的v-model默认会利用名为value的prop和名为input的事件,但是像单选框、复选框等类型的输入控件可能会将value特性用于不同的目的。model选项可以用来避免这样的冲突:
<base-checkbox v-model="lovingVue"></base-checkbox>
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)"
>
`
})
上面我们可以看到model将从父组件传过来的属性不管是vlaue还是checked统一名称为checked,监听的事件不管是@input还是@change统一为change,这样子就不需要根据不同的情况写不同的属性和方法。
这里的lovingVue的值将会传入这个名为checked的prop。同时当<base-checkbox>触发一个change事件并附带一个新的值的时候,这个logingVue的属性将会被更新。
注意你仍然需要在组件的props选项里声明checked这个prop。
.sync修饰符
在有些情况下,我们可能需要对一个prop进行双向绑定。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,造成理解困难。
这时候我们可以使用.sync修饰符,它的本质和v-model类似,只是一种缩写。
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
上面的代码使用.sync就可以写成
<text-document v-bind:title.sync="doc.title"></text-document>
这样在子组件中,就可以通过下面的代码来实现对这个prop重新赋值的意图了。
this.$emit('update:title', newTitle)
v-model和.sync背景
父子组件传递数据通过props,props传递的数据是单向数据流,但如果传递的是引用数据类型,那么改变子组件传递过来的值父组件的值也会发生改变,这样子虽然可以实现父子组件的双向绑定,但会牺牲数据流向的简洁性,使得数据难以理解,最好不要这样去做,在这个背景下v-model和.sync就出现了,这两种方式都实现父子组件数据的双向绑定。
v-mode和.sync对比
.sync从功能上看和v-model十分类似,都是为了实现数据的双向绑定,本质上也都不是真正的双向绑定,而是语法糖。
相比较之下,.sync更加灵活,它可以给多个prop使用,而v-model在一个组件中只能有一个。
从语义上来看,v-model绑定的值是指这个组件的绑定值,比如input组件,select组件,各种表单元素,这些组件所绑定的值使用v-model比较合适,因为v-model是一个语法糖,传递的各种类型的值比如vlaue、checked等等,vue已经处理了各种情况,不需要自己去分别定义传递的属性。其他情况没有这种语义,.sync会更合适。
从写法上来看,.sync更加简洁,不需要在 子组件中定义props就可以直接实现数据双向绑定。
这两种方式子组件都没有直接改变prop的值,只是通过事件监听的方式直接改变父组件的值。
注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的属性名,类似 v-model。
当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:
<text-document v-bind.sync="doc"></text-document>
这样会把 doc 对象中的每一个属性 (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。
将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
参考文章:https://juejin.im/post/5d0489dff265da1ba84a8e41
插槽
我们为具名插槽和作用域插槽引入了一个新的统一的语法,即v-slot指令。它取代了slot和slot-scope这两个目前已被废弃但未被移除且仍在文档中的特性。
插槽内容
Vue实现了一套内容分发的API,将slot元素作为承载分发内容的出口,也被称为插槽,插槽的内容是写在自定义组件中间,插槽内容可以是包含任何模版代码或其他组件,这些内容会将slot元素替换,如果自定义组件内没有插槽,那么这个组件中间写的任何内容都会被抛弃。
编译作用域
当你想在一个插槽内容中使用数据时,例如:
<template>
<div id="demo">
<navigation-link url="/profile">
name: {{name}}
url: {{url}}
</navigation-link>
</div>
</template>
<script>
import navigationLink from './navigationLink'
export default {
components: {navigationLink},
data () {
return {
name: 'jack'
}
}
}
</script>
你会发现插槽内容跟模版其他地方一样可以访问相同的实例属性(也就是相同的作用域),而不能访问navigation-link子组件的作用域。
作为一条规则,请记住:父级模版里的所有内容都是在父级作用域中编译的,子模版里的所有内容都是在子作用域中编译的。
后备内容(备用内容)
如果插槽内没有内容,我们希望能显示默认的内容,这时候我们可以在slot元素中写,如果没有插槽内容就会显示slot元素中内容,如果有内容就会替换slot元素中的内容。
具名插槽
有时我们需要多个插槽,例如对于一个带有如下模版的<base-layout>组件:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
一个不带name的slot出口会带有隐含的名字default。
在向具名插槽提供内容的时候,我们可以在一个template元素上使用v-slot指令,并以v-slot的参数的形式提供其名称:
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
现在 <template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。
注意:v-slot只能添加在一个template元素上(只有一种例外情况就是独占默认插槽的缩写语法,下面会讲到)
作用域插槽
如果我们想让插槽内容访问组件内的数据,这时候我们可以使用作用域插槽,将数据作为slot元素的一个特性绑定上去:
<span>
<slot v-bind:user="user">
{{ user.lastName }}
</slot>
</span>
绑定在slot元素上的特性被称为插槽porp。在父级作用域中,我们可以给v-slot带一个值来定义我们提供的插槽prop的名字:
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
独占默认插槽的缩写语法
当组件内只有默认插槽时,组件的标签就可以被当作插槽的模版来使用。这样我们就可以把v-slot直接用在组件上:
<current-user v-slot:default="slotProps">
{{slotProps.user.firstName}}
</current-user>
// 不带参数的v-slot被假定对应默认插槽,可以简写为
<current-user v-slot="slotProps">
{{slotProps.user.firstName}}
</current-user>
注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确,只要出现多个插槽,请始终为所有的插槽使用完整的基于<template>的语法。
解构插槽prop
<current-user v-slot="{ user }">
{{ user.firstName }}
</current-user>
// prop重命名
<current-user v-slot="{ user: person }">
{{ person.firstName }}
</current-user>
// 插槽prop是undefined
<current-user v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
</current-user>
动态插槽名
动态指令参数也可以用在v-slot上,来定义动态的插槽名:
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
具名插槽的缩写
跟v-on和v-bind一样,v-slot也有缩写,即把参数之前的所有内容(v-slot:)替换为字符#。例如v-slot:header可以重写为#header:
<base-layout>
<template #header>
<h1>Here might be a page title</h1>
</template>
</base-layout>
如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:
<current-user #default="{ user }">
{{ user.firstName }}
</current-user>
总结:插槽有点像一个单独的组件,可以定义它的名称,也可以传值给插槽内容,插槽在设计基础组件的时候非常有用,比如我之前写的弹框组件,element-ui中的很多组件都有用到插槽,你可以在组件里面写你自定义的其他组件。
处理边界的情况
在大多数情况下,我们最好不要触达另一个组件实例内部或手动操作DOM元素。比如使用$refs、$parent、$children、$root等等,最好使用Vuex来管理应用的状态。
混入
混入提供了一种非常灵活的方式,来分发Vue组件中的可复用功能。
选项合并
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行合并。
data数据合并
data中数据有同名选项时,如果是基本类型就会以组件数据为准,如果是引用类型就会合并。
生命周期合并
生命周期里的代码都会执行,并且混入对象的代码会先执行。
methods、components、directives
这三个对象将被合并为同一个对象,如果发生同名选项,以组件的为准。
注意:Vue.extend()也使用同样的策略进行合并。
自定义指令
基本用法
除了核心功能默认内置的指令比如v-model、v-show,Vue也允许注册自定义指令。一般用在需要对普通DOM元素进行底层操作,这时候就会用到自定义指令。比如业务需求是input输入框初始是聚焦的状态:
// 注册一个全局自定义指令v-focus
Vue.directive('focus', {
// 当被绑定的元素插入到DOM中时
inserted (el, binding) {
el.focus()
}
})
// 注册一个局部的指令
directives: {
focus: {
inserted (el, binding) {
el.focus()
}
}
}
动态指令参数
指令的参数可以是动态的。例如,在v-myditective:[argument]="value"中,argument参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用。
<div id="dynamicexample">
<h3>Scroll down inside this section ↓</h3>
<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>
Vue.directive('pin', {
bind: function (el, binding, vnode) {
el.style.position = 'fixed'
var s = (binding.arg == 'left' ? 'left' : 'top')
el.style[s] = binding.value + 'px'
}
})
new Vue({
el: '#dynamicexample',
data: function () {
return {
direction: 'left'
}
}
})
过滤器
Vue允许你自定义过滤器,可被用于一些常见的文本格式化,或者是一些数据转换等等,但是过滤器在实践当中可以被其他方式取代,比如写一些公共方法去处理或者直接直接在methods去调用某个方法等等,所以现在用到的越来越少,这个功能就显得有点鸡肋。不过它有一个优势是别的方法实现比较麻烦的,链式调用,后面会讲到。
基本用法
过滤器可以用在两个地方:双花括号插值和v-bind表达式。
// 在双花括号中
{{ message | capitalize }}
// 在v-bind中
<div v-bind="rawId | formatId"></div>
你可以在一个组件的选项中定义本地的过滤器:
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
或者在创建Vue实例之前全局 定义过滤器:
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
new Vue({
// ...
})
过滤器可以串联:
{{ message | filterA('arg1', 'arg2') | filterB }}
在这个例子中,message表达式将作为filterA过滤器函数的第一个参数,'arg1'和'arg2'将作为第二个、第三个参数,filterA函数返回的结果又会作为filterB函数的第一个参数。
key
key的特殊属性主要用在Vue虚拟DOM算法,在新旧nodes对比时辨识vnodes。如果不实用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用key,它会基于key的变化重新排列元素顺序,并且会移除key不存在的元素。
有相同父元素的子元素必须有特殊的key。重复的key会造成渲染错误。它可以用于强制替换元素/组件而不是重复使用它。
Vue.extend()
使用基础Vue构造器,创建一个'子类'。参数是一个包含组件选项的对象,data选项必须是函数。
<div id="mount-point"></div>
// 创建构造器
var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
结果如下:
<p>Walter White aka Heisenberg</p>
Vue.observable()
让一个对象可响应。Vue内部会用它来处理data函数返回的对象。
返回的对象可以直接用于渲染函数和计算属性内,并且会在发生改变时触发相应的更新。也可以作为最小化的跨组件状态存储器,用于简单的场景:
const state = Vue.observable({ count: 0 })
const Demo = {
render(h) {
return h('button', {
on: { click: () => { state.count++ }}
}, `count is: ${state.count}`)
}
}