Vue深入组件

参考资料

组件注册

组件名

在注册一个组件的时候,需要给它一个名字。比如在全局注册的时候我们已经看到了:

const app=Vue.createApp({...})

app.component('my-component-name',{
/****/
})

该组件名就是app.component的第一个参数。

你定义的组件名字可能依赖于你打算它来做什么。当直接在DOM中使用一个组件(而不是在字符串模板或单文件组件)的时候,强烈推荐遵循W3C规范中自定义组件名。

  1. 全部小写
  2. 包含连字符(及:即有多个单词与连字符符号连接)

这样会帮助我们避免与当前及未来的HTML元素发生冲突。

你可以在风格指南中查阅到关于组件名的其他建议。

组件名大小写

在字符串模板或者单个文件组件中定义组件时,定义组件名的方式有两种:

使用kebab-case

app.component('my-component-name', {
  /* ... */
})

当使用该方式定义一个组件时,你也必须在引用这个自定义元素时使用kebab-base,例如<my-component-name>

使用PascalCase

app.component('MyComponentName', {
  /* ... */
})

当使用该命名方式定义一个组件,在引用这个自定义元素时两种命名法都可以使用。也就是说<my-component-name><MyComponentName>都是可接受的。注意,尽管如此,直接在DOM(即非字符串的模板)中使用时只有kebab-case是有效的。

全局注册

到目前为止,我们只用了app.component来创建组件:

Vue.createApp({...}).component('my-component-name',{
//...
})

这些组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的组件实例的模板中。比如:

const app = Vue.createApp({})

app.component('component-a', {
  /* ... */
})
app.component('component-b', {
  /* ... */
})
app.component('component-c', {
  /* ... */
})

app.mount('#app')
<div id="app">
  <component-a></component-a>
  <component-b></component-b>
  <component-c></component-c>
</div>

在所有子组件中也是如此,也就是说这三个组件在各自的内部也可以相互使用。

局部注册

全局注册往往是不够理想的。比如,如果你使用一个像webpack这样的构建系统,全局注册所有的组件意味着即便已经不使用其中一个组件,它仍然会被包含在最终的构建结果中。

在这些情况下,可以通过一个普通的JavaScript对象来定义组件

const ComponentA = {
  /* ... */
}
const ComponentB = {
  /* ... */
}
const ComponentC = {
  /* ... */
}

然后在components选项中定义你想要使用的组件:

const app=Vue.createApp({
	components:{
		'component-a':ComponentA,
		'component-b':ComponentB
	}
})

对于components对象中的每个property来说,其property名就是自定义元素的名字,其property值就是每个组件的选项对象。

注意局部注册的组件在其子组件中不可用。例如,如果你希望ComponentAComponentB中可用,则你需要这样写:

const ComponentA={
/*...*/
}

const ComponentB={
	components:{
		'component-a':ComponentA
	}
}

或者如果你通过Babel和webpack使用ES2015模块,那么代码看起来像这样:

import ComponentA from './ComponentA.vue'

export default{
	components:{
		ComponentA
	}
}

注意在ES2015+中,在对象中放一个类似ComponentA的变量名其实是ComponentA:ComponentA的缩写,即这个变量名同时是:

  • 用在模板中的自定义元素的名称
  • 包含了这个组件选项的变量名

模块系统

在模块系统中局部注册

如果你使用了诸如Babel和webpack的模块系统。在这些情况下,我们推荐创建一个component目录,并将每个组件放置在其各自的文件中。

然后你需要在局部注册之前导入每个你想使用的组件。例如,在一个假设的ComponentB.jsComponentB.vue文件中:

import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default{
	components:{
		ComponentA,
		ComponentC
	}
}

现在CompomentAComponentC都可以在ComponentB的模板中使用了

Props

Prop类型

到这里,我们只看到了以字符串数组形式列出的prop:

props:['title','likes','isPublished','commentIDs','author']

但是,通常你希望每个prop都有指定的值类型。这是,你可以用对象形式列出prop,这些property的名称和值分别是prop各自的名称和类型:

props:{
	title:String,
	likes:Number,
	isPublished:Boolean,
	commentIds:Array,
  	author: Object,
  	callback: Function,
  	contactsPromise: Promise // 或任何其他构造函数
}

这不仅为你的组件提供了文档,它会在它们遇到错误的类型时从浏览器的JavaScript控制台提示用户。

传递静态或动态的Prop

这样,你已经知道了可以像这样给prop传入一个静态的值:

<blog-post title="My journey with Vue"></blog-post>

你也知道prop可以通过v-bind或简写:动态赋值

实际上,任何类型的值都可以传给一个prop

传入一个数字

<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue     -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。             -->
<blog-post :likes="42"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post :likes="post.likes"></blog-post>

传入一个布尔值

<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。          -->
<blog-post is-published></blog-post>

<!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue  -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。             -->
<blog-post :is-published="false"></blog-post>

<!-- 用一个变量进行动态赋值。                                -->
<blog-post :is-published="post.isPublished"></blog-post>

传入一个数组

<!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue        -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。             -->
<blog-post :comment-ids="[234, 266, 273]"></blog-post>

<!-- 用一个变量进行动态赋值。                                -->
<blog-post :comment-ids="post.commentIds"></blog-post>

传入一个对象

<!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue        -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。             -->
<blog-post
  :author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
></blog-post>

<!-- 用一个变量进行动态赋值。                                 -->
<blog-post :author="post.author"></blog-post>

传入一个对象的所有property

如果你想要将一个对象的所有property都作为prop传入,你可以使用不带参数的v-bind。例如,对于一个给定的对象post

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都使得其父子prop之间形成了一个单向下行绑定:父级prop的更新会向下流动到子组件中,但是反过来则不行。

另外,每次父组件发生变更时,子组件中所有的prop都会刷新为最新的值。这意味着你不应该在一个子组件内部改变prop。如果这样做了,Vue会在浏览器的控制台中发出警告。

有两种常见的试图变更一个prop的情形:

  1. 这个prop用于传递一个初始值;这个子组件接下来希望将其作为一个本地prop数据来使用。在这种情况下,最好定义一个本地的data property并将这个prop作为其初始值:
props:['initialCounter'],
data(){
	return{
		counter:this.initialCounter
	}
}
  1. 这个prop以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个prop的值来定义一个计算属性
props:['size'],
computed:{
	normalizedSize(){
		return this.size.trim().toLowerCase()
	}
}

提示 注意在JS中对象和数组是通过引用传入的,所以对于一个数组或对象类型的prop来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态。

Prop验证

我们可以为组件的prop指定验证要求,例如你知道的这些类型。如果有一个需求没有被满足,则Vue会在浏览器控制台中警告你。

为了定制prop的验证方式,你可以为props中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:

app.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default() {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator(value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    // 具有默认值的函数
    propG: {
      type: Function,
      // 与对象或数组默认值不同,这不是一个工厂函数 —— 这是一个用作默认值的函数
      default() {
        return 'Default function'
      }
    }
  }
})

当验证失败的时候,Vue将会产生一个控制台的警告。
提示 注意那些prop会在一个组件实例创建之前进行验证,所以实例的property(如datacomputed等)在defaultvalidator函数中是不可用的。

类型检查

type可以是以下原生构造函数中的一个:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol
    此外,type还可以是一个自定义的构造函数,并且通过instanceof来进行检查确认。例如,给定下列现成的构造函数:
function Person(firstName,lastName)
{
	this.firstName=firstName
	this.lastName=lastName
}

你可以使用:

app.component('blog-post',{
	props:{
		author:Person
	}
})

你可以使用:

app.component('blog-post', {
  props: {
    author: Person
  }
})

用于验证authorprop的值是否是通过new Person创建的。

Prop的大小写命名(camelCase vs kebab-case)

