组件
简介
- 地位:组件是Vue.js最核心的功能,也是整个框架设计最精彩的地方
- 何为组件:没见过的自定义标签就是组件
- 使用条件:在任何使用Vue的地方都可以直接使用
- 命名:推荐用小写+减号分隔的形式命名
简单使用
例1:
<div id="app">
<table>
<tbody is="my-component"></tbody>
</table>
</div>
<script type="text/javascript" src="../js/vue.js">
</script>
<script type="text/javascript">
var Child = {template:'<div>这是一个组件</div>'};
// Vue.component('my-component', Child); 全局注册
var app = new Vue({
el:'#app',
components:{ // 局部注册
'my-component':Child
}
})
</script>
这个例子是在table中使用组件,其中涉及到两个需要注意的知识点:
- Vue组件的模板在某些情况下会受到HTML的限制,比如
内规定只允许是<table>
、<tr>
、<td>
等表格元素,所以在<th>
中直接使用组件是无效的,这种情况下,就使用我们上述例子中的方法——使用特殊的is属性来挂载组件,最终tbody在渲染的时候,会被替换为组件的内容。常见的限制元素还有<table>
、<ul>
、<ol>
。<select>
- template的DOM结构必须被一个元素包含,如果直接写成“这是一个组件”,不带"
"是无法渲染的。<div></div>
例2:
知识点:
在组件中像Vue实例一样使用其他选项(如data、computed、methods等)。但在使用data时,和实例稍有区别,data必须是函数,然后将数据return出去。
<div id="app">
<table>
<tbody is="my-component"></tbody>
</table>
</div>
<script type="text/javascript" src="../js/vue.js">
</script>
<script type="text/javascript">
var Child = {template:'<div>{{message}}</div>', data:function(){
return {
message:'这是一个组件2'
}
}
};
// Vue.component('my-component', Child); 全局注册
var app = new Vue({
el:'#app',
components:{ // 局部注册
'my-component':Child
}
})
</script>
使用props传递数据
- 简介:一种父组件向子组件传递数据或参数的方式。
- 使用方法:在子组件中通过使用选项
来声明需要从父级接收的数据,props
的值可以是两种,一种是字符串数组,一种是对象。props
- props与组件data的区别:props来自父级,data中的是组件自己的数据,作用域是组件本身,这两数据都可以在模板template及计算属性computed和方法methods中使用。
- 例子:
- props的值为字符串数组:
<div id="app"> <my-component warning-text="提示信息"></my-component> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> var Child = {template:'<div>{{warningText}}</div>',props:['warningText']}; // Vue.component('my-component', Child); 全局注册 var app = new Vue({ el:'#app', components:{ // 局部注册 'my-component':Child } }) </script>
- 注意:
- 由于HTML特性不区分大小写,当使用DOM模板时,驼峰命名(camelCase)的props名称要转为短横分隔命名(kebab-case)。在字符串模板中可以忽略这些限制。
- 如果要传递的数据并不是直接写死的,可以使用指令v-bind来动态绑定props的值,当父组件的数据变化时,也会传递给子组件。
- 如果要直接传递数字、布尔值、数组、对象,而且不使用v-bind,传递的仅是字符串。
- 在Vue2.x通过props传递数据是单向的了,也就是父组件数据变化时会传递给子组件,反过来不行。(这里指的数据是指除对象和数组外的一般数据)
- 在业务中遇到两种需要修改prop的情况,一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改(在组件data中声明一个数据,引用父组件的prop)。另一种就是prop作为需要被转变的原始值传入,这种情况使用计算属性就可以了。
- 在JavaScript中对象和数组是引用类型,指向同一个内存空间,所以props是对象和数组时,在子组件内改变是会影响父组件的。
- 注意:
- props值为对象:
-
何时用?
答:当prop需要验证时,就需要对象写法。如果传入数据不符合规则,会在控制台发出警告。
- 验证的type类型可以是(type也可以是一个自定义的构造器,使用instanceof 检测):
- String
- Number
- Boolean
- Object
- Array
- Function
Vue.component('my-component',{ props:{ // 必须是数字类型 propA:Number, // 必须是数字或字符串类型 propB:[Number,String], // 布尔值,如果没有定义,默认就是true propC:{ type:Boolean, default:true }, // 数字,而且是必传 propD:{ type:Number, required:true }, // 如果是数组或对象,设置默认值时必须是一个函数来返回 propE:{ type:Array, default:function(){ return []; } }, // 自定义一个验证函数 propF:{ validator:function(value){ return value > 10; } } } })
-
- props的值为字符串数组:
使用$emit传递数据
- 简介:一种子组件向父组件传递数据或参数的方式。
- 使用方法:子组件通过
来触发事件,父组件用$emit()
来监听子组件的事件或直接在子组件的自定义标签上使用$on()
来监听子组件触发的自定义事件。v-on
- 解除监听:
$off()
- 示例:
- 描述:子组件有两个按钮,分别实现+1和-1效果,在改变组件的data的
后,通过counter
再把它传递给父组件,父组件通过$emit()
或$on()
来监听子组件触发的事件。v-on
<div id="app"> <p>{{total}}</p> <!-- <my-component @increase="handleGetTotal" @decrease="handleGetTotal"></my-component> --> <my-component ref="child"></my-component> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> Vue.component('my-component', { template:'\ <div>\ <button @click="handleIncrease">+1</button>\ <button @click="handleDecrease">-1</button>\ </div>', data:function(){ return { count:0 } }, methods:{ handleIncrease:function(){ this.count++; this.$emit('increase', this.count); }, handleDecrease:function(){ this.count--; this.$emit('decrease', this.count); } } }) var app = new Vue({ el:'#app', data:{ total:0 }, methods:{ handleGetTotal:function(total){ this.total = total; } }, mounted:function(){ this.$refs.child.$on('increase', this.handleGetTotal); // this.$refs.child.$once('increase', this.handleGetTotal); 自定义事件只触发一次 this.$refs.child.$on('decrease', this.handleGetTotal); } }) </script>
- 描述:子组件有两个按钮,分别实现+1和-1效果,在改变组件的data的
- 补充:除了用
在组件上监听自定义事件外,也可以监听DOM事件,这时可以用v-on
修饰符表示监听的是一个原生事件,监听的是该组件的根元素。例:.native
<my-component v-on:click.native="handleClick"></my-component>
- 使用v-model:
- Vue2.x可以在自定义组件上使用v-model指令
- 在下面示例中,实现的效果仍是点击按钮+1,不过这次组件
的事件名是特殊的input,在使用组件的父级,并没有在$emit()
上使用<my-component>
,而是直接用@input="handler"
绑定的一个v-model
。数据total
在上例的v-model,相当于下面的操作:<div id="app"> <p>总数:{{total}}</p> <my-component v-model="total" ></my-component> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> Vue.component('my-component',{ template:'<button @click="handleClick">+1</button>', data:function(){ return { counter:0 } }, methods:{ handleClick:function(){ this.counter++; this.$emit('input', this.counter); } } }); var app = new Vue({ el:'#app', data:{ total:0 } }) </script>
利用v-model这一特殊用途,还可以创建自定义表单输入组件,进行数据双向绑定,例如:<div id="app"> <p>总数:{{total}}</p> <my-component @input="handleGetTotal" ></my-component> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> Vue.component('my-component',{ template:'<button @click="handleClick">+1</button>', data:function(){ return { counter:0 } }, methods:{ handleClick:function(){ this.counter++; this.$emit('input', this.counter); } } }); var app = new Vue({ el:'#app', data:{ total:0 }, methods:{ handleGetTotal(total){ this.total = total; } } }) </script>
实现上述一个具有双向绑定的v-model组件要满足如下2个要求:<div id="app"> <p>总数:{{total}}</p> <my-component v-model="total" ></my-component> <button @click="handleReduce">-1</button> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> 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>
- 接收一个value属性
- 在有新的value时触发input事件
使用中央事件总线bus传递数据
- 简介:一种适用于任意组件间通信的方式
- 安装:
new Vue({ ...... beforeCreate() { Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm }, ...... })
- 使用事件总线:
- 接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身。
methods(){ demo(data){......} } ...... mounted() { this.$bus.$on('xxxx',this.demo) }
- 提供数据:
this.$bus.$emit('xxxx',数据)
- 接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身。
- 解绑时机:在
钩子中,用beforeDestroy
去解绑当前当前组件所用到的事件。$off
- 示例:
<div id="app"> <component-a></component-a> <br/> <component-b></component-b> <button @click="sendMessage">点我向儿子组件发送数据</button> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> var app = new Vue({ el:'#app', data:{ message:'我是根组件的数据' }, components:{ 'component-a':{ template:'<div>\ 这是来自根组件的信息:{{messageRoot}}<br/>\ 这是来自兄弟组件的信息:{{messageBrother}}\ </div>', data:function(){ return { messageRoot:'', messageBrother:'' } }, methods:{ getRootMessage:function(message){ this.messageRoot = message; }, getBrotherMessage:function(message){ this.messageBrother = message; } }, mounted:function(){ // 给bus绑定自定义事件,回调留在自身,说明我想获得数据 this.$bus.$on('RootMessage', this.getRootMessage); this.$bus.$on('BrotherMessage', this.getBrotherMessage); } }, 'component-b':{ template:'<div>\ <button @click="sendMessage">点我向兄弟组件发送数据</button>\ </div>', data:function(){ return { message:'我是兄弟组件的数据' } }, methods:{ sendMessage(){ this.$bus.$emit('BrotherMessage', this.message); } } } }, methods:{ sendMessage(){ this.$bus.$emit('RootMessage', this.message); } }, beforeCreate(){ Vue.prototype.$bus = this; // 安装中央事件总线bus } }) </script>
父链
- 引言:在子组件中,使用
可以直接访问该组件的父实例或组件,父组件也可以通过this.$parent
访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件。this.$children
- 示例代码:
<div id="app"> <p>这是来自子组件传递的信息:{{message}}</p> <component-a></component-a> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> Vue.component('component-a',{ template:'\ <div>\ <input type="text" v-model="text" placeholder="请输入需要传递的消息"><br/>\ <button @click="handleClick">向父组件发送消息</button>\ </div>', methods:{ handleClick(){ this.$parent.message = this.text; } }, data(){ return { text:'' } } }) var app = new Vue({ el:"#app", data:{ message:'' } }) </script>
- 补充:尽管Vue允许这样操作,但在业务中,子组件应该尽可能避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只有组件自己能修改它的状态。父子组件最好还是通过
和props
来通信。$emit
子组件索引
- 引言:当子组件较多时,通过
来一一遍历出我们需要的一个组件实例是比较困难的,尤其是组件动态渲染时,它们的序列是不固定的。Vue提供了子组件索引的方法,用特殊的属性this.$children
来为子组件指定一个索引名称。ref
- 示例代码:
- 在父组件模板中,子组件标签上使用ref指定一个名称,并在父组件内通过
来访问指定名称的子组件。this.$refs
- 注意:
只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用$refs
。以计算属性为例,它是在created之后mounted之前,$refs
是在mounted之后。$refs
<div id="app"> <p>这是来自子组件传递的信息:{{message}}</p> <component-a ref="comA"></component-a> <br/> <button @click="handleClick">打印子组件中文本信息</button> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> Vue.component('component-a',{ template:'\ <div>\ <input type="text" v-model="text" placeholder="请输入需要传递的消息"><br/>\ <button @click="handleClick">向父组件发送消息</button>\ </div>', methods:{ handleClick(){ this.$parent.message = this.text; } }, data(){ return { text:'' } } }) var app = new Vue({ el:"#app", data:{ message:'' }, methods:{ handleClick(){ console.log(this.$refs.comA.text); } } }) </script>
- 在父组件模板中,子组件标签上使用ref指定一个名称,并在父组件内通过
- 补充:与Vue1.x不同的是,Vue2.x将v-el和v-ref合并为了ref,Vue会自动去判断是普通标签还是组件。可以使用下面代码去查看:
<div id="app"> <p ref="tag">这是来自子组件传递的信息:{{message}}</p> <component-a ref="comA"></component-a> <br/> <button @click="handleClick">点我查看ref特性</button> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> Vue.component('component-a',{ template:'\ <div>\ <input type="text" v-model="text" placeholder="请输入需要传递的消息"><br/>\ <button @click="handleClick">向父组件发送消息</button>\ </div>', methods:{ handleClick(){ this.$parent.message = this.text; } }, data(){ return { text:'' } } }) var app = new Vue({ el:"#app", data:{ message:'' }, methods:{ handleClick(){ console.log(this.$refs.comA, this.$refs.tag); } } }) </script>
使用Slot分发内容
- 使用场景:当需要让组件组合使用,混合父组件的内容与子组件的模板时,就会用到Slot,这个过程叫作内容分发(transclusion)。
- 用法:
- 单个Slot:
- 在子组件内使用特殊的
元素就可以为这个子组件开启一个Slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的<slot>
标签及它的内容。示例代码:<slot>
<div id="app"> <component-a></component-a> <hr/> <component-a> <ul> <li>苹果</li> <li>梨</li> </ul> </component-a> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> Vue.component('component-a',{ template:'<div>\ <slot>\ <p>默认内容</p>\ </slot>\ </div>' }) var app = new Vue({ el:'#app' }) </script>
- 上述代码解释:子组件componet-a的模板内定义了一个
元素,并且用一个<slot>
作为默认的内容,在父组件没有使用Slot时,会渲染这段默认的文本;如果写入了Slot,那就会替换整个<p>
。所以上例渲染后结果为:<slot>
<div id="app"> <div> <p>默认内容</p> </div> <hr/> <div> <ul> <li>苹果</li> <li>梨</li> </ul> </div> </div>
- 上述代码解释:子组件componet-a的模板内定义了一个
- 注意:
- Slot分发的内容,作用域是在父组件上的。
- 子组件
内的备用内容,它的作用域是子组件本身。<slot>
- 在子组件内使用特殊的
- 具名Slot:
- 定义:指定了name的
元素称为具名Slot。<slot>
- 补:具名Slot可以与单个Slot共存,用于分发多个内容。
- 代码示例:
<div id="app"> <component-a> <h2 slot="header">标题</h2> <p>正文内容</p> <p>更多的正文内容</p> <div slot="footer">底部信息</div> <p>正文内容2</p> </component-a> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> Vue.component('component-a',{ template:'<div>\ <div class="container">\ <div class="header">\ <slot name="header">\ <p>默认头部内容</p>\ </slot>\ </div>\ <div class="main">\ <slot>\ <p>默认主体内容</p>\ </slot>\ </div>\ <div class="footer">\ <slot name="footer">\ <p>默认尾部内容</p>\ </slot>\ </div>\ </div>\ </div>' }) var app = new Vue({ el:'#app' }) </script>
- 上述代码解释:子组件内声明了3个
元素,其中在<slot>
内的<div class="main">
没有使用name特性,它将作为默认slot出现,父组件没有使用slot特性的元素与内容都将出现在这里。故最终渲染结果为:<slot>
<div id="app"> <div> <div class="container"> <div class="header"> <h2> 标题 </h2> </div> <div class="body"> <p> 正文内容 </p> <p> 更多的正文内容 </p> <p> 正文内容2 </p> </div> <div class="footer"> <div> 底部信息 </div> </div> </div> </div> </div>
- 定义:指定了name的
- 单个Slot:
- 作用域插槽:
- 简介:一种特殊的slot,使用一个可以复用的模板替换已渲染元素。
- 使用场景:既可以复用子组件的slot,又可以使slot内容不一致。
- 简单示例:
<div id="app"> <component-a> <template scope="props"> <p>来自父组件的内容</p> <p>{{props.msg}}</p> </template> </component-a> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> Vue.component('component-a',{ template:'<div class="container">\ <slot msg="来自子组件的内容"></slot>\ </div>' }) var app = new Vue({ el:'#app' }) </script>
- 上述代码分析:观察子组件的模板,在
元素上有一个类似props传递数据给组件的写法msg=“xxxx”,将数据传到了插槽。父组件使用了<slot>
元素,而且拥有了一个scope="props"的特性,这里的props知识一个临时变量,就像v-for="item in items"里面的item一样。template内可以通过临时变量 props访问来自子组件插槽的数据msg。上例代码渲染后最终结果为:<template>
<div> <div class="container"> <p> 来自父组件的内容 </p> <p> 来自子组件的内容 </p> </div> </div>
- 上述代码分析:观察子组件的模板,在
- 代表性用例—列表组件:允许组件自定义应该如何渲染列表每一项。
<div id="app"> <my-list :books="books"> <!-- 作用域插槽也可以是具名的slot --> <template slot="book" scope="props"> <li>{{props.bookName}}</li> </template> </my-list> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> Vue.component('my-list',{ props:{ books:{ type:Array, default:function(){ return []; } } }, template:'\ <ul>\ <slot name="book"\ v-for="book in books"\ :book-name="book.name">\ <!--此处也可以写slot的默认内容-->\ </slot>\ </ul>\ ' }) var app = new Vue({ el:'#app', data:{ books:[ {name:'《Vue.js实战》'}, {name:'《JavaScript语言精粹》'} ] } }) </script>
- 上述代码分析:子组件my-list接收一个来自父级的props数组books,并将它在name为book的slot上使用v-for指令循环,同时暴露一个变量bookName。
- 访问slot:在Vue.js. 1.x中,想要获取某个slot是比较麻烦的,需要用v-el间接获取。而Vue.js 2.x提供了用来访问被slot分发的内容的方法
。$slots
- 代码示例:
<div id="app"> <component-a> <h2 slot="header">标题</h2> <p>正文内容</p> <p>更多正文内容</p> <div slot="footer">底部信息</div> </component-a> </div> <script type="text/javascript" src="../js/vue.js"> </script> <script type="text/javascript"> Vue.component('component-a',{ template:'\ <div class="container">\ <div class="header">\ <slot name="header"></slot>\ </div>\ <div class="body">\ <slot></slot>\ </div>\ <div class="footer">\ <slot name="footer"></slot>\ </div>\ </div>\ ', mounted(){ var header = this.$slots.header;// 都是vnode类型数组 var main = this.$slots.default; var footer = this.$slots.footer; console.log(header); console.log(footer); console.log(footer[0].elm.innerHTML); } }) var app = new Vue({ el:'#app' }) </script>
- 示例代码分析:通过
可以访问某个具名slot,$slots
包括了所有没有被包含在具名slot中的节点。this.$slots.default
- 使用场景:独立组件开发中,业务中几乎用不到。
- 代码示例: