组件注册
组件名
推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。
组件名大小写
定义组件名的两种方式
1 | // kebab-case(短横线分隔命名) |
当使用PascalCase定义组件后,引用组件时,两种命名风格均可使用。
全局注册
1 | Vue.component('my-component-name', { |
当直接在Vue上注册组件时,该组件是全局注册的,在任何新创建的 Vue 根实例 (new Vue
) 的模板中均可以使用。
局部注册
当使用webpack这样的构建系统时,全局注册的方法会导致当不再使用该组件,仍包含在构建结果中。
可以通过定义一个普通的js对象定义组件,之后在需要使用时引用该对象。
1 | var ComponentA = { /* ... */ } |
模块系统
在模块系统中局部注册
1 | // 在ComponentB中 |
基础组件的自动化全局注册
思路:通过目录+文件名识别出基础组件,将其统一注册为全局组件,详见:基础组件的自动化全局注册
Prop
Prop的大小写(camelCase vs kebab-case)
HTML 中的 attribute 名是大小写不敏感的,因此向组件传递值时,使用kebab-case形式。
1 | Vue.component('blog-post', { |
1 | <!-- 在 HTML 中是 kebab-case 的 --> |
Prop类型
1 | props: { |
传递静态或动态Prop
使用v-bind
向Prop传值,意味着动态传入,具体内容看后面的js表达式;不使用v-bind
,则直接传入的是字符串。
传入数字
1 | <!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue --> |
传入布尔值
1 | <!-- 包含该 prop 没有值的情况在内,都意味着 `true`。--> |
传入数组
1 | <!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue --> |
传入对象
1 | <!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue --> |
传入对象的所有属性
将一个对象的所有属性传入prop,可以不用将所有参数单独传递。
1 | post: { |
1 | <blog-post v-bind="post"></blog-post> |
单向数据流
prop形成了从父组件到子组件的单向数据传输,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。不应该在子组件中直接变更prop的值,当需要变更prop时,可以采用以下两种操作:
1.将传输的值作为本地数据使用:定义一个本地的data
1 | props: ['initialCounter'], |
2.需要对prop传输的值进行一定转换:使用计算属性
1 | props: ['initialCounter'], |
注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。
Prop验证
除了定义prop的类型,还可以定制prop的验证方式
1 | Vue.component('my-component', { |
注意 prop 会在一个组件实例创建之前进行验证,所以实例的 property (如
data
、computed
等) 在default
或validator
函数中是不可用的。(不能动态设置验证规则)
类型检查
type
可以是下列原生构造函数中的一个:
String
Number
Boolean
Array
Object
Date
Function
Symbol
额外的,type
还可以是一个自定义的构造函数,并且通过 instanceof
来进行检查确认。例如,给定下列现成的构造函数:
1 | function Person (firstName, lastName) { |
1 | // 验证 author prop 的值是否是通过 new Person 创建的。 |
非Prop的Attribute
替换/合并已有Attribute
由prop接收的Attribute可以直接使用,未接收的Attribute将绑定到根元素(显示在HTML内容中)上且替换掉已有内容(style、class除外,会合并内容)
禁用Attribute继承
不希望组件的根元素继承 attribute,可以在组件的选项中设置 inheritAttrs: false
,此时这些Attribute将绑定到实例的$attrs
属性上,通过调动该属性进一步获取相关内容。
自定义事件
事件名
不同于组件和 prop,事件名不存在任何自动化的大小写转换;
并且 v-on
事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的);
因此,推荐始终使用 kebab-case 的事件名。
自定义组件的v-model
组件上v-model
语法糖默认利用了名为 value
的 prop 和名为 input
的事件。但单选框、复选框等组件的value有可能用于其他目的,此时在组件中配置model
选项可以避免这样的冲突。
理解:对于单选框、复选框等组件,数据双向绑定的用处通常为改变选中状态。该属性在元素中由checked控制,但v-model
默认使用value属性绑定值,因此想要用v-model
控制是否勾选,就要将原本绑定到value属性的值绑定到checked上,且状态改变时也要将checked的值回传。
因此使用model
属性声明传入的prop用于checked,触发的事件为change。同时规定仍需要在组件的props中声明传入的值给到了checked。
1 | Vue.component('base-checkbox', { |
原生事件绑定到组件
想要在一个组件的根元素上直接监听一个原生事件,可以使用 v-on
的 .native
修饰符:
1 | <base-input v-on:focus.native="onFocus"></base-input> |
预期为子组件为input,当其获取焦点时,触发onFocus
方法。但当子组件被重构为非input类型元素时,该方法将无法被调用。
1 | <label> |
为了解决这个问题,Vue 提供了一个 $listeners
property,它是一个对象,里面包含了作用在这个组件上的所有监听器。如:
1 | { |
将其绑定到子组件上,即可不使用.native
方法也能监听各种事件。
1 | Vue.component('base-input', { |
此时,父组件想要监听子组件的Focus方法,直接监听即可,因为子组件会通过$listeners
将父组件想要监听的focus
事件绑定到自身。父组件等待触发即可。
1 | <base-input v-on:focus="onFocus"></base-input> |
.sync
修饰符
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件两侧都没有明显的变更来源。
此时推荐以 update:myPropName
的模式触发事件取而代之。
举个例子,在一个包含 title
prop 的假设的子组件中,我们可以用以下方法表达对其赋新值的意图:
1 | this.$emit('update:title', newTitle) |
之后父组件监听update:title
事件并根据获取的值更新本地数据:
1 | <text-document |
为了方便起见,为这种模式在父组件中提供一个缩写,即 .sync
修饰符:
1 | <text-document v-bind:title.sync="doc.title"></text-document> |
插槽
插槽内容
引用子组件时,组件名中间的内容会替换子组件中的<slot></slot>
。若子组件中未定义<slot></slot>
,则传递的内容将被舍弃。
1 | <!-- 父组件中 --> |
编译作用域
动态传递内容时,不同组件不能直接互相读取对方的内容。
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
后备内容
子组件中,<slot></slot>
中间可以填入默认内容,当父组件未传递内容时,将显示默认内容;传递内容时,该默认内容将被替换掉。
具名插槽
当需要多个插槽时,通过slot
元素的name
属性定义额外的插槽
1 | <div class="container"> |
一个不带 name
的 <slot>
出口会带有隐含的名字“default”。
使用时在一个 <template>
元素上使用 v-slot
指令,并以 v-slot
的参数的形式提供其名称
1 | <base-layout> |
此时,含有v-slot:header
属性的template中的内容将替换组件中的<slot name="header"></slot>
未设置该属性的,将替换掉组件中的<slot></slot>
注意 v-slot
只能添加在 <template>
上
作用域插槽
为了让插槽能够访问到子组件中的数据,可以将子组件数据绑定到<slot></slot>
上,进而在父组件中接收该值并使用。
1 | <!-- 子组件 --> |
1 | <!-- 父组件 --> |
独占默认插槽的缩写
当被只有默认插槽时,父组件中可以省略<template>
,直接将 v-slot
用在组件上:
1 | <current-user v-slot:default="slotProps"> |
进一步简写,可以省去default
:
1 | <current-user v-slot="slotProps"> |
但当存在多个插槽时,仍需为所有的插槽采用完整的基于 <template>
的语法:
1 | <current-user> |
1 | <template> |
解构插槽Prop
传回父组件的插槽内容可以用ES6解构:
1 | <!-- 独占默认插槽简写 --> |
甚至可以定义后备内容,在子组件传回undefined时的情形:
1 | <!-- 子组件插槽中传回的内容中无法解析user属性时,使用默认值 --> |
动态插槽名
1 | <base-layout> |
具名插槽的缩写
使用 #
代替v-slot
,此时后面必须跟插槽名(包括默认插槽)
1 | <base-layout> |
动态组件&异步组件
在动态组件上使用 keep-alive
在多标签页面使用is
切换不同组件时,组件会经历创造和销毁的过程。
1 | <component v-bind:is="currentTabComponent"></component> |
当在这些组件之间切换的时候,有时会想保持这些组件的状态,以避免反复重新渲染导致的性能问题。
为了解决这个问题,可以用一个 <keep-alive>
元素将其动态组件包裹起来。
1 | <!-- 失活的组件将会被缓存!--> |
注意这个
<keep-alive>
要求被切换到的组件都有自己的名字,不论是通过组件的name
选项还是局部/全局注册。
异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。例如:
1 | Vue.component('async-example', function (resolve, reject) { |
一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用:
1 | Vue.component('async-webpack-example', function (resolve) { |
也可以在工厂函数中返回一个 Promise
,所以把 webpack 2 和 ES2015 语法加在一起,可以这样使用动态导入:
1 | Vue.component( |
当使用局部注册的时候,你也可以直接提供一个返回 Promise
的函数:
1 | new Vue({ |
处理加载状态
异步组件工厂函数也可以返回一个如下格式的对象:
1 | const AsyncComponent = () => ({ |
边界情况
访问元素&组件
访问根实例
在每个 new Vue
实例的子组件中,其根实例可以通过 $root
property 进行访问。
1 | // 获取根组件的数据 |
在绝大多数情况下,强烈推荐使用
Vuex
来管理应用的状态。
访问父组件实例
和 $root
类似,$parent
property 可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。
访问子组件或子元素
为子组件或元素定义ref
属性,之后通过this.$refs.NAME
获取到该组件或元素。
$refs
只会在组件渲染完成之后生效,并且它们不是响应式的。
依赖注入
prop
和$parent
提供了父子组件间的数据传递,但对于更深层级的组件来说,数据传递仍然比较困难。通过provide
和inject
能够跨越层级向下传递数据。
1 | // 与data属性类似的用法 |
在任何后代中,都可以使用inject
进行接收
1 | // 与prop属性类似的用法 |
实际上,可以把依赖注入看作一部分“大范围有效的 prop”,除了:
- 祖先组件不需要知道哪些后代组件使用它提供的 property
- 后代组件不需要知道被注入的 property 来自哪里
然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的 property 是非响应式的。
程序化的事件监听器
除了$emit
可以被v-on
监听外, Vue 实例同时在其事件接口中提供了其它的方法。我们可以:
- 通过
$on(eventName, eventHandler)
侦听一个事件 - 通过
$once(eventName, eventHandler)
一次性侦听一个事件 - 通过
$off(eventName, eventHandler)
停止侦听一个事件
当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的,比如下面的实例集成了一个第三方库:
1 | // 一次性将这个日期选择器附加到一个输入框上 |
这里有两个潜在的问题:
- 它需要在这个组件实例中保存这个
picker
,如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。 - 我们的建立代码独立于我们的清理代码,这使得我们比较难于程序化地清理我们建立的所有东西。
通过一个程序化的侦听器解决这两个问题:
1 | mounted: function () { |
此时,每当元素/实例销毁,会自动调用第三方库的清理程序。甚至可以让多个元素同时使用不同的 Pikaday,每个新的实例都程序化地在后期清理它自己:
1 | mounted: function () { |