组件可以扩展 HTML 元素,封装可重用的代码
在较高层面上,组件是自定义元素, Vue.js 的编译器为它添加特殊功能
在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。
<div id="example"> <!--web组件的定义脱离了一般的dom元素的写法,相当于自定义了元素--> <my-component></my-component> </div>
// 注册全局组件,指定之前设定的元素名,然后传入对象 Vue.component('my-component', { template: '<div>A custom component!</div>' }) // 创建根实例 new Vue({ el: '#example' })
不必在全局注册每个组件。通过使用组件实例选项注册,可以使组件仅在另一个实例/组件的作用域中可用
//将传入给组件的对象单独写 var Child = { template: '<div>A custom component!</div>' } new Vue({ //通过components语法创建局部组件 //将组件仅仅放在这个vue实例里面使用 components: { // <my-component> 将只在父模板可用 'my-component': Child } })
当使用 DOM 作为模版时(例如,将 el 选项挂载到一个已存在的元素上), 你会受到 HTML 的一些限制,
因为 Vue 只有在浏览器解析和标准化 HTML 后才能获取模版内容。
尤其像这些元素 <ul> , <ol>, <table> , <select> 限制了能被它包裹的元素, <option> 只能出现在其它元素内部。
<!--这种是不行的,会报错--> <table> <my-row>...</my-row> </table> <!--要通过is属性来处理--> <table> <tr is="my-row"></tr> </table>
使用组件时,大多数可以传入到 Vue 构造器中的选项可以在注册组件时使用,有一个例外: data 必须是函数。 实际上
//这样会报错,提示data必须是一个函数 Vue.component('my-component', { template: '<span>{{ message }}</span>', data: { message: 'hello' } })
<div id="example-2"> <simple-counter></simple-counter> <simple-counter></simple-counter> <simple-counter></simple-counter> </div>
var data = { counter: 0 } Vue.component('simple-counter', { template: '<button v-on:click="counter += 1">{{ counter }}</button>', // data 是一个函数,因此 Vue 不会警告, // 但是我们为每一个组件返回了同一个对象引用,所以改变其中一个会把其他都改变了 data: function () { return data } }) new Vue({ el: '#example-2' })
避免出现同时改变数据的情况
//返回一个新的对象,而不是返回同一个data对象引用 data: function () { return { //字面量写法会创建新对象 counter: 0 } }
组件意味着协同工作,通常父子组件会是这样的关系:
组件 A 在它的模版中使用了组件 B 。它们之间必然需要相互通信
父组件要给子组件传递数据,子组件需要将它内部发生的事情告知给父组件
然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的。这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性。
在 Vue.js 中,父子组件的关系可以总结为 props down, events up 。
父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。看看它们是怎么工作的。
使用prop传递数据
组件实例的作用域是孤立的。这意味着不能并且不应该在子组件的模板内直接引用父组件的数据。
使用 props 把数据传给子组件。
prop 是父组件用来传递数据的一个自定义属性
子组件需要显式地用 props 选项声明 “prop”
<div id="example-2"> <!--向这个组件传入一个字符串--> <child message="hello!"></child> </div>
Vue.component('child', { // 声明 props,用数组形式的对象 props: ['message'], // 就像 data 一样,prop 可以用在模板内 // 同样也可以在 vm 实例中像 “this.message” 这样使用 template: '<span>{{ message }}</span>' }); new Vue({ el: '#example-2' })
用 v-bind 动态绑定 props 的值到父组件的数据中。每当父组件的数据变化时,该变化也会传导给子组件
<div id="example-2"> <!--使用v-modal实现双向绑定--> <input v-model="parentMsg"> <br> <!--需要注意这里使用短横线的变量,因为在html下是使用短横线变量的,但是在vue下使用驼峰变量--> <!--将父组件的parentMsg和子组件的my-message进行绑定--> <child v-bind:my-message="parentMsg"></child> </div>
Vue.component('child', { // 声明 props props: ['my-message'], template: '<span>{{ myMessage }}</span>' //如果写my-message会报错,需要转换为驼峰写法 }); new Vue({ el: '#example-2', data: { parentMsg: '' } })
HTML 特性不区分大小写。当使用非字符串模版时,prop的名字形式会从 camelCase 转为 kebab-case(短横线隔开)
在javascript里面使用驼峰写法,但是在html里面需要转成短横线写法
反之亦然,vue会自动处理来自html的短横线写法转为驼峰写法
<!-- 默认只传递了一个字符串"1" --> <comp some-prop="1"></comp> <!-- 用v-bind实现传递实际的数字 --> <comp v-bind:some-prop="1"></comp>
prop 是单向绑定的
当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。
每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop 。如果你这么做了,Vue 会在控制台给出警告。
通常有两种改变 prop 的情况:
prop 作为初始值传入,子组件之后只是将它的初始值作为本地数据的初始值使用
定义一个局部 data 属性,并将 prop 的初始值作为局部数据的初始值。
<div id="example-2"> <!--这里用短横线写法--> <child initial-counter="10"></child> </div>
Vue.component('child', { props: ['initialCounter'],//这里用驼峰写法 data: function () { //转为一个局部变量,写一个data对象给组件使用 return {counter: this.initialCounter} }, template: '<span>{{ counter }}</span>' }); new Vue({ el: '#example-2' })
prop 作为需要被转变的原始值传入。
定义一个 computed 属性,此属性从 prop 的值计算得出。
//例子没有写完,但是根据第一个例子可以知道利用computed的手法原理其实跟写一个data差不多 props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。
组件可以为 props 指定验证要求,当组件给其他人使用时这很有用。
Vue.component('example', { props: { // 基础类型检测 (`null` 意思是任何类型都可以) propA: Number, // 多种类型 propB: [String, Number], // 必传且是字符串 propC: { type: String, required: true }, // 数字,有默认值 propD: { type: Number, default: 100 }, // 数组/对象的默认值应当由一个工厂函数返回 propE: { type: Object, default: function () { return { message: 'hello' } } }, // 自定义验证函数 propF: { validator: function (value) { return value > 10 } } } })
每个 Vue 实例都实现了事件接口(Events interface)
使用 $on(eventName) 监听事件
使用 $emit(eventName) 触发事件
父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件。
<div id="counter-event-example"> <p>{{ total }}</p> <!--监听子组件的事件触发,监听increment1事件,处理程序为incrementTotal事件--> <button-counter v-on:increment1="incrementTotal"></button-counter> <!--关键在于这里v-on绑定的是一个子组件的事件,并且赋值了一个父组件的方法给他,那么子组件里面就可以使用这个方法--> <button-counter v-on:increment1="incrementTotal"></button-counter> </div>
Vue.component('button-counter', { //监听click事件,处理程序为increment(子组件定义的方法) template: '<button v-on:click="increment">{{ counter }}</button>', //每一个counter都是独立的对象属性 data: function () { return { counter: 0 } }, //子组件的方法 methods: { increment: function () { this.counter += 1; //在子组件里面直接触发之前监听的increment1事件来执行父组件的方法 this.$emit('increment1'); } }, }) new Vue({ el: '#counter-event-example', data: { total: 0 }, //父组件的方法 methods: { incrementTotal: function () { this.total += 1 } } })
1.组件之间因为作用域不同的关系,所以相互独立,所以子组件想要使用父组件的方法的话需要做一个新的监听映射
<!--代替.on,这么就能够绑定原生js的事件了--> <my-component v-on:click.native="doTheThing"></my-component>
自定义事件也可以用来创建自定义的表单输入组件,使用 v-model 来进行数据双向绑定。
所以要让组件的 v-model 生效,它必须:
接受一个 value 属性
在有新的 value 时触发 input 事件
<!--直接使用v-model,v-modal默认处理input事件--> <input v-model="something"> <!--v-modal是语法糖,翻译过来原理是这样:--> <!--绑定一个value,然后监听input事件,通过获取input的输入来不断改变绑定的value的值,满足了v-modal的触发条件就可以实现v-modal了--> <input v-bind:value="something" v-on:input="something = $event.target.value">
一个非常简单的货币输入:
<!--绑定一个v-model为price,其实是绑定了一个value--> <currency-input v-model="price"></currency-input>
Vue.component('currency-input', { template: '/ <span>/ $/ <input/ ref="input"/ //注册为input,是DOM的节点元素 v-bind:value="value"/ //v-model的value(也是prop) v-on:input="updateValue($event.target.value)"/ //封装更新value的函数 >/ </span>/ ', props: ['value'], //父组件将绑定的value传给子组件 methods: { // 不是直接更新值,而是使用此方法来对输入值进行格式化和位数限制 updateValue: function (value) { var formattedValue = value //对值进行处理 // 删除两侧的空格符 .trim() // 保留 2 小数位和2位数 .slice(0, value.indexOf('.') + 3) // 如果值不统一,手动覆盖以保持一致,为了保持输入框显示内容跟格式化内容一致 if (formattedValue !== value) { //因为注册是一个input元素,所以this.$refs 就是input元素 this.$refs.input.value = formattedValue } //手动触发input事件,将格式化后的值传过去,这是最终显示输入框的输出 this.$emit('input', Number(formattedValue)) } } }) //实例化vue实例的 new Vue({ el: '#aa', //要绑定一个vue实例,例如包裹一个id为aa的div data:{ price:'' //v-model要有数据源 } })
ref 被用来给元素或子组件注册引用信息。引用信息会根据父组件的 $refs 对象进行注册。如果在普通的DOM元素上使用,引用信息就是元素; 如果用在子组件上,引用信息就是组件实例 ref
这是一个比较完整的例子:
<div id="app"> <!--有3个组件,分别不同的v-model--> <currency-input label="Price" v-model="price" ></currency-input> <currency-input label="Shipping" v-model="shipping" ></currency-input> <currency-input label="Handling" v-model="handling" ></currency-input> <currency-input label="Discount" v-model="discount" ></currency-input> <p>Total: ${{ total }}</p> </div>
Vue.component('currency-input', { template: '/ <div>/ <label v-if="label">{{ label }}</label>/ $/ <input/ ref="input"/ // 这些没什么特别,引用注册为input DOM元素 v-bind:value="value"/ v-on:input="updateValue($event.target.value)"/ v-on:focus="selectAll"/ //这里多了focus事件监听,焦点在的时候全选,也只是多了处理而已,对整体逻辑理解没啥影响 v-on:blur="formatValue"/ //这里多了blur事件监听,焦点离开的时候格式化 >/ </div>/ ', props: { //多个prop传递,因为prop是对象,只要是对象格式就行 value: { type: Number, default: 0 }, label: { type: String, default: '' } }, mounted: function () { //这是vue的过渡状态,暂时忽略不影响理解 this.formatValue() }, methods: { updateValue: function (value) { var result = currencyValidator.parse(value, this.value) if (result.warning) { // 这里也使用了$refs获取引用注册信息 this.$refs.input.value = result.value } this.$emit('input', result.value) }, formatValue: function () { this.$refs.input.value = currencyValidator.format(this.value) //这里注意下,这个this是prop传递过来的,也相当于这个组件作用域 }, selectAll: function (event) { //event可以获取原生的js事件 // Workaround for Safari bug // http://stackoverflow.com/questions/1269722/selecting-text-on-focus-using-jquery-not-working-in-safari-and-chrome setTimeout(function () { event.target.select() }, 0) } } }) new Vue({ el: '#app', data: { price: 0, shipping: 0, handling: 0, discount: 0 }, computed: { total: function () { return (( this.price * 100 + this.shipping * 100 + this.handling * 100 - this.discount * 100 ) / 100).toFixed(2) } } })
在简单的场景下,使用一个空的 Vue 实例作为中央事件总线:
var bus = new Vue() // 触发组件 A 中的事件 bus.$emit('id-selected', 1) /* 通过on来监听子组件的事件来实现传递 */ // 在组件 B 创建的钩子中监听事件 bus.$on('id-selected', function (id) { // ... })
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为 内容分发 (或 “transclusion” 如果你熟悉 Angular)
组件作用域简单地说是:父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。
假定 someChildProperty 是子组件的属性,上例不会如预期那样工作。父组件模板不应该知道子组件的状态。
<!-- 无效 --> <child-component v-show="someChildProperty"></child-component>
如果要绑定子组件内的指令到一个组件的根节点,应当在它的模板内这么做:
Vue.component('child-component', { // 有效,因为是在正确的作用域内 template: '<div v-show="someChildProperty">Child</div>', data: function () { return { //因为这个属性在当前组件内编译(创建了) someChildProperty: true } } })
类似地,分发内容是在父组件作用域内编译。
除非子组件模板包含至少一个 <slot> 插口,否则父组件的内容将会被丢弃。
当子组件模板只有一个没有属性的 slot 时,父组件整个内容片段将插入到 slot 所在的 DOM 位置,并替换掉 slot 标签本身。
备用内容在子组件的作用域内编译,并且只有在宿主元素为空,且没有要插入的内容时才显示备用内容。
<!--父组件模版:--> <div id="aa"> <h1>我是父组件的标题</h1> <!--子组件的作用域内编译,宿主元素为空,且没有要插入的内容--> <my-component></my-component> <my-component> <p>这是一些初始内容</p> <p>这是更多的初始内容</p> </my-component> </div>
Vue.component('my-component', { //my-component 组件有下面模板 template: '/ <div>/ <h2>我是子组件的标题</h2> / <slot> / //有slot插口,所以没有被父组件丢弃 只有在没有要分发的内容时才会显示。/ </slot> / </div> / ' }) new Vue({ el: '#aa', })
渲染结果:
<div id="aa"><h1>我是父组件的标题</h1> <div> <h2>我是子组件的标题</h2> <!--这里是直接插入,没有使用DOM元素--> 只有在没有要分发的内容时才会显示。 </div> <div> <h2>我是子组件的标题</h2> <p>这是一些初始内容</p> <p>这是更多的初始内容</p> </div> </div>
<slot> 元素可以用一个特殊的属性 name 来配置如何分发内容。多个 slot 可以有不同的名字。具名 slot 将匹配内容片段中有对应 slot 特性的元素。
仍然可以有一个匿名 slot ,它是默认 slot ,作为找不到匹配的内容片段的备用插槽。如果没有默认的 slot ,这些找不到匹配的内容片段将被抛弃。
<div id="aa"> <app-layout> <!--这是header--> <h1 slot="header">这里可能是一个页面标题</h1> <p>主要内容的一个段落。</p> <p>另一个主要段落。</p> <!--这是footer--> <p slot="footer">这里有一些联系信息</p> </app-layout> </div>
Vue.component('app-layout', { template: '/ <div class="container"> / <header> / //找到名字叫header的slot之后替换内容,这里替换的是整个DOM <slot name="header"></slot> / </header> / <main> / //因为slot没有属性,会将内容插入到slot的所在的DOM位置 <slot></slot> / </main> / <footer>/ //跟header类似 <slot name="footer"></slot> / </footer> / </div> / ' }); new Vue({ el: '#aa', })
渲染结果为:
<div class="container"> <header> <h1>这里可能是一个页面标题</h1> </header> <main> <p>主要内容的一个段落。</p> <p>另一个主要段落。</p> </main> <footer> <p>这里有一些联系信息</p> </footer> </div>
作用域插槽是一种特殊类型的插槽,用作使用一个(能够传递数据到)可重用模板替换已渲染元素。
在子组件中,只需将数据传递到插槽,就像你将 prop 传递给组件一样
在父级中,具有特殊属性 scope 的 <template> 元素,表示它是作用域插槽的模板。scope 的值对应一个临时变量名,此变量接收从子组件中传递的 prop 对象
<div id="parent" class="parent"> <child> <!--接收从子组件中传递的prop对象(这个就是作用域插槽)--> <template scope="props"> <span>hello from parent</span> <!--使用这个prop对象--> <span>{{ props.text }}</span> </template> </child> </div>
Vue.component('child', { props: ['props'], //这个写不写都可以,作用域插槽固定会接收prop对象,而且这个prop对象是肯定存在的 template: '/ <div class="child"> / <slot text="hello from child"></slot> / //在子组件里直接将数据传递给slot </div> / ' }); new Vue({ el: '#parent', })
渲染结果:
<div class="parent"> <div class="child"> <span>hello from parent</span> <!--子组件的东西出现在这里了--> <span>hello from child</span> </div> </div>
另外一个例子,作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项
<div id="parent"> <!--绑定一个组件的prop ,位置1--> <my-awesome-list :items="items"> <!-- 作用域插槽也可以在这里命名 --> <!--这里props只代表确定接受prop对象的东西,不关注prop对象里面有什么,位置2--> <template slot="item" scope="props"> <li class="my-fancy-item">{{ props.text }}</li> </template> </my-awesome-list> </div>
Vue.component('my-awesome-list', { props:['items'], //需要声明prop为items,需要是为下面的循环遍历的items的数据源做设定,位置3 template: '/ <ul> / <slot name="item" v-for="item in items" :text="item.text"> / //在slot中,循环遍历输出items的text,位置4 </slot> / </ul> / ' }); new Vue({ el: '#parent', data : { items:[ //初始化items数据 {text:"aa"}, {text:"bb"} ] } })
位置1,实现了一个组件的prop绑定,prop需要在组件里面声明,这里绑定的是items,这是要将父组件的items传递到子组件,所以在位置3里面需要声明,在vue实例要初始化
位置2,这里scope的props是代表作用域插槽接收来自prop对象的数据,props.text是代表每一个li要输出的是prop对象的text属性
位置3,在组件里声明props,为了接收父组件绑定的items属性,然后将其给位置4的循环使用
位置4,这里绑定了text属性,就是前呼位置2里面输出的prop对象的text属性
多个组件可以使用同一个挂载点,然后动态地在它们之间切换。使用保留的 <component> 元素,动态地绑定到它的 is 特性
var vm = new Vue({ el: '#example', data: { currentView: 'home' //默认值 }, components: { //根据不同的值进行不同的组件切换,这里用components写法 home: { /* ... */ }, posts: { /* ... */ }, archive: { /* ... */ } } })
<!--这个is是一个字符串,根据返回值来给组件进行v-bind--> <component v-bind:is="currentView"> <!-- 组件在 vm.currentview 变化时改变! --> </component>
如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个 keep-alive 指令参数
<keep-alive> <component :is="currentView"> <!-- 非活动组件将被缓存! --> </component> </keep-alive>
在编写组件时,记住是否要复用组件有好处。一次性组件跟其它组件紧密耦合没关系,但是可复用组件应当定义一个清晰的公开接口。
Vue 组件的 API 来自三部分 - props, events 和 slots :
Props 允许外部环境传递数据给组件
Events 允许组件触发外部环境的副作用
Slots 允许外部环境将额外的内容组合在组件中。
<!--v-bind,缩写:,绑定prop--> <!--v-on,缩写@,监听事件--> <!--slot插槽--> <my-component :foo="baz" :bar="qux" @event-a="doThis" @event-b="doThat" > <img slot="icon" src="..."> <p slot="main-text">Hello!</p> </my-component>
尽管有 props 和 events ,但是有时仍然需要在 JavaScript 中直接访问子组件。为此可以使用 ref 为子组件指定一个索引 ID 。
<div id="parent"> <user-profile ref="profile"></user-profile> </div>
var parent = new Vue({ el: '#parent' }) // 访问子组件 var child = parent.$refs.profile
当 ref 和 v-for 一起使用时, ref 是一个数组或对象,包含相应的子组件。
$refs 只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案——应当避免在模版或计算属性中使用 $refs 。
ref 被用来给元素或子组件注册引用信息。引用信息会根据父组件的 $refs 对象进行注册。如果在普通的DOM元素上使用,引用信息就是元素; 如果用在子组件上,引用信息就是组件实例 ref
当注册组件(或者 props)时,可以使用 kebab-case ,camelCase ,或 TitleCase 。Vue 不关心这个。
在 HTML 模版中,请使用 kebab-case 形式:
// 在组件定义中 components: { // 使用 kebab-case 形式注册--横线写法 'kebab-cased-component': { /* ... */ }, // register using camelCase --驼峰写法 'camelCasedComponent': { /* ... */ }, // register using TitleCase --标题写法 'TitleCasedComponent': { /* ... */ } }
<!-- 在HTML模版中始终使用 kebab-case--横线写法 --> <kebab-cased-component></kebab-cased-component> <camel-cased-component></camel-cased-component> <title-cased-component></title-cased-component>
组件在它的模板内可以递归地调用自己,不过,只有当它有 name 选项时才可以
当你利用Vue.component全局注册了一个组件, 全局的ID作为组件的 name 选项,被自动设置.
//组件可以用name来写名字 name: 'unique-name-of-my-component' //也可以在创建的时候默认添加名字 Vue.component('unique-name-of-my-component', { // ... }) //如果同时使用的话,递归的时候就会不断递归自己,导致溢出 name: 'stack-overflow', template: '<div><stack-overflow></stack-overflow></div>'
尽管在 Vue 中渲染 HTML 很快,不过当组件中包含大量静态内容时,可以考虑使用 v-once 将渲染结果缓存起来,就像这样:
Vue.component('terms-of-service', { template: '/ <div v-once>/ <h1>Terms of Service</h1>/ ... a lot of static content .../ </div>/ ' })
v-once只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。