Vue学习-组件和生命周期

本文最后更新于:1 年前

一、组件基础

通常一个应用会以一棵嵌套的组件树的形式来组织:

例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。

1、为什么我们要用组件

了解传统方式到组件化方式的更新。

传统方式编写网页,会导致依赖关系混乱,代码复用率低,所以我们需要用到组件,不同组件的用来实现局部功能的代码和资源。

2、基本使用

为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册和局部注册。至此,我们的组件都只是通过 component 方法全局注册的:

1)全局注册

组件是用来实现局部功能的代码和资源的集合,是带有名称的可复用实例

1
2
3
4
5
const app = Vue.createApp({})

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

全局注册的组件可以在应用中的任何组件的模板中使用。

2)局部注册

父组件 App.vue
子组件 header.vue、message.vue、bottom.vue

  • header.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
export default {
data() {
return {
msg: "这里是头部子组件"
}
}
}
</script>

<template>
<h1>{{ msg }}</h1>
</template>
  • message.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
export default {
data() {
return {
msg: "这里是内容子组件"
}
}
}
</script>

<template>
<h1>{{ msg }}</h1>
</template>
  • bottom.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
export default {
data() {
return {
msg: "这里是底部子组件"
}
}
}
</script>

<template>
<h1>{{ msg }}</h1>
</template>
  • App.vue
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
<script>
//声明式渲染,可以提高开发效率
//导入各个组件
import heade from "./components/header.vue"
import bottom from "./components/bottom.vue"
import message from "./components/message.vue"
export default {
components: {
heade,
bottom,
message,
}
};
</script>

<template>
<div>
<heade></heade>
</div>
<div>
<message></message>
</div>
<div>
<bottom></bottom>
</div>
</template>

3、通过 Prop 向子组件传递数据

Prop 是你可以在组件上注册的一些自定义 attribute。为了给hello子组件传递一个message,我们可以用 props 选项将其包含在该组件可接受的 prop 列表中:

1)传递值

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
//声明式渲染,可以提高开发效率
import hello from './components/hello.vue'
export default {
data() {
return {
}
},
components: {
hello
}
};
</script>

<template>
<div>
<!-- 组件复用 -->
<hello title="我是头部"></hello>
<hello title="我是内容"></hello>
<hello title="我是底部"></hello>
</div>
</template >

hello.vue

1
2
3
4
5
6
7
8
9
10
<script>
export default {
props: ['title'],
}
</script>
<template>
<div>
<h1>{{ title }}</h1>
</div>
</template>

当一个值被传递给一个 prop attribute 时,它就成为该组件实例中的一个 property。该 property 的值可以在模板中访问,就像任何其他组件 property 一样。
一个组件可以拥有任意数量的 prop,并且在默认情况下,无论任何值都可以传递给 prop。

2)用 v-bind 来动态传递 prop

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
//声明式渲染,可以提高开发效率
import hello from './components/hello.vue'
export default {
data() {
return {
message: "hello vue",
}
},
components: {
hello
}
};
</script>

<template>
<div>
<hello :msg="message"></hello>
<h1>父组件中</h1>
<input type="text" v-model="message">
</div>
</template >

hello.vue

1
2
3
4
5
6
7
8
9
10
11
<script>
export default {
props: ['msg'],
}
</script>
<template>
<div>
<h1>hello子组件中</h1>
<h1>{{ msg }}</h1>
</div>
</template>

我们传递给子组件的值是双向绑定的,我在父组件更新内容,子组件也会同样获取更新后的内容

4、监听子组件事件

我们在开发子组件时,它的一些功能可能需要与父级组件进行沟通,例如我在子组件改变特定值时,我们想要父组件也同样接收到,就需要父组件去监听子组件事件

父组件去监听子组件事件

  1. 在子组件中设置方法,通过$emit来触发事件
  2. 在父组件中,通过v-on监听子组件中自定义的事件,接收子组件传来的值

