组件是Vue.js中一个非常重要的概念,它是一种抽象,提供独立,可复用的小组件来构建大型应用。你可认为所见任意类型的网页都可抽象为一个组件树,比如像下面这样的:
引入组件的目的是扩展HTML元素,并不是为了代替HTML元素(事实上组件更像是自定义的HTML标签,类似于XML),同时组件可封装一些使用频率较高的HTML代码,实现代码的高复用。
组件的使用分为3个步骤:第一,创建组件构造器;第二,注册组件;第三,使用组件。第一,创建组件构造器,使用Vue.extend()方法(请注意不是extends方法),里面传入一个选项对象。第二,注册组件,使用Vue.components()方法,里面传入2个参数,第1个是参数组件标签,第2个参数是一个组件构造器(当然也可以是选项对象,使用选项对象的template属性定义组件模板。使用这种方式Vue会自动地调用Vue.extend()方法)。第三,使用组件,必须在Vue实例的作用范围内使用组件,否则不会生效。
<body><div id="app"><!--3、在Vue实例的作用范围内使用组件(#app是Vue实例挂载的元素,x需要在该元素内使用组件) --> <my-component></my-component></div><script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script type="text/javascript"> //1、创建一个组件构造器 var myComponent = Vue.extend({ template:"<h1 style='color: red'>hello,Vue,js!</h1>" }); //2、注册组件并指定组件的标签,组件的HTML标签为<my-component> Vue.component("my-component",myComponent); //实例化一个Vue对象 new Vue({ el:"#app", });</script></body>
运行结果:
仔细观察发现组件确实就是自定义的XML标签。注意:调用Vue.component()注册组件时,组件的注册是全局的,这意味着该组件可以在任意Vue示例下使用。
new Vue({ el:"#test", });<div id="test"> <my-component></my-component></div>
运行结果:
结果和上面一样!但是你单纯的只是使用这个组件,而不放在挂载元素下方是不会生效的,就像下面的例子。
<!--该组件不会被渲染--><my-component></my-component>
前面说了调用Vue.component()注册组件时,组件是被全局注册,那如果仅仅只是想让某个组件只在某个实例内使用,那该如何注册呢?这时局部注册就产生了。你可以在选项对象的components属性中实现局部注册,其实就是直接在Vue的实例中注册,举个例子:
<!--3、在在Vue实例的作用范围内使用组件--><div id="hello"> <local-component></local-component></div><script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script type="text/javascript"> //局部注册 //1、创建一个局部的组件构造器 var localComponent = Vue.extend({ //注意是extend方法,而不是extends方法 template: "<h1 style='color: blue'>Local Component!</h1>" }); new Vue({ el:"#hello", components:{ //2、将localComponent组件注册到#hello这个Vue实例下面 "local-component":localComponent, } });</script>
运行结果:
此时你再将这个组件挂载到id非hello的元素上时,运行发现没有内容输出:
<div id="test"><!-- 无法使用local-component组件,因为local-component是局部组件,仅仅属于#hello --> <local-component></local-component></div><script> new Vue({ el: '#test' });</script>
组件之间的嵌套使用形成了父组件与子组件这种关系。
<div id="app"> <parent-component></parent-component></div><script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script type="text/javascript"> var child = Vue.extend({ template:"<p style='color: green'> This is the child component!</p>" }) var parent = Vue.extend({ template:"<p>This is the parent component!<child-component></child-component></p>", components:{ "child-component":child } }); Vue.component("parent-component",parent); new Vue({ el:"#app", })</script>
运行结果:
首先创建了一个child组件构造器,然后在parent组件中直接引入child的组件标签,同时将child组件以局部注册方式注册到parent中,最后将parent组件全局注册到Vue实例中。下面是该过程的流程示意图:
需要说明的是child组件在Parent组件以局部方式注册,因此仅在parent组件中使用,child组件标签只能在parent 组件的template中使用。这句话怎么理解?看下面的例子你就知道了:
去掉parent组件中引入的child组件标签<child-component></child-component></p>
var parent = Vue.extend({ template:"<p>This is the parent component!", components:{ "child-component":child } });
然后再来运行一下代码:
这似乎是意料之中的事,因为尚未使用child组件标签,此时按照如下方式引入:
<div id="app"> <parent-component> <child-component></child-component> </parent-component></div>
运行结果:
为什么还是无法显示呢,明明已经引入了子组件标签?其实原因是:当子组件注册到父组件时,Vue.js会编译好父组件的模板,模板的内容决定父组件将要渲染的HTML。<parent-component></parent-component>父组件之间的内容在运行时,子标签只会当作普通的HTML来执行,而<child-component></child-component>不是标准的HTML标签,因而会被浏览器直接忽视掉,所以无法显示。
理解了这个,再来看这种方式的引入:
<div id="app"> <parent-component></parent-component> <child-component></child-component></div>
自然这种方式也是会报错的,相反只要你在父组件的template中使用了子组件标签,那么这两种方式其实都是可以运行的,即下面这三种方式可以达到相同的效果:
<div id="app"> <parent-component></parent-component></div><div id="app"> <parent-component> <child-component></child-component> </parent-component></div><div id="app"> <parent-component></parent-component> <child-component></child-component></div>
在前面组件使用的第一步是创建组件构造器,每次都需要使用Vue.extend()方法,这个步骤有些多余,Vue.js提供了注册语法糖来解决这个问题。在前面也多次提到过,注册组件可以使用Vue.components()方法,里面传入2个参数,第1个是参数组件标签,第2个参数是一个组件构造器(也可以是选项对象,使用选项对象的template属性定义组件模板,使用这种方式Vue会自动地调用Vue.extend()方法)。其实所谓的语法糖就是直接选项对象来进行构造:
<div id="app"> <my-component></my-component></div><script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script type="text/javascript"> var myComponent = Vue.extend({ template:"<h1 style='color: red'>hello,Vue,js!</h1>" }); Vue.component("my-component",myComponent); new Vue({ el:"#app", });</script>
上面是之前的方式,下面是使用语法糖的代码:
<div id="app"> <my-component></my-component></div><script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script type="text/javascript"> Vue.component('my-component',{ template: "<p>This is the first component!</p>" } ) new Vue({ el:"#app", });</script>
上面是使用注册语法糖实现的全局注册,接下来则是使用语法糖实现的局部注册:
<div id="app2"> <my-components2> </my-components2> <my-components3> </my-components3></div><script type="text/javascript"> //在选项对象的components属性中实现局部注册:(必须在Vue实例内部进行局部注册) new Vue({ el:"#app2", components:{ // 局部注册,my-component2是标签名称 "my-components2":{ template:"<p>This is the second component!</p>", }, // 局部注册,my-component3是标签名称 "my-components3":{ template:"<p>This is the third component!</p>", } } })</script>
运行结果:
细心的你会发现无论是否使用语法糖,局部注册都必须在Vue实例的内部进行,且在components属性中实现局部注册。
看到这里肯定会发现我们在template中引入了html代码,这就好像是Java中的JSP一样,这种方式会导致html和jS的高度耦合,不利于代码的扩展,同时降低了代码的可读性。
针对这个情况,Vue提供了三种解决方案:第一:使用<script>标签;第二:使用<template>标签;第三:使用定义组件的.vue格式文件(此处不介绍其用法)。
第一,使用<script>标签,如下述代码:
<body><div id="app"> <my-component></my-component></div><script type="text/x-template" id="myComponent"><p>This is a component used script!</p></script><script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script type="text/javascript"> Vue.component("my-component",{ template:"#myComponent", }); new Vue({ el:"#app" })</script></body>
此时template选项对象对应的值不再是HTML元素,而是一个id选择器,此时Vue.js会根据这个id查找对应的元素,然后将改元素内的HTML作为模板进行编译。这种方式其实很好理解,因为JS善于使用选择器来进行元素的选择、定位、修改等操作。
注意:使用<script>标签时,必须指定type类型为text/x-template,目的就是告诉浏览器这不是一段js脚本,那么浏览器在解析HTML文档时会忽略<script>标签内定义的内容。
第二:使用<template>标签,代码如下:
<body><div id="app"> <my-component></my-component></div><template id="templateMyComponent"> <p>This is a component used template!</p></template><script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script type="text/javascript"> Vue.component("my-component",{ template:"#templateMyComponent", }); new Vue({ el:"#app" })</script></body>
使用template标签很容易使人想到这是模板信息,因此不需要再指定type类型了。个人建议使用template标签,因为可读性非常好。
前面我们曾多次在Vue实例中使用el和data对象选项,比如:
new Vue({ el:"#app2", data: { name: "envy", age: 23, } })
这个没什么需要介绍的,不过它们两个特殊在于定义组件选项对象时,data和el选项对象值必须使用函数!
其实就是说:其他的参数对象在 Vue.extend() 或Vue.component()中可以传入对象,而data和el参数对象只能是函数。
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script type="text/javascript"> Vue.component("my-component",{ template:"#templateMyComponent", data:{ name:"envy" } });</script>
运行时,后台会报错:
vue.js:634 [Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.
即data应该是一个函数,用于在组件定义中返回每个实例的值。
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script type="text/javascript"> Vue.component("my-component",{ template:"#templateMyComponent", data:function(){ return {name:"envy"} } });</script>
由于各个组件实例的作用域是独立的,因此想在子组件中使用父组件的数据似乎是难以实现的,此时props就派上用场了。
举一个非常简单的例子,子组件如何通过props来获取父组件的数据。
先定义一个Vue的实例,且将子组件my-component进行局部注册,同时定义Vue实例的data选项属性值,如下代码所示:
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script type="text/javascript"> var vm = new Vue({ el: "#app", data: { name: "xiaoming", age: 28, }, components: { "my-component": { template: "#mytemplate", props: ["myName", "myAge"], } } })</script>
注意,如果我们想获取父组件的数据,那么必须提前在子组件中定义props属性,也就是代码中的props: ['myName', 'myAge']。便于理解可以将Vue的实例作为my-component的父组件,那么我们想要的则是在子组件中输出name: "xiaoming",age: 28。
接着定义子组件的html模板:
<template id="mytemplate"> <table> <tr> <th colspan="2">子组件数据</th> </tr> <tr> <td>myName</td> <td>{{ myName }}</td> </tr> <tr> <td>myAge</td> <td>{{ myAge }}</td> </tr> </table></template>
最后也是最重要的一步,将父组件的数据通过props传递给子组件:
<div id="app"> <my-component v-bind:my-name="name" v-bind:my-age="age"></my-component></div>
注意:在子组件中定义props时,使用了驼峰命名法进行变量命名。由于html不区分大小写,故使用驼峰命名法命名的props用于特性时,需要转为 短横线隔开的形式。在本例中props定义的myName,在用作特性时需要转换为my-name这种形式。
运行结果:
总结:在父组件中使用子组件时,可通过以下语法将数据传递给子组件(注意可使用多个v-bind指令):
<child-component v-bind:子组件props="父组件数据属性"></child-component>
前面父组件可以将数据传递给子组件,那么现在有一个问题就是假设子组件修改了数据,那么相应的父组件数据是否也发生了改变?
同样是先定义一个Vue的实例,且将子组件my-component进行局部注册,同时定义Vue实例的data选项属性值,如下代码所示:
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script type="text/javascript"> var vm = new Vue({ el: "#app", data: { name: "xiaoming", age: 28, }, components: { "my-component": { template: "#mytemplate", props: ["myName", "myAge"], } } })</script>
接着定义子组件的html模板:
<template id="mytemplate"> <table> <tr> <th colspan="3">子组件数据</th> </tr> <tr> <td>myName</td> <td>{{ myName }}</td> <td><input type="text" v-model="myName"></td> </tr> <tr> <td>myAge</td> <td>{{ myAge }}</td> <td><input type="text" v-model="myAge"></td> </tr> </table></template>
然后为了展示父组件的数据是否发生改变,则需要将数据进行展示:
<div id="app"> <table> <tr> <th colspan="3">父组件数据</th> </tr> <tr> <td>Name</td> <td>{{ name }}</td> <td><input type="text" v-model="name"></td> <!--将name绑定到文本框--> </tr> <tr> <td>Age</td> <td>{{ age }}</td> <td><input type="text" v-model="age"></td> <!--将age绑定到文本框--> </tr> </table></div>
最后也是最重要的一步,将父组件的数据通过props传递给子组件(注意代码必须放到<div id="app"></div>之间):
<my-component v-bind:my-name="name" v-bind:my-age="age"></my-component>
运行结果:(由于无法获得动图,此处是静态图片)
结果是修改子组件的数据,父组件的信息不会发生变化。但是修改了父组件的数据,则子组件得数据也会发生变化,这个很好理解。为什么会有这种单向的数据绑定机制呢?其实是防止子组件的随意修改,导致父组件状态发生改变。props默认是单向数据绑定!有了单向的数据绑定肯定也有双向的数据绑定,对是的,因为确实在某些场景下需要联动,一个页面发生变化另一个页面也需要对这个变化作出响应。关于props双向数据绑定的介绍将会在后续文章中进行分享。