数据与方法
当一个Vue
实例被创建时,它向Vue
的响应式系统中加入了其 data
对象中能找到的所有的属性。当这些属性的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。
// 我们的数据对象
var data = { a: 1 }
// 该对象被加入到一个 Vue 实例中
var vm = new Vue({
data: data
})
// 获得这个实例上的属性
// 返回源数据中对应的字段
vm.a == data.a // => true
// 设置属性也会影响到原始数据
vm.a = 2
data.a // => 2
// ……反之亦然
data.a = 3
vm.a // => 3
当这些数据改变时,视图会进行重渲染。值得注意的是__只有当实例被创建时data
中存在的属性才是响应式的__。也就是说如果你添加一个新的属性,比如:vm.b = 'hi'
那么对b
的改动将不会触发任何视图的更新。如果你知道你会在晚些时候需要一个属性,但是一开始它为空或不存在,那么你仅需要设置一些初始值。比如:
data: {
newTodoText: '',
visitCount: 0,
hideCompletedTodos: false,
todos: [],
error: null
}
唯一的例外是__使用Object.freeze()
会阻止修改现有的属性,也意味着响应系统无法再追踪变化__。
var obj = {
foo: 'bar'
}
Object.freeze(obj)
new Vue({
el: '#app',
data: obj
})
<div id="app">
<p>{{ foo }}</p>
<buttonv-on:click="foo = 'baz'">Change it</button>
</div>
实例生命周期钩子
每个Vue
实例在被创建时都要经过一系列的初始化过程。例如,需要设置数据监听、编译模板、将实例挂载到DOM
并在数据变化时更新 DOM
等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这使我们可以在不同阶段添加自己的代码。
比如created
钩子可以用来在一个实例被创建之后执行代码:
new Vue({
data: {
a: 1
},
created: function () {
// this指向vm实例
console.log('a is: ' + this.a) // => "a is: 1"
}
})
也有一些其它的钩子,在实例生命周期的不同阶段被调用,如mounted
、updated
和 destroyed
。生命周期钩子的this
上下文指向调用它的 Vue
实例。
不要在选项属性或回调上使用箭头函数,比如created: () => console.log(this.a)
或vm.$watch('a', newValue => this.myMethod())
。因为箭头函数是和父级上下文绑定在一起的,this
不会是如你所预期的 Vue
实例,经常导致Uncaught TypeError: Cannot read property of undefined 或 Uncaught TypeError: this.myMethod is not a function
之类的错误。
计算属性缓存 vs 方法
你可能已经注意到我们可以通过在表达式中调用方法来达到同样的效果:
<p>Reversed message: "{{ reversedMessage() }}"</p>
// 在组件中
methods: {
reversedMessage: function () {
returnthis.message.split('').reverse().join('')
}
}
我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是__计算属性是基于它们的依赖进行缓存的。只在相关依赖发生改变时它们才会重新求值。这就意味着只要message
还没有发生改变,多次访问reversedMessage
计算属性会立即返回之前的计算结果,而不必再次执行函数。
这也同样意味着下面的计算属性将不再更新,因为Date.now()
不是响应式依赖__:computed: { now: function () {returnDate.now() }}
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
侦听器
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么Vue
通过watch
选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。例如:
<div id="watch-example">
<p>
Ask a yes/no question:
<input v-model="question">
</p>
<p>{{ answer }}</p>
</div>
<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 -->
<!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
el: '#watch-example',
data: {
question: '',
answer: 'I cannot give you an answer until you ask a question!'
},
watch: {
// 如果 `question` 发生改变,这个函数就会运行
question: function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
}
},
created: function () {
// _.debounce 是一个通过 Lodash 限制操作频率的函数。
// 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
// AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
// _.debounce 函数 (及其近亲 _.throttle) 的知识,
// 请参考:https://lodash.com/docs#debounce
this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
},
methods: {
getAnswer: function () {
if (this.question.indexOf('?') === -1) {
this.answer = 'Questions usually contain a question mark. ;-)'
return
}
this.answer = 'Thinking...'
var vm = this
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answer = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answer = 'Error! Could not reach the API. ' + error
})
}
}
})
</script>
在这个示例中,使用watch
选项允许我们执行异步操作 (访问一个API
),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
在 <template>
元素上使用v-if
条件渲染分组
因为v-if
是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 <template>
元素当做不可见的包裹元素,并在上面使用v-if
。最终的渲染结果将不包含<template>
元素。
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
你可以使用v-else
指令来表示v-if
的“else
块”:
<div v-if="Math.random() > 0.5">Now you see me</div>
<div v-else>Now you don't</div>
v-else
元素必须紧跟在带v-if
或者v-else-if
的元素的后面,否则它将不会被识别。且v-show
不支持 <template>
元素
用key
管理可复用的元素
Vue
为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的key
属性即可:
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address" key="email-input">
</template>
一个对象的v-for
在遍历对象时,是按Object.keys()
的结果遍历,但是不能保证它的结果在不同的JavaScript
引擎下是一致的。
为了给Vue
一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key
属性。理想的key
值是每项都有的且唯一的id
。
你需要用 v-bind
来绑定动态值 (在这里使用简写):<div v-for="item in items" :key="item.id"> <!-- 内容 --></div>
建议尽可能在使用v-for
时提供key
,除非遍历输出的DOM
内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。
由于JavaScript
的限制,Vue
不能检测以下变动的数组:
- 当你利用索引直接设置一个项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
举个例子:
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的
为了解决第一类问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue
相同的效果,同时也将触发状态更新:
// Vue.setVue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splicevm.items.splice(indexOfItem, 1, newValue)
你也可以使用 vm.
s
e
t
实
例
方
法
,
该
方
法
是
全
局
方
法
‘
V
u
e
.
s
e
t
‘
的
一
个
别
名
:
‘
v
m
.
set 实例方法,该方法是全局方法 `Vue.set `的一个别名:`vm.
set实例方法,该方法是全局方法‘Vue.set‘的一个别名:‘vm.set(vm.items, indexOfItem, newValue)。为了解决第二类问题,你可以使用
splice:
vm.items.splice(newLength)`
一个组件的v-for
__2.2.0+ 的版本里,当在组件中使用v-for
时,key
现在是必须的。__下面是一个简单的todo list
的完整例子:
<div id="todo-list-example">
<form v-on:submit.prevent="addNewTodo">
<label for="new-todo">Add a todo</label>
<input
v-model="newTodoText"
id="new-todo"
placeholder="E.g. Feed the cat"
>
<button>Add</button>
</form>
<ul>
<li
is="todo-item"
v-for="(todo, index) in todos"
v-bind:key="todo.id"
v-bind:title="todo.title"
v-on:remove="todos.splice(index, 1)"
></li>
</ul>
</div>
注意这里的is="todo-item"
属性。这种做法在使用 DOM
模板时是十分必要的,因为在<ul>
元素内只有<li>
元素会被看作有效内容。这样做实现的效果与<todo-item>
相同,但是可以避开一些潜在的浏览器解析错误。
Vue.component('todo-item', {
template: '\
<li>\
{{ title }}\
<button v-on:click="$emit(\'remove\')">Remove</button>\
</li>\
',
props: ['title']
})
new Vue({
el: '#todo-list-example',
data: {
newTodoText: '',
todos: [
{ id: 1, title: 'Do the dishes',},
{ id: 2, title: 'Take out the trash',},
{ id: 3, title: 'Mow the lawn'}
],
nextTodoId: 4
},
methods: {
addNewTodo: function () {
this.todos.push({
id: this.nextTodoId++,
title: this.newTodoText
})
this.newTodoText = ''
}
}
})
对象更改检测注意事项
由于JavaScript
的限制,Vue
不能检测对象属性的添加或删除:
var vm = new Vue({
data: {
a: 1
}
})
// `vm.a` 现在是响应式的
vm.b = 2
// `vm.b` 不是响应式的
对于已经创建的实例,Vue
不能动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, key, value)
方法向嵌套对象添加响应式属性。例如,对于:
var vm = new Vue({
data: {
userProfile: {
name: 'Anika'
}
}
})
你可以添加一个新的age
属性到嵌套的userProfile
对象:
Vue.set(vm.userProfile, 'age', 27)
你还可以使用vm.$set
实例方法,它只是全局Vue.set
的别名:
vm.$set(vm.userProfile, 'age', 27)
有时你可能需要为已有对象赋予多个新属性,比如使用Object.assign()
或 _.extend()
。在这种情况下,你应该用两个对象的属性创建一个新的对象。所以,如果你想添加新的响应式属性,不要像这样:
Object.assign(vm.userProfile, {
age: 27, favoriteColor: 'Vue Green'
})
你应该这样做:
vm.userProfile = Object.assign({}, vm.userProfile, {
age: 27, favoriteColor: 'Vue Green'
})
事件修饰符
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即元素自身触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
<!-- 点击事件将只会触发一次 -->
<a v-on:click.once="doThis"></a>
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>
按键修饰符
在监听键盘事件时,我们经常需要检查常见的键值。 Vue
允许为v-on
在监听键盘事件时添加按键修饰符:
<!-- 只有在 `keyCode` 是 13 时调用 `vm.submit()` -->
<input v-on:keyup.13="submit">
记住所有的keyCode
比较困难,所以Vue
为最常用的按键提供了别名:
<!-- 同上 -->
<input v-on:keyup.enter="submit">
<!-- 缩写语法 -->
<input @keyup.enter="submit">
全部的按键别名:
- .enter
- .tab
- .delete(捕获“删除”和“退格”键)
- .esc
- .space
- .up
- .down
- .left
- .right
可以通过全局config.keyCodes
对象自定义按键修饰符别名:
// 可以使用 `v-on:keyup.f1`
Vue.config.keyCodes.f1 = 112
自动匹配按键修饰符(2.5.0 新增)
你也可直接将KeyboardEvent.key
暴露的任意有效按键名转换为kebab-case
来作为修饰符:<input @keyup.page-down="onPageDown">
在上面的例子中,处理函数仅在$event.key === 'PageDown'
时被调用。
有一些按键 (.esc
以及所有的方向键) 在 IE9
中有不同的key
值, 如果你想支持IE9
,它们的内置别名应该是首选。
系统修饰键(2.1.0 新增)
可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。
- .ctrl
- .alt
- .shift
- .meta
注意:在Mac
系统键盘上,meta
对应command
键 (⌘)。在Windows
系统键盘meta
对应Windows
徽标键 (⊞)。在Sun
操作系统键盘上,meta
对应实心宝石键 (◆)。在其他特定键盘上,尤其在MIT
和Lisp
机器的键盘、以及其后继产品,比如Knight
键盘、space-cadet
键盘,meta
被标记为“META
”。在Symbolics
键盘上,meta
被标记为“META
”或者“Meta
”。
例如:
<!-- Alt + C -->
<input @keyup.alt.67="clear">
<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>
请注意修饰键与常规按键不同,在和keyup
事件一起用时,事件触发时修饰键必须处于按下状态。换句话说,只有在按住 ctrl
的情况下释放其它按键,才能触发 keyup.ctrl
。而单单释放ctrl
也不会触发事件。如果你想要这样的行为,请为ctrl
换用keyCode:keyup.17
。
.exact
修饰符(2.5.0 新增)
.exact
修饰符允许你控制由精确的系统修饰符组合触发的事件。
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>
<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>
选择框
单选时:
<div id="example-5">
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>Selected: {{ selected }}</span>
</div>
new Vue({
el: '...',
data: {
selected: ''
}
})
如果v-model
表达式的初始值未能匹配任何选项,<select>
元素将被渲染为“未选中”状态。在 iOS
中,这会使用户无法选择第一个选项。因为这样的情况下,iOS
不会触发change
事件。因此,更推荐像上面这样提供一个值为空的禁用选项。
修饰符
<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg" >
<--将用户的输入值转为数值类型-->
<input v-model.number="age" type="number">
<--自动过滤用户输入的首尾空白字符-->
<input v-model.trim="msg">
在组件上使用 v-model
自定义事件也可以用于创建支持v-model
的自定义输入组件。记住:
<input v-model="searchText">
等价于:
<input v-bind:value="searchText" v-on:input="searchText = $event.target.value">
当用在组件上时,v-model
则会这样:
<custom-input v-bind:value="searchText" v-on:input="searchText = $event"></custom-input>
为了让它正常工作,这个组件内的 <input>
必须:
- 将其
value
特性绑定到一个名叫value
的prop
上 - 在其
input
事件被触发时,将新的值通过自定义的input
事件抛出
写成代码之后是这样的:
Vue.component('custom-input', {
props: ['value'],
template: `
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
`
})
通过插槽分发内容
和 HTML
元素一样,我们经常需要向一个组件传递内容,像这样:
<alert-box> Something bad happened.</alert-box>
可能会渲染出这样的东西:Error! Something bad happened.
幸好,Vue
自定义的<slot>
元素让这变得非常简单:
Vue.component('alert-box', {
template: `
<div class="demo-alert-box">
<strong>Error!</strong>
<slot></slot>
</div>
`
})
基础组件的自动化全局注册
可能你的许多组件只是包裹了一个输入框或按钮之类的元素,是相对通用的。我们有时候会把它们称为基础组件,它们会在各个组件中被频繁的用到。所以会导致很多组件里都会有一个包含基础组件的长列表:
import BaseButton from './BaseButton.vue'
import BaseIcon from './BaseIcon.vue'
import BaseInput from './BaseInput.vue'
export default {
components: {
BaseButton,
BaseIcon,
BaseInput
}
}
而只是用于模板中的一小部分:
<BaseInput
v-model="searchText"
@keydown.enter="search"
/>
<BaseButton @click="search">
<BaseIcon name="search"/>
</BaseButton>
幸好如果你使用了 webpack
(或在内部使用了webpack
的Vue CLI
3+),那么就可以使用require.context
只全局注册这些非常通用的基础组件。这里有一份可以让你在应用入口文件 (比如 src/main.js
) 中全局导入基础组件的示例代码:
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
// 其组件目录的相对路径
'./components',
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/Base[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
// 获取组件配置
const componentConfig = requireComponent(fileName)
// 获取组件的 PascalCase 命名
const componentName = upperFirst(
camelCase(
// 剥去文件名开头的 `./` 和结尾的扩展名
fileName.replace(/^\.\/(.*)\.\w+$/, '$1')
)
)
// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
)
})
记住全局注册的行为必须在根Vue
实例 (通过new Vue
) 创建之前发生。这里有一个真实项目情景下的示例。
Prop 验证
我们可以为组件的prop
指定验证要求,例如你知道的这些类型。如果有一个需求没有被满足,则Vue
会在浏览器控制台中警告你。这在开发一个会被别人用到的组件时尤其有帮助。
为了定制prop
的验证方式,你可以为props
中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:
Vue.component('my-component', {
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 ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})
当prop
验证失败的时候,(开发环境构建版本的)Vue
将会产生一个控制台的警告。
注意那些 prop
会在一个组件实例创建之前进行验证,所以实例的属性 (如 data
、computed
等) 在default
或 validator
函数中是不可用的。
组件替换/合并已有的特性
想象一下<bootstrap-date-input>
的模板是这样的:
<input type="date" class="form-control">
为了给我们的日期选择器插件定制一个主题,我们可能需要像这样添加一个特别的类名:
<bootstrap-date-input
data-date-picker="activated"
class="date-picker-theme-dark"
></bootstrap-date-input>
在这种情况下,我们定义了两个不同的class
的值:
form-control
,这是在组件的模板内设置好的date-picker-theme-dark
,这是从组件的父级传入的
对于绝大多数特性来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果传入type="text"
就会替换掉type="date"
并把它破坏!庆幸的是,class
和style
特性会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:form-control date-picker-theme-dark
。
禁用特性继承
如果你不希望组件的根元素继承特性,你可以设置在组件的选项中设置inheritAttrs: false
。例如:
Vue.component('my-component', { inheritAttrs: false, // ...})
这尤其适合配合实例的$attrs
属性使用,该属性包含了传递给一个组件的特性名和特性值,例如:
{ class: 'username-input', placeholder: 'Enter your username'}
有了inheritAttrs: false
和$attrs
,你就可以手动决定这些特性会被赋予哪个元素。在撰写基础组件的时候是常会用到的:
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>
`
})
这个模式允许你在使用基础组件的时候更像是使用原始的 HTML
元素,而不会担心哪个元素是真正的根元素:
<base-input
v-model="username"
class="username-input"
placeholder="Enter your username"
></base-input>
事件名
跟组件和prop
不同,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。举个例子,如果触发一个camelCase
名字的事件:this.$emit('myEvent')
则监听这个名字的kebab-case
版本是不会有任何效果的:
<my-component v-on:my-event="doSomething"></my-component>
跟组件和prop
不同,事件名不会被用作一个JavaScript
变量名或属性名,所以就没有理由使用camelCase
或 PascalCase
了。并且 v-on
事件监听器在DOM
模板中会被自动转换为全小写 (因为HTML
是大小写不敏感的),所以v-on:myEvent
将会变成v-on:myevent
——导致myEvent
不可能被监听到。
因此,我们推荐你始终使用kebab-case
的事件名。
自定义组件的 v-model
(2.2.0+ 新增)
一个组件上的 v-model
默认会利用名为 value
的prop
和名为 input
的事件,但是像单选框、复选框等类型的输入控件可能会将value
特性用于不同的目的。model
选项可以用来避免这样的冲突:
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)"
>
`
})
现在在这个组件上使用v-model
的时候:
<base-checkbox v-model="lovingVue"></base-checkbox>
这里的 lovingVue
的值将会传入这个名为checked
的prop
。同时当 <base-checkbox>
触发一个change
事件并附带一个新的值的时候,这个 lovingVue
的属性将会被更新。
注意你仍然需要在组件的props
选项里声明 checked
这个prop
。
将原生事件绑定到组件
你可能有很多次想要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用v-on
的.native
修饰符:
<base-input v-on:focus.native="onFocus"></base-input>
在有的时候这是很有用的,不过在你尝试监听一个类似<input>
的非常特定的元素时,这并不是个好主意。比如上述<base-input>
组件可能做了如下重构,所以根元素实际上是一个<label>
元素:
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
这时,父级的 .native
监听器将静默失败。它不会产生任何报错,但是onFocus
处理函数不会如你预期地被调用。
为了解决这个问题,Vue
__提供了一个$listeners
属性,它是一个对象,里面包含了作用在这个组件上的所有监听器。__例如:
{ focus: function (event) { /* ... */ } input: function (value) { /* ... */ },}
有了这个 $listeners
属性,你就可以__配合v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。__对于类似 <input>
的你希望它也可以配合 v-model
工作的组件来说,为这些监听器创建一个类似下述 inputListeners
的计算属性通常是非常有用的:
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
computed: {
inputListeners: function () {
var vm = this
// `Object.assign` 将所有的对象合并为一个新对象
return Object.assign({},
// 我们从父级添加所有的监听器
this.$listeners,
// 然后我们添加自定义监听器,
// 或覆写一些监听器的行为
{
// 这里确保组件配合 `v-model` 的工作
input: function (event) {
vm.$emit('input', event.target.value)
}
}
)
}
},
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
>
</label>
`
})
现在<base-input>
组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的<input>
元素一样使用了:所有跟它相同的特性和监听器的都可以工作。
.sync
修饰符(2.3.0+ 新增)
在有些情况下,我们可能需要__对一个prop
进行“双向绑定”__。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。
这也是为什么我们__推荐以update:my-prop-name
的模式触发事件__取而代之。举个例子,在一个包含title prop
的假设的组件中,我们可以用以下方法表达对其赋新值的意图:this.$emit('update:title', newTitle)
然后父组件可以监听那个事件并根据需要更新一个本地的数据属性。例如:
<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
的时候,也可以将这个.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 }
”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
作用于插槽(2.1.0+ 新增) 为什么要在父组件上绑定子组件的数据
有的时候你希望提供的组件带有一个可从子组件获取数据的可复用的插槽。例如一个简单的<todo-list>
组件的模板可能包含了如下代码:
<ul>
<li v-for="todo in todos" v-bind:key="todo.id" >
{{ todo.text }}
</li>
</ul>
但是在我们应用的某些部分,我们希望每个独立的待办项渲染出和todo.text
不太一样的东西。这也是作用域插槽的用武之地。
为了让这个特性成为可能,你需要做的全部事情就是__将待办项内容包裹在一个 <slot>
元素上,然后将所有和其上下文相关的数据传递给这个插槽__:在这个例子中,这个数据是todo
对象:
<ul>
<li
v-for="todo in todos"
v-bind:key="todo.id"
>
<!-- 我们为每个 todo 准备了一个插槽,-->
<!-- 将 `todo` 对象作为一个插槽的 prop 传入。-->
<slot v-bind:todo="todo">
<!-- 回退的内容 -->
{{ todo.text }}
</slot>
</li>
</ul>
现在当我们使用<todo-list>
组件的时候,我们可以选择为待办项定义一个不一样的 <template>
作为替代方案,并且可以__通过slot-scope
特性从子组件获取数据__:
<todo-list v-bind:todos="todos">
<!-- 将 `slotProps` 定义为插槽作用域的名字 -->
<template slot-scope="slotProps">
<!-- 为待办项自定义一个模板,-->
<!-- 通过 `slotProps` 定制每个待办项。-->
<span v-if="slotProps.todo.isComplete">✓</span>
{{ slotProps.todo.text }}
</template>
</todo-list>
在 2.5.0+,slot-scope
不再限制在<template>
元素上使用,而可以用在插槽内的任何元素或组件上。
解构slot-scope
如果一个JavaScrip
t 表达式在一个函数定义的参数位置有效,那么这个表达式实际上就可以被slot-scope
接受。也就是说你可以在支持的环境下 (单文件组件或现代浏览器),在这些表达式中使用ES2015
解构语法。例如:
<todo-list v-bind:todos="todos">
<template slot-scope="{ todo }">
<span v-if="todo.isComplete">✓</span>
{{ todo.text }}
</template>
</todo-list>
这会使作用域插槽变得更干净一些。
在动态组件上使用keep-alive
为了解决 那些标签的组件实例能够被在它们第一次被创建的时候缓存下来,我们可以用一个<keep-alive>
元素将其动态组件包裹起来。
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
<keep-alive>
要求被切换到的组件都有自己的名字,不论是通过组件的name
选项还是局部/全局注册。
异步组件(按需加载)
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块(即按需加载)。为了简化,Vue
__允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue
只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。__例如:
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
如你所见,这个工厂函数会收到一个resolve
回调,这个回调函数会在你从服务器得到组件定义的时候被调用。你也可以调用reject(reason)
来表示加载失败。这里的setTimeout
是为了演示用的,如何获取组件取决于你自己。一个推荐的做法是将异步组件和 webpack
的 code-splitting
功能一起配合使用:
Vue.component('async-webpack-example', function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(['./my-async-component'], resolve)
})
你也可以在工厂函数中返回一个Promise
,所以把webpack 2
和ES2015
语法加在一起,我们可以写成这样:
Vue.component(
'async-webpack-example',
// 这个 `import` 函数会返回一个 `Promise` 对象。
() => import('./my-async-component')
)
当使用局部注册的时候,你也可以直接提供一个返回Promise
的函数:
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component')
}
})
异步加载“并不会被 Browserify
支持
处理加载状态(2.3.0+ 新增)
这里的异步组件工厂函数也可以返回一个如下格式的对象:
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
处理边界情况
自定义指令
渲染函数
用来访问被插槽分发的内容。每个具名插槽 有其相应的属性 (例如:slot="foo"
中的内容将会在vm.$slots.foo
中被找到)。default
属性包括了所有没有被包含在具名插槽中的节点。
在使用渲染函数书写一个组件时,访问vm.$slots
最有帮助。示例:
<blog-post>
<h1 slot="header">
About Me
</h1>
<p>Here's some page content, which will be included in vm.$slots.default, because it's not inside a named slot.</p>
<p slot="footer">
Copyright 2016 Evan You
</p>
<p>If I have some content down here, it will also be included in vm.$slots.default.</p>.
</blog-post>
Vue.component('blog-post', {
render: function (createElement) {
var header = this.$slots.header
var body = this.$slots.default
var footer = this.$slots.footer
return createElement('div', [
createElement('header', header),
createElement('main', body),
createElement('footer', footer)
])
}
})