App.vue

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>
import Content from './components/Content.vue'
//声明式渲染,可以提高开发效率
export default {
data() {
return {
message: "hello vue",
}
},
components: {
Content
},
methods: {
getMsg: function (msg) {
console.log(msg);
this.message = msg
}
}
};
</script>

<template>
<div>
<!-- 拿到子组件Content的数据,通过自定义事件 -->
<!-- 2、在父组件中,通过v-on监听子组件中自定义的事件 -->
<content @injectMsg="getMsg"></content>
<h1>{{ message }}</h1>
</div>
</template>

父级组件可以像处理原生 DOM 事件一样通过 v-on 或 @ 监听子组件实例的任意事件

content.vue

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
<script>
import Hello from './HelloWorld.vue'
export default {
data() { //让每个组件都返回一个新的对象,不会造成数据污染
//局部变量
return {
msg: "我是content",
list: [1, 2, 3, 4]
}
},
components: {
Hello
},
methods: {
//1、在子组件中,通过$emit来触发事件
sendParent: function () {
// this.emit('自定义事件名称', '发送参数')
this.$emit('injectMsg', this.msg)
}
}
}
</script>

<template>
<div>
<!-- 组件是带有名称的可复用实例,带有单独模板的封装 -->
<Hello :message="msg" aaa="123" :list="list"></Hello>
<h2>我是content组件</h2>
<h2>{{ msg }}</h2>
<button @click="msg = '你好'">改变msg</button>
<!-- <Hello></Hello> -->
<button @click="sendParent">发送数据到父组件</button>
</div>
</template>

同时子组件可以通过调用内建的 $emit 方法并传入事件名称来触发一个事件

helloworld.vue

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
<script>
export default {
data() {
return {
msg: "我是hello"
}
},
props: {//对象
//1、类型限制
message: String,
//2、设置默认值、必须传值
message: {
type: String,
default: "我是linzy",
required: true,
},
list: {
type: Array,
default: [],
}
},
mounted() {
console.log(this.$parent);
console.log(this.$root);
}
}
</script>

<template>
<div>
<h4>hello</h4>
<h1>{{ message }}</h1>
<!-- <h1>{{ aaa }}</h1> -->
<h1>{{ list }}</h1>
</div>
</template>

我们可以把props变为对象形式,这样我们在接收到的值进行条件限定,可以限定类型、设置默认值、必须传入值等。


5、插槽

和 HTML 元素一样,我们经常需要向一个组件传递内容,就是我们使用 < slot> 作为我们想要插入内容的占位符

1)插槽内容

App.vue

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
<script>
//声明式渲染,可以提高开发效率
import Content from './components/Content.vue'
export default {
data() {
return {
message: "hello vue",
}
},
components: {
Content
}
};
</script>

<template>
<div>
<!-- 传递字符串 -->
<Content>我是插槽</Content>
<!-- 传递一个按钮 -->
<Content><button>按钮</button></Content>
<!-- 传递一个文本框 -->
<Content><input type="text"></Content>
</div>
</template>

content.vue

1
2
3
4
5
6
7
8
<template>
<div>
<h1>我是Content组件</h1>
<div>
<slot></slot>
</div>
</div>
</template>

如果有多个值,同时放入组件进行替换时,一起作为替换元素

2)渲染作用域

父级模板里的所有内容都在父级作用域中编译 子模板里的所有内容都是在子作用域中编译

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

3)备用内容

有时为一个插槽指定备用 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个 组件中:

1
2
3
<button type="submit">
<slot>Submit</slot>
</button>

现在当我们在一个父级组件中使用 并且不提供任何插槽内容时

1
<submit-button></submit-button>

备用内容“Submit”将会被渲染:

1
2
3
<button type="submit">
Submit
</button>

但是如果我们提供内容,则这个提供的内容将会被渲染从而取代备用内容

4)具名插槽

