深入了解组件
组件注册
组件名
注册一个组件时,要给组件起名,如在全局注册中,组件名就是Vue.component
的第一个参数:
1 Vue component ('my-component-name' ,{})
组件名应当遵守W3C规范中的自定义组件名(字母全小写且必须包含一个连字符)
组件名大小写
定义组件名的方式由两种:
使用短横线分隔命名:my-component-name
使用首字母大写命名:MyComponentName
两种命名法都可以使用,但需注意的是,直接在DOM(即非字符串的模板)中使用时,只有短横线分隔命名是有效的。
全局注册
使用全局注册的组件,在它们注册之后可以用在任何新创建的Vue根实例的模板中,在子组件的内部也可以相互使用。
1 2 Vue component ('my-component-name' ,{})new Vue ({el :'#app' })
局部注册
若使用webpack这个的构建系统,全局注册所有的组件便意味着即使不再使用这个组件了,它仍然会被包含在最终的构建结果中。此时,可以将组件局部注册:
1 2 3 4 5 6 7 8 9 10 11 var ComponentA ={}var ComponentB ={}new Vue ({ el :'#app' , components :{ 'components-a' :ComponentA , 'components-b' :ComponentB } })
对于components
对象中的每个property来说,其property名就是自定义元素的名字,其property值就是这个组件的选项对象。
局部注册的组件在其子组件中不可用。 所希望上述代码中的ComponentA
在ComponentB
中可用,则要写成:
1 2 3 4 5 6 var ComponentA ={}var ComponentB ={ components :{ 'components-a' :ComponentA } }
模块系统
在模块系统中局部注册
若使用了注入Babel和webpack的模块系统,需要创建一个components
目录,并将每个组件放置在其各自的文件中,然后在局部注册之前导入每个想使用的组件。如在一个ComponentB.vue
文件中使用ComponentA
:
1 2 3 4 5 6 import ComponentA from './ComponentA.vue' export default { components :{ ComponentA } }
在ES2015+中,在对象中放一个类似ComponentA
的变量名便是'ComponentsA':ComponentA
的缩写,即这个变量名同时是用在模板中的自定义元素的名称,也包含了这个组件选项的变量名。
基础组件的自动化全局注册
当组件中只是包裹了一个输入框或按钮之类的通用元素时,将其称之为基础组件,它们会在各个组件中被频繁地用到。
这往往会导致组件里都会由一个包含基础组件的长列表,但用于模板中的仅是一小部分组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <body> <div id="example"> <BaseInput v-model="searchText" @keydown.enter="search" /> <BaseButton @click="search"> <BaseIcon name="search" /> </BaseButton> </div> </body> <script> import BaseButton from './BaseButton.vue' import BaseIcon from './BaseIcon.vue' import BaseInput from './BaseInput.vue' export default{ components:{ BaseButton, BaseIcon, BaseInput } } </script>
若使用webpack(或在内部使用了webpack的Vue CLI 3+),则可以使用require.context
只全局注册这些通用的基础组件。如在应用入口文件(如src/main.js
)中全局导入基础组件的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <body> <div id="example"> <BaseInput v-model="searchText" @keydown.enter="search" /> <BaseButton @click="search"> <BaseIcon name="search" /> </BaseButton> </div> </body> <script> import Vue from 'vue' import upperFirst from 'lodash/upperFirst' import camelCase from 'lodash/camelCase' const requireComponent=require.context( './components',//组件目录的相对路径 false,//是否查询其子目录 //匹配基础组件文件名的正则表达式 /Base[A-Z]\w+\.(vue|js)$/ ) requireComponent.keys().forEach(fileName=>{ const componentConfig=requireComponent(fileName)//获取组件配置 const componentName=upperFirst(//获取组件的PascalCase命名 camelCase( fileName//获取和目录深度无关的文件名 .split('/') .pop() .replace(/\.\w+$/,'') ) ) //全局注册组件 Vue.component( componentName, // 若这个组件选项是通过export default导出的,就优先使用.default,否则回退到使用模块的根 componentConfig.default||componentConfig ) }) </script>
Prop
Prop的大小写
HTML中的attribute名是大小写不敏感的,浏览器会把所有大写字符解释为小写字符。这意味着在使用DOM模板时,驼峰命名的prop名需要使用其等价的短横线分隔来命名:
1 2 3 4 5 6 7 Vue.component('blog-post',{ //在JavaScript中是驼峰命名 props:{'postTitle'}, template:'<h3>{{postTitle}}</h3>' }) // 在HTML中是短横线分隔命名 <blog-post post-title="hello!"></blog-post>
而在字符串模板中便不存在这个限制。
Prop类型
Prop的类型有以下八种:
String
Number
Boolean
Array
Object
Date
Function
Symbol
props的写法(无默认值):
1 2 3 4 5 6 7 8 9 props:{ title:String, likes:Number, isPublished:Boolean, commentIds:Array, author:Object, callback:Function, contactsPromise:promise }
传递静态或动态Prop
任何类型的值都可以传给一个prop,不仅能传入静态的值,还能通过v-bind
动态赋值:
1 2 3 4 5 6 7 8 // 给prop传入静态的值 <blog-post title="My journey with Vue"></blog-post> // 通过v-bind动态赋予一个变量的值 <blog-post v-bind:title="post.title"></blog-post> // 通过v-bind动态赋予一个复杂表达式的值 <blog-post v-bind:title="post.title+'by'+post.author.name"></blog-post>
传入一个数字
1 2 3 4 5 6 // 即使42是静态的,但仍需v-bind来告诉Vue // 这是一个JavaScript表达式而不是一个字符串 <blog-post v-bind:likes="42"></blog-post> // 通过v-bind动态赋予一个变量的值 <blog-post v-bind:likes="post.likes"></blog-post>
传入一个布尔值
1 2 3 4 5 6 7 8 9 // 即使prop没有值,结果都是true <blog-post is-published></blog-post> // 即使false是静态的,但仍需v-bind来告诉Vue // 这是一个JavaScript表达式而不是一个字符串 <blog-post v-bind:is-published="false"></blog-post> // 通过v-bind动态赋予一个变量的值 <blog-post v-bind:is-published="post.isPublished"></blog-post>
传入一个数组
1 2 3 4 5 6 // 即使数组是静态的,但仍需v-bind来告诉Vue // 这是一个JavaScript表达式而不是一个字符串 <blog-post v-bind:comment-ids="[234,266,273]"></blog-post> // 通过v-bind动态赋予一个变量的值 <blog-post v-bind:comment-ids="post.commentIds"></blog-post>
传入一个对象
1 2 3 4 5 6 7 8 9 10 // 即使对象是静态的,但仍需v-bind来告诉Vue // 这是一个JavaScript表达式而不是一个字符串 <blog-post v-bind:author="{ name:'Veronica', company:'Veridian Dynamics' }" ></blog-post> // 通过v-bind动态赋予一个变量的值 <blog-post v-bind:author="post.author"></blog-post>
传入一个对象的所有property
若要将一个对象的所有property都作为prop传入,可以使用不带参数的v-bind
来取代v-bind:prop-name
1 2 3 4 5 6 7 8 9 10 11 12 13 14 //给定的对象post post:{ id:1, title:'My Journey with Vue' } // 通过v-bind动态赋予一个变量的值 <blog-post v-bind="post"></blog-post> //等价于 <blog-post v-bind:id="post.id" v-bind:title="post.title" ></blog-post>
单向数据流
所有的prop都使得其父子prop之间形成了一个单向下行绑定:父级prop的更新会向下流动到子组件中 ,但反过来则不行,这样防止了从子组件意外变更父级组件的状态。
每次父级组件发生变更时,子组件中所有的prop都将会刷新为最新的值 ,因此不应在一个子组件内部改变prop。
若要变更一个prop,可通过以下方法进行变更:
这样prop用来传递一个初始值,子组件希望将其作为一个本地的prop数据来使用。此时最好定义一个本地的data property并将这个prop用作其初始值:
1 2 3 4 5 6 props :['initialCounter' ],data :function ( ){ return { counter :this .initialCounter } }
这个prop以一种原始的值传入且需要进行转化。此时最好使用这个prop的值来定义一个计算属性:
1 2 3 4 5 6 props :['size' ],computed :{ normalizeSize :function ( ){ return this .size .trim ().toLowerCase () } }
注意: 在JavaScript中对象和数组是通过引用传入的,所以对于一个数组或对象类型的prop来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。
Prop验证
可以通过在props
中的值提供一个带有验证需求的对象,从而指定组件的prop验证方式。即为props
设定默认值如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 props:{ 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 ['success','warning','danger'].indexOf(value)!==-1 } } }
当prop验证失败时,Vue将会产生一个控制台的警告。
prop会在组件实例创建之前进行验证,因此实例的property(如data
、computed
等)在default
或validator
函数中是不可用的。
类型检查
prop类型除了可以是八种原生类型之外,还可以是自定义的构造函数,并且通过instanceof
来进行检查确认:
1 2 3 4 5 6 7 8 9 10 11 12 function Person (firstName,lastName){ this .firstName =firstName this .lastName =lastName } Vue .component ('blog-post' ,{ props :{ author :Person } })
非Prop的Attribute
传向一个组件,但组件中没有相应prop定义的attribute称之为非prop的attribute。显式定义的prop适用于向一个子组件传入信息。组件可以接收任意的attribute,而这些attribute会被添加到这个组件的根元素上。
替换/合并已有的Attribute
现有一个Bootstrap插件里使用了第三方组件<bootstrap-date-input>
,其模板如下:
1 <input type ="data" class ="form-control" >
若要为这个插件定制一个主体,则需要添加一个特别的类名如下:
1 2 3 4 <bootstrap-date-input data-date-picker ="activated" class ="date-picker-theme-dark" > </bootstrap-date-input >
在这种情况下共定义了两个不同的class值:
form-control
:组件模板内设置好的class
date-picker-theme-dark
:从组件父级传入的
对于绝大多数attribute来说,从外部提供给组件的值会替换掉组件内部设置好的值,因此若直接传入type="text"
就会替换掉type="date"
并将其破坏掉。 而class
和style
attribute会将两边的值合起来 ,从而得到最终值:form-control date-picker-theme-dark
。
禁用Attribute继承
若不希望组件的根元素继承attribute,可以在组件的选项中设置inheritAttris:false
,配合实例的$attrs
property使用,便可以手动决定这些attribute会被赋予哪个元素,常用于撰写基础组件中 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Vue .component ('base-input' ,{ inheritAttrs :false , props :['label' ,'value' ], template : ` <label> {{label}} <input v-bind="$attrs" v-bind:value="value" v-on:input="$emit('input',$event.target.value)" > </label> ` })xxxxxxxxxx16 1Vue.component ('base-input' ,{2 inheritAttrs :false ,3 props :['label' ,'value' ],4 template : `5 <label>6 {{label}}7 <input8 v-bind="$attrs"9 v-bind:value="value"10 v-on:input="$emit('input',$event.target.value)"11 >12 </label>13 ` 14 })Vue .component ('my-component' ,{15 inheritAttrs :false ,16 })
其中,inheritAttris:false
选项不会影响style
和class
的绑定。
这个模式允许在使用基础组件时更像是使用原始的HTML元素,而不用担心哪个元素才是真正的根元素。
自定义事件
事件名
不同于组件和prop,事件名不存在任何自动化的大小写转化,触发的事件名需要完全匹配监听这个事件所用的名称 。如触发事件名是驼峰式命名的,则监听该名字的短横线分隔命名版本是不会有任何效果的。
不同于组件和prop,事件名不会被用作一个JavaScript变量名或property名,因此不会使用驼峰命名或首字母大写命名。且v-on
事件监听器在DOM模板中会被自动转换为全小写,因此推荐事件名使用短横线分隔命名 。
自定义组件的v-model
一个组件上的v-model
默认会利用名为value
的prop和名为input
的事件。但像单选框、复选框等类型的输入控件可能会将value
attribute用于不同的目的,为避免冲突,可使用model
选项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <body> <div id="example"> <!-- lovingVue的值将会传如名为checked的prop --> <!-- 当<base-checkbox>触发一个change事件并附带一个新值时,lovingVue的property将会被更新 --> <base-checkbox v-model="lovingVue"></base-checkbox> </div> </body> <script> Vue.component('base-checkbox',{ model:{ prop:'checked', event:'change' }, props:{ checked:Boolean//props里仍需要声明checked这个prop }, template:` <input type="checkbox" v-bind:checked="checked" v-on:change="$emit('change',$event.target.checked)" > ` }) </script>
将原生事件绑定到组件
若要在一个组件的根元素上直接监听一个原生事件,可以使用v-on
的.native
修饰符:
1 <base-input v-on:focus.native ="onFocus" > </base-input >
当监听一个类似<input>
的特定元素时,父级的.native
监听器将会静默失败,其不会产生任何报错,但onFocus
处理函数也不会被调用。
Vue提供了一个$listeners
property来解决这一问题,它是一个对象,里面包含了作用在这个组件上的所有监听器,如:
1 2 3 4 { focus :function (event ){}, input :function (event ){} }
有了$listeners
property,再配合v-on=$listeners
便可以将所有的事件监听器指向这个组件的某个特定的子元素。
对于希望<input>
也能配合v-model
工作的组件,应为这些监听器创建一个inputListeners
计算属性如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <script> Vue.component('base-input',{ inheritAttrs:false, props:['label','value'], computed:{ inputListeners:function(){ var vm=this return Object.assign({}, this.$listeners, { input:function(event){ vm.$emit('input',event.target.value) } } ) } }, template: ` <label> {{label}} <input v-bind="$attrs" v-bind:value="value" v-on="inputListeners" > </label> ` }) </script>
此时<base-input>
组件是一个完全透明的包裹器了,即它可以像一个普通的<input>
元素一样使用。所有跟<base-input>
组件相同的attribute和监听器都可以工作,不必再使用.native
监听器来监听事件。
.sync
修饰符
若需要对一个prop进行”双向绑定“时,可以用update:myPropName
的模式触发事件来处理。
如在一个包含title
prop的组件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <script> Vue.component('text-document',{ props:['title'], template: ` <div> 我是子节点 <button @click="handleClick">点击我改变父级Title</button> </div> `, methods:{ handleClick(){ this.count+=1; newTitle=this.title+this.count;//对title赋新值 this.$emit('update:childTitle',newTitle) } }, data(){ return{ count:0 } }, }) const app=new Vue({ el:'#example', data:{ parentTitle:"我是父级title", }, //父组件可以监听那个事件并根据需要更新一个本地的数据property template: ` <div> 我是父级节点 <text-document v-bind:title="parentTitle" v-on:update:title="parentTitle=$event" ></text-document> {{parentTitle}} </div> ` //上述代码可用.sync修饰符缩写为 //<text-document :title="parentTitle" :title.sync="parentTitle"></text-document> }) </script>
注意带有.sync
修饰符的v-bind
不能和表达式一起使用(如v-bind:title.sync="doc.title+'!'"
是无效的),只能提供想绑定的property名,如v-model
。
当用一个对象同时设置多个prop时,也可以将.sync
修饰符和v-bind
配合使用:
1 <text-document v-bind.sync="doc"></text-document>
这样会把doc
对象中的每个property都作为一个独立的prop传进去,然后各自添加用于更新的v-on
监听器。
注意: 将v-bind.sync
用在一个字面量的对象上(如整数、浮点数及字符串等)时是无法正常工作的,如v-bind.sync="{title:doc.title}"
。
插槽
在Vue2.6.0版本中引入了具名插槽和作用域插槽的因语法v-slot
指令,它取代了slot
和slot-scope
这两个已被废除但未被移除的attribute。
插槽内容
Vue实现了一套内容分发的API,其将<slot>
元素作为承载分发内容的出口。
它允许像这样合成组件:
1 2 3 <navigation-link url ="/profile" > Your Profile </navigation-link >
其中,</navigation-link>
的模板如下:
1 2 3 <a v-bind:href ="url" class ="nav-link" > <slot > </slot > </a >
当组件渲染时,<slot></slot>
将会被替换为Your Profile的内容。插槽内可以包含任何模板代码,包括HTML或是它的组件:
1 2 3 4 5 6 7 8 9 10 11 <navigation-link url="/profile"> <!-- 添加一个Font Awesome图标 --> <span class="fa fa-user"></span> Your Profile </navigation-link> <navigation-link url="/profile"> <!-- 添加一个图标的组件 --> <font-awesome-icon name="user"></font-awesome-icon> Your Profile </navigation-link>
若<navigation-link>
的template
中没有包含一个<slot>
元素,则该组件起始标签和结束标签之间的内容都会被抛弃。
编译作用域
当想在一个插槽中使用数据时,该插槽跟模板其他地方一样可以访问相同的实例property,而不能访问<navigation-link>
的作用域 :
1 2 3 4 5 6 7 <navigation-link url="/profile"> <!-- user.name的数据能正常访问 --> Logged in as {{user.name}} <!-- 这里的url的值是undefined --> <!-- 因为该插槽的内容是传递给<navigation-link>的而不是在其组件内部定义的 --> Clicking here will send you to:{{url}} </navigation-link>
父级模板里的所有内容都是在父级作用域中编译的,子模板里的所有内容都是在子作用域中编译的。
后备内容
为一个插槽设置具体的后备内容(即默认内容)是很有必要的,它只会在没有提供内容的时候被渲染。
1 2 3 4 <button type="submit"> <!-- button绝大多数都渲染文本Submit --> <slot>Submit</slot> </button>
此时若在父级组件中使用<submit-button>
且不提供任何插槽内容时,后备内容“Submit”将会被渲染;若提供内容时,在这个提供的内容将会取代后备内容被渲染
具名插槽
有时在一个模板里需要多个插槽时,可以使用<slot>
元素中的name
attribute,其可以用来定义额外的插槽:
1 2 3 4 5 6 7 8 9 10 11 12 <div class="container"> <header> <slot name="header"></slot> </header> <main> <!-- 不带name的<slot>,会默认带有一个隐含的名字default --> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>
在向具名插槽提供内容时,可以在一个<template>
元素上使用v-slot
指令,并以v-slot
参数的形式提供其名称:
1 2 3 4 5 6 7 8 9 10 11 12 <base-layout> <template v-slot:header> <h1>Here might be a page title</h1> </template> <p>A paragraph for the main content.</p> <p>And another one.</p> <template v-slot:footer> <p>Here's some contact info</p> </template> </base-layout>
现在<template>
元素中的所有内容都将会被传入相应的插槽,任何没有被包裹在带有v-slot
的<template>
中的内容都会被视为默认插槽的内容。
即上述代码等价于:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <base-layout> <template v-slot:header> <h1>Here might be a page title</h1> </template> <template v-slot:default> <p>A paragraph for the main content.</p> <p>And another one.</p> </template> <template v-slot:footer> <p>Here's some contact info</p> </template> </base-layout>
注意:v-slot
只能添加在<template>
上。
作用域插槽
有时会让插槽内容能够访问子组件中才有的数据。如这里有个组件<current-user>
:
1 2 3 <span> <slot>{{user.lastName}}</slot> </span>
若想换掉备用内容,用名来显示,直接将组件中的user.lastName
换成user.firstName
是不行的:
1 2 3 <current-user> {{user.firstName}} </current-user>
由于只有<current-user>
组件可以访问到user
,而所提供的内容是在父级渲染的。因此为了让user
在父级的插槽内容中可用,将user
作为<slot>
元素的一个attribute绑定上去 :
1 2 3 <span> <slot v-bind:user="user">{{user.lastName}}</slot> </span>
绑定在<slot>
元素上的attribute被称为插槽prop。在父级作用域中,可以使用带值的v-slot
来定义所提供插槽prop的名字:
1 2 3 4 5 6 <current-user> <!-- slotProps为包含所有插槽prop的对象 --> <template v-slot:default="slotProps"> {{slotProps.user.firstName}} </template> </current-user>
独占默认插槽的缩写语法
当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。 此时可以把v-slot
直接用在组件上:
1 2 3 <current-user v-slot:default="slotProps"> {{slotProps.user.firstName}} </current-user>
由于不带参数的v-slot
被假定为对应默认的插槽,因此还有更简化的写法:
1 2 3 <current-user v-slot="slotProps"> {{slotProps.user.firstName}} </current-user>
默认插槽的缩写写法不能和具名插槽混用,这会导致作用域不明确。
只要出现多个插槽,则始终要为所有的插槽使用完整的基于<template>
的语法:
1 2 3 4 5 6 7 8 9 <current-user> <template v-slot:default="slotProps"> {{slotProps.user.firstName}} </template> <template v-slot:other="otherSlotProps"> ... </template> </current-user>
解析插槽Prop
作用域插槽的内部工作原理是将插槽内容包裹在一个拥有单个参数的函数里:
1 2 3 function (slotProps ){ }
这意味着v-slot
的值实际上可以是任何能够作为函数定义中参数的JavaScript表达式。因此在支持的环境下(单文件组件或现代浏览器),也可以使用ES2015解构来传入具体的插槽prop:
1 2 3 <current-user v-slot="{user}"> {{user.firstName}} </current-user>
这样可以在该插槽提供了多个prop时让代码更简洁。它同样开启了prop重命名等功能,如把user
重命名为person
:
1 2 3 <current-user v-slot="{user:person}"> {{person.firstName}} </current-user>
还可以定义后备内容,用于插槽prop是undefined的情形:
1 2 3 <current-user v-slot="{user={firstName:'Guest'}}"> {{user.firstName}} </current-user>
动态插槽名
动态指令参数也可以用在v-slot
上,来定义动态的插槽名:
1 2 3 4 5 <base-layout> <template v-slot:[dynamicSlotName]> ... </template> </base-layout>
具名插槽的缩写
与v-on
和v-bind
一样,v-slot
也有缩写,即把参数之前的所有内容(v-slot:
)替换为字符#
:
1 2 3 4 5 6 7 8 9 10 11 12 <base-layout> <template #header> <h1>Here might be a page title</h1> </template> <p>A paragraph for the main content.</p> <p>And another one.</p> <template #footer> <p>Here's some contact info</p> </template> </base-layout>
与其他指令相同,该缩写只在其有参数时才可用。若希望无参数时也能使用缩写的话,就必须始终明确插槽名:
1 2 3 <current-user #default="{user}"> {{user.firstName}} </current-user>
其他实例
插槽prop允许将插槽转换为可复用的模板,这些模板可以基于输入的prop渲染出不同的内容。这在设计封装数据逻辑的同时允许父级组件自定义部分布局的可复用组件 是最有用的。
如要实现一个<todo-list>
组件,它是一个列表且包含布局和过滤逻辑:
1 2 3 4 5 6 7 8 <ul> <li v-for="todo in filteredTodos" v-bind:key="todo.id" > {{todo.text}} </li> </ul>
可以考虑将每个todo作为父级组件的插槽,以此通过父级组件对其进行控制,然后将todo
作为一个插槽prop进行绑定:
1 2 3 4 5 6 7 8 9 10 11 <ul> <li v-for="todo in filteredTodos" v-bind:key="todo.id" > <!-- 为每个todo准备了插槽,将todo对象作为一个插槽的prop传入 --> <slot name="todo" v-bind:todo="todo"> {{todo.text}} </slot> </li> </ul>
现在使用<itodo-list>
组件时,可以选择为todo定义一个不一样的<template>
作为替代方案,并且可以从子组件中获取数据:
1 2 3 4 5 6 <todo-list v-bind:todo="todos"> <template v-slot:todo="{todo}"> <span v-if="todo.isComplete"></span> {{todo.text}} </template> </todo-list>