Vue指南的要点笔记(六)

本篇开始重点学习组件开发的要点:

  1. 非prop的特性
  2. 自定义事件

非prop的特性

prop类型的特性用的比较多,也很好理解,所以不重复记录学习内容。非prop特性用得少,不是很熟悉,所以本篇也重点学习。一个非 prop 特性是指传向一个组件,但是该组件并没有相应 prop 定义的特性。如:

1
<checkbox :value="agree" label="是否同意"></checkbox>

checkbox这个组件,可能定义了value这个prop,所以value是一个prop类型的特性;而不一定定义了label这个prop,label是一个非prop类型的特性。非prop的特性,会自动添加到组件的根元素上。

替换/合并已有的特性

假设有一个自定义的bootstrap-date-input组件,模板为:

1
<input type="date" class="form-control">

如果外部使用的时候,加了下面的特性:

1
2
3
4
<bootstrap-date-input
data-date-picker="activated"
class="date-picker-theme-dark"
></bootstrap-date-input>

在外部使用组件时,附加了class这个特性,同时组件内部也有自带的class特性。对于大部分特性,如果同时存在于:外部使用的组件元素和组件定义内部的根元素,那么外部特性值会覆盖内部根元素上定义的值,比如type="text"如果附加到外部使用的组件元素上,组件内部的type="date"就会被替换掉;class和style除外,vue会采用合并的机制,保证这两个特性的内外部值都能存在。

禁用非prop特性继承

假如不想根元素,自动继承非prop的特性,可以将组件定义的inheritAttrs设置为false。

1
2
3
4
Vue.component('my-component', {
inheritAttrs: false,
// ...
})