有时我们需要多个插槽,具名插槽可以根据slot的name进行分配,< slot> 元素有一个特殊的 attribute:name。通过它可以为不同的插槽分配独立的 ID,也就能够以此来决定内容应该渲染到什么地方。

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<h1>我是Content组件</h1>
<div>
<slot></slot>
<slot name="header"></slot>
<slot name="main"></slot>
<slot name="bottom"></slot>
</div>
</div>
</template>

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

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

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
<script>
//声明式渲染,可以提高开发效率
import Content from './components/Content.vue'
export default {
components: {
Content
}
};
</script>

<template>
<div>
<Content>
<!-- v-slot 只能添加在 <template> 上 -->
<template v-slot:default>
<p>我不是具名插槽.</p>
</template>
<template v-slot:header><button>header按钮</button></template>
<template v-slot:main><input type="text" value="main文本框"></template>
<template v-slot:bottom>
<h1>我是bottom插槽</h1>
</template>
</Content>
</div>
</template>

注意,v-slot 只能添加在 < template> 上

5)作用域插槽

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

简而言之,可能每个页面插槽渲染的数据可能会不同,所以我们通过访问子组件中的数据来渲染,来实现相同的效果

App.vue

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
<script>
//声明式渲染,可以提高开发效率
import Content from './components/Content.vue'
export default {
components: {
Content
}
};
</script>

<template>
<div>
<!-- 无序列表 -->
<Content>
<template v-slot:default="slotProps">
<ul>
<li v-for="item in slotProps.item" :key="item">{{ item }}</li>
</ul>
</template>
</Content>
<!-- 有序列表 -->
<Content>
<template v-slot:default="slotProps">
<ol>
<li v-for="item in slotProps.item" :key="item">{{ item }}</li>
</ol>
</template>
</Content>
</div>
</template>

content.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
export default {
data() {
return {
arr: [1, 3, 6, 1, 7]
}
}

}
</script>

<template>
<div>
<h1>我是Content组件</h1>
<div>
<slot :item="arr"></slot>
</div>
</div>
</template>


6、跨级通信Provide / Inject

通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。

对于这种情况,我们可以使用一对 provide 和 inject。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。

例如,我们有这样的层次结构:

1
2
3
4
5
6
Root
└─ TodoList
├─ TodoItem
└─ TodoListFooter
├─ ClearTodosButton
└─ TodoListStatistics

如果要将 todo-items 的长度直接传递给 TodoListStatistics,我们要将 prop 逐级传递下去:TodoList -> TodoListFooter -> TodoListStatistics。通过 provide/inject 的方式,我们可以直接执行以下操作:

  1. 祖先组件用provide值传递
  2. 子孙组件用inject接收值传递

1)值传递/引用传递

App.vue

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>
import Content from "./components/Content.vue"
//声明式渲染,可以提高开发效率
export default {
data() {
return {
message: "hello vue",
obj: {
message: "hello linzy",
}
}
},
components: {
Content
},
//provide/inject并不是响应式的
// provide: { message: this.message }
//如果想去访问组件实例的属性
provide() {
return {
//值传递
message: this.message,
//引用传递
//1、响应式对象方式
obj: this.obj
}
}
};
</script>

<template>
<div>
<h1>HomeView----{{ message }}</h1>
<h1>HomeView----{{ obj.message }}</h1>
<p>--------------------</p>
<Content></Content>
<button @click="message = '勇敢牛牛'">改变message按钮</button>
<button @click="obj.message = '哈哈哈'">改变对象里的message按钮</button>
</div>
</template>


content.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
export default {
//值传递
inject: ['message'],
//引用传递
inject: ['obj']
}
</script>

<template>
<h2>hello---{{ obj.message }}</h2>
<h1>{{ message }}</h1>
</template>


注意:值传递就相当于,在子组件开辟了新的属性变量,跟父组件的属性变量没有关系了,引用传递,可以看做c语言里的指针的概念,传递了一个地址,都是属于一个内存空间的,所以子组件里对象的属性变量改变了,父组件的也会改变

父组件不需要知道哪些子组件使用了它 provide 的 property
子组件不需要知道 inject 的 property 来自哪里

