Vue之组件

全局注册

注册一个全局组件:Vue.component(tagName, options)

1
2
3
4
Vue.component('my-component', {
// 选项
})
// 自定义标签的命名最好遵循 W3C 规则(小写,并且包含一个短杠)

组件在注册之后,便可以作为自定义元素 <my-component></my-component> 在一个实例的模板中使用。
注意确保在初始化根实例之前注册组件:

1
2
3
<div id="example">
<my-component></my-component>
</div>

1
2
3
4
5
6
7
8
// 注册
Vue.component('my-component', {
template: '<div>A custom component!</div>'
})
// 创建根实例
new Vue({
el: '#example'
})

渲染为:

1
2
3
<div id="example">
<div>A custom component!</div>
</div>

局部注册

你不必把每个组件都注册到全局。你可以通过某个 Vue 实例/组件的实例选项 components 注册仅在其作用域中可用的组件:

1
2
3
4
5
6
7
8
9
10
var Child = {
template: '<div>A custom component!</div>'
}
new Vue({
// ...
components: {
// <my-component> 将只在父组件模板中可用
'my-component': Child
}
})

这种封装也适用于其它可注册的 Vue 功能,比如指令。

DOM 模板解析注意事项

当使用 DOM 作为模板时 (例如,使用 el 选项来把 Vue 实例挂载到一个已有内容的元素上),你会受到 HTML 本身的一些限制,因为 Vue 只有在浏览器解析、规范化模板之后才能获取其内容。尤其要注意,像 <ul>、<ol>、<table>、<select>这样的元素里允许包含的元素有限制,而另一些像 <option> 这样的元素只能出现在某些特定元素的内部。
在自定义组件中使用这些受限制的元素时会导致一些问题,例如:

1
2
3
<table>
<my-row>...</my-row>
</table>

自定义组件 <my-row> 会被当作无效的内容,因此会导致错误的渲染结果。变通的方案是使用特殊的 is 特性:

1
2
3
<table>
<tr is="my-row"></tr>
</table>

应当注意,如果使用来自以下来源之一的字符串模板,则没有这些限制:

1、 <script type="text/x-template">
使用此方法,你的模板被定义在例如 index.html 文件中的 script 标签里。此 script 标签使用 text/x-template 标记,并由组件定义的 id 引用。
它允许你使用适当的 HTML 标记编写你的 HTML,不过不好的一面是,它把模板和组件定义的其它部分分离开来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.component('my-checkbox', {
template: '#checkbox-template',
data() {
return {
checked: false,
title: 'Check me'
}
},
methods: {
check() {
this.checked = !this.checked;
}
}
});

1
2
3
4
5
6
<script type="text/x-template" id="checkbox-template">
<div class="checkbox-wrapper" @click="check">
<div :class="{ checkbox: true, checked: checked }"></div>
<div class="title"></div>
</div>
</script>

2、 JavaScript 内联模板字符串
默认情况下,模板会被定义为一个字符串。然而字符串中的模板是非常难以理解的。除了广泛的浏览器支持之外,这种方法没有太多用处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Vue.component('my-checkbox', {
template: '<div class="checkbox-wrapper" @click="check"><div :class="{ checkbox: true, checked: checked }"></div><div class="title"></div></div>',
// 如果要支持分行的话,必须要在每一行末尾加上'\'
// template: '\
// <div\
// class="checkbox-wrapper"\
// @click="check"\
// >\
// <div\
// :class="{ checkbox: true, checked: checked }"\
// >\
// </div>\
// <div\
// class="title"\
// >\
// </div>\
// </div>\
// ',
data() {
return {
checked: false,
title: 'Check me'
}
},
methods: {
check() {
this.checked = !this.checked;
}
}
})

3、 .vue 组件

因此,请尽可能使用字符串模板。

这篇文章有详细阐述,点击查看

data 必须是函数

构造 Vue 实例时传入的各种选项大多数都可以在组件里使用。只有一个例外:data 必须是函数。实际上,如果你这么做:

1
2
3
4
5
6
Vue.component('my-component', {
template: '<span>{{ message }}</span>',
data: {
message: 'hello'
}
})