HTML中的attribute名是大小写不敏感的,所以浏览器会把所有大写字符解释成小写字符。这意味着当使用DOM中的模板时,camelCase的prop名需要使用等价的kebab-case(短横线分隔命名)命名:

const app=Vue.createApp({})

app.component('blog-post', {
  // camelCase in JavaScript
  props: ['postTitle'],
  template: '<h3>{{ postTitle }}</h3>'
})
<!-- kebab-case in HTML -->
<blog-post post-title="hello!"></blog-post>

如果你使用字符串模板,那么这个限制就不存在了。

非Prop的Attribute

一个非prop的attribute是指传向一个组件,但是该组件并没有相应props或emits定义的attribute。
常见的示例包括classstyleid属性。

Attribute继承

当组件返回单个根节点时,非prop attribute将自动添加到根节点的attribute中。例如,在<date-picker>组件的实例中:

app.component('date-picker', {
  template: `
    <div class="date-picker">
      <input type="datetime-local" />
    </div>
  `
})

如果我们需要通过data status property 定义 <date-picker> 组件的状态,它将应用于根节点 (即 div.date-picker)。

<!-- 具有非prop attribute的Date-picker组件-->
<date-picker data-status="activated"></date-picker>

<!-- 渲染 date-picker 组件 -->
<div class="date-picker" data-status="activated">
  <input type="datetime-local" />
</div>

同样的规则也适用于事件监听器:

<date-picker @change="submitChange"></date-picker>
app.component('date-picker', {
  created() {
    console.log(this.$attrs) // { onChange: () => {}  }
  }
})

当有一个具有 change 事件的 HTML 元素将作为 date-picker 的根元素时,这可能会有帮助。

app.component('date-picker', {
  template: `
    <select>
      <option value="1">Yesterday</option>
      <option value="2">Today</option>
      <option value="3">Tomorrow</option>
    </select>
  `
})

在这种情况下,change事件监听器从父组件传递到子组件,它将在原生selectchange事件上触发。我们不需要显示地从date-picker发出事件:

<div id="date-picker" class="demo">
  <date-picker @change="showChange"></date-picker>
</div>
const app = Vue.createApp({
  methods: {
    showChange(event) {
      console.log(event.target.value) // 将记录所选选项的值
    }
  }
})

禁用Attribute继承

如果你不希望组件的根元素继承attribute,你可以在组件的选项中设置inheritAttrs:false。例如:禁用attribute继承的常见情况是需要将attribute应用于根节点之外的其他元素。

通过将inheritAttrs选项设置为false,你可以访问组件的$attrsproperty,该property包括组件propsemitsproperty中从未包含的所有属性(例如,class style v-on监听器等)。

如果需要将所有非prop attribute应用与非根元素,可以使用v-bind缩写(注意是所有非prop attribute的情况)来完成。

app.component('date-picker', {
  inheritAttrs: false,
  template: `
    <div class="date-picker">
      <input type="datetime-local" v-bind="$attrs" />
    </div>
  `
})

有了这个新配置,data statusattribute将应用于input元素

<!-- Date-picker 组件 使用非 prop attribute -->
<date-picker data-status="activated"></date-picker>

<!-- 渲染 date-picker 组件 -->
<div class="date-picker">
  <input type="datetime-local" data-status="activated" />
</div>

多个根节点上的Attribute继承

与单个根节点组件不同,具有多个根节点的组件不具有自动attribute回退行为。如果未显式绑定$attrs,将发出运行时警告。

自定义事件

事件名

与组件和prop一样,事件名提供了自动的大小写转换。如果用驼峰命名的字符集触发一个事件,你将可以在父组件中添加一个kebab-case(短横线分隔命名)的监听器。

this.$emit('myEvent')
<my-component @my-event="doSomething"></my-component>

与props的命名一样,当你使用 DOM 模板时,我们建议使用 kebab-case 事件监听器。如果你使用的是字符串模板,这个限制就不适用。

定义自定义事件

可以通过emits选项在组件上定义已发出的事件。

app.component('custom-form', {
  emits: ['inFocus', 'submit']
})