2)处理响应性

这是因为默认情况下,provide/inject 绑定并不是响应式的
我们可以通过传递一个 ref property 或 reactive 对象给 provide 来改变这种行为。在我们的例子中,如果我们想对祖先组件中的更改做出响应。
App.vue

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
<script>
import Content from "./components/Content.vue"
//声明式渲染,可以提高开发效率
export default {
data() {
return {
message: "hello vue",
obj: {
message: "hello linzy",
}
}
},
components: {
Content
},
//provide/inject并不是响应式的
// provide: { message: this.message }
//如果想去访问组件实例的属性
provide() {
return {
//函数返回响应式数据
message: () => this.message,
}
}
};
</script>

<template>
<div>
<h1>HomeView----{{ message }}</h1>
<p>--------------------</p>
<Content></Content>
<button @click="message = '勇敢牛牛'">改变message按钮</button>
</div>
</template>

content.vue

1
2
3
4
5
6
7
8
9
10
<script>
export default {
//值传递
inject: ['message'],
}
</script>

<template>
<h1>{{ message() }}</h1>
</template>

二、生命周期

  1. 实例化一个Vue对象
  2. 初始化事件和生命周期
  3. 执行beforeCreate钩子函数(Vue实例还没创建,获取不到Dom节点,拿不到data> 数据和methods方法)
  4. 初始化响应式、数据代理和数据监测
  5. 执行created钩子函数(Vue实例已经创建完毕,data数据和methods方法这些已经成功绑定到Vue实例中,但是Dom元素还没有生成,还不能调用)
  6. 有没有template选项
    1)Yes,把template编译成渲染函数
    2)No,我们将el挂载的html编译成template
  7. 执行beforeMount钩子函数(页面还没渲染前,el还挂载在虚拟Dom里)
  8. 将编译好的html去替换掉el属性里的Dom对象 ,把虚拟Dom变为真实Dom放入页面
  9. 执行mounted钩子函数(页面已经渲染出来,用来获取数据或者发送网络请求)
  10. 实时监听数据变化,随时更新Dom,当我们的数据发生变化时
    1) 执行beforeUpdate钩子函数(在数据改变之前,虚拟Dom已经更新完了,做一些更新之前需要做的事情)
    2)虚拟Dom重新渲染成真实Dom,对比虚拟Dom和真实Dom节点的不同,找需要更新的节点,从而更新
    3)执行updated钩子函数(数据改变之后)
    4)直到Vue实例对象被销毁
  11. 执行beforeUnmout钩子函数(Vue实例销毁之前,data数据和methods方法之类还没有被销毁,还能调用,可以解绑事件监听或者清除掉定时器之类事件)
  12. 执行unmouted钩子函数(Vue实例销毁之后)

三、生命周期钩子

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
//声明式渲染,可以提高开发效率
import content from './components/Content.vue'
export default {
data() {
return {
message: "hello vue",
isShow: true,
}
},
components: {
content
}
};
</script>

<template>
<div>
<content v-if="isShow"></content>
<button @click="isShow = !isShow">销毁hello组件</button>
</div>
</template>

content.vue

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>
export default {
data() {
return {
counter: 0
}
},
beforeCreate() {
console.log('beforeCreate');
},
created() {
console.log('created');
},
beforeMount() {
console.log('beforeMount');
},
mounted() {
console.log('mounted');
},
//页面渲染之后
beforeUpdate() {
console.log('beforeUpdate');
},
updated() {
console.log('updated');
},
beforeUnmount() {
console.log('beforeUnmount');
},
unmounted() {
console.log('unmounted');
}
}
</script>

<template>
<div>
<h1>hello</h1>
<h1>{{ counter }}</h1>
<button @click="counter++">按钮</button>
</div>
</template>


Vue学习-组件和生命周期
https://gopherlinzy.github.io/2022/07/28/vue-Components-lifecycle/
作者
孙禄毅
发布于
2022年7月28日
许可协议