Vue 组件详解

参考书籍:《Vue.js 实战》数组

组件与复用

为何使用组件

Vue 的组件就是提升复用性,让代码可复用。app

组件用法

<div id="app">
    <my-component></my-component>
</div>

组件须要注册后才可使用,注册有全局注册和局部注册两种方式。函数

  1. 全局注册后,任何 Vue 实例均可以使用。组件化

    // 要在父实例中使用这个组件,必需要在实例建立前注册。
    Vue.component('my-component', {
        template: '<div>my component</div>'
    });
    
    var app = new Vue({
        el: '#app'
    });
  2. 在 Vue 实例中,使用 components 选项能够局部注册组件,注册后的组件只有在该实例做用域下有效。布局

    var Child = {
        template: '<div>my component</div>'
    };
    
    var app = new Vue({
        el: '#app',
        components: {
            'my-component': Child
        }
    });

渲染后的结果是:网站

<div id="app">
    <div>my component</div>
</div>

Vue 组件的模板在某些状况下会收到 HTML 的限制,好比 <table> 内规定只容许是 <tr><td> 等这些表格元素,因此在 <table> 内直接使用组件是无效的,这种状况下可使用特殊的
is 属性来挂载组件。ui

<div id="app">
    <table>
        <tbody is="my-component"></tbody>
    </table>
</div>
Vue.component('my-component', {
    template: '<div>my component</div>'
});

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

渲染后的结果是:this

<div id="app">
    <table>
        <div>my component</div>
    </table>
</div>

在组件中使用 data 时,必须是函数,而后将数据 return 出去。设计

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

使用 props 传递数据

基本用法

组件不只仅是要把模板的内容进行复用,更重要的是组件间要进行通讯。一般父组件的模板中包含子组件,父组件要正向地向子组件传递数据或参数,子组件接收到后根据参数的不一样来渲染不一样的内容或执行操做。这个正向传递数据的过程就是经过 props 来实现的。双向绑定

props 中声明的数据与组件 data 函数内的数据主要区别就是 props 的数据来自父级,而 data 的数据是组件本身的数据,这两种数据均可以在模板 template 、计算属性 computed 和方法 methods 中使用。

一般,传递的数据并非直接写死的,而是来自父级的动态数据,这时可使用指令 v-bind 来动态绑定 props 的值,当父组件的数据变化时,也会传递给子组件。

因为 HTML 特性不区分大小写,当时用 DOM 模板时,驼峰命名(camelCase)的 props 名称要转为短横分隔命名(kebab-case)。

<div id="app">
    <input type="text" v-model="parentMessage" />
    <my-component :my-message="parentMessage"></my-component>
</div>
Vue.component('my-component', {
    props: ['myMessage'],
    template: '<div>{{ myMessage}}</div>'
});

var app = new Vue({
    el: '#app',
    data: {
        parentMessage: ''
    }
});

渲染后的结果是:

<div id="app">
    <div>dataMes</div>
</div>

这里用 v-model 绑定了父级的数据 parentMessage,当经过输入框任意输入时,子组件接收到的 props 也会实时响应,并更新组件模板。

单向数据流

Vue 经过 props 传递数据是单向的,也就是父组件数据变化时会传递给子组件,可是反过来不行。之因此这样设计,是尽量将父子组件解耦,避免子组件无心中修改了父组件的状态。

业务中会常常遇到两种须要改变 prop 的状况。

  1. 一种是父组件传递初始值进来,子组件将它做为初始值保存起来,在本身的做用域下能够随意使用和修改。

    <div id="app">
        <my-component :init-count="1"></my-component>
    </div>
    Vue.component('my-component', {
        props: ['initCount'],
        template: '<div>{{ count }}</div>',
        data() {
            return {
                count: this.initCount
            }
        }
    });
    
    var app = new Vue({
        el: '#app'
    });

    组件中声明了数据 count,它在组件初始化时会获取来自父组件的 initCount,以后就与之无关了,只用维护 count,这样就能够避免直接操做 initCount

  2. 另外一种状况就是 prop 做为须要被转变的原始值传入。

    <div id="app">
        <my-component :width="100"></my-component>
    </div>
    Vue.component('my-component', {
        props: ['width'],
        template: '<div :style="style">组件内容</div>',
        computed: {
            style() {
                return {
                    width: this.width + 'px'
                }
            }
        }
    });
    
    var app = new Vue({
        el: '#app'
    });

    由于用 CSS 传递宽度要带单位(px),可是每次都写太麻烦了,并且数值计算通常是不带单位的,因此统一在组件内使用计算属性就能够了。

数据验证

Vue.component('my-component', {
    props: {
        propA: Number,
        propB: [String, Number],
        propC: {
            type: Boolean,
            default: true
        },
        propD: {
            type: Number,
            required: true
        },
        // 若是是数组或对象,默认值必须是一个函数来返回
        propE: {
            type: Array,
            default() {
                return [];
            }
        },
        propF: {
            validator(value) {
                return value > 10;
            }
        }
    }
});

组件通讯

组件关系可分为父子组件通讯、兄弟组件通讯和跨级组件通讯。

自定义事件

当子组件须要向父组件传递数据时,就要用到自定义事件。

子组件用 $emit() 来触发事件,父组件用 $on 来监听子组件的事件。

父组件也能够直接在子组件的自定义标签上使用 v-on 来监听子组件触发的自定义事件。

<div id="app">
    <p>总数:{{ total }}</p>
    <my-component
        @increase="handleGetTotal"
        @reduce="handleGetTotal">
    </my-component>
</div>
Vue.component('my-component', {
    template: `
        <div>
            <button @click="handleIncrease">+1</button>
            <button @click="handleReduce">-1</button>
        </div>
    `,
    data() {
        return {
            counter: 0
        }
    },
    methods: {
        handleIncrease() {
            this.counter++;
            this.$emit('increase', this.counter);
        },
        handleReduce() {
            this.counter--;
            this.$emit('reduce', this.counter);
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        total: 0
    },
    methods: {
        handleGetTotal(total) {
            this.total = total;
        }
    }
});

上面示例中,在改变组件的 counter 后,经过 $emit() 再把它传递给父组件。$emit() 方法的第一个参数是自定义事件的名称,后面的参数是要传递的数据,能够不填或填写多个。

除了用 v-on 在组件上监听自定义事件外,也能够监听 DOM 事件,这时能够用 .native 修饰符表示监听的是一个原生事件,监听的是该组件的根元素。

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

使用 v-model

Vue 能够在自定义组件上使用 v-model 指令。

<div id="app">
    <p>总数:{{ total }}</p>
    <my-component v-model="total"></my-component>
</div>
Vue.component('my-component', {
    template: '<button @click="handleIncrease">+1</button>',
    data() {
        return {
            counter: 0
        }
    },
    methods: {
        handleClick() {
            this.counter++;
            this.$emit('input', this.counter);
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        total: 0
    }
});

在使用组件的父级,并无在 <my-component> 使用 @input="handler",而是直接用了 v-model 绑定的一个数据 total。这也能够称做是一个语法糖,由于上面的示例能够间接地用自定义事件来实现:

<div id="app">
    <p>总数:{{ total }}</p>
    <my-component @input="handleGetTotal"></my-component>
</div>
// 省略组件代码

var app = new Vue({
    el: '#app',
    data: {
        total: 0
    },
    methods: {
        handleGetTotal() {
            this.total = total;
        }
    }
});

v-model 还能够用来建立自定义的表单输入组件,进行数据双向绑定。

<div id="app">
    <p>总数:{{ total }}</p>
    <my-component v-model="total"></my-component>
    <button @click="handleReduce">-1</button>
</div>
Vue.component('my-component', {
    props: ['value'],
    template: '<input :value="value" @input="updateValue" />',
    methods: {
        updateValue(event) {
            this.$emit('input', event.target.value);
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        total: 0
    },
    methods: {
        handleReduce() {
            this.total--;
        }
    }
});

实现这样一个具备双向绑定的 v-model 组件要知足下面两个条件:

  1. 接收一个 value 属性。
  2. 在有新的 value 时触发 input 事件。

非父子组件通讯

非父子组件通常有两种,兄弟组件和跨多级组件。

在 Vue 中,推荐使用一个空的 Vue 实例做为中央事件总线(bus),也就是一个中介。

<div id="app">
    {{ message }}
    <component-a></component-a>
</div>
var bus = new Vue();

Vue.component('component-a', {
    template: '<button @click="handleEvent">传递事件</button>',
    methods: {
        handleEvent() {
            bus.$emit('on-message', '来自组件 component-a 的内容');
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        message: ''
    },
    mounted() {
        var _this = this;
        
        // 在实例初始化时,监听来自 bus 实例的事件
        bus.$on('on-message', function(msg) {
            _this.message = msg;
        });
    }
});

首先建立一个名为 bus 的空 Vue 实例,而后定义全局组件 component-a,最后建立 Vue 实例 app。在 app 初始化时,监听了来自 bus 的事件 on-message,而在组件 component-a 中,点击按钮会经过 bus 把事件 on-message 发出去,此时 app 就会接收到来自 bus 的事件,进而在回调里完成本身的业务逻辑。

这种方法巧妙而轻量地实现了任何组件间的通讯,包括父子、兄弟和跨级。若是深刻使用,能够扩展 bus 实例,给它添加 datamethodscomputed 等选项,这些都是能够共用的,在业务中,尤为是协同开发时很是有用,由于常常须要共享一些通用的信息,好比用户登陆的昵称、性别、邮箱和受权等。只需子安初始化时让 bus 获取一次,任什么时候间、任何组件就能够从中直接使用,在单页面富应用(SPA)中会很实用。

除了中央事件总线 bus 外,还有两种方法能够实现组件间通讯:父链和子组件索引。

父链

在子组件中,使用 this.$parent 能够直接访问该组件的父实例或组件,父组件也能够经过 this.$children 访问它全部的子组件,并且能够递归向上或向下无限访问,直到根实例或最内层的组件。

<div id="app">
    {{ message }}
    <component-a></component-a>
</div>
Vue.component('component-a', {
    template: '<button @click="handleEvent">经过父链直接修改数据</button>',
    methods: {
        handleEvent() {
            // 访问到父链后,能够作任何操做,好比直接修改数据
            this.$parent.message = '来自组件 component-a 的内容'
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        message: ''
    }
});

尽管 Vue 容许这样操做,但在业务中,子组件应该尽量避免依赖父组件的数据,更不该该去主动修改它的数据,由于这样使得父子组件耦合,只看父组件,很难理解父组件的状态,由于它可能被任意组件修改,理想状况下,只有组件本身能修改它的状态。父子组件最好仍是经过 props$emit() 来通讯

子组件索引

当子组件较多时,经过 this.$children 来一一遍历出咱们须要的一个组件实例时比较困难的,尤为是组件动态渲染时,它们的序列是不固定的。Vue 提供了子组件索引的方法,用特殊的属性 ref 来为子组件指定一个索引名称。

<div id="app">
    <button @click="handleRef">经过 ref 获取子组件实例</button>
    <component-a ref="comA"></component-a>
</div>
Vue.component('component-a', {
    template: '<div>子组件</div>',
    data() {
        return {
            message: '子组件内容'
        }
    }
});

var app = new Vue({
    el: '#app',
    methods: {
        handleRef() {
            // 经过 $refs 来访问指定的实例
            var msg = this.$refs.comA.message;
            console.log(msg);
        }
    }
});

提示:$refs 只在组件渲染完成后才填充,而且它是非响应式的。它仅仅做为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用 $refs

使用 slot 分发内容

什么是 slot

下面是一个常规的网站布局组件化后的机构:

<app>
    <menu-main></menu-main>
    <menu-sub></menu-sub>
    <div class="container">
        <menu-left></menu-left>
        <container></container>
    </div>
    <app-footer><app-footer>
</app>

当须要让组件组合使用,混合父组件的内容与子组件的模板时,就会用到 slot,这个过程叫作内容分发(transclusion)。以 <app> 为例,它有两个特色:

  1. <app> 组件不知道它的挂载点会有什么内容。挂载点的内容由 <app> 的父组件决定。
  2. <app> 组件极可能有它本身的模板。

props 传递数据、events 触发事件和 slot 内容分发就构成了 Vue 组件的 3 个 API来源,再复杂的组件也是由这 3 部分构成的。

做用域

父组件模板的内容是在父组件做用域内编译,子组件模板的内容是在子组件做用域内编译。

<div id="app">
    <child-component v-show="showChild"></child-component>
</div>
Vue.component('child-component', {
    template: '<div>子组件</div>'
});

var app = new Vue({
    el: '#app',
    data: {
        showChild: true
    }
});

这里的状态 showChild 绑定的是父组件的数据,若是想在子组件上绑定,应该是:

<div id="app">
    <child-component></child-component>
</div>
Vue.component('child-component', {
    template: '<div v-show="showChild">子组件</div>',
    data() {
        return {
            showChild: true
        }
    }
});

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

所以,slot 分发的内容,做用域是在父组件上的。

slot 用法

单个 slot

在子组件内使用特殊的 <slot> 元素就能够为这个子组件开启一个 slot(插槽),在父组件模板里,插入在子组件标签内的全部内容将替代子组件的 <slot> 标签及它的内容。

<div id="app">
    <child-component>
        <p>分发的内容</p>
        <p>更多分发的内容</p>
    </child-component>
</div>
Vue.component('child-component', {
    template: `
        <div>
            <slot>
                <p>若是父组件没有插入内容,我将做为默认出现</p>
            </slot>
        </div>
    `
});

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

上例渲染后的结果是:

<div id="app">
    <div>
        <p>分发的内容</p>
        <p>更多分发的内容</p>
    </div>
</div>

注意:子组件 <slot> 内的备用内容,它的做用域是子组件自己。

具名 slot

<slot> 元素指定一个 name 后能够分发多个内容,具名 slot 能够与单个 slot 共存。

<div id="app">
    <child-component>
        <h2 slot="header">标题</h2>
        <p>正文内容</p>
        <p>更多的正文内容</p>
        <div slot="footer">底部信息</div>
    </child-component>
</div>
Vue.component('child-component', {
    template: `
        <div class="container">
            <div class="header">
                <slot name="header"></slot>
            </div>
            <div class="main">
                <slot></slot>
            </div>
            <div class="footer">
                <slot name="footer"></slot>
            </div>
        </div>
    `
});

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

上例渲染后的结果是:

<div id="app">
    <div class="container">
        <div class="header">
            <h2>标题</h2>
        </div>
        <div class="main">
            <p>正文内容</p>
            <p>更多的正文内容</p>
        </div>
        <div class="footer">
            <div>底部信息</div>
        </div>
    </div>
</div>

注意:若是没有指定默认的匿名 slot,父组件内多余的内容片断都将被抛弃。

做用域插槽

做用域插槽是一种特殊的 slot,使用一个能够复用的模板替换已渲染元素。

<div id="app">
    <child-component>
        <template scope="props">
            <p>来自父组件的内容</p>
            <p>{{ props.msg }}</p>
        </template>
    </child-component>
</div>
Vue.component('child-component', {
    template: `
        <div class="container">
            <slot msg="来自子组件的内容"><slot>
        </div>
    `
});

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

观察子组件的模板,在 <slot> 元素上有一个相似 props 传递数据给组件的写法 msg="xxx",将数据传到了插槽。父组件中使用了 <template> 元素,并且拥有一个 scope="props" 的特性,这里的 props 只是一个临时变量,就像 v-for="item in items" 里面的 item 同样。template 内能够经过临时变量 props 访问来自子组件插槽的数据 msg

上例渲染后的结果是:

<div id="app">
    <child-component>
        <p>来自父组件的内容</p>
        <p>来自子组件的内容</p>
    </child-component>
</div>

做用域插槽更具表明性的用例是列表组件,容许组件自定义应该如何渲染列表每一项。

<div id="app">
    <my-list :books="books">
        <!-- 做用域插槽也能够是具名的 slot -->
        <template slot="book" scope="props">
            <li>{{ props.bookName }}</li>
        </template>
    </my-list>
</div>
Vue.component('my-list', {
    props: {
        books: {
            type: Array,
            default() {
                return [];
            }
        }
    },
    template: `
        <ul>
            <slot name="book"
                v-for="book in books"
                :book-name="book.name"
            ></slot>
        </ul>
    `
});

var app = new Vue({
    el: '#app',
    data: {
        books: [
            { name: '《book1》' },
            { name: '《book2》' },
            { name: '《book3》' }
        ]
    }
});

子组件 my-list 接收一个来自父级的 prop 数组 books,而且将它在 namebookslot 上使用 v-for 指令循环,同时暴露一个变量 bookName

此例的用意主要是介绍做用域插槽的用法,并无加入使用场景,而做用域插槽的使用场景既能够复用子组件的 slot,又能够是 slot 内容不一致。若是此例还在其余组件内使用,<li> 的内容渲染权是由使用者掌握的,而数据却能够经过临时变量(好比 props)从子组件内获取。

访问 slot

Vue 提供了用来访问被 slot 分发的内容的方法 $slots

<div id="app">
    <child-component>
        <h2 slot="header">标题</h2>
        <p>正文内容</p>
        <p>更多的正文内容</p>
        <div slot="footer">底部信息</div>
    </child-component>
</div>
Vue.component('child-component', {
    template: `
        <div class="container">
            <div class="header">
                <slot name="header"></slot>
            </div>
            <div class="main">
                <slot></slot>
            </div>
            <div class="footer">
                <slot name="footer"></slot>
            </div>
        </div>
    `,
    mounted() {
        var header = this.$slots.header,
            main = this.$slots.default,
            footer = this.$slots.footer;
            
        console.log(footer);
        console.log(footer[0].elm.innerHTML);
    }
});

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

经过 $slots 能够访问某个具名 slotthis.$slots.default 包括了全部没有被包含在具名 slot 中的节点。

$slots 在业务中几乎用不到,在用 render 函数建立组件时会比较有用,但主要仍是用于独立组件开发中。

相关文章
相关标签/搜索