根元素不想继承的特性,可以转移到需要的子元素上面去,这对于编写更加与标准html元素具备一致性的页面组件很有意义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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>
`
})

这样编写出来的自定义组件,在使用起来更像在使用原生元素一样:

1
2
3
4
5
<base-input
v-model="username"
required
placeholder="Enter your username"
></base-input>

注意:class与style始终会作用到自定义组件的根元素上,即使设置了inheritAttrs: false

自定义事件

不同于组件和prop,自定义组件的事件名不存在任何自动化的大小写转换;所以自定义事件名官方推荐始终用kebab-case 的事件名,因为camelCase的事件名如myEvent,在模板中监听的时候@myEvent,如果模板是在html中,会被自动转为小写:@myevent,从而导致监听失败。

自定义组件的v-model

v-model需要用到一个propevent,默认值是valueinput,可以在定义Vue组件的时候,指定新的名称:

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
<div id="vue">
<checkbox v-model="agree" @model="showModel" label="是否同意"></checkbox>{{agree}}
</div>

<script type="text/javascript">
Vue.component('checkbox', {
props: ['checked', 'label'],
model: {
prop: 'checked',
event: 'model'
},
template: `
<label><input type="checkbox" :checked="checked" @change="$emit('model',$event.target.checked)">{{label}}</label>
`
});

let vue = new Vue({
el: '#vue',
data: {
agree: true
},
methods: {
showModel(){
console.log('model event');
}
}
});
</script>

原生事件绑定到组件

如果要在自定义组件元素上,监听原生事件,可以使用.native修饰符,如:

1
2
3
<div id="vue">
<checkbox @click.native="testClick" v-model="agree" @model="showModel" label="是否同意"></checkbox>{{agree}}
</div>

@click.native如果没有加.native,这个监听是不会生效的。自定义组件的原生事件,是在自定义组件的根元素上监听的。下面这个例子,会发现click这个原生事件是能监听到的,但是focus这个事件不会监听到:

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
<div id="vue">
<checkbox @click.native="testNativeClick" @focus.native="testNativeFocus" v-model="agree" @model="showModel" label="是否同意"></checkbox>{{agree}}
</div>

<script type="text/javascript">
Vue.component('checkbox', {
props: ['checked', 'label'],
model: {
prop: 'checked',
event: 'model'
},
template: `
<label><input type="checkbox" :checked="checked" @change="$emit('model',$event.target.checked)">{{label}}</label>
`
});

let vue = new Vue({
el: '#vue',
data: {
agree: true
},
methods: {
showModel(){
console.log('model event');
},
testNativeClick(){
console.log('native click');
},
testNativeFocus(){
console.log('native focus');
}
}
});
</script>

这是因为这个组件的根元素是label,不支持focus事件。为了让focus事件能够在自定义组件上监听到,需要把focus事件的监听,添加到能被监听到的元素上,也就是内部的checkbox元素。Vue提供了一个$listeners属性,它是一个对象,里面包含了作用在这个组件上的所有监听器(不包含带.native修饰符的事件)。可以用下面的办法把父级响应不了的事件监听,移动到内部元素上:

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
35
36
37
38
39
40
41
42
<div id="vue">
<checkbox @click="testNativeClick" @focus="testNativeFocus" v-model="agree" @model="showModel" label="是否同意"></checkbox>{{agree}}
</div>

<script type="text/javascript">
Vue.component('checkbox', {
props: ['checked', 'label'],
model: {
prop: 'checked',
event: 'model'
},
computed: {
checkboxListeners(){
let events = ['focus', 'click', 'change'];
return Object.fromEntries(events.filter(e=>!!this.$listeners[e]).map(e=>{
return [e,this.$listeners[e]];
}));
}
},
template: `
<label><input v-on="checkboxListeners" type="checkbox" :checked="checked" @change="$emit('model',$event.target.checked)">{{label}}</label>
`
});

let vue = new Vue({
el: '#vue',
data: {
agree: true
},
methods: {
showModel(){
console.log('model event');
},
testNativeClick(){
console.log('native click');
},
testNativeFocus(){
console.log('native focus');
}
}
});
</script>

这个例子中去掉了.native修饰符,这样$listeners属性就能包含click和focus两个事件,然后添加了一个计算属性:

1
2
3
4
5
6
checkboxListeners(){
let events = ['focus', 'click', 'change'];
return Object.fromEntries(events.filter(e=>!!this.$listeners[e]).map(e=>{
return [e,this.$listeners[e]];
}));
}

这个计算属性试图从父级的$listeners那里拿到focus click change这几个原生事件的监听(没有就不拿),并组合成一个新对象,然后通过v-on="checkboxListeners"把新的事件监听对象全部附加到内部的checkbox元素上。这样focus change这种在自定义组件根元素label上监听不到的事件,也能监听到了。

.sync修饰符

自定义组件的prop是不允许在组件内部修改的,数据流只允许从上往下流动,如果某个组件内部的逻辑导致需要修改prop,Vue建议的方式是先派发一个:update:propName的事件,然后父级接收这个事件,同步数据,举例如下:

1
2
//title是一个prop,组件内派发update:title事件,父级接收title这个prop的最新值
this.$emit('update:title', newTitle)

1
2
3
4
5
<!--父级接收update:title事件,并同步本地的数据,本地数据同步后,会向下流动更新组件title这个prop值,从而保证了数据流动的一致性 -->
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>

这个模式有了一个简写的方式,就是使用.sync修饰符:

1
2
3
<text-document
v-bind:title.sync="doc.title"
></text-document>

有了.sync修饰符,等于自动帮你做了v-on:update:title="doc.title = $event"这个逻辑。但是组件内部update:title事件还是少不了的。例子如下:

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
<div id="vue">
<blog-post :post.sync="doc"></blog-post>
</div>

<script type="text/javascript">
Vue.component('blog-post', {
props: {
post: {
type: Object
}
},
template: `
<div>{{post.title}}<div style="text-align:right"><a href="#" @click.prevent.stop="next">下一篇</a></div></div>
`,
methods:{
next(){
this.$emit("update:post", {
title: 'ES 入门02'
});
}
}
});

let vue = new Vue({
el: '#vue',
data: {
doc: {
title: 'ES6 入门01'
}
}
});
</script>

如果有多个prop,都需要做.sync,可以把多个prop写到一个对象里面,然后对象整体进行bind和sync:

1
<text-document v-bind.sync="propsCombinedObj"></text-document>

举例如下:

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
35
<div id="vue">
<blog-post v-bind.sync="doc"></blog-post>
</div>

<script type="text/javascript">
Vue.component('blog-post', {
props: {
title: {
type: String
},
id: {
type: Number
}
},
template: `
<div>{{title}}<div style="text-align:right"><a href="#" @click.prevent.stop="next">下一篇</a></div></div>
`,
methods:{
next(){
this.$emit("update:title", 'ES 入门0' + (this.id+1));
this.$emit("update:id", this.id+1);
}
}
});

let vue = new Vue({
el: '#vue',
data: {
doc: {
title: 'ES6 入门01',
id: 1
}
}
});
</script>

这个方式感觉还怪麻烦的,尤其是这个地方:

1
2
this.$emit("update:title", 'ES 入门0' + (this.id+1));
this.$emit("update:id", this.id+1);

用的时候还得考虑哪种用法更加简洁才行。