当在emits选项中定义了原生事件(如click),将使用组件中的事件替代原生事件监听器。
TIP 建议定义所有发出的事件,以便更好地记录组件应该如何工作。

验证抛出的事件

与prop类型验证类似,如果使用对象语法而不是数组语法定义发出的事件,则可以验证它。

要添加验证,将为事件分配一个函数,该函数接收传递给$emit调用的参数,并返回一个布尔值以指示事件是否有效。

app.component('custom-form',{
	emits:{
		click:null,
		
		submit:({emial,password})=>{
			if(emial&&password)
			{
				return true
			}
			else{
				return false
			}
		}
	},
	methods:{
		submitForm(email,password)
		{
			this.$emit('submit',{email,password})
		}
	}
})

v-model参数

默认情况下,组件上的v-model使用modelValue作为prop和update:modelValue作为事件。我们可以通过向v-model传递参数来修改这些名称:

<my-component v-model:title="bookTitle"></my-component>

在本例子中,子组件需要一个titleprop并发出update:title要同步的事件:

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绑定

通过利用以特定prop和事件为目标的能力,可以在单个组件实例上创建多个v-model绑定。

每个v-model将同步到不同的prop,而不需要在组件中添加额外的选项:

<user-name 
	v-model:first-name="firstName" 
	v-model:last-name="lastName"
></user-name>
app.component('user-name',{
props:{
	firstName:String,
	lastName:String
},
emits:['update:firstName','update:lastName'],
template:`
<input
type="text"
:value="firstName"
@input="$emit('update:firstName',$event.target.value)>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName',$event.target.value)>
`
}

处理v-model修饰符

在2.x中,我们对组件v-model上的.trim等修饰符提供了硬编码支持。但是,如果组件可以支持自定义修饰符,则会更有用。在3.x中,添加到组件v-model的修饰符将通过modelModifiersprop提供给组件:

在之前的表单输入绑定内容模块,可以知道v-model内置修饰符。但是,在某些情况下,可能还需要添加自己的自定义修饰符。

例如,创建一个自定义修饰符capitalize,将v-model绑定提供的字符串的第一个字母大写。

添加到组件v-model的修饰符通过modelModifiersprop提供给组件。

请注意,当组件上的created生命周期钩子触发时,modelModifiersprop会包含capitalize,且其值为true,因为capitalize被设置在了写为v-model.capitalize="myText"v-model绑定上。

<my-component v-model.capitalize="myText"></my-component>
app.component('my-component',{
		props:{
			modelValue:String,
			modelModifiers:{
				default:()=>({})
			}
		},
		emits:['update:modelValue'],
		template:`
			<input type="text"
			:value="modelValue"
			@input="$emit('update:modelValue',$event.target.value)"
			>
			`
		created(){
		 console.log(this.modelModifiers) // { capitalize: true }
		}	
	}
)

现在已经设置了prop,我们可以检查modelModifiers对象键并编写一个处理器来更改发出的值。在下面代码中,每当<input/>元素触发input事件时,我们都将字符串大写。

<div id="app">
	<my-component v-model.capitalize="myText"></my-component>
	{{myText}}
</div>
const app=Vue.createApp({
	data(){
		return{
			myText:''
		}
	}
})

app.component('my-component',{
	props:{
		modelValue:String,
		modelModifiers:{
			default:()=>({})
		}
	},
emits:['update:modelValue'],
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)
	}
},
template:`
<input type="text"
:value="modelValue"
@input="emitValue"
>
`
})

app.mount('#app')

对于带参数的v-model绑定,生成的prop名称将为arg+"modifiers"

<my-component v-model:description.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 }
	}
})

插槽

插槽内容

Vue实现了一套内容分发的API,将<slot>元素作为承载分发内容的出口。
它允许你像这样合成组件:

<todo-button>
 add todo
</todo-button>

然后在<todo-button>的模板中,你可能有

<button class="btn-primary">
	<slot></slot>
</button>

