愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

Vue.js学习总结笔记(二)组件

一、注册组件

1、全局注册

注册组件的方式为:

Vue.component(tagName, options);

注意:组件的注册,要在实例化实例之前完成。即:

Vue.component('my-component', {
    template: '<div>This is a component</div>'
});

new Vue({
    el: '#app'
});

执行效果:

<div id="app">
    <my-component></my-component>
</div>
<!-- 渲染为 -->
<div id="app">
    <div>This is a component</div>
</div>

2、局部注册

除了全局注册一个组件外,我们还可以使用局部注册来注册一个组件,这种情况下,组件仅在另一个实例/组件的作用域中可用,使用方法为:

var COM = {
    template: '<div>This is a local component</div>'
};

new Vue({
    el: '#app',
    components: {
        'my-component': COM
    }
})

3、模板解析

由于一些HTML元素限制了其自身能够包裹的内容(如<ul><table><select>这些元素),因此,对于这些受限制的组件,不宜用以下的方式来使用组件:

<table>
    <my-row></my-row>
</table>

而应该使用以下的方式:

<table>
    <tr is="my-row"></tr>
</table>

不过,如果使用以下来源之一的字符串模板,就可以避开这些限制:

  • <script type="text/x-template">
  • JavaScript内联模板字符串
  • .vue组件


二、组件中的data

在Vue中,组件中使用的data,应该是一个函数,而不应该是一个对象。即:

Vue.component('my-component', {
    template: '<span>{{ message }}</span>',
    data: {
        message: 'Hello'
    }
});

是会报错的,正确的使用方式应该为:

Vue.component('my-component', {
    template: '<span>{{ message }}</span>',
    data: function() {
        return {
            message: 'Hello'
        }
    }
});

这是为了让每个组件都拥有各自的内部状态

三、父子组件协作

Vue中,父子组件之间的协作,主要通过:props down,events up来实现,即:

  • Parent → 传递props → Child
  • Parent ← 发送事件 ← Child

1、父组件给子组件传递数据(通过props

组件的实例的作用域是孤立的,因此,子组件内是没有办法直接引用到父组件数据的。因此,Vue提供了props选项,来实现父组件数据到子组件的传递。当子组件需要用到父组件的数据时,子组件就在自己的option中的props里声明期望得到的数据,然后由父组件传递给它。如:

Vue.component('say', {
    props: ['message'], // 期望得到message这个数据
    template: '<span>{{ message }}</span>'
});

new Vue({
    el: '#app'
});

那么,父组件就可以通过给子组件添加一个props中对应的属性,来将数据传递给子组件了。即:

<div id="app">
    <say message="Hello, world"></say>
</div>

如此一来,message属性中的内容便传入了子组件,最终会渲染得到:

<div id="app">
    <span>Hello, world</span>
</div>

除了使用字符串字面量来传入props的值,我们还可以使用动态的props,如:

<div id="app">
    <input type="text" v-model="message" />
    <say v-bind:message="message"></say>
</div>

注意: props中的属性,如果是camelCase形式的,那么在HTML中需要转化为kebab-case(因为HTML不区分大小写),即:props: ['myMessage'],传入的时候应该使用形如<say my-message="Hello, world"></say>
此外,直接使用字面量形式传递的属性,其值类型为String,这种情况下,会导致一定的问题,如<comp age="21"></comp>,age的值类型为string,而非预期的number。所以,如果要解决这个问题的话,那么就应当使用v-bind,即:<comp v-bind:age="21"></comp>
单向数据流
prop的传递是单向传递的,即:父组件属性变化时,会传递给子组件,但是子组件的数据变化时,不会传给父组件。如此设计,是为了避免子组件无意中改变了父组件的状态。
如果想修改props中的数据,正确的方式应该是:
1)定义一个局部变量,使用prop的值初始化,即:

props: ['message'],
data: function() {
    return {
        mymessage: this.message
    }
}

2)定义一个计算属性,处理并返回

props: ['message'],
computed: {
    mymessage: function() {
        this.message.toUpperCase();
    }
}

限制props

我们可以为props传入一些参数,来限制props的数据规则。此时,就不能再使用字符串数组指定props了,而是传入一个对象。如:

