想看上一节的请点击:

Vue.js实战之系统学习第六节

接下来我们要学习第七节了,时间过的好快。

组件详解

组件是Vue.js的核心功能,也是整个框架设计最精彩的地方,当然也是最难掌握的。本章节将带你由浅入深地学习组件地全部内容,并通过几个实战项目熟练使用Vue组件。

组件与复用

在正式介绍组件前,我们先来看一个简单地场景

这是很常见的聊天界面,有一些标准的空间,比如右上角的关闭按钮、输入框、发送按钮等。你可能要问了,这有什么难的,不久几个div、input吗?好,那现在需求升级了,这几个控件还有别的地方要用到。没问题,复制粘贴呗。那如果输入框要带数数验证,按钮的图片支持自定义呢?这样用JavaScript封装后一起复制把,那等到项目快结束时,产品经理说,所有使用输入框的地方,都要改成支持回车键提交。好吧,给我一天时间,我一个一个加上去。

上面的需求虽然有点变态,但却是业务中很常见的,那就是一些控件、JavaScript能力的复用。没错,Vue.js的组件就是提高重用性的,让代码可复用,当学习完组件后,上面的问题就可以分分钟搞定了,再也不用还哦啊产品经理的奇葩需求了。

这是上面效果图的示例代码,那些没见过的就是自定义标签,每一个标签代表一个组件,在任何可以i使用Vue的地方都可以直接使用。接下来我们就看看组件的具体用法。

组件用法

组件与创建Vue实例类似,需要注册后才可以使用。注册有全局注册和局部注册两种方式。全局注册后,任何Vue实例都可以使用。全局注册实例代码如下:

Vue.component('my-component'){// 选项
}

my-component就是注册组件自定义标签的名称,推荐使用小写加减号分割的形式命名。

要在父类实例中使用这个组件,必须要在实例创建前注册,之后就乐意用<my-component></my-component>的形式来使用组件了,代码如下:

        <div id="app"><my-component></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{// 选项});var app = new Vue({el:'#app',data:{message:''}})</script>

此时打开页面还是空白的,因为我们注册的组件还没有任何内容,在组件选项中添加template就可以显示组件内容了,

Vue.component('my-component',{template:'<div>这里是组件的内容</div>'
});

渲染后的结果是:

<div id = "app"><div>这里是组件的内容</div>
</div>

template的DOM结构必须被一个元素包含,如果直接写成“这里是组件的内容”,不带“<div></div>”是无法渲染的。

在Vue实例中,使用components选项可以局部注册组件,注册后的组件只有在该实例作用域下有效。组件中也可以使用components选项来注册组件,使组件可以嵌套。示例代码如下:

        <div id="app"><my-component></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>var Child={template:'<div>这里是组件的内容</div>'}var app = new Vue({el:'#app',components:{'my-component':Child}})</script>

Vue组件的模板在某些情况下会受到HTML的限制,比如<table>内规定只允许是<tr>、<td>、<th>等这些表格元素,所以在<table>内直接使用组件是无效的。这种情况下,可以使用特殊的is属性来挂载组件,示例代码如下:

        <div id="app"><table><tbody is="my-component"></tbody></table></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>var Child={template:'<div>这里是组件的内容</div>'}var app = new Vue({el:'#app',components:{'my-component':Child}})</script>

tbody在渲染时,会被替换为组件的内容。常见的限制元素还有<ul>、<ol>、<select>。

注意:如果使用的时字符串模板,是不受限制的,比如后面章节介绍的   .vue 单文件用法等。

除了template选项外,组件中还可以像Vue实例那样使用其他的选项,比如data、computed、methods等。但是在使用data时,和实例稍有区别,data必须是函数,然后将数据return出去,例如:

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

JavaScript对象是引用关系,所以如果return出的对象引用了外部的一个对象,那这个对象就是共享的,任何一方修改都会同步。比如下面的示例:

        <div id="app"><my-component></my-component><my-component></my-component><my-component></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>var data = {counter:0};Vue.component('my-component',{template:'<button @click="counter++">{{counter}}</button>',data:function(){return data;}});var app = new Vue({el:'#app'})</script>

组件使用了三次,但是点击任意一个<button>,3个的数字都会加一,那是因为组件的data引用的是外部的对象,这肯定不是我们期望的效果,所以给组件返回的一个新的data对象来独立,示例代码如下:

        <div id="app"><my-component></my-component><my-component></my-component><my-component></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{template:'<button @click="counter++">{{counter}}</button>',data:function(){return {counter:0};}});var app = new Vue({el:'#app'})</script>

这样,点击3个按钮就互补影响了,完全达到了复用的目的。

使用props传递参数

基本用法

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

在组件中,使用选项props来生命需要从父级接收的数据,props的值可以是两种,一种是字符串数组,一种是对象,本小节先介绍数组的用法。比如我们构造一个数组,接收一个来自父级的数据message,并把它的组件模板中渲染,示例代码如下:

        <div id="app"><my-component message="来自父组件的数据"></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{props:['message'],template:'<div>{{message}}</div>'});var app = new Vue({el:'#app'})</script>