当组件渲染的时候,<slot></slot>将会被替换为"Add Todo"。

<button class="btn-primary">
 add todo
</button>

不过,字符串只是开始!插槽还可以包含任何模板代码,包括HTML:

<toto-button>
 <i class="fas fa-plus"></i>
 Add todo
</todo-button>

或其它组件

<todo-button>
 <font-awesome-icon name="plus"></font-awesome-icon>
 Add todo
</todo-button>

如果<todo-button>的template中没有包含一个<slot>元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。

<!-- todo-button 组件模板 -->

<button class="btn-primary">
  Create a new item
</button>
<todo-button>
  <!-- 以下文本不会渲染 -->
  Add todo
</todo-button>

渲染作用域

当你想在一个插槽中使用数据时,例如:

<todo-button>
 Delete a {{item.name}}
</todo-button>

该插槽可以访问与模板其余部分相同的实例property(即相同的“作用域”)。

插槽不能访问<todo-button>的作用域。例如,尝试访问action将不起作用:

<todo-button action="delete">
	Clicking here will {{action}} an item
	<!-- `action` 未被定义,因为它的内容是传递*到* <todo-button>,而不是*在* <todo-button>里定义的。  -->
</todo-button>

作为一条规则,请记住:
父级模板里的所有内容都是在父级作用域中编译的;子级模板里的所有内容都是在子作用域编译。

备用内容

有时为一个插槽设置具体的备用(也就是默认的)内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个<submit-button>组件中:

<button type="submit">
 <slot></slot>
</button>

我们可能希望这个<button>内绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为备用内容,我们可以将它放在<slot>标签内:

<button type="submit">
 <slot>submit</slot>
</button>

具名插槽

有时我们需要多个插槽。例如对于一个带有如下模板的<base-layout>组件:

<div class="container">
	<header>
	</header>
	<main>
	</main>
	<footer>
	</footer>
</div>

对于这样的情况,<slot>元素有一个特殊的attribute:name。这个attribute可以用来定义额外的插槽:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

一个不带 name 的 <slot> 出口会带有隐含的名字“default”。

向具名插槽提供内容的时候,我们可以在一个<template>元素上使用v-slot指令,并以v-slot的参数的形式提供其名称:

<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>

现在<template>元素中的所有内容都将会被传入相应的插槽。

作用域插槽

有时让插槽内容能够访问子组件中才有的数据是很有用的。当一个组件被用来渲染一个项目数组时,这是一个常见的情况,我们希望能够自定义每个项目的渲染方式。

例如,我们有一个组件,包含todo-items的列表。

app.component('todo-list',{
	data(){
		return{
			items:['feed a cat','buy milk']
		}
	},
	template:`
	<ul>
		<li v-for="(item,index) in items">
			{{item}}
		</li>
	</ul>
	`
})

我们可能会想把{{item}}替换为<slot>,以便在父组件上自定义。

<todo-list>
 <i class="fas fa-check"></i>
 <span class="green>{{item}}</span>
</todo-list>

但这是行不通的,因为只有<todo-list>组件可以访问item,我们将从其父组件提供插槽内容。

要使item可用于父级提供的插槽内容,我们可以添加一个<slot>元素并将其绑定为属性:

<ul>
	<li v-for="(item,index) in items">
		<slot :item="item">
		</slot>
	</li>
</ul>

可以根据自己的需要将很多的attribute绑定到slot上。

<ul>
	<li v-for="(item,index) in items">
		<slot :item="item" :index="index" :another-attribute="anotherAttribute">
		</slot>
	</li>
</ul>

绑定在<slot>元素上的attribute被称为插槽prop。现在在父级作用域中,我们可以使用带值的v-slot来定义我们提供的插槽prop的名字:

<todo-list>
	<template v-slot:default="slotProps">
		<i class="fas fa-check"></i>
		<span class="green">{{slotProps.item}}</span>
	</template>
</todo-list>

独占默认插槽的缩写语法

在上述情况,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样就可以把v-slot直接用在组件上:

