这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战
组件注册
组件是网页的一部分,一个复杂的应用可以拆分表示为组件树的形式。
const app = Vue.createApp({
// ...
});
创建一个 Vue 实例 / 应用,接收一个参数决定根节点如何渲染。
注册组件有两种方式,全局注册和局部注册。
全局注册是指我们在 Vue 的应用实例上调用 component 方法注册,该组件可以在 Vue 应用中的所有组件的模板中使用,但是性能不高:
app.component('my-counter',{
data() {
return { count: 1 }
},
template:` <div @click='count += 1'>{{counter}}</div> `
})
app.component('my-counter-parent',{
template:`
<counter />
<counter />
`
})
组件可以被复用,组件中的数据被当前组件独享。
局部注册,定义注册之后才能使用,性能较高:
// 在 JavaScript 的对象中定义组件
const Counter = {
// ...
}
// 使用局部组件,在 component 选项中定义想用的组件
const app = Vue.createApp({
components: {
'my-counter': Counter
},
template: `<my-counter />`
});
在字符串模板template和单文件组件中定义组件时,我们可以使用帕斯卡命名法PascalCase和短横线命名法kebab-case来命名;
由于 DOM 是大小写不敏感的,在 DOM 中只能使用短横线命名法;
由于 JavaScript 的-会被识别为减号,在 JavaScript 中只能使用帕斯卡命名法。
注意局部注册的组件在其子组件中不可用。例如,如果你希望 ComponentA 在 ComponentB 中可用,则你需要这样写:
const ComponentA = {
/* ... */
}
const ComponentB = {
components: {
'component-a': ComponentA
}
// ...
}
组件传参
静态传参和动态传参
我们在调用组件的时候通过组件的attribute来进行传参,通过是否使用v-bind指令来区分参数是静态参数还是动态参数:
const app = Vue.createApp({
data() {
return { age: 100 }
}
template:`
<my-component name='Evan' :age='age'>
`
})
在组件中通过props选项来接收参数。数组形式的props:
app.component('my-component',{
props: ['name', 'age']
})
还可以使用对象形式的props来对参数进行约束:
props: {
name: String,
age: {
type: Number,
required: true,
default: 18
}
}
动态属性可以是数字、布尔值、数组和对象。
传入一个对象的所有 property:将一个对象的所有 property 都作为 prop 传入,使用不带参数的v-bind。
post: {
id: 1,
title: 'My Journey with Vue'
}
<blog-post v-bind="post"></blog-post>
// 等价于
<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>
单向数据流
父组件到子组件的参数传递是单向的,即父组件的 prop 更新会影响到子组件,反之则不行,这种现象叫做单向下行绑定。我们不希望子组件能够变更影响到父组件的状态。
所以当子组件想要使用变更后的参数时,我们可以使用 data 属性或者计算属性。
Prop 验证
我们可以对子组件接收的 prop 提出验证要求,当接收到的 prop 不满足要求时,Vue 会发出警告。
Prop 验证是在组件实例创建之前,所以此时的 data 属性不可用。
Prop 的大小写命名
由于 HTML 大小写不敏感, JavaScript 不能使用短横线命名(前文有提及)。所以 prop 在 HTML 中使用短横线命名法,在 JavaScript 中要使用驼峰命名法camelCase与之对应。
<my-component :post-title='post.title' />
props: ['postTitle']
同样的,字符串模板中不受此限制。
Non-Prop 的 Attribute
Non-Prop 就是说父组件给子组件传递属性时,子组件不通过 prop 接收的属性。这些属性将会被直接添加到子组件最外层标签的属性中。
const app = Vue.createApp({
template:`<my-counter status='active' />`
})
app.component('my-counter',{
// props: ['status'], 这里不通过 props 接收
template:`<div />`
})
<!-- 最终渲染的结果 -->
<div status='active' />
一般用于style,class,id属性。
禁用 Attribute 继承
使用inheritAttrs: false禁用 attribute 继承,这适用你不希望子组件的最外层继承 attribute 的情况。
同时可以访问$attrs property ,它包括 props 和 emitsproperty 未包含的所有属性。
app.component('date-picker', {
inheritAttrs: false,
template: `
<div class="date-picker">
<input type="datetime-local" v-bind="$attrs" />
<input :msg='$attrs.msg'>
</div>
`
})
多个根节点的组件不具有自动 attribute fallthrough (隐式贯穿) 行为。(上一篇有提及)
父子组件之间的通信
前面提到过,子组件不能直接修改父组件传递过来的 props。
我们可以通过$emit方法来向父组件传入一个事件名,父组件从而可以监听这个事件。
const app = Vue.createApp({
data() {
return {count: 1 }
},
// template:`<counter :count='count' />`
methods: {
handleAddOne() {
this.count += 1
}
},
template:`<counter :count='count' @add-one='handleAddOne'/>`
})
app.component('counter',{
props: ['count'],
methods: {
handleClick() {
// this.count += 1;
this.$emit('addOne');
}
},
template:`<div @click='handleClick'>{{count}}</div>`
// 或者直接 template:`<div @click='$emit('addOne')'>{{count}}</div>`
})
父组件模板中的 add-one 事件不同于click等原生事件,它是自定义事件。
$emit函数可以接收更多参数:
methods: {
handleAddOne(param1, param2) {
this.count = this.count + param1 + param2;
}
}
handleClick() {
this.$emit('add', 2, 3);
}
验证抛出的事件
与 prop 的验证类似,我们可以通过emits选项在组件中定义发出的事件。
emits: ['add', 'minus']
emits: {
// 检验向外传递的参数的值
add: (count1, count2) => {
if(count1 > 0 && count2 > 0) {
return true;
}
return false;
},
// 没有检验
minus: null
}
使用v-model更新父组件参数
父子组件之间的参数传递类似v-model,我们可以通过该指令进行数据的修改。
默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。我们可以通过向 v-model 传递参数来修改这些名称:
const app = Vue.createApp({
data() {
return { bookTitle: 'HowToLearnVue' }
},
template:`<my-component v-model:title="bookTitle"></my-component>`
})
app.component('my-component', {
props: {
title: String
},
emits: ['update:title'],
template: `
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
`
})
可以同时使用多个v-model:
<my-component
v-model:title="bookTitle"
v-model:author='bookAuthor'
/>
处理 v-model 修饰符
当我们学习表单输入绑定时,我们看到 v-model 有内置修饰——.trim、.number 和 .lazy。但是,在某些情况下,我们可能还需要添加自己的自定义修饰符。这些修饰符以 modelModifiers prop 传入到子组件。
<my-component v-model.capitalize="myText"></my-component>
app.component('my-component', {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
methods: {
emitValue(e){
this.$emit('update:modelValue', $event.target.value)
}
},
template: `
<input type="text"
:value="modelValue"
@input="emitValue">
`,
created() {
console.log(this.modelModifiers) // { capitalize: true }
}
})
当组件的 created 生命周期钩子触发时,modelModifiers prop 会包含 capitalize,且其值为 true——因为 capitalize 被设置在了写为 v-model.capitalize="myText" 的 v-model 绑定上。
现在我们已经设置了 prop,我们可以检查 modelModifiers 对象键并编写一个处理器来更改发出的值。在下面的代码中,每当 <input/> 元素触发 input 事件时,我们都将字符串大写。
methods: {
emitValue(e){
let value = e.target.value;
if(this.modelModifiers.capitalize){
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
},
对于带参数的 v-model 绑定,生成的 prop 名称将为 arg + "Modifiers":
<my-component v-model:desciption.capitalize="myText"></my-component>
app.component('my-component', {
props: ['description', 'descriptionModifiers'],
emits: ['update:description'],
template: `
<input type="text"
:value="description"
@input="$emit('update:description', $event.target.value)">
`,
created() {
console.log(this.descriptionModifiers) // { capitalize: true }
}
})
插槽
在调用组件时,组件的起始标签和结束标签的内容可以替换组件模板中的<slot>。
<todo-button>
<i class="fas fa-plus"></i>
Add todo
</todo-button>
<!-- todo-button 组件模板 -->
<button class="btn-primary">
<slot></slot>
</button>
<!-- 渲染 HTML -->
<button class="btn-primary">
<i class="fas fa-plus"></i>
Add todo
</button>
如果组件模板中没有<slot>元素,则内容会被抛弃。
渲染作用域
父模板调用的数据属性使用的都是父模板里的数据,子模板调用的数据属性使用的都是子模板里的数据。
const app = Vue.createApp({
data() {
return { message: 'component1' }
},
template:`<my-component>{{message}}</ my-component>`
})
app.component('my-component',{
data() {
return { message: 'component2' }
},
template:`
<div>
<slot></slot>
</div>`
})
<!-- 渲染结果 -->
<div>componet1</div>
备用内容
如果调用子组件时起始标签和结束标签之间没有内容,则可以使用<slot>的备用内容。备用内容放在<slot>标签内。
<todo-button></todo-button>
<!-- todo-button 组件模板 -->
<div>
<slot>submit</slot>
</div>
<!-- 渲染 HTML -->
<div>submit</div>
具名插槽
有时我们需要使用多个插槽,这时候我们需要为每个<slot>添加一个name attribute 为其命名。
<div>
<slot name='header'></slot>
- body -
<slot name='footer'></slot>
</div>
同时,在父组件上将提供的内容写在<template>元素上,并用v-slot指令的参数形式提供其名称。
<layout>
<template v-slot:header>header</template>
<template v-slot:footer>footer</template>
</layout>
<!-- 渲染 HTML -->
<div>
header - body - footer
</div>
具名插槽有简写形式,v-slot:可以简写为#,例如上面的代码可以简写为:
<layout>
<template #header>header</template>
<template #footer>footer</template>
</layout>
作用域插槽
父组件可以通过作用域插槽来访问子组件中的数据。
对于子组件,我们需要把向父组件传递的数据作为一个 attribute 写到<slot>元素中。可以绑定多个属性,这些属性被称作插槽 prop。
<ul>
<li v-for="( item, index ) in items">
<slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot>
</li>
</ul>
父组件通过带值的v-slot来定义插槽 prop的名字。
<todo-list>
<template v-slot="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
</todo-list>
独占默认插槽的缩写语法
同时使用具名插槽和作用域插槽时,可以这样写:
<todo-list>
<template v-slot:default="slotProps">
<span class="green">{{ slotProps.item }}</span>
</template>
<template v-slot:other="otherSlotProps">
...
</template>
</todo-list>
解构插槽 Prop
插槽 Prop 适用 ES6 的结构语法。可以重命名或者定义备用内容。
动态插槽名
动态指令参数也可以用在 v-slot 上,来定义动态的插槽名:
<template v-slot:[dynamicSlotName] />
Provide / Inject
provide和inject解决了父组件到它的子孙组件数据的传输需要层层传递的问题。父组件使用provide选项提供数据,子组件使用inject选项接收数据即可。
const app = Vue.createApp({})
app.component('todo-list', {
provide: {
user: 'John Doe'
}
})
app.component('todo-list-statistics', {
inject: ['user']
})
要访问组件实例 property,我们需要将 provide 转换为返回对象的函数,这样更加安全。
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide() {
return {
todoLength: this.todos.length
}
}
})
处理响应性
provide和inject传递的数据并非响应式的。
我们可以通过传递一个 ref property 或 reactive 对象给 provide 来改变这种行为。在我们的例子中,如果我们想对祖先组件中的更改做出响应,我们需要为 provide 的 todoLength 分配一个组合式 API computed property:
app.component('todo-list', {
// ...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})
app.component('todo-list-statistics', {
inject: ['todoLength'],
created() {
console.log(`Injected property: ${this.todoLength.value}`)
}
})
动态组件 & 异步组件
动态组件
我们可以在<component>元素中通过is attribute 来切换不同的组件。
const app = Vue.createApp({
data() {
return { currentComponent: 'input'}
},
methods: {
handleClick() {
handleClick() {
this.currentComponent === 'input' ?
this.currentComponent = 'hello' :
this.currentComponent = 'input'
}
}
},
template:`
<component :is='currentComponent' />
<button @click='handleClick'>change</button>
`
})
app.component('input',{
template:`<input />`
})
app.component('hello',{
template:`<div>hello</div>`
})
如果我们想在切换组件时保持这些组件的状态,以避免来回切换重复渲染造成的性能问题,我们可以用<keep-alive>元素包裹这些组件,使得失活的组件被缓存下来。
<keep-alive>
<component :is='currentComponent' />
</keep-alive>
<button @click='handleClick'>change</button>
在本例中,input 框输入的内容将会在切换之后得到缓存。
异步组件
在父组件调用子组件时,组件随即渲染,这种组件称为同步组件。
const app = Vue.createApp({
template:`
<hello />
<async-component />
`
})
app.component('hello',{
template:`<div>hello</div>`
})
下面通过 Vue 中的defineAsyncComponent方法定义一个异步组件:
app.component('async-component', Vue.defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
setTimeout(() =>{
resolve({template:`<div>This is a async component.</div>`})
}, 3000)
})
})
)
<div>hello</div>
<!-- 3 秒后 -->
<div>This is a async component.</div>
通过异步组件,我们可以将大型项目拆分成小的 JS 文件,在需要的时候通过异步组件引入 JS 文件。
模板引用
ref 被用来给元素或子组件注册引用信息。引用信息将会被注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是那个 DOM 元素;如果用在子组件上,引用就指向组件实例:
const app = Vue.createApp({
mounted() {
console.log(this.$refs.count);
console.log(this.$refs.hello);
this.$refs.hello.sayHello();
},
template:`
<div ref='count' />
<hello ref='hello'/>
`
})
app.component('hello',{
methods:{
sayHello() { alert('hello')}
},
template:`<div>hello</div>`
})
控制台输出 DOM 元素和对子组件的引用。
应该避免访问 $refs 。