那么 Vue 会停止运行,并在控制台发出警告,告诉你在组件实例中 data 必须是一个函数。

如果在组件注册外声明了一个 data 对象,则多个组件实例化时共享了同一个 data 对象,因此对其中一个组件操作改变数据时会影响所有组件的数据!

我们可以通过为每个组件返回全新的数据对象来修复这个问题:

1
2
3
4
5
data: function () {
return {
counter: 0
}
}

现在每个 counter 都有它自己内部的状态了。

组件组合

组件是配合使用的,最常见的就是形成父子组件的关系:组件 A 在它的模板中使用了组件 B。它们之间必然需要相互通信:父组件可能要给子组件下发数据,子组件则可能要将它内部发生的事情告知父组件。
在 Vue 中,父子组件的关系可以总结为 prop 向下传递,事件向上传递。父组件通过 prop 给子组件下发数据,子组件通过事件给父组件发送消息。如下图:

Prop

使用 Prop 传递数据

组件实例的作用域是孤立的。这意味着不能 (也不应该) 在子组件的模板内直接引用父组件的数据。父组件的数据需要通过 prop 才能下发到子组件中。
子组件要显式地用 props 选项声明它预期的数据:

1
2
3
4
5
6
7
Vue.component('child', {
// 声明 props
props: ['message'],
// 就像 data 一样,prop 也可以在模板中使用
// 同样也可以在 vm 实例中通过 this.message 来使用
template: '<span>{{ message }}</span>'
})

然后我们可以这样向它传入一个普通字符串:

1
<child message="hello!"></child>

渲染为: <span>hello</span>

我自己对父组件的理解是:HTML中组件标签为父组件,模板中的组件标签为子组件。例如:

1
2
3
4
5
<div id="app">
<!--<root> 隐性的根组件,相对于注册的全局组件child来说,是父组件-->
<child msg="hello world"></child><!--child相对root来说是子组件,而 msg 这个变量是存在于父组件内的,应该属于父组件里的数据,虽然这个变量不是初始化就定义了的-->
<!--</root>-->
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//注册一个全局组件,组件标签名为component
Vue.component('child', {
// 组件构造器,此为一个对象,其中内容为多选项
props: ['msg'], // 接收父组件传递的数据
template: '<span>{{msg}}</span>',
data() {
return {
val: 2
};
}, //这才是子组件里面的数据
});
//实例化Vue
new Vue({
el: '#app',
data: {
val: 1
}, //这个数据是初始化 Vue 实例就定义的
});

命名规则

HTML 特性是不区分大小写的。所以,当使用的不是字符串模板时,camelCase (驼峰式命名) 的 prop 需要转换为相对应的 kebab-case (短横线分隔式命名):

1
2
3
4
5
Vue.component('child', {
// 在 JavaScript 中使用 camelCase
props: ['myMessage'],
template: '<span>{{ myMessage }}</span>'
})

1
2
<!-- 在 HTML 中使用 kebab-case -->
<child my-message="hello!"></child>

如果你使用字符串模板,则没有这些限制。

动态 Prop

与绑定到任何普通的 HTML 特性相类似,我们可以用 v-bind 来动态地将 prop 绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件:

1
2
3
4
5
<div id="component-3">
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
Vue.component('child', {
props: ['myMessage'],
template: '<span>{{ myMessage }}</span>'
})
new Vue({
el: '#component-3',
data: function() {
return {
parentMsg: "", // 一定要写预设信息,不然会出错
}
}
})

如果你想把一个对象的所有属性作为 prop 进行传递,可以使用不带任何参数的 v-bind (即用 v-bind 而不是 v-bind:prop-name)。例如,已知一个 todo 对象:

1
2
3
4
todo: {
text: 'Learn Vue',
isComplete: false
}

1
2
3
4
5
6
<todo-item v-bind="todo"></todo-item>
// 将等价于
<todo-item
v-bind:text="todo.text"
v-bind:is-complete="todo.isComplete"
></todo-item>

字面量语法 vs 动态语法

常犯的一个错误是使用字面量语法传递数值:

1
2
<!-- 传递了一个字符串 "1" -->
<comp some-prop="1"></comp>