props中声明的数据与组件data函数return的数据主要区别就是props的来自父级,而data中的是组件自己的数据,作用域是组件本身,这两种数据都可以在模板template及计算属性computd和方法methods中使用。上例的数据message就可以通过props从父级传递过来的,在组件的自定义标签上直接写props的名称,如果要传递多个数据,在props数组中添加项即可。

由于HTML特性不区分大小写,当使用DOM模板时,驼峰命名的props名称要转为短分割命名,例如:

        <div id="app"><my-component warning-text="来自父组件的数据"></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{props:['warningText'],template:'<div>{{warningText}}</div>'});var app = new Vue({el:'#app'})</script>

注意:如果使用的是字符串模板,仍然可以忽略这些限制。

有时候,传递的数据并不是直接写死的,而是来自父级的动态数据,这时可以使用指令v-bind来动态绑定props的值,当父组件的数据变化时,也会传递给子组件。示例代码如下:

        <div id="app"><input type="text" v-model="parentMessage"><my-component :message="parentMessage"></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{props:['message'],template:'<div>{{message}}</div>'});var app = new Vue({el:'#app',data:{parentMessage:''}})</script>

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

注意:如果你要直接传递数字、布尔值、数组、对象,而且不使用v-bind,传递的仅仅时字符串,尝试下面的示例来对比:

        <div id="app"><my-component message="[1,2,3]"></my-component><my-component :message="[1,2,3]"></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{props:['message'],template:'<div>{{message.length}}</div>'});var app = new Vue({el:'#app'})</script>

同一个组件使用了两次,区别仅仅时第二个使用的是v-bind。渲染后的结果,第一个是7,第二个才是数组的长度3.

单向数据流

Vue  2.x与Vue  1.x比较的一个改变就是,Vue  2.x通过props传递数据是单向的了,也就是父组件数据变化会传给子组件,但是反过来不行。而在Vue  1.x里提供了 .sync修饰符来支持双向绑定。之所以这样设计,是尽可能将父子组件解耦,避免子组件无意中修改了父组件的状态。

业务中会经常遇到两种需要改变prop的情况,一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况写可以在组件data内再声明一个数据,引用父组件prop,示例代码如下:

        <div id="app"><my-component :init-count="1"></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{props:['initCount'],template:'<div>{{count}}</div>',data:function(){return {count:this.initCount};}});var app = new Vue({el:'#app'})</script>

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

另一种情况就是prop作为需要被转变的原始值传入。这种情况 用计算属性就可以了,示例代码如下:

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

因为用CSS传递宽度要带单位(px),但是每次都写太麻烦,而且数值计算一般时不带单位的,所以统一在组件没使用计算属性就可以了。

注意:再JavaScript中对象和数组是引用类型,指向同一内存空间,所以props是对象和数组时,在子组件内改变时会影响父组件的。

数据验证

我们上面所介绍的props选项的值都是一个数组,一开始也介绍过,除了数组外,还可以是对象,当props需要验证时,就需要对象写法。

一般当你的组件需要提供给别人使用时,推荐都进行数据验证,比如某个数据必须是数字类型,如果传入字符串,就会在控制台弹出警告。一下是几个props的示例:

验证type类型可以是:

String

Number

Boolean

Object

Array

Function

type也可以是一个自定义构造器,使用instanceof检测。

当prop验证失败时,在开发版本下会在控制台抛出一条警告。

组件通信

我们已经知道,从父组件向子组件通信,通过props传递数据就可以了,单Vue组件通信的场景不止有这一种,归纳起来,组件之间通信可以用下图表示:

组件关系可分为父子组件通信、兄弟组件通信、跨级组件通信。本节将介绍各种组件之间通信的方法。

自定义事件

当子组件需要向父组件传递数据时,就要用到自定义事件。我们在介绍指令v-on时有提到,v-on除了监听DOM事件外,还可以用于组件之间的自定义事件。

如果你了解过JavaScript的设计模式——观察者模式,一定知道dispatchEvent和addEventListener这两个方法。Vue组件也有与之类似的一套模式,子组件用$emit()来触发事件,父组件用$on()来监听子组件的事件。

父组件也可以直接在子组件的自定义标签上使用v-on来监听子组件触发的自定义事件,示例代码如下:

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Vue 示例</title></head><body><div id="app"><p>总数:{{total}}</p><my-component @increase="handleGetTotal" @reduce="handleGetTotal"></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{template:'\<div>\<button @click="handleIncrease">+1</button>\<button @click="handleReduce">-1</button>\</div>',data:function(){return{counter:0}},methods:{handleIncrease:function(){this.counter++;this.$emit('increase',this.counter);},handleReduce:function(){this.counter--;this.$emit('reduce',this.counter);}}});var app = new Vue({el:'#app',data:{total:0},methods:{handleGetTotal:function(total){this.total = total;}}})</script></body>
</html>