<todo-list v-slot:default="slotProps">
		<i class="fas fa-check"></i>
		<span class="green">{{slotProps.item}}</span>
</todo-list>

这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的v-slot被假定对应默认插槽:

<todo-list v-slot="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>
</todo-list>

注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确:

<!-- 无效,会导致警告 -->
<todo-list v-slot="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>
  
  <template v-slot:other="otherSlotProps">
    slotProps is NOT available here
  </template>
</todo-list>

只要出现多个插槽,请始终为所有的插槽使用完整的基于 的语法:

解构插槽Prop

作用域插槽的内部工作原理是将插槽内容包括在一个传入单个参数的函数里

动态插槽名

具名插槽的缩写

v-onv-bind一样,v-slot也有缩写,即把参数之前的所有内容(v-slot:)替换为#

<base-layout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:

<!-- This will trigger a warning -->

<todo-list #="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:

<todo-list #default="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

Provide/Inject

通常,当我们需要从父组件向子组件传递数据,我们使用props,但对于深度嵌套的组件,如果深层的子组件只需要父组件的部分内容,在这种情况下,通过prop沿着组件链逐级传递可能会比较麻烦。

对于这种情况,可以使用provide/inject。无论组件层级有几层,父组件都可以为所有子组件提供依赖。这个特性有两个部分:父组件有一个provide选项来提供数据,子组件有一个indect选项来开始使用这些数据。

例如,对于这样的层次结构:

Root
	-TodoList
		-TodoItem
		-TodoListFooter
			-ClearTodosButton
			-TodoListStatistics

如果要将todo-list的长度直接传递给TodoListStatistics,通过provide/indect方法,我们可以直接执行以下操作:

const app=Vue.createApp({})

app.component('todo-list',{
	data(){
		return{
			todos:['a','b']
		}
	},
	provide:{
		user:'John'
	},
	template:`
	<div>
		{{todos.length}}
	</div>
	`
})

app.component('todo-list-statistics',{
	inject:['user'],
	created(){
		console.log(`Injected property: ${this.user}`) // > 注入 property: John
	}
})

但是,如果尝试provide一些组件的实例property,将是不起作用的:

app.component('todo-list', {
  data() {
    return {
      todos: ['Feed a cat', 'Buy tickets']
    }
  },
  provide: {
    todoLength: this.todos.length // 将会导致错误 `Cannot read property 'length' of undefined`
  },
  template: `
    ...
  `
})

要访问组件实例property,我们需要将provide转换为返回对象的函数

app.component('todo-list', {
  data() {
    return {
      todos: ['Feed a cat', 'Buy tickets']
    }
  },
  provide() {
    return {
      todoLength: this.todos.length
    }
  },
  template: `
    ...
  `
})

处理响应性

在上面的例子中,如果我们更改了todos的列表,这个变化并不会反映在inject的todoLengthproperty中。因为默认情况下,provice/indect不是响应式的。我们可以通过传递一个refproperty或reactive对象来改变这种行为。可以为provide的todoLength分配一个组合式APIcomputed属性。

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}`) // > Injected property: 5
  }
})

动态组件&异步组件

模板引用

尽管存在prop和事件,但有时可能仍然需要直接访问子组件。因此,可以使用refattribute为子组件或HTML元素指定引用ID。例如:

<input ref="input">

例如,你希望在组件挂载时,以编程的方式focus到这个input上,这可能有用

const app=Vue.createApp({})

app.component('base-input',{
	template:`
	<input ref="input">
	`,
	methods:{
		focusInput(){
			this.$refs.input.focus()
		}
	},
	mounted()
	{
		this.focusInput()
	}
})

此外,还可以向组件本身添加另一个ref,并使用它从父组件触发focusInput事件:

<base-input ref="usernameInput"></base-input>
this.$refs.usernameInput.focusInput()

注意 $refs只会在组件渲染完成之后生效。这仅作为一个用于直接操作子元素的备用方式,应该避免在模板或计算属性中访问$refs

处理边界情况

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值