Vue.component('example', {
    props: {
        // 限制必须为基础类型,如果传入null,则任何类型都可以
        A: Number, 
        // 限制可以为一组的基础类型
        B: [Number, String],
        // 既要限制为某个基础类型,也要求必传这个参数,且指定默认值
        C: {
            type: String,
            required: true,
            default: 'hello' // 默认值可以是字面量,也可以是一个工厂函数返回
        },
        // 数组/对象的默认值应该使用工厂函数返回,如:
        D: {
            type: Object,
            default: function() {
                return { message: 'Hello' }
            }
        },
        // 自定义验证函数
        E: {
            validator: function(value) {
                return value > 10
            }
        }
    }
});

props验证失败的时候,如果当前使用的是开发版本,那么Vue会抛出一个警告。此外,props的校验,是在组件实例创建之前进行的

2、子组件给父组件传递消息

子组件给父组件传递数据,主要使用自定义事件。Vue的自定义事件,主要是:

  • 使用$on(eventName)来监听事件
  • 使用$emit(eventName)来触发事件

$on行为类似于addEventListener,而$emit行为类似于dispatchEvent,但是它们并不是这两个函数的别名。事实上,Vue中的这两个函数,是分离自EventTarget API

例子:我们想实现一个父子组件,父组件有一个计数器,它的值为子组件的计数器值之和。那么我们可以使用Vue的事件机制来实现,即如:

<div id="app">
    {{ parentCounter }}
    <sub-counter v-on:subinc="addParentCounter"></sub-counter>
    <sub-counter v-on:subinc="addParentCounter"></sub-counter>
</div>
Vue.component('sub-counter', {
    template: '<button v-on:click="increment">{{counter}}</button>',
    data() {
        return {
            counter: 0
        }
    },
    methods: {
        increment() {
            this.counter++;       // 自身+1
            this.$emit('subinc'); // 发送+1事件给父组件
        }
    }
});

new Vue({
    el: '#app',
    data: {
        parentCounter: 0
    },
    methods: {
        addParentCounter() {
            this.parentCounter++;
        }
    }
});

如果想要监听一个原生事件的话,可以使用.native修饰v-on,如:

<my-component v-on:click.native="doTheThing"></my-component>

3、自定义事件的表单输入组件

对于<input v-model="something">,它相当于<input v-bind:value="something" v-on:input="something = $event.target.value">。如果要在组件中使用v-model,让它生效的话,组件需要满足:

  • 接受一个value属性
  • 在有新的value时触发input事件

示例:一个简单的货币输入的自定义控件

<currency-input v-model="price"></currency-input>
Vue.component('currency-input', {
    template: '<span>$ <input ref="input" v-bind:value="value" v-on:input="updateValue($event.target.value)"></span>',
    props: ['value'], // 父组件需要给子组件传递一个value
    methods: {
        updateValue: function(value) {
            var formattedValue = value.trim().slice(0, value.indexOf('.')+3);
            if(formattedValue !== value) {
                this.$refs.input.value = formattedValue;
            }
            this.$emit('input', Number(formattedValue));
        }
    }
});

4、非prop属性

prop属性,是指可以直接传入子组件,但是不需要预先写在props里的属性。
这种情况下,属性会被自动添加到组件模板中的根元素上,如:

<bs-date-input data-3d-date-picker="true"></bs-date-input>

其中bs-date-input组件的模板为:

<div>Hello!!</div>

那么添加data-3d-date-picker="true"后,将有:

<div data-3d-date-picker="true">Hello!!</div>

而如果data-3d-date-picker是props属性,那么则会生成:

<div>Hello!!</div>

现在,我们会遇到有一种情况:在父组件使用子组件的时候,给子组件传了一个非prop属性,但是子组件里有同名的属性,如:

父组件中:
<my-input class="special"></my-input>

子组件中:

Vue.component('my-input', {
    template: `<input class="form-control">`,
    // ...
})

这种情况下,Vue会如何处理呢?情况是这样子的:
1)对于classtype这种属性,vue会做合并处理,所以上例子中最后会生成:

<input class="form-control special">

2)其他的属性,则会做替换处理(父组件中传入的值替换子组件模板中相应属性的值)

5、.sync修饰符

在Vue1.x中,提供了.sync修饰符。上面中提到,vue中子组件与父组件通信的方式为子组件给父组件emit一个事件,父组件监听事件。而.sync则可以对一个prop进行双向绑定,即当父组件传给子组件一个prop时,当子组件改变了这个prop对应的状态后,那么父组件里的状态也被改变。如:

父组件中:
<subcom :someprop.sync="someState"></subcom>

当在子组件中改变了someprop对应状态的时候,父组件中someState也会随之变化。
但是这个特征在vue2.0中移除了,又在vue2.3中重新引入,但实际上vue2.3中.sync只是一个语法糖,它实际上是做了以下的事情:

<comp :foo.sync="bar"></comp>

会被扩展为:

<comp :foo="bar" @update:foo="val => bar = val"></comp>

这种情况下,当子组件需要更新foo的值时,需要显示地触发:

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

6、v-model只是一个语法糖

对于<input v-model="something">来说,它实现的是input数据和js中数据的双向绑定,而这种写法实际上是一种语法糖,它相当于:

<input
    v-bind:value="something"
    v-on:input="something = $event.target.value"
>

而同样的,当我们在一个组件里写了:

<comp v-model="something"></comp>

实际上也相当于:

<comp
    v-bind:value="something"
    v-on:input="something = arguments[0]"
></comp>

所以要让一个自定义组件的v-model生效,那么这个组件需要满足:

  • 接受一个value属性
  • 在有新的值输入时,触发input事件,更新和value属性绑定的那个值

7、定制组件的v-model

通常情况下,一个组件的v-model会使用value属性和input事件,但某些情况下,如checkbox和radio,它们的value属性可能用作了其他目的,那么这种情况下就可以使用model选项来定制v-model。
原生的vue中,checkbox中v-model是和一个数组进行绑定的,但是现在我们希望有另外的一种checkbox,其v-model和其绑定的属性的Boolean值挂钩,那么,实现如下:

Vue.component('my-checkbox', {
    model: {
        prop: 'checked',
        event: 'change'
        /*
        这么写之后,<my-checkbox v-model="_v" /> 就相当于:
        <my-checkbox :checked="_v" @change="val => { _v = val }" />
        */
    },
    props: ['checked'],
    template: '<input type="checkbox" :checked="checked" @click="update($event)" />',
    methods: {
        update($event) {
            this.$emit('change', $event.target.checked);
        }
    }
});

8、非父子组件通信

当两个组件不是父子组件关系,但是又需要进行通信的时候,在简单场景下,可以这么做:

let bus = new Vue();
// 组件A
bus.$emit('eventName', carriedData);
// 组件B
bus.$on('eventName', function(carriedData) {
    // ...
});

更复杂情况下,则应该使用vuex来进行状态管理

9、内容分发

Vue中使用slot来实现内容分发,内容分发就是子组件模板的部分内容,不由子组件自身提供,而是由父组件来传递,从而实现父子组件更灵活的组合。例子如:

匿名slot

父组件模板:

<div>
    <h1>Headline</h1>
    <child>
        <p>这些内容是传递给子组件的</p>
    </child>
</div>

那么,子组件child中如何获得<p>这些内容是传递给子组件的</p>这段内容呢?那么,子组件的模板可以如:

<div>
    <p>子组件自己的内容</p>
    <slot></slot>
</div>

那么,组合后,子组件就会渲染为:

<div>
    <p>子组件自己的内容</p>
    <p>这些内容是传递给子组件的</p>
</div>

最后组合为:

<div>
    <h1>Headline</h1>
    <div>
        <p>子组件自己的内容</p>
        <p>这些内容是传递给子组件的</p>
    </div>
</div>

具名slot

具名slot可以实现更精准的内容分发,如:
父组件模板:

<child>
    <h1>Hello, world!</h1>
    <template slot="header"><p>传给子组件的头部</p></template>
    <template slot="footer"><p>传给子组件的尾部</p></template>
</child>

子组件模板:

<div>
    <p>我是一个子组件</p>
    <header>
        <slot name="header"></slot>
    </header>
    <slot></slot>
    <footer>
        <slot name="footer"></slot>
    </footer>
</div>

那么,组合的时候,<slot name="header"></slot>会被以下代码所替换:

<p>传给子组件的头部</p>

<slot name="footer"></slot>则被以下代码所替换:

<p>传给子组件的尾部</p>

而匿名的slot,则由排除掉具名slot后剩余的内容所替换,为:

<h1>Hello, world!</h1>