因为它是一个字面量 prop,它的值是字符串 “1” 而不是一个数值。如果想传递一个真正的 JavaScript 数值,则需要使用 v-bind,从而让它的值被当作 JavaScript 表达式计算:

1
2
<!-- 传递真正的数值 -->
<comp v-bind:some-prop="1"></comp>

单向数据流

Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。

另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。

在两种情况下,我们很容易忍不住想去修改 prop 中数据:

  • Prop 作为初始值传入后,子组件想把它当作局部数据来用;
  • Prop 作为原始数据传入,由子组件处理成其它数据输出。

  • 对这两种情况,正确的应对方式是:

    定义一个局部变量,并用 prop 的值初始化它:

    1
    2
    3
    4
    props: ['initialCounter'],
    data: function () {
    return { counter: this.initialCounter }
    }

    定义一个计算属性,处理 prop 的值并返回:

    1
    2
    3
    4
    5
    6
    props: ['size'],
    computed: {
    normalizedSize: function () {
    return this.size.trim().toLowerCase()
    }
    }

    注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。

    替换/合并现有的特性

    如果在某个例子中定义了两个不同的 class 值:1、来自组件自身的模板。 2、来自父组件。
    对于多数特性来说,传递给组件的值会覆盖组件本身设定的值。即例如传递 type=”large” 将会覆盖 type=”date” 且有可能破坏该组件!所幸我们对待 class 和 style 特性会更聪明一些,这两个特性的值都会做合并 (merge) 操作,让最终生成的 class 值为:form-control date-picker-theme-dark。

    使用 v-on 绑定自定义事件

    每个 Vue 实例都实现了事件接口,即:使用 $on(eventName) 监听事件、 使用 $emit(eventName) 触发事件

    另外,父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件。注意:不能用 $on 侦听子组件释放的事件,而必须在模板里直接用 v-on 绑定。
    下面是一个例子:

    1
    2
    3
    4
    5
    <div id="counter-event-example">
    <p>{{ total }}</p>
    <button-counter v-on:increment="incrementTotal"></button-counter>
    <button-counter v-on:increment="incrementTotal"></button-counter>
    </div>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    Vue.component('button-counter', {
    template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
    data: function () {
    return {
    counter: 0
    }
    },
    methods: {
    incrementCounter: function () {
    this.counter += 1
    this.$emit('increment')
    }
    },
    })
    new Vue({
    el: '#counter-event-example',
    data: {
    total: 0
    },
    methods: {
    incrementTotal: function () {
    this.total += 1
    }
    }
    })

    this.$emit(‘increment’) 可以让父组件知道子组件调用了什么函数,即类似于子组件跟父组件说“爸爸 我调用了我自己的increment函数”,通知父组件。
    v-on:increment="incrementTotal 相当于父组件通知子组件说“孩子,当你调用了increment函数的时候,我将调用incrementTotal函数来回应你”。

    给组件绑定原生事件

    有时候,你可能想在某个组件的根元素上监听一个原生事件。可以使用 v-on 的修饰符 .native。例如:<my-component v-on:click.native="doTheThing"></my-component>
    意思就是当你给一个 vue 组件绑定原生 click 事件时候,要加上 .native。如果是普通的 html 元素,如 a 标签,就不需要。

    .sync 修饰符(2.3.0+ 支持)

    在一些情况下,我们可能会需要对一个 prop 进行“双向绑定”。但它破坏了单向数据流,给代码带来很高的维护成本。
    因此在 2.3.0+ 版本它只是作为一个编译时的语法糖存在,会被扩展为一个自动更新父组件属性的 v-on 监听器。如下代码:

    1
    2
    3
    <comp :foo.sync="bar"></comp>
    // 会被扩展为:
    <comp :foo="bar" @update:foo="val => bar = val"></comp>

    当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件:

    1
    this.$emit('update:foo', newValue)

    使用自定义事件的表单输入组件

    自定义事件可以用来创建自定义的表单输入组件,使用 v-model 来进行数据双向绑定。要牢记:

    1
    2
    3
    4
    5
    <input v-model="something">
    // 其实是以下示例的语法糖:
    <input
    v-bind:value="something"
    v-on:input="something = $event.target.value">

    所以在组件中使用时,它相当于下面的简写:

    1
    2
    3
    4
    <custom-input
    v-bind:value="something"
    v-on:input="something = arguments[0]">
    </custom-input>

    在给 <input /> 元素添加 v-model 属性时,默认会把 value 作为元素的属性,然后把 ‘input’ 事件作为实时传递 value 的触发事件。

    所以要让组件的 v-model 生效,它应该 (从 2.2.0 起是可配置的): 1、接受一个 value prop 2、在有新的值时触发 input 事件并将新值作为参数
    一个非常简单的货币输入的自定义控件:

    1
    <currency-input v-model="price"></currency-input>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    Vue.component('currency-input', {
    template: '\
    <span>\
    $\
    <input\
    ref="input"\
    v-bind:value="value"\
    v-on:input="updateValue($event.target.value)"\
    >\
    </span>\
    ',
    props: ['value'],
    methods: {
    // 不是直接更新值,而是使用此方法来对输入值进行格式化和位数限制
    updateValue: function (value) {
    var formattedValue = value
    // 删除两侧的空格符
    .trim()
    // 保留 2 位小数
    .slice(
    0,
    value.indexOf('.') === -1
    ? value.length
    : value.indexOf('.') + 3
    )
    // 如果值尚不合规,则手动覆盖为合规的值
    if (formattedValue !== value) {
    this.$refs.input.value = formattedValue
    }
    // 通过 input 事件带出数值
    this.$emit('input', Number(formattedValue))
    }
    }
    })

    自定义组件的 v-model(2.2.0 新增)

    默认情况下,一个组件的 v-model 会使用 value prop 和 input 事件。但是诸如单选框、复选框之类的输入类型可能把 value 用作了别的目的。model 选项可以避免这样的冲突:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Vue.component('my-checkbox', {
    model: {
    prop: 'checked',
    event: 'change'
    },
    props: {
    checked: Boolean,
    // 这样就允许拿 `value` 这个 prop 做其它事了
    value: String
    },
    // ...
    })

    1
    2
    3
    4
    5
    6
    7
    <my-checkbox v-model="foo" value="some value"></my-checkbox>
    // 上述代码等价于:
    <my-checkbox
    :checked="foo"
    @change="val => { foo = val }"
    value="some value">
    </my-checkbox>

    注意你仍然需要显式声明 checked 这个 prop。

    非父子组件的通信

    有时候,非父子关系的两个组件之间也需要通信。在简单的场景下,可以使用一个空的 Vue 实例作为事件总线:

    1
    2
    3
    4
    5
    6
    7
    var bus = new Vue()
    // 触发组件 A 中的事件
    bus.$emit('id-selected', 1)
    // 在组件 B 创建的钩子中监听事件
    bus.$on('id-selected', function (id) {
    // ...
    })

    在复杂的情况下,我们应该考虑使用专门的状态管理模式。

    使用插槽分发内容

    在使用组件时,我们常常要像这样组合它们:

    1
    2
    3
    4
    <app>
    <app-header></app-header>
    <app-footer></app-footer>
    </app>

    注意两点:1、 <app> 组件不知道它会收到什么内容。这是由使用 <app> 的父组件决定的。 2、 <app> 组件很可能有它自己的模板。
    为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为内容分发。Vue.js 实现了一个内容分发 API,<slot> 元素作为原始内容的插槽。

    编译作用域

    在深入内容分发 API 之前,我们先明确内容在哪个作用域里编译。假定模板为:

    1
    2
    3
    <child-component>
    {{ message }}
    </child-component>

    message 应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件。组件作用域简单地说是:父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。

    一个常见错误是试图在父组件模板内将一个指令绑定到子组件的属性/方法:

    1
    2
    <!-- 无效 -->
    <child-component v-show="someChildProperty"></child-component>

    假定 someChildProperty 是子组件的属性,上例不会如预期那样工作。父组件模板并不感知子组件的状态。
    如果要绑定子组件作用域内的指令到一个组件的根节点,你应当在子组件自己的模板里做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Vue.component('child-component', {
    // 有效,因为是在正确的作用域内
    template: '<div v-show="someChildProperty">Child</div>',
    data: function () {
    return {
    someChildProperty: true
    }
    }
    })

    类似地,被分发的内容会在父作用域内编译。

    单个插槽

    除非子组件模板包含至少一个 <slot> 插口,否则父组件的内容将会被丢弃。当子组件模板只有一个没有属性的插槽时,父组件传入的整个内容片段将插入到插槽所在的 DOM 位置,并替换掉插槽标签本身。

    最初在 <slot> 标签中的任何内容都被视为备用内容。备用内容在子组件的作用域内编译,并且只有在宿主元素为空,且没有要插入的内容时才显示备用内容。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 假定 my-component 组件有如下模板:
    <div>
    <h2>我是子组件的标题</h2>
    <slot>
    只有在没有要分发的内容时才会显示。
    </slot>
    </div>
    // 父组件模板:
    <div>
    <h1>我是父组件的标题</h1>
    <my-component>
    <p>这是一些初始内容</p>
    <p>这是更多的初始内容</p>
    </my-component>
    </div>
    // 渲染结果:
    <div>
    <h1>我是父组件的标题</h1>
    <div>
    <h2>我是子组件的标题</h2>
    <p>这是一些初始内容</p>
    <p>这是更多的初始内容</p>
    </div>
    </div>

    具名插槽

    <slot> 元素可以用一个特殊的特性 name 来进一步配置如何分发内容。多个插槽可以有不同的名字。具名插槽将匹配内容片段中有对应 slot 特性的元素。
    仍然可以有一个匿名插槽,它是默认插槽,作为找不到匹配的内容片段的备用插槽。如果没有默认插槽,这些找不到匹配的内容片段将被抛弃。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // app-layout 组件的模板为:
    <div class="container">
    <header>
    <slot name="header"></slot>
    </header>
    <main>
    <slot></slot>
    </main>
    <footer>
    <slot name="footer"></slot>
    </footer>
    </div>
    // 父组件模板:
    <app-layout>
    <h1 slot="header">这里可能是一个页面标题</h1>
    <p>主要内容的一个段落。</p>
    <p>另一个主要段落。</p>
    <p slot="footer">这里有一些联系信息</p>
    </app-layout>
    // 渲染结果为:
    <div class="container">
    <header>
    <h1>这里可能是一个页面标题</h1>
    </header>
    <main>
    <p>主要内容的一个段落。</p>
    <p>另一个主要段落。</p>
    </main>
    <footer>
    <p>这里有一些联系信息</p>
    </footer>
    </div>

    在设计组合使用的组件时,内容分发 API 是非常有用的机制。

    子组件引用

    尽管有 prop 和事件,但是有时仍然需要在 JavaScript 中直接访问子组件。为此可以使用 ref 为子组件指定一个引用 ID。例如:

    1
    2
    3
    <div id="parent">
    <user-profile ref="profile"></user-profile>
    </div>

    1
    2
    3
    var parent = new Vue({ el: '#parent' })
    // 访问子组件实例
    var child = parent.$refs.profile

    当 ref 和 v-for 一起使用时,获取到的引用会是一个数组,包含和循环数据源对应的子组件。
    注意:$refs 只在组件渲染完成后才填充,并且它是非响应式的。它仅仅是一个直接操作子组件的应急方案。应当避免在模板或计算属性中使用 $refs。

    递归组件

    组件在它的模板内可以递归地调用自己。不过,只有当它有 name 选项时才可以这么做:name: 'unique-name-of-my-component'
    当你利用 Vue.component 全局注册了一个组件,全局的 ID 会被自动设置为组件的 name。
    如果稍有不慎,递归组件可能导致死循环:

    1
    2
    name: 'stack-overflow',
    template: '<div><stack-overflow></stack-overflow></div>'

    上面组件会导致一个“max stack size exceeded”错误,所以要确保递归调用有终止条件 (比如递归调用时使用 v-if 并最终解析为 false)。

    对低开销的静态组件使用 v-once

    尽管在 Vue 中渲染 HTML 很快,不过当组件中包含大量静态内容时,可以考虑使用 v-once 将渲染结果缓存起来,就像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    Vue.component('terms-of-service', {
    template: '\
    <div v-once>\
    <h1>Terms of Service</h1>\
    ...很多静态内容...\
    </div>\
    '
    })