上面实例中,子组件有两个按钮,分别是实现加1和减一的效果,在改变组件的data“counter”后,通过$emit()再把它传递给父组件,父组件用v-on:increase和v-on:reduce(示例使用的语法糖)。$emit()方法的第一个参数是自定义事件的名称,例如示例increase和reduce后面的参数都是要传递的数据,可以不填或填写多个。

除了用v-on在组件上监听自定义事件外,也可以监听DOM事件,这时可以用.native修饰符表示听的是一个原生事件,监听的是该组件的根元素,示例代码如下:

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

使用v-model

Vue  2.x可以在自定义组件上使用v-model指令,先来个示例:

        <div id="app"><p>总数:{{total}}</p><my-component v-model="total"></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{template:'\<div>\<button @click="handleIncrease">+1</button>\<button @click="handleReduce">-1</button>\</div>',data:function(){return{counter:0}},methods:{handleIncrease:function(){this.counter++;this.$emit('input',this.counter);},handleReduce:function(){this.counter--;this.$emit('input',this.counter);}}});var app = new Vue({el:'#app',data:{total:0}})</script>

仍然点击的是按钮加1的效果,不过这次组件$emit()的事件名是特殊的input,在使用组件的父级,并没有在<my-component>上使用@input=“handle”,而是直接用v-model绑定的一个数据total。这也可以称作是一个语法糖,因为上面的示例可以间接地用自定义事件来实现。

        <div id="app"><p>总数:{{total}}</p><my-component @input="handleGetTotal"></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{template:'\<div>\<button @click="handleIncrease">+1</button>\<button @click="handleReduce">-1</button>\</div>',data:function(){return{counter:0}},methods:{handleIncrease:function(){this.counter++;this.$emit('input',this.counter);},handleReduce:function(){this.counter--;this.$emit('input',this.counter);}}});var app = new Vue({el:'#app',data:{total:0},methods:{handleGetTotal:function(total){this.total = total;}}})</script>

v-model还可以用来创建自定义的表单输入组件,进行数据双向绑定,例如:

        <div id="app"><p>总数:{{total}}</p><my-component v-model="total"></my-component><button @click="handleReduce">-1</button></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{props:['value'],template:'<input :value="value" @input="updateValue">',methods:{updateValue:function(event){this.$emit('input',event.target.value);}}});var app = new Vue({el:'#app',data:{total:0},methods:{handleReduce:function(){this.total--;}}})</script>

实现这样一个具有双向绑定的v-model组件要满足下面两个需求:

接收一个value属性。

在有新的value时触发input事件。

非父子组件通信

在实际业务中,除了父子组件通信外,还有很多非父子组件通信的场景,非父子组件一般有两种,兄弟组件和跨多级组件。为了更彻底的了解Vue.js  2.x中的通信方法,我们先来看一下在Vue.js  1.x中是如何实现的,这样便于我们了解Vue.js的设计思想。

在Vue.js 1.x中,除了$emit()方法外,还提供了$dispatch()和$broadcast()这两个方法。$dispatch()用于向上级派发事件,只要是它的父级(一级或多级以上),都可以在Vue实例的events选项内接收,示例代码如下:

<!-- 注意:该示例需要使用Vue  1.x的版本 --><div id="app">{{message}}<my-component></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{template:'<button @click="handleDispatch">派发事件</button>',methods:{handleDispatch:function(){this.$dispatch('on-message','来自内部组件的数据');}}});var app = new Vue({el:'#app',data:{message:''},events:{'on-message':function(msg){this.message = msg;}}})</script>

同理,$broadcast()是由上级向下级广播事件的,用法完全一致,只是方向相反。

这两张方法一旦发出事件后,任何组件都是可以接收到的,就近原则,而且会在第一次接收到后停止冒泡,除非返回true。

这两个方法虽然看起来很好用,但是在Vue.js 2.x中都废弃了,因为基于组件树结构的事件流方式让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱,并且不能解决兄弟组件通信的问题。

在Vue.js  2.x中,推荐使用一个空的Vue实例作为中央事件总线(bus),也就是一个中介。为了更形象的了解它,这里举个生活中的例子。

比如你需要盖房子,你可能会找房产中介来登记你的需求,然后中介把你的信息发给满足要求的出租者,出租者再把报价和看房的时间告诉中介,由中介再转达给你,整个过程中,买家和卖家并没有任何交流,都是通过中间人来传话的。

或者你最近可能要换房子了,你会找房产中介登记你的信息,订阅你找房需求相关的资讯,一旦有符合你的房子出现时,中介会通知你,并传达你房子的具体信息。

