Vue.js每天必学之组件与组件间的通信
什么是组件?
组件(Component)是Vue.js最强大的功能之一。组件可以扩展HTML元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js的编译器为它添加特殊功能。在有些情况下,组件也可以是原生HTML元素的形式,以is特性扩展。
使用组件
注册
之前说过,我们可以用Vue.extend()创建一个组件构造器:
varMyComponent=Vue.extend({ //选项... })
要把这个构造器用作组件,需要用`Vue.component(tag,constructor)`**注册**:
//全局注册组件,tag为my-component Vue.component('my-component',MyComponent)
<pclass="tip">对于自定义标签名字,Vue.js不强制要求遵循W3C规则(小写,并且包含一个短杠),尽管遵循这个规则比较好。
组件在注册之后,便可以在父实例的模块中以自定义元素<my-component>的形式使用。要确保在初始化根实例之前注册了组件:
<divid="example"> <my-component></my-component> </div> //定义 varMyComponent=Vue.extend({ template:'<div>Acustomcomponent!</div>' }) //注册 Vue.component('my-component',MyComponent) //创建根实例 newVue({ el:'#example' })
渲染为:
<divid="example"> <div>Acustomcomponent!</div> </div>
注意组件的模板替换了自定义元素,自定义元素的作用只是作为一个挂载点。可以用实例选项replace决定是否替换。
局部注册
不需要全局注册每个组件。可以让组件只能用在其它组件内,用实例选项components注册:
varChild=Vue.extend({/*...*/}) varParent=Vue.extend({ template:'...', components:{ //<my-component>只能用在父组件模板内 'my-component':Child } })
这种封装也适用于其它资源,如指令、过滤器和过渡。
注册语法糖
为了让事件更简单,可以直接传入选项对象而不是构造器给Vue.component()和component选项。Vue.js在背后自动调用Vue.extend():
//在一个步骤中扩展与注册 Vue.component('my-component',{ template:'<div>Acustomcomponent!</div>' }) //局部注册也可以这么做 varParent=Vue.extend({ components:{ 'my-component':{ template:'<div>Acustomcomponent!</div>' } } })
组件选项问题
传入Vue构造器的多数选项也可以用在Vue.extend()中,不过有两个特例:data和el。试想如果我们简单地把一个对象作为data选项传给Vue.extend():
vardata={a:1} varMyComponent=Vue.extend({ data:data })
这么做的问题是`MyComponent`所有的实例将共享同一个`data`对象!这基本不是我们想要的,因此我们应当使用一个函数作为`data`选项,让这个函数返回一个新对象:
varMyComponent=Vue.extend({ data:function(){ return{a:1} } })
同理,`el`选项用在`Vue.extend()`中时也须是一个函数。
模板解析
Vue的模板是DOM模板,使用浏览器原生的解析器而不是自己实现一个。相比字符串模板,DOM模板有一些好处,但是也有问题,它必须是有效的HTML片段。一些HTML元素对什么元素可以放在它里面有限制。常见的限制:
•a不能包含其它的交互元素(如按钮,链接)
•ul和ol只能直接包含li
•select只能包含option和optgroup
•table只能直接包含thead,tbody,tfoot,tr,caption,col,colgroup
•tr只能直接包含th和td
在实际中,这些限制会导致意外的结果。尽管在简单的情况下它可能可以工作,但是你不能依赖自定义组件在浏览器验证之前的展开结果。例如<my-select><option>...</option></my-select>不是有效的模板,即使my-select组件最终展开为<select>...</select>。
另一个结果是,自定义标签(包括自定义元素和特殊标签,如<component>、<template>、<partial>)不能用在ul,select,table等对内部元素有限制的标签内。放在这些元素内部的自定义标签将被提到元素的外面,因而渲染不正确。
对于自定义元素,应当使用is特性:
<table> <tris="my-component"></tr> </table>
``不能用在``内,这时应使用``,`
`可以有多个``:
<table> <tbodyv-for="iteminitems"> <tr>Evenrow</tr> <tr>Oddrow</tr> </tbody> </table>
Props
使用Props传递数据
组件实例的作用域是孤立的。这意味着不能并且不应该在子组件的模板内直接引用父组件的数据。可以使用props把数据传给子组件。
“prop”是组件数据的一个字段,期望从父组件传下来。子组件需要显式地用props选项声明props:
Vue.component('child',{ //声明props props:['msg'], //prop可以用在模板内 //可以用`this.msg`设置 template:'<span>{{msg}}</span>' })
然后向它传入一个普通字符串:
<childmsg="hello!"></child>
驼峰式vs.横杠式
HTML特性不区分大小写。名字形式为camelCase的prop用作特性时,需要转为kebab-case(短横线隔开):
Vue.component('child',{ //camelCaseinJavaScript props:['myMessage'], template:'<span>{{myMessage}}</span>' }) <!--kebab-caseinHTML--> <childmy-message="hello!"></child>
动态Props
类似于用v-bind绑定HTML特性到一个表达式,也可以用v-bind绑定动态Props到父组件的数据。每当父组件的数据变化时,也会传导给子组件:
<div> <inputv-model="parentMsg"> <br> <childv-bind:my-message="parentMsg"></child> </div>
使用`v-bind`的缩写语法通常更简单:
<child:my-message="parentMsg"></child>
字面量语法vs.动态语法
初学者常犯的一个错误是使用字面量语法传递数值:
<!--传递了一个字符串"1"-->
<compsome-prop="1"></comp>
因为它是一个字面prop,它的值以字符串`”1”`而不是以实际的数字传下去。如果想传递一个实际的JavaScript数字,需要使用动态语法,从而让它的值被当作JavaScript表达式计算:
<!--传递实际的数字 -->
<comp:some-prop="1"></comp>
Prop绑定类型
prop默认是单向绑定:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。不过,也可以使用.sync或.once绑定修饰符显式地强制双向或单次绑定:
比较语法:
<!--默认为单向绑定--> <child:msg="parentMsg"></child> <!--双向绑定--> <child:msg.sync="parentMsg"></child> <!--单次绑定--> <child:msg.once="parentMsg"></child>
双向绑定会把子组件的msg属性同步回父组件的parentMsg属性。单次绑定在建立之后不会同步之后的变化。
注意如果prop是一个对象或数组,是按引用传递。在子组件内修改它会影响父组件的状态,不管是使用哪种绑定类型。
Prop验证
组件可以为props指定验证要求。当组件给其他人使用时这很有用,因为这些验证要求构成了组件的API,确保其他人正确地使用组件。此时props的值是一个对象,包含验证要求:
Vue.component('example',{ props:{ //基础类型检测(`null`意思是任何类型都可以) propA:Number, //多种类型(1.0.21+) propM:[String,Number], //必需且是字符串 propB:{ type:String, required:true }, //数字,有默认值 propC:{ type:Number, default:100 }, //对象/数组的默认值应当由一个函数返回 propD:{ type:Object, default:function(){ return{msg:'hello'} } }, //指定这个prop为双向绑定 //如果绑定类型不对将抛出一条警告 propE:{ twoWay:true }, //自定义验证函数 propF:{ validator:function(value){ returnvalue>10 } }, //转换函数(1.0.12新增) //在设置值之前转换值 propG:{ coerce:function(val){ returnval+''//将值转换为字符串 } }, propH:{ coerce:function(val){ returnJSON.parse(val)//将JSON字符串转换为对象 } } } })
type可以是下面原生构造器:
•String
•Number
•Boolean
•Function
•Object
•Array
type也可以是一个自定义构造器,使用instanceof检测。
当prop验证失败了,Vue将拒绝在子组件上设置此值,如果使用的是开发版本会抛出一条警告。
父子组件通信
父链
子组件可以用this.$parent访问它的父组件。根实例的后代可以用this.$root访问它。父组件有一个数组this.$children,包含它所有的子元素。
尽管可以访问父链上任意的实例,不过子组件应当避免直接依赖父组件的数据,尽量显式地使用props传递数据。另外,在子组件中修改父组件的状态是非常糟糕的做法,因为:
1.这让父组件与子组件紧密地耦合;
2.只看父组件,很难理解父组件的状态。因为它可能被任意子组件修改!理想情况下,只有组件自己能修改它的状态。
自定义事件
Vue实例实现了一个自定义事件接口,用于在组件树中通信。这个事件系统独立于原生DOM事件,用法也不同。
每个Vue实例都是一个事件触发器:
•使用$on()监听事件;
•使用$emit()在它上面触发事件;
•使用$dispatch()派发事件,事件沿着父链冒泡;
•使用$broadcast()广播事件,事件向下传导给所有的后代。
不同于DOM事件,Vue事件在冒泡过程中第一次触发回调之后自动停止冒泡,除非回调明确返回true。
简单例子:
<!--子组件模板--> <templateid="child-template"> <inputv-model="msg"> <buttonv-on:click="notify">DispatchEvent</button> </template> <!--父组件模板--> <divid="events-example"> <p>Messages:{{messages|json}}</p> <child></child> </div> //注册子组件 //将当前消息派发出去 Vue.component('child',{ template:'#child-template', data:function(){ return{msg:'hello'} }, methods:{ notify:function(){ if(this.msg.trim()){ this.$dispatch('child-msg',this.msg) this.msg='' } } } }) //初始化父组件 //将收到消息时将事件推入一个数组 varparent=newVue({ el:'#events-example', data:{ messages:[] }, //在创建实例时`events`选项简单地调用`$on` events:{ 'child-msg':function(msg){ //事件回调内的`this`自动绑定到注册它的实例上 this.messages.push(msg) } } })
使用v-on绑定自定义事件
上例非常好,不过从父组件的代码中不能直观的看到"child-msg"事件来自哪里。如果我们在模板中子组件用到的地方声明事件处理器会更好。为此子组件可以用v-on监听自定义事件:
<childv-on:child-msg="handleIt"></child>
这样就很清楚了:当子组件触发了`”child-msg”`事件,父组件的`handleIt`方法将被调用。所有影响父组件状态的代码放到父组件的`handleIt`方法中;子组件只关注触发事件。
子组件索引
尽管有props和events,但是有时仍然需要在JavaScript中直接访问子组件。为此可以使用v-ref为子组件指定一个索引ID。例如:
<divid="parent"> <user-profilev-ref:profile></user-profile> </div> varparent=newVue({el:'#parent'}) //访问子组件 varchild=parent.$refs.profile
v-ref和v-for一起用时,ref是一个数组或对象,包含相应的子组件。
使用Slot分发内容
在使用组件时,常常要像这样组合它们:
<app> <app-header></app-header> <app-footer></app-footer> </app>
注意两点:
1.<app>组件不知道它的挂载点会有什么内容,挂载点的内容是由<app>的父组件决定的。
2.<app>组件很可能有它自己的模板。
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个处理称为内容分发(或“transclusion”,如果你熟悉Angular)。Vue.js实现了一个内容分发API,参照了当前Web组件规范草稿,使用特殊的<slot>元素作为原始内容的插槽。
编译作用域
在深入内容分发API之前,我们先明确内容的编译作用域。假定模板为:
<child-component>
{{msg}}
</child-component>
msg应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件。组件作用域简单地说是:
父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译
一个常见错误是试图在父组件模板内将一个指令绑定到子组件的属性/方法:
<!--无效-->
<child-componentv-show="someChildProperty"></child-component>
假定someChildProperty是子组件的属性,上例不会如预期那样工作。父组件模板不应该知道子组件的状态。
如果要绑定子组件内的指令到一个组件的根节点,应当在它的模板内这么做:
Vue.component('child-component',{ //有效,因为是在正确的作用域内 template:'<divv-show="someChildProperty">Child</div>', data:function(){ return{ someChildProperty:true } } })
类似地,分发内容是在父组件作用域内编译。
单个Slot
父组件的内容将被抛弃,除非子组件模板包含<slot>。如果子组件模板只有一个没有特性的slot,父组件的整个内容将插到slot所在的地方并替换它。
<slot>标签的内容视为回退内容。回退内容在子组件的作用域内编译,当宿主元素为空并且没有内容供插入时显示这个回退内容。
假定my-component组件有下面模板:
<div> <h1>Thisismycomponent!</h1> <slot> 如果没有分发内容则显示我。 </slot> </div>
父组件模板:
<my-component>
<p>Thisissomeoriginalcontent</p>
<p>Thisissomemoreoriginalcontent</p>
</my-component>
渲染结果:
<div> <h1>Thisismycomponent!</h1> <p>Thisissomeoriginalcontent</p> <p>Thisissomemoreoriginalcontent</p> </div>
具名Slot
<slot>元素可以用一个特殊特性name配置如何分发内容。多个slot可以有不同的名字。具名slot将匹配内容片段中有对应slot特性的元素。
仍然可以有一个匿名slot,它是默认slot,作为找不到匹配的内容片段的回退插槽。如果没有默认的slot,这些找不到匹配的内容片段将被抛弃。
例如,假定我们有一个multi-insertion组件,它的模板为:
<div> <slotname="one"></slot> <slot></slot> <slotname="two"></slot> </div>
父组件模板:
<multi-insertion> <pslot="one">One</p> <pslot="two">Two</p> <p>DefaultA</p> </multi-insertion>
渲染结果为:
<div> <pslot="one">One</p> <p>DefaultA</p> <pslot="two">Two</p> </div>
在组合组件时,内容分发API是非常有用的机制。
动态组件
多个组件可以使用同一个挂载点,然后动态地在它们之间切换。使用保留的<component>元素,动态地绑定到它的is特性:
newVue({ el:'body', data:{ currentView:'home' }, components:{ home:{/*...*/}, posts:{/*...*/}, archive:{/*...*/} } }) <component:is="currentView"> <!--组件在vm.currentview变化时改变--> </component>
keep-alive
如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个keep-alive指令参数:
<component:is="currentView"keep-alive>
<!--非活动组件将被缓存-->
</component>
activate钩子
在切换组件时,切入组件在切入前可能需要进行一些异步操作。为了控制组件切换时长,给切入组件添加activate钩子:
Vue.component('activate-example',{ activate:function(done){ varself=this loadDataAsync(function(data){ self.someData=data done() }) } })
注意`activate`钩子只作用于动态组件切换或静态组件初始化渲染的过程中,不作用于使用实例方法手工插入的过程中。
transition-mode
transition-mode特性用于指定两个动态组件之间如何过渡。
在默认情况下,进入与离开平滑地过渡。这个特性可以指定另外两种模式:
•in-out:新组件先过渡进入,等它的过渡完成之后当前组件过渡出去。
•out-in:当前组件先过渡出去,等它的过渡完成之后新组件过渡进入。
示例:
<!--先淡出再淡入--> <component :is="view" transition="fade" transition-mode="out-in"> </component> .fade-transition{ transition:opacity.3sease; } .fade-enter,.fade-leave{ opacity:0; }
杂项
组件和v-for
自定义组件可以像普通元素一样直接使用v-for:
<my-componentv-for="iteminitems"></my-component>
但是,不能传递数据给组件,因为组件的作用域是孤立的。为了传递数据给组件,应当使用props:
<my-component
v-for="iteminitems"
:item="item"
:index="$index">
</my-component>
不自动把item注入组件的原因是这会导致组件跟当前v-for紧密耦合。显式声明数据来自哪里可以让组件复用在其它地方。
编写可复用组件
在编写组件时,记住是否要复用组件有好处。一次性组件跟其它组件紧密耦合没关系,但是可复用组件应当定义一个清晰的公开接口。
Vue.js组件API来自三部分——prop,事件和slot:
•prop允许外部环境传递数据给组件;
•事件允许组件触发外部环境的action;
•slot允许外部环境插入内容到组件的视图结构内。
使用v-bind和v-on的简写语法,模板的缩进清楚且简洁:
<my-component
:foo="baz"
:bar="qux"
@event-a="doThis"
@event-b="doThat">
<!--content-->
<imgslot="icon"src="...">
<pslot="main-text">Hello!</p>
</my-component>
异步组件
在大型应用中,我们可能需要将应用拆分为小块,只在需要时才从服务器下载。为了让事情更简单,Vue.js允许将组件定义为一个工厂函数,动态地解析组件的定义。Vue.js只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。例如:
Vue.component('async-example',function(resolve,reject){ setTimeout(function(){ resolve({ template:'<div>Iamasync!</div>' }) },1000) })
工厂函数接收一个resolve回调,在收到从服务器下载的组件定义时调用。也可以调用reject(reason)指示加载失败。这里setTimeout只是为了演示。怎么获取组件完全由你决定。推荐配合使用Webpack的代码分割功能:
Vue.component('async-webpack-example',function(resolve){ //这个特殊的require语法告诉webpack //自动将编译后的代码分割成不同的块, //这些块将通过ajax请求自动下载。 require(['./my-async-component'],resolve) })
资源命名约定
一些资源,如组件和指令,是以HTML特性或HTML自定义元素的形式出现在模板中。因为HTML特性的名字和标签的名字不区分大小写,所以资源的名字通常需使用kebab-case而不是camelCase的形式,这不大方便。
Vue.js支持资源的名字使用camelCase或PascalCase的形式,并且在模板中自动将它们转为kebab-case(类似于prop的命名约定):
//在组件定义中 components:{ //使用camelCase形式注册 myComponent:{/*...*/} } <!--在模板中使用kebab-case形式--> <my-component></my-component> ES6对象字面量缩写也没问题: //PascalCase importTextBoxfrom'./components/text-box'; importDropdownMenufrom'./components/dropdown-menu'; exportdefault{ components:{ //在模板中写作<text-box>和<dropdown-menu> TextBox, DropdownMenu } }
递归组件
组件在它的模板内可以递归地调用自己,不过,只有当它有name选项时才可以:
varStackOverflow=Vue.extend({ name:'stack-overflow', template: '<div>'+ //递归地调用它自己 '<stack-overflow></stack-overflow>'+ '</div>' })
上面组件会导致一个错误“maxstacksizeexceeded”,所以要确保递归调用有终止条件。当使用Vue.component()全局注册一个组件时,组件ID自动设置为组件的name选项。
片断实例
在使用template选项时,模板的内容将替换实例的挂载元素。因而推荐模板的顶级元素始终是单个元素。
不这么写模板:
<div>rootnode1</div>
<div>rootnode2</div>
推荐这么写:
<div>
Ihaveasinglerootnode!
<div>node1</div>
<div>node2</div>
</div>
下面几种情况会让实例变成一个片断实例:
1.模板包含多个顶级元素。
2.模板只包含普通文本。
3.模板只包含其它组件(其它组件可能是一个片段实例)。
4.模板只包含一个元素指令,如<partial>或vue-router的<router-view>。
5.模板根节点有一个流程控制指令,如v-if或v-for。
这些情况让实例有未知数量的顶级元素,它将把它的DOM内容当作片断。片断实例仍然会正确地渲染内容。不过,它没有一个根节点,它的$el指向一个锚节点,即一个空的文本节点(在开发模式下是一个注释节点)。
但是更重要的是,组件元素上的非流程控制指令,非prop特性和过渡将被忽略,因为没有根元素供绑定:
<!--不可以,因为没有根元素-->
<examplev-show="ok"transition="fade"></example>
<!--props可以-->
<example:prop="someData"></example>
<!--流程控制可以,但是不能有过渡-->
<examplev-if="ok"></example>
当然片断实例有它的用处,不过通常给组件一个根节点比较好。它会保证组件元素上的指令和特性能正确地转换,同时性能也稍微好些。
内联模板
如果子组件有inline-template特性,组件将把它的内容当作它的模板,而不是把它当作分发内容。这让模板更灵活。
<my-componentinline-template>
<p>Thesearecompiledasthecomponent'sowntemplate</p>
<p>Notparent'stransclusioncontent.</p>
</my-component>
但是inline-template让模板的作用域难以理解,并且不能缓存模板编译结果。最佳实践是使用template选项在组件内定义模板。
本文已被整理到了《Vue.js前端组件学习教程》,欢迎大家学习阅读。
关于vue.js组件的教程,请大家点击专题vue.js组件学习教程进行学习。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。