所以最终会渲染为:

<div>
    <p>我是一个子组件</p>
    <header>
        <p>传给子组件的头部</p>
    </header>
    <h1>Hello, world!</h1>
    <footer>
        <p>传给子组件的尾部</p>
    </footer>
</div>

作用域插槽

作用域插槽允许子组件在获得父组件传递的内容的同时,可以给父组件传递的内容运用自身的属性。例子如下:
父组件模板:

<div class="parent">
    <child>
        <h1>Hello, world!</h1>
        <template scope="item">
            <span>{{item.title}}</span>
        </template>
    </child>
</div>

子组件模板:

<div class="child">
    <p>我是一个子组件</p>
    <slot title="Message from child"></slot>
    <!-- 这里子组件中把title属性传递给父组件 -->
</div>

所以,子组件中的slot部分会被父组件中的下列部分替换:
** 这里,只有包裹在<template scope="item"></template>内的内容有效,所以是:

<span>{{item.title}}</span>

从而得到:

<div class="child">
    <p>我是一个子组件</p>
    <span>{{item.title}}</span>
</div>

而其中的item.title将被替换为Message from child,从而得到:

<div class="child">
    <p>我是一个子组件</p>
    <span>Message from child</span>
</div>

因此最终渲染结果为:

<div class="parent">
    <div class="child">
        <p>我是一个子组件</p>
        <span>Message from child</span>
    </div>
</div>

此外,作用域插槽还可以是具名的,通常的作用是用作列表组件,允许父组件来定义如何渲染列表的每一项,如:
父组件中:

<mylist>
    <template slot="item" scope="props">
        <li class="listItem">{{props.title}}</li>
    </template>
</mylist>

子组件中:

<ul>
    <slot name="item" v-for="item in items" :title="item.title"></slot>
</ul>

10、动态组件

可以通过<component>元素,让多个组件使用同一个挂载点,动态切换,如:

<component v-bind:is="currentView"></component>

其中JS部分为:

let vm = new Vue({
    el: '#app',
    data: {
        currentView: 'home'
    },
    components: {
        home: { /* ... */ },
        posts: { /* ... */ },
        archive: { /* ... */ }
    }
})

keep-alive

如果希望把切换出去的组件保留在内存中,保留其状态或者避免重新渲染,可以使用keep-alive,如:

<keep-alive>
    <component :is="currentView"></component>
</keep-alive>

11、其他杂项

1)可复用组件

Vue中组件的API主要来自于三个部分:

  • Props 允许外部环境传递数据给组件
  • Events 允许组件传递数据给外部组件
  • Slots 允许外部组件将额外的内容组合在组件中

2)子组件索引ref

可以使用ref为子组件指定一个索引ID,如:

<child ref="childCom"></child>

然后父组件中可以通过以下方式访问子组件:

this.$refs.childCom

refv-for一起使用时,ref是一个数组,包含相应的子组件。此外,应该注意的是:

$refs是在组件渲染完成后填充的,且是非响应式的

3)递归组件

组件在它的模板内可以递归调用自己,但是需要有name选项时才可以,如:

name: 'my-comp'

不过,当我们使用Vue.component全局注册一个组件的时候,全局的ID就会作为name选项被自动设置。使用递归的时候应该特别注意:要确保递归调用有终止条件,否则会引起死循环

4)内联模板

当为子组件设置inline-template属性时,其内容将作为模板,而不是内容分发,如:

<child inline-template>
    <div>
        <p>这里的内容都会被编译为组件组件的模板</p>
        <p>是不会作为父组件的分发内容的</p>
    </div>
</child>

child组件的template选项将被指定为:

<div>
    <p>这里的内容都会被编译为组件组件的模板</p>
    <p>是不会作为父组件的分发内容的</p>
</div>

5)X-Templates

也可以使用X-Templates来定义模板,如:

<script type="text/x-templates" id="app">
    <p>Hello!!</p>
</script>
Vue.component('my-comp', {
    template: '#app'
})

6)推荐对低开销的静态组件使用v-once

当组件中包含大量的静态内容时,可以使用v-once将渲染结果缓存起来,如:

Vue.component('terms-of-service', {
    template: `
        <div v-once>
            <h1>Terms of Service</h1>
            ... a lot of static contents ...
        </div>
    `;
});