在这两个例子中,你和你出租者担任的就是两个跨级组件,而房产中介就是这个中央事件总线(bus)。比如下面的示例代码:

        <div id="app">{{message}}<my-component></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>var bus = new Vue();Vue.component('my-component',{template:'<button @click="handleEvent">传递事件</button>',methods:{handleEvent:function(){bus.$emit('on-message','来自组件的数据');}}});var app = new Vue({el:'#app',data:{message:''},mounted:function(){var _this = this;// 在实例初始化时,监听来自bus实例的事件bus.$on('on-message',function(msg){_this.message = msg;});}})</script>

首先创建了一个名为bus的空Vue实例,里面没有任何内容;然后全局定义了组件my-component;最后创建Vue实例app,在app初始化时,也就是在生命周期mounted钩子函数里面监听了来自bus的事件on-message,而在组件my-component中,点击按钮会通过bus把事件on-message发出去,此时app就会接收来自bus的事件,进而在回调里完成自己的业务逻辑。

这种方法巧妙而轻量的实现了任何组件间的通信,包括父子、兄弟、跨级、而且Vue  1.x和 Vue  2.x都适用。如果深入使用,可以扩展bus实例,给它添加data,methods、computed等选项,这些都是可以公用的,在业务中,尤其时协同开发时非常有用,因为经常需要共享一些通用的信息,比如用户登陆的昵称、性别、邮箱等,还有用户的授权token等。只需在初始化时让bus获取一次,任何时间、任何组件就可以从中直接使用了,在单页面富应用(SPA)中会很实用,我们会在进阶篇里逐步介绍这些内容。

当你项目比较大,有更多的小伙伴参与开发时,也可以选择更好的状态管理解决方案vuex,在进阶篇会详细介绍关于它的用法。

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

父链

在子组件中,使用this.$parent可以直接访问该组件的父实例或组件,父组件也可以通过this.$chidren访问它所有饿子组件,而且可以递归向上或向下无线访问,直到根实例或最内层的组件。示例代码如下:

        <div id="app">{{message}}<my-component></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{template:'<button @click="handleEvent">传递事件</button>',methods:{handleEvent:function(){// 访问到父链后,可以做任何操作,比如直接修改数据this.$parent.message='来自组件的数据';}}});var app = new Vue({el:'#app',data:{message:''}})</script>

尽管Vue允许这样操作,但在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只有组件自己能修改它的状态。父子组件最好还是通过props和$emit来通信。

子组件索引

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

        <div id="app"><button @click="handleRef">通过ref</button><my-component ref="comA"></my-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('my-component',{template:'<div>子组件</div>',data:function(){return {message:'子组件的内容'};}});var app = new Vue({el:'#app',methods:{handleRef:function(){// 通过 $refs来访问指定的实例var msg = this.$refs.comA.message;console.log(msg);}}})</script>

在父组件模板中,子组件标签上使用ref指定一个名称,并在父组件内通过this.$refs来访问指定名称的子组件。

注意:$refs只在租金渲染完成后才填充,并且它时非响应式的。它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用$refs.

与Vue 1.x不同的是,Vue 2.x将v-el和v-ref,Vue会自动去判断是普通标签还是组件。可以尝试补全下面的代码,分别打印出两个ref看看都是什么:

使用slot分发你内容

什么是slot

我们先看一个比较常规的网站布局:

这个网站由一级导航、二级导航、左侧列表、正文以及底部版权信息5个模板组成,如果要将它们都组件化,这个结构可能会是:

<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部分构成的。

作用域

正式介绍slot前,需要先知道一个概念:编译的作用域。比如父组件中有如下模板:

<child-component>{{message}}
</child-component>

这里是message就是一个slot,但是它绑定的是父组件的数据,而不是组件<child-component>的数据。

父组件模板的内容就是在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。例如下面的代码示例:

        <div id="app"><child-component v-show="showChild"></child-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('child-component',{template:'<div>子组件</div>'});var app = new Vue({el:'#app',data:{showChild:true}})</script>

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

        <div id="app"><child-component></child-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('child-component',{template:'<div v-show="showChild">子组件</div>',data:function(){return{showChild:true}}});var app = new Vue({el:'#app'})</script>

因此,slot分发的内容,作用域是在父组件上的。

slot用法

单个Slot

在子组件内使用特殊的<slot>元素就可以为这个子组件开启一个slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的<slot>标签及它的内容。示例代码如下:

        <div id="app"><child-component><p>分发的内容</p><p>更多分发的内容</p></child-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('child-component',{template:'\<div>\<slot>\<p>如果父组件没有插入内容,我将作为默认出现</p>\</slot>\</div>',data:function(){return{showChild:true}}});var app = new Vue({el:'#app'})</script>

子组件child-component的模板内定义了一个<slot>元素,并且用一个<p>作为默认的内容,在父组件没有使用slot时,会渲染这端默认的文本;如果写入了slot,那会替代整个<slot>。所以上例渲染后的结果是:

注意:子组件<slot>内的备用内容,它的作用域时子组件本身。

具名Slot

给<slot>元素指定一个name后可以分发更多的内容,具名Slot可以与单个Slot共存,例如下面的示例:

        <div id="app"><child-component><p>正文内容</p><p>更多正文内容</p><div slot="footer">底部信息</div></child-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>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'})</script>

子组件内声明了3个<slot>元素,其中在<div class="main">内的<slot>没有使用name特性,它将作为默认的slot出现,父组件没有使用slot特性的元素与内容都将出现在这里。

如果没有指定默认的匿名slot,父组件的多余内容片段都将被抛弃。

上例渲染后的结果为:

在组合使用组件时,内容分发API至关重要。

作用域插槽

作用域插槽slot,使用一个可以复用的模板替换已经渲染元素。概念比较难以理解,我们先看一个简单的示例来了解它的基本用法。示例代码如下:

        <div id="app"><child-component><template scope="props"><p>来自父组件的内容</p><p>{{props.msg}}</p></template></child-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('child-component',{template:'\<div class="container">\<slot msg="来自子组件的内容"></slot>\</div>'});var app = new Vue({el:'#app'})</script>

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

将上面的示例渲染后的最终结果为:

作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项。示例代码如下:

(很难受,垃圾csdn,吃了我几个小时的文章)

        <div id="app"><child-component :books="books"><!-- 作用域插槽也可以是具名的Slot --><template scope="props" slot="book"><li>{{props.bookName}}</li></template></child-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('child-component',{props:{books:{type:Array,default:function(){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:'《Vue.js实战》'},{name:'《JavaScript语言精粹》'},{name:'《Javascript高级程序设计》'}]}})</script>

子组件child-component接收来自一个父级的prop数组books,并且将它name为book的slot上使用v-for指令尊换,同时暴露一个变量bookName。

如果你仔细揣摩上面的写法,你可能会产生这样的疑问:我直接在父组件用v-for不就好了吗,为什么还要绕一步,在子组件里面循环呢?的确,如果只是针对上面的示例,这样写是多此一举的。此例的用意主要是介绍作用域插槽的用法,并没有加入使用场景,而作用域插槽的使用场景就是既可以复用子组件的slot,又可以使slot内容不一致。如果上例还在其他组件内使用,<li>的内容渲染权是由使用者掌握的,而数据却可以通过临时变量(比如props)从子组件内获取。

访问slot

在Vue.js  1.x中,想要获取某个slot是比较麻烦的,需要用v-el间接获取。为Vue.js  2.x提供了用来访问被slot分发的内容的方法$slots,请看下面的示例:

        <div id="app"><child-component><h2 slot="header">标题</h2><p>正文内容</p><p>更多的正文内容</p><div slot="footer">底部信息</div></child-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>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:function(){var header = this.$slots.header;var main = this.$slots.default;var footer = this.$slots.footer;console.log(footer);console.log(footer[0].elm.innerHTML);}});var app = new Vue({el:'#app'})</script>

通过$slot可以访问某个具名slot,this.$slot.default包括了所有没有被包含的具名slot中的字节点。尝试编写代码,查看两个console打印的内容。

$slot在业务中几乎用不到,在用render函数(进阶篇中将介绍)创建组件时会比较有用,但主要还是用于独立组件开发中。

组件高级用法

本节会介绍组件的一些高级用法,这些用法在实际业务中不是很常用,但在独立组件开发时可能会用到。如果你感觉以上内容已经足够你完成业务中的开发了们可以跳过本章节;如果你想继续探索Vue组件的奥秘,读完本节会对你有很大的启发。

递归组件

组件在它的模板内可以递归的调用自己,只要给组件设置name的选项就可以了。示例代码如下:

        <div id="app"><child-component :count="1"></child-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('child-component',{name:'child-component',props:{count:{type:Number,default:1}},template:'\<div class="child">\<child-component\ :count="count+1"\ v-if="count < 3"></child-component></div>'});var app = new Vue({el:'#app'})</script>

设置name后,在组件模板内就可以递归使用了,不过需要注意的是,必须给一个条件来限制递归数量,否则会抛出错误:max stack  size exceeded。

组件递归使用可以用来开发一些具有位置层级关系的独立组件,比如级联选择器和树形控件等,如下:

在实战篇里,我们会详细介绍级联选择器的实现。

内联模板

组件的模板一般都是在template选项内定义的,Vue提供了一个内联模板的功能,在使用组件时,给组件标签使用inline-template特性,组件就会把内容当作模板,而不是把内容分发,这让模板更灵活。实例代码如下:

        <div id="app"><child-component inline-template><div><h2>在父组件中定义组件的模板</h2><p>{{message}}</p><p>{{msg}}</p></div></child-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('child-component',{data:function(){return{msg:'在子组件中声明的数据'}}});var app = new Vue({el:'#app',data:{message:'在父组件声明的数据'}})</script>

渲染后的结果为:

在父组件中声明的数据message和子组件中声明的数据msg,两个都可以渲染(如果同名,优先使用子组件的数据)。这反而是内联模板的缺点,就是作用域比较难以理解,如果不是非常特殊的场景,建议不要轻易 使用内联模板。

动态组件

Vue提供了一个特殊的元素<component>用来动态地挂载不同地组件,使用js特性来选择要挂载地组件。示例代码如下;

        <div id="app"><component :is="currentView"></component><button @click="handleChangeView('A')">切换到A</button><button @click="handleChangeView('B')">切换到B</button><button @click="handleChangeView('C')">切换到C</button></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>var app = new Vue({el:'#app',components:{comA:{template:'<div>组件A</div>'},comB:{template:'<div>组件B</div>'},comC:{template:'<div>组件C</div>'}},data:{currentView:'comA'},methods:{handleChangeView:function(component){this.currentView = 'com'+component;}}})</script>

动态地改变courrentView地值就可以动态挂载组件。也可以直接绑定在组件对象上:

异步组件

当你的工程足够大,使用的组件足够多时,是时候考虑下性能问题了,因为一开始就把所有的组件都加载是没有必要的开销。好在Vue允许将组件定义为一个工厂函数,动态的解析组件。Vue只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的渲染。例如下面的示例:

        <div id="app"><child-component></child-component></div><script src="https://unpkg.com/vue/dist/vue.min.js"></script><script>Vue.component('child-component',function(resolve,reject){window.setTimeout(function(){resolve({template:'<div>我是异步渲染的</div>'});},2000);});var app = new Vue({el:'#app'})</script>

工厂函数接收一个resolve回调,在收到从服务器下载的组件定义时回调。也可以调用reject(reason)指示加载失败。这里setTimeout只是为了演示异步,具体的下载逻辑可以自己决定,比如把组件配置写成一个对象配置,通过Ajax来请求,然后调用resolve传入的配置选项。

在进阶篇里,我们还会介绍主流的打包编译工具webpack和.vue单文件的用法,更优雅地实现异步组件(路由);

其他

$nextTick

我们先来看这样的一个场景:有一个div,默认用v-if将它隐藏,点击一个按钮后,改变v-if的值,让它显示出来,同时拿到这个div的文本内容。如果v-if的值是false,直接去获取div的内容是获取不到的,因为此时div还没有被创建出来,那么应该在点击按钮后,改变v-if 的值为true,div才会被创建,此时再去获取,示例代码如下:

这段代码并不难理解,但是运行到控制台会抛出一个错误:Cannot read property 'innerHTML' of null ,意思就是获取不到div元素。这里就涉及Vue一个重要的概念:异步更新队列。

Vue在观察到数据变化时并不是直接更新DOM,而是开启一个队列,并缓冲在同一个事件循环中发生的所有数九改变。在缓冲时会除去重复数据,从而避免不必要的计算和DOM操作。然后,在下一个事件循环tick中,Vue刷新队列并执行实际(已去重的)工作。所以如果你用一个for循环来动态改变数据100+次,其实他只会应用最后一次改变,如果没有这种机制,DOM就要重绘100次,这固然是一个很大的开销。

Vue会根据当前浏览器环境优先使用原生的Promise.then和MutationObserver,如果都不支持,就会采用setTimeout代替。

知道了Vue一部更新DOM的原理,上面示例的报错也不难理解了。事实上,在执行this.showDiv=true;时,div仍然还没有被

创建出来,直到下一个Vue事件循环时,才开始创建。$nextTick就是用来知道什么时候DOM更新完成的,所以上面的示例代码修改为:

这时再点击按钮,控制台就打印出div的内容“这是一段文本”了。

理论上,我们应该不用去主动操作DOM,因为Vue核心思想就是数据驱动DOM,但在很多业务里,我们避免不了会使用一些第三方库,比如popper.js(https://popper.js.org/)、swiper(http://idangero.us/swiper/)等,这些基于原生JavaScript的库都有创建和更新及销毁的完整生命周期,与Vue配合使用时,就要利用好$nextTick。

X-Templates

如果你没有使用webpack、gulp等工具,试想一下你的组件template的内容很冗长、复杂,如果都在JavaScript里拼接字符串,效率是很低的,因为不能像写HTML那样舒服。Vue提供了另一种定义模板的方式,在<script>标签里使用text/x-template类型,并且指定一个id,将这个id赋给template。示例代码如下:

在<script>标签里,你可以愉快的写HTML代码,不用考虑换行的问题。

很多刚接触Vue开发的新手会非常喜欢这个功能,因为用它,就可以很轻松完成交互相对复杂的页面和应用了。如果再配合一些构建工具(gulp)组织好代码结构,开发一些中小型产品是没有问题的。不过,Vue的初衷并不是滥用它,因为它将模板和组件的其他定义隔离了。再进阶篇里,我们会介绍如何使用webpack来编译 .vue 的单文件,从而优雅的解决HTML书写 的问题。

手动挂载实例

我们现在所创建的实例都是通过new Vue()的形式创建出来的。在一些非常特殊的情况下,我们需要动态的去创建Vue实例,Vue提供了Vue.entend和$mount两个方法来手动挂载一个实例。

Vue.entend是基础Vue构造器,创建一个“子类”,参加是一个包含组件选项的对象。

如果Vue实例在实例化时没有收到el选项,它就处于“未挂载”状态,没有关联DOM元素。可以使用$mount()手动地挂载一个未挂载地实例。这个方法返回实例自身,因而可以链式调用其他实例方法。示例代码如下:

运行后,id为mount-div地div元素会被替换为组件MyComponent的template的内容:

除了这种写法外,以下了两种写法也是可以的:

或者,在文档之外渲染并且随后挂载

手动挂载示例(组件)是一种比较极端的高级用法,在业务中几乎用不到,只在开发一些复杂的独立组件时可能会使用,所以只做了解就好。

实战:两个常用组件的开发

本节以组件知识为基础,整合指令、事件等前面章节的内容,开发两个业务中常用的组件,即数字输入框和标签页。

开发一个数字输入框组件

数字输入框是对普通输入框的扩展,用来快捷输入一个标准的数字,如下图:

数字输入框是对普通输入框的扩展,用来快捷输入一个标准的数字。数字输入框只能输入数字,而且有两个快捷按钮,可以直接减一或者加一。除此之外,还可以设置初始值、最大值、最小值,在数值改变时,触发一个自定义事件来通知父组件。

了解了基本的需求后,我们先定义目录文件:

index.html 入口页

input-number.js  数字输入框组件

index.js  根实例

因为该示例是以交互功能为主,所以就不写CSS美化样式了。

首先写入基本的结构代码,初始化项目。

该示例的主角是input-number.js,所有的组件配置都在这里面定义。现在template里定义了组件的根节点,因为是独立组件,所以应该对每个props机型校验。这里根据需求有最大值、最小值、默认值(也就是绑定值)3个props,max和min都是数字类型,默认值正无穷大和负无穷大;value也是数字类型,默认值是0.

接下来,我们先在父组件引入input-number组件,并给它一个默认值5,最大值10,最小值0.

value是一个关键的绑定值,所以用了v-model,这样既优雅地实现了双向绑定,也让API看起来很合理。大都数地表单类组件都应该有一个v-model,比如输入框、单选框、多选框、下拉选择器等。

生于地代码都聚焦到了input-number.js上。

我们之前介绍过,Vue组件是单向数据流,所以无法从组件内部直接修改props  value的值。解决的办法也介绍过,就是给组件声明一个data,默认引用value的值,然后再组件内部维护这个data:

这样只解决了初始化时引用父组件value的问题,但如果从父组件修改了value,input-number组件的currentValue也要一起更新。为了实现这个功能,我们需要用到一个新的概念:监听(watch)。watch选项用来监听某个props或data的改变,当它们发生变化时,就会触发watch配置的函数,从而完成我们的业务逻辑。在本例中,我们要监听两个量:value和currentValue。监听value是要知晓从父组件修改了value,监听currentValue是为了当currentValue改变时,更新value。相关代码如下:

从父组件传递过来的value有可能时不符合当前条件的(大于max,或小于min),所以再选项methods里面写了一个方法updatevalue,用来过滤一个正确的currentValue。

watch监听的数据的回调函数就有两个参数可用,第一个时新的值,第二个时旧的值,这里没有太复杂的逻辑,就只用了第一个参数。在回调函数里,this是指向当前数组实例的,所以可以直接调用this.updateValue(),因为Vue代理了props、data、computed及methods。

监听currentValue的回调里,this.$emit('input',this)是在使用v-model时改变value的;this.$emit('on-change',val)是触发自定义事件on-change,用于告知父组件数字输入框的值所改变(示例中没有使用该事件)。

在生命周期mounted钩子里也调用了updateValue()方法,是因为第一次初始化时,也对value进行了过滤。这里也有另一种说法,在data选项返回对象前进行过滤:

实现的效果是一样的。

最后剩余的就是补全模板template,内容是一个输入框和连个按钮,相关代码如下:

input绑定了数据currentValue和原生的change事件,在句柄handleChange函数中,判断了当前输入框的是否是数字。注意,这里绑定的currentValue也是单数据流,并没有用v-model,所以在输入时,currentValue的值并没有实时改变。如果输入的是符合要求的数字,就把输入的值赋给currentValue。

数字输入框组件的核心逻辑就是这些。回顾一下我们设计一个通用组件的思路,首先,在写代码前一定明确需求,然后再规划好API。一个Vue组件的API只来自props、events和slots,确定好这3部分的命名、规则,剩下的逻辑即使第一版没有做好,后续可以迭代完善。但是API如果没有设计好,后续再改对使用者成本就很大了。

完整代码如下:

下面来两个练习:

开发一个标签页组件

本小节将开发一个比较有挑战的组件:标签页组件。标签页(即选项卡切换组件)是网页布局中经常用到的元素,常用于平级区域大块内容的收纳和展现:

根据上个示例的经验,我们先分析业务需求,指定API,这样不至于一上来就无从下手。

每个标签页的主体内容肯定是由使用组件的父级控制的,所以这部分是一个slot,而且slot的数量决定了

标签切换按钮的数量。假设我们有三个标签页,点击每个标签按钮时,另外的两个标签对应的slot应该被隐藏掉。一般这个时候,比较容易想到的解决办法是,再slot里写三个div,在接收到切换通知时,显示和隐藏相关的div。这样的设计没有问题,只不过体现不出来组件的价值来,因为我们还是写了一些与业务无关的交互逻辑,而这部分逻辑最好组件本身帮忙处理了,我们只用聚焦在slot内容本身,这才是我们业务最相关的。在这种情况下,我们再定义一个子组件pane,嵌套在标签组件tabs里,我们的业务代码都放在pane的solt内,而3个pane组件作为整体成为tabs和slot。

由于tabs和pane两个组件时分离的,但是

(垃圾csdn,又给我吃了一个小时的文章)

直接看下一节把

Vue.js实战之系统学习第八节

Vue.js实战之系统学习第七节相关推荐

  1. Vue.js实战之系统学习第一节

    为什么叫系统学习呢?因为我以前接触过Vue.js,但是没学过它的原理,只是简单的使用了,使用的时候就觉得很好用,没有其他的什么感觉,但当我进入职场后,学习了很多的前端技术后,才发现这个技术的友好,被它 ...

  2. 《Vue.js实战》第七章.组件

    7.1 组件作用: 提高代码复用性,使项目易于维护 7.1 组件的使用 7.1.1 组件注册-全局注册 全局注册后,任何vue的实例都可以使用该组件. Vue.component('my-compon ...

  3. 3.Vue.js 实战 调查问卷WebApp项目

    问卷调查demo已上传,欢迎大家指正,欢迎大家下载:https://download.csdn.net/download/lzb348110175/11085995 如果您没积分的话,可以私信/评论, ...

  4. [Vue.js] 一篇超级长的笔记,给《Vue.js 实战》划个重点

    本文前言 本笔记建立在书籍<Vue.js实战 / 梁灏编著>的基础上,旨在帮助有 Vue.js 基础的朋友快速回忆 Vue.js 的细碎内容.初学者建议阅读<Vue.js实战> ...

  5. 热烈庆祝《Vue.js 实战教程 V2.x(一)基础篇》上线了!

    热烈庆祝<Vue.js 实战教程 V2.x(一)基础篇>上线了! 课程简介 课程地址:https://edu.csdn.net/course/detail/25641 机构名称:大华软件学 ...

  6. js 查错_7年前端开发经验的我,写了本Vue.js实战开发,开源高清PDF下载

    Vue作为目前发展最迅速的前端框架越来越多的受到前端T程师青睐,Vue社区也是Web前端最活跃的社区之一. 更多的公司在转为Vue框架,但针对Vue优秀权威.实战的图书相对欠缺,梁灏著<Vue. ...

  7. axios vue 动态date_Web前端Vue系列之-Vue.js 实战

    课程简介: 课程目标:通过本课程的学习,让大家掌握Vue.js的进阶知识并在项目中应用. 适用人群:具有一定vue开发基础的开发人员. 课程概述:Vue (读音 /vjuː/,类似于 view) 是一 ...

  8. 分享Node.js + Koa2 + MySQL + Vue.js 实战开发一套完整个人博客项目网站

    这是个什么的项目? 使用 Node.js + Koa2 + MySQL + Vue.js 实战开发一套完整个人博客项目网站. 博客线上地址:www.boblog.com Github地址:https: ...

  9. vue.js实战 百度网盘下载 梁灏著

    在刚刚开始学习vue.js的时候,vue.js实战这本书,给于了我很大的帮助,非常适合用来入门,代码案例浅显易懂却又不失优美,我已经购买了实体书籍,算是对作者的一种支持和信任,也在网上找到了pdf版的 ...

最新文章

  1. mvc-3模型和数据(1)
  2. BurpSuite实例教程讲解
  3. [转]SupSite页面文件与模块关系说明
  4. return 关键字 c
  5. 【LeetCode】687. 最长同值路径
  6. php flink,plus/flink.php · 辉辉菜/三强源码 - Gitee.com
  7. java join 源码_java并发:join源码分析
  8. 【转载】Python中collection.Counter的用法
  9. Ali-Tomcat 安装
  10. 推荐几个e书下载地址
  11. 小米max刷鸿蒙,用了小米Max2,这简直是浪费我一天一夜的时间!
  12. AspUpload组件的安装及使用方法介绍
  13. android studio代理设置
  14. 中e管家理财小方法让闲钱变成活钱
  15. 鸟哥linux视频教程密码,[鸟哥linux视频教程整理]04_01_Linux用户管理命令详解
  16. TOP20W词库,双十一,直通车,用超级推荐获取手淘流量的方法——上篇
  17. centOS 7 安装可视化用户界面
  18. NEERC 2014, Eastern subregional contest(汇总)
  19. 【艾特淘】店铺没流量?教你如何获取免费流量来源
  20. 读书笔记-干法-乐观构思悲观计划乐观执行

热门文章

  1. AllWinner T113编译篇
  2. JavDroider的作品展示
  3. MySQL在没有可视化界面下查看数据库内容
  4. MediaCodec硬编码成H264视频流
  5. D:/Vitis/export/RF47DR/RF47DRxpfm‘ is invalid. please choose a valid platform.
  6. 方阵A可逆的充分必要条件是A 行等价于 E
  7. 对接腾讯云及时通讯总结
  8. 速学堂 Java300 第五章 作业
  9. ajaxSubmit提交数据重复
  10. 安装Adobe Illustrator(AI) CC 2017 后弹出quot;无法启动此程序,因为计算机中丢失api-ms-win-crt-runtime-l1-1-0.dllquot;