Vue学习笔记06-组件化开发

props,emits,Provide/Inject,mitt,slot,动态组件,异步组件,生命周v-model

1. 基础认知

1.1. 组件化思想

  • 现在可以说整个的大前端开发都是组件化的天下,无论从三大框架(Vue、React、Angular),还是跨平台方案的Flutter,甚至是移动端都在转向组件化开发,包括小程序的开发也是采用组件化开发的思想

  • 所以,学习组件化最重要的是它的思想,每个框架或者平台可能实现方法不同,但是思想都是一样的

  • 通过组件化的思想来思考整个应用程序:

    • 将一个完整的页面分成很多个组件;
    • 每个组件都用于实现页面的一个功能块;
    • 而每一个组件又可以进行细分;
    • 而组件本身又可以在多个地方进行复用;

1.2. Vue的组件化

组件化是Vue、React、Angular的核心思想:

  • createApp函数传入了一个对象App,这个对象其实本质上就是一个组件,也是应用程序的根组件;
  • 组件化提供了一种抽象,开发出一个个独立可复用的小组件来构造应用;
  • 任何的应用都会被抽象成一颗组件树;

1.3. 注册组件的方式

  • 如果现在有一部分内容(模板、逻辑等),希望将这部分内容抽取到一个独立的组件中去维护,这个时候如何注册一个组件呢?

  • 从简单的开始谈起,比如下面的模板希望抽离到一个单独的组件:

    1
    2
    <h2>{{title}}</h2>
    <p>{{message}}</p>
  • 注册组件分成两种:

    • 全局组件:在任何其他的组件中都可以使用的组件;
    • 局部组件:只有在注册的组件中才能使用的组件;

1.4. 注册全局组件

  • 全局组件需要使用全局创建的app来注册组件;
  • 通过component方法传入组件名称、组件对象即可注册一个全局组件了;
  • 之后,可以在App组件的template中直接使用这个全局组件
  • 组件本身也可以有自己的代码逻辑:
    • 比如data、computed、methods等等
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
<body>
<div id="app"></div>

<template id="vue">
<component-a></component-a>
<component-a></component-a>
<component-a></component-a>
</template>

<template id="component-a">
<h2>title</h2>
<p>{{message}}</p>
</template>

<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
template: '#vue'
});

app.component('component-a',{
template: '#component-a',
data(){
return {
message: 'Hello component'
}
}
});

app.mount('#app');
</script>
</body>

1.5. 组件的名称

  • 在通过app.component注册一个组件的时候,第一个参数是组件的名称,定义组件名的方式有两种:

  • 方式一:使用kebab-case(短横线分割符)

    • 当使用 kebab-case (短横线分隔命名) 定义一个组件时,必须在引用这个自定义元素时使用 kebab-case, 例如 <my-component-name>;
  • 方式二:使用PascalCase(驼峰标识符)

    • 当使用 PascalCase (首字母大写命名) 定义一个组件时,在引用这个自定义元素时两种命名法都可以使用
    • 也就是说 <my-component-name><MyComponentName> 都是可接受的;

1.6. 注册局部组件

  • 全局组件往往是在应用程序一开始就会全局组件完成,那么就意味着如果某些组件并没有用到,也会一起被注册:

    • 比如注册了三个全局组件:ComponentA、ComponentB、ComponentC;
    • 在开发中只使用了ComponentA、ComponentB,如果ComponentC没有用到但是依然在全局进行了注册,那么就意味着类似于webpack这种打包工具在打包项目时,依然会对其进行打包;
    • 这样最终打包出的JavaScript包就会有关于ComponentC的内容,用户在下载对应的JavaScript时也会增加包的大小;
  • 所以在开发中通常使用组件的时候采用的都是局部注册:

    • 局部注册是在需要使用到的组件中,通过components属性选项来进行注册;
    • 比如之前的App组件中,有data、computed、methods等选项了,事实上还可以有一个components选 项;
    • 该components选项对应的是一个对象,对象中的键值对是 组件的名称: 组件对象;
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
<body>
<div id="app"></div>

<template id="vue">
<component-a></component-a>
<component-a></component-a>
<component-a></component-a>
</template>

<template id="component-a">
<h2>title</h2>
<p>{{message}}</p>
</template>

<script src="https://unpkg.com/vue@next"></script>
<script>

const ComponentA = {
template: '#component-a',
data(){
return {
message: 'Hello component'
}
}
};

const app = Vue.createApp({
template: '#vue',
components: {
'ComponentA': ComponentA
}
});
app.mount('#app');
</script>
</body>

2. Vue的开发模式

  • 目前使用vue的过程都是在html文件中,通过 template编写自己的模板、脚本逻辑、样式等。

  • 但是随着项目越来越复杂,采用组件化的方式来进行开发:

    • 就意味着每个组件都会有自己的模板、脚本逻辑、样式等;
    • 当然依然可以把它们抽离到单独的js、css文件中,但是它们还是会分离开来;
    • 也包括script是在一个全局的作用域下,很容易出现命名冲突的问题;
    • 并且代码为了适配一些浏览器,必须使用ES5的语法;
    • 在编写代码完成之后,依然需要通过工具对代码进行构建、代码;
  • 所以在真实开发中,通过一个后缀名为 .vue 的 single-file components (单文件组件) 来解决,并且可以使用webpack或者vite或者rollup等构建工具来对其进行处理

2.1. 单文件的特点

  • 在这个组件中可以获得非常多的特性:
    • 代码的高亮;
    • ES6、CommonJS的模块化能力;
    • 组件作用域的CSS;
    • 可以使用预处理器来构建更加丰富的组件,比 如TypeScript、Babel、Less、Sass等;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<h2>{{message}}</h2>
</div>
</template>

<script>
module.exports={
data(){
return {
message: 'Hello .vue'
}
}
}
</script>

<style scoped>
h2{
text-align: center;
}
</style>

2.2. 如何支持SFC

  • 如果想要使用这一的SFC的.vue文件,比较常见的是两种方式:

    1. 方式一:使用Vue CLI来创建项目,项目会默认配置好所有的配置选项,可以在其中直接使用.vue文件;
    2. 方式二:自己使用webpack或rollup或vite这类打包工具,对其进行打包处理;
  • 最终,无论是后期做项目,还是在公司进行开发,通常都会采用 Vue CLI的方式来完成

3. 组件的嵌套与拆分

3.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
<template>
<div id="app">
<div class="header">
<h2>Header</h2>
<h2>NavBar</h2>
</div>
<div class="main">
<h2>Banner</h2>
<ul>
<li>商品列表1</li>
<li>商品列表2</li>
<li>商品列表3</li>
<li>商品列表4</li>
<li>商品列表5</li>
</ul>
</div>
<div class="footer">
<h2>Footer</h2>
<h2>免责声明</h2>
</div>
</div>
</template>

3.2. VSCode 协助开发

  • 安装插件:

    • Vetur
    • Vue VSCode Snippets
  • vue文件生成快捷键

    vbase-css

3.3. 组件的拆分

3.3.1. Header.vue

1
2
3
4
5
6
<template>
<div class="header">
<h2>Header</h2>
<h2>NavBar</h2>
</div>
</template>

3.3.2. Main.vue

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div class="main">
<h2>Banner</h2>
<ul>
<li>商品列表1</li>
<li>商品列表2</li>
<li>商品列表3</li>
<li>商品列表4</li>
<li>商品列表5</li>
</ul>
</div>
</template>
1
2
3
4
5
6
<template>  
<div class="footer">
<h2>Footer</h2>
<h2>免责声明</h2>
</div>
</template>

3.3.4. App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div id="app">
<Header></Header>
<Main></Main>
<Footer></Footer>

</div>
</template>

<script>
import Header from './components/Header.vue';
import Main from './components/Main.vue';
import Footer from './components/Footer.vue';

export default {
components: {
Header,
Main,
Footer
}
}
</script>

4. 组件的通信

  • 上面的嵌套逻辑如下,它们存在如下关系:

    • App组件是Header、Main、Footer组件的父组件;
    • Main组件是Banner、ProductList组件的父组件;
  • 在开发过程中,经常遇到需要组件之间相互进行通信:

    • 比如App可能使用了多个Header,每个地方的Header展示的内容不同,需要使用者传递给Header一些数据,让其进行展示;
    • 又比如在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给它们来进行展示;
    • 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;
  • 总之,在一个Vue项目中,组件之间的通信是非常重要的环节;

4.1. 父子组件之间通信的方式

  • 父组件传递给子组件:通过props属性;

  • 子组件传递给父组件:通过$emit触发事件;

4.1.1. 父组件传递给子组件

  • 在开发中很常见的就是父子组件之间通信,比如父组件有一些数据,需要子组件来进行展示:

    • 通过props来完成组件之间的通信;
  • 什么是Props呢?

    • Props 可以在组件上注册一些自定义的attribute;
    • 父组件给这些attribute赋值,子组件通过attribute的名称获取到对应的值;
  • Props有两种常见的用法:

    1. 方式一:字符串数组,数组中的字符串就是attribute的名称;
    2. 方式二:对象类型,对象类型可以在指定attribute名称的同时,指定它需要传递的类型、是否是必须的、 默认值等等;

4.1.2. Props的数组用法

Main.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
<template>
<div class="main">
<main-banner title="Banner"></main-banner>
<!-- v-bind形式 -->
<main-banner :title="message.title"></main-banner>
<main-banner v-bind="message"></main-banner>
<main-productList></main-productList>
</div>
</template>

<script>
import MainBanner from './MainBanner.vue';
import MainProductList from './MainProductList.vue';

export default {
components: {
MainBanner,
MainProductList
},
data(){
return {
message: {
title: "banner"
}
}
}

}
</script>

MainBanner.vue

1
2
3
4
5
6
7
8
9
10
11
<template>
<h2>{{title}}</h2>
</template>

<script>
export default {
props: [
"title"
]
}
</script>

4.1.3. Props的对象用法

  • 数组用法中 只能说明传入的attribute的名称,并不能对其进行任何形式的限制,对象的写法可以让props变得更加完善

  • 当使用对象语法的时候,可以对传入的内容限制更多:

    • 比如指定传入的attribute的类型;
    • 比如指定传入的attribute是否是必传的;
    • 比如指定没有传入时,attribute的默认值;

MainBanner.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<h2>{{title}}</h2>
</template>

<script>
export default {
props: {
title: {
type: String,
require: true,
default: 'title'
}
}
}
</script>

4.1.4. type的类型

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

4.1.5. 对象类型的其他写法

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
props: {
title: {
type: String,
// 必填字段
require: true,
// 默认值
default: 'title'
},
// 基础类型检查中
// null和undefined会通过任何类型验证
propA: Number,
// 多可能类型
propB: [String,Number],
// 对象
propE: {
type: Object,
// 对象或数组默认值从一个工厂函数获取
default(){
return {
message: 'hello'
}
}
},
// 自定义验证函数
propF:{
validator(value){
// 值必须匹配下列字符串中的一个
return ['succrss','warning','danger'].includes(value);
}
},
// 具有默认值得函数
propG: {
type: Function,
// 与对象或数组默认值不同,这是一个用作默认值的函数
default(){
return 'Default function';
}
}
}

4.1.6. Prop 的大小写命名

  • Prop 的大小写命名(camelCase vs kebab-case)
    • HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符;
    • 使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名;
1
2
3
4
5
<template>
<div class="main">
<main-banner message-info="Banner"></main-banner>
</div>
</template>

4.1.7. 非Prop的Attribute

  • 什么是非Prop的Attribute呢?

    • 传递一个组件某个属性,但是该属性并没有定义对应的props或者emits时,就称之为非Prop的Attribute;
    • 常见的包括class、style、id属性等;
  • Attribute继承

    • 当组件有单个根节点时,非Prop的Attribute将自动添加到根节点的Attribute中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- Main.vue -->
<template>
<div class="main">
<main-banner title="Banner" class="titleStyle"></main-banner>
<main-banner :title="message.title"></main-banner>
<main-banner v-bind="message"></main-banner>
<main-productList></main-productList>
</div>
</template>

<!-- MainBanner.vue -->
<template>
<div>
<h2>{{title}}</h2>
</div>
</template>

显示的效果

1
<div class="titleStyle"><h2>Banner</h2></div>

4.1.8. 禁用Attribute继承和多根节点

  • 如果不希望组件的根元素继承attribute**,可以在组件中设置 **inheritAttrs: false:

    • 禁用attribute继承的常见情况是需要将attribute应用于根元素之外的其他元素;

    • 可以通过 $attrs来访问所有的非props的attribute;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <!-- MainBanner.vue -->
      <template>
      <div>
      <h2 :class="$attrs.class">{{title}}</h2>
      </div>

      </template>

      <script>
      export default {
      inheritAttrs: false,
      props: ["title"]
      }
      </script>
  • 多个根节点的attribute

    • 多个根节点的attribute如果没有显示的绑定,那么会报警告,必须手动的指定要绑定到哪一个属性上:

      使用 v-bind="$attrs"

      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
      <!-- Main.vue -->
      <template>
      <div class="main">
      <main-banner :title="message.title" id="banner" class="main">
      </div>
      </template>
      <script>
      import MainBanner from './MainBanner.vue';
      import MainProductList from './MainProductList.vue';

      export default {
      components: {
      MainBanner,
      MainProductList
      },
      data(){
      return {
      message: {
      title: "banner"
      }
      }
      }

      }
      </script>

      <!-- MainBanner.vue -->
      <template>
      <div>
      <h2 v-bind="$attrs">{{title}}</h2>
      </div>
      </template>
      <script>
      export default {
      inheritAttrs: false,
      props: ["title"]
      }
      </script>

      显示结果

      1
      <h2 id="banner" class="main">banner</h2>

4.2. 子组件传递给父组件

  • 什么情况下子组件需要传递内容到父组件呢?

    • 当子组件有一些事件发生的时候,比如在组件中发生了点击,父组件需要切换内容;
    • 子组件有一些内容想要传递给父组件的时候;
  • 如何完成上面的操作呢?

    • 首先,在子组件中定义好在某些情况下触发的事件名称;
    • 其次,在父组件中以v-on的方式传入要监听的事件名称,并且绑定到对应的方法中;
    • 最后,在子组件中发生某个事件的时候,根据事件名称触发对应的事件

4.2.1. 自定义事件的流程

封装一个CounterOperation.vue的组件:

  • 内部其实是监听两个按钮的点击,点击之后通过 this.$emit的方式发出去事件;

CounterOperation.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<button @click= "increment">+1</button>
<button @click= "decrement">-1</button>
</div>
</template>

<script>
export default {
emits: ["addOne","subOne"],
methods: {
increment(){
this.$emit("addOne");
},
decrement(){
this.$emit("subOne");
}
}
}
</script>

Main.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
<template>
<div>
<h2>{{counter}}</h2>
<counter-operation @addOne="add" @subOne="sub"></counter-operation>
</div>
</template>

<script>
import CounterOperation from './CounterOperation.vue';

export default {
components: {
CounterOperation
},
data(){
return {
counter: 0
}
},
methods:{
add(){
this.counter++;
},
sub(){
this.counter--;
}
}
}
</script>

4.2.2. 自定义事件的参数和验证

  • 自定义事件的时候,也可以传递一些参数给父组件

    CounterOperation.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <template>
    <div>
    <button @click= "addTen">+10</button>
    </div>
    </template>

    <script>
    export default {
    emits: ["addTen"],
    methods: {
    addTen(){
    this.$emit("addTen",10);
    }
    }
    }
    </script>

    Main.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
    <template>
    <div>
    <h2>{{counter}}</h2>
    <counter-operation @addTen="ten"></counter-operation>
    </div>
    </template>

    <script>
    import CounterOperation from './CounterOperation.vue';

    export default {
    components: {
    CounterOperation
    },
    data(){
    return {
    counter: 0
    }
    },
    methods:{
    ten(num){
    // console.log(num);
    this.counter+=num;
    }
    }
    }
    </script>
  • 还可以对传递的参数进行验证

    CounterOperation.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
    43
    44
    45
    <template>
    <div>
    <button @click= "increment">+1</button>
    <button @click= "decrement">-1</button>
    <button @click= "addTen">+10</button>
    <input type="text" v-model.number="num">
    <button @click= "addN">+N</button>
    </div>
    </template>

    <script>
    export default {
    emits: {
    addOne: null,
    subOne: null,
    addTen: null,
    addN: function(n){
    if(n%2==0){
    return true;
    }else{
    return false;
    }
    }
    },
    data(){
    return {
    num: 0
    }
    },
    methods: {
    increment(){
    this.$emit("addOne",10);
    },
    decrement(){
    this.$emit("subOne");
    },
    addTen(){
    this.$emit("addTen",10);
    },
    addN(){
    this.$emit("addN",this.num);
    }
    }
    }
    </script>

    Main.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
    <template>
    <div>
    <h2>{{counter}}</h2>
    <counter-operation
    @addOne="add"
    @subOne="sub"
    @addTen="ten"
    @addN="addN"
    >
    </counter-operation>
    </div>
    </template>

    <script>
    import CounterOperation from './CounterOperation.vue';

    export default {
    components: {
    CounterOperation
    },
    data(){
    return {
    counter: 0
    }
    },
    methods:{
    add(){
    this.counter++;
    },
    sub(){
    this.counter--;
    },
    ten(num){
    // console.log(num);
    this.counter+=num;
    },
    addN(num){
    this.counter+=num;
    }
    }
    }
    </script>

    浏览器显示警告信息

    1
    [Vue warn]: Invalid event arguments: event validation failed for event "addN".

4.3. 组件间通信案例练习

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
<template>
<div>
<tab-control :titles="titles" @itemClick="itemClick"></tab-control>
<h2>{{contents[currentIndex]}}</h2>
</div>
</template>

<script>
import TabControl from './TabControl.vue';

export default {
components: {
TabControl
},
data(){
return {
titles: ["衣服","鞋子","裤子"],
contents: ["衣服页面","鞋子页面","裤子页面"],
currentIndex: 0

}
},
methods: {
itemClick(index){
this.currentIndex=index;
}
}
}
</script>

<style scoped>

</style>

TabControl.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
43
44
45
<template>
<div>
<ul>
<li v-for="(title,index) in titles" :key="title"
:class="{active:currentIndex==index}"
@click="itemClick(index)">
<span>{{title}}</span>
</li>
</ul>
</div>
</template>

<script>
export default {
emits: ["itemClick"],
props: ["titles"],
data(){
return {
currentIndex: 0
}
},
methods: {
itemClick(index){
this.currentIndex=index;
this.$emit("itemClick",index);
}
}
}
</script>

<style scoped>
ul{
list-style: none;
width: 400px;
display: flex;
justify-content: space-around;
}
ul li{
cursor: pointer;
}
li.active{
color: red;
border-bottom: 1px solid red;
}
</style>

4.4. 非父子组件的通信

4.4.1. Provide和Inject

  • Provide/Inject用于非父子组件之间共享数据:

    • 比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容;
    • 在这种情况下,如果仍然将props沿着组件链逐级传递下去,就会非常的麻烦;
  • 对于这种情况下,使用 Provide 和 Inject :

    • 论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者;
    • 父组件有一个 provide 选项来提供数据;
    • 子组件有一个 inject 选项来开始使用这些数据;
  • 实际上,可以将依赖注入看作是“long range props”,除了:

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

4.4.1.1. 基本使用

开发这样的结构:App.vue -> Home.vue -> HomeContent.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
23
<template>
<div>
<home></home>
</div>
</template>

<script>
import Home from './Home.vue';

export default {
components: {
Home
},
provide: {
name: "Hello Vue",
id: 1
}
}
</script>

<style scoped>

</style>

Home.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<h1>Home</h1>
<home-content></home-content>
</div>
</template>

<script>
import HomeContent from './HomeContent.vue';

export default {
components: {
HomeContent
}
}
</script>

<style scoped>

</style>

HomeContent.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<h1>HomeContent</h1>
<h2>{{id}}-{{name}}</h2>
</div>
</template>

<script>
export default {
inject: ["name","id"]
}
</script>

<style scoped>

</style>

4.4.1.2. 处理响应式数据

Main.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { computed } from 'vue';
export default {
provide(){
return {
name: "Hello Vue",
id: 1,
length: computed(()=>this.names.length)
}
},
data(){
return {
names: ['A','B','C']
}
}
}

HomeContent.vue

1
2
3
export default {
inject: ["name","id","length"]
}

Main.js

1
2
3
4
5
6
7
const app = createApp(App);
/**
[Vue warn]: injected property "length" is a ref and will be auto-unwrapped and no longer needs `.value` in the next minor release. To opt-in to the new behavior now, set `app.config.unwrapInjectedRef = true`
*/
app.config.unwrapInjectedRef = true;

app.mount('#app');

4.4.2. Mitt全局事件总线

  • Vue3从实例中移除了 $on、$off 和 $once 方法,如果希望 继续使用全局事件总线,要通过第三方的库:

  • Vue3官方有推荐一些库,例如 mitt 或 tiny-emitter;

  • 首先,安装这个库:

    1
    npm install mitt
  • 其次,封装一个工具eventbus.js:

    1
    2
    3
    import mitt from 'mitt';
    const emitter = mitt();
    export default emitter;

4.4.2.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
    <template>
    <div>
    <button @click="click">click</button>
    <hello></hello>
    </div>
    </template>

    <script>
    import emitter from './util/eventbus';
    import Hello from './Home.vue';

    export default {
    components: {
    Hello
    },
    methods: {
    click(){
    emitter.emit("click1",{
    name: "Hello vue1",
    id: '0001'
    }),
    emitter.emit("click2",{
    name: "Hello vue2",
    id: '0002'
    }),
    emitter.emit("click3",{
    name: "Hello vue3",
    id: '0003'
    })
    }
    }
    }
    </script>

    <style scoped>

    </style>
  • 在Home.vue中监听事件;

    * 表示监听所有

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <template>
    <div>
    </div>
    </template>

    <script>
    import emitter from './util/eventbus';

    export default {
    created(){
    emitter.on("click1",(info)=>{
    console.log(info);
    }),
    emitter.on("*",(type,info)=>{
    console.log(type,info);
    })
    }
    }
    </script>

    <style scoped>

    </style>

4.4.2.2. Mitt的事件取消

1
2
3
4
5
6
7
8
// 取消所有的监听
emitter.all.clear();

// 定义一个函数
function onFoo{}

emitter.on('foo',onFoo);
emitter.off('foo',onFoo);

5. 插槽Slot

5.1. 认识插槽

  • 在开发中,经常会封装一个个可复用的组件:

    • 通过props传递给组件一些数据,让组件来进行展示;但是为了让这个组件具备更强的通用性,不能将组件中的内容限制为固定的div、span等等这些元素;
    • 比如某种情况下使用组件,希望组件显示的是一个按钮,一张图片;
    • 应该是让使用者决定某一块区域到底存放什么内容和元素;
  • 举个例子:定制一个通用的导航组件 - NavBar

    • 这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定;

    • 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示;

    • 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等;

    • 右边可能是一个文字,也可能是一个图标,也可能什么都不显示;

5.2. 使用插槽slot

  • 定义插槽slot:

    • 插槽的使用过程其实是抽取共性、预留不同;
    • 将共同的元素、内容依然在组件内进行封装;
    • 同时会将不同的元素使用slot作为占位,让外部决定到底显示什么样的元素;
  • 如何使用slot呢?

    • Vue中将 <slot> 元素作为承载分发内容的出口;
    • 在封装组件中,使用特殊的元素<slot>就可以为封装组件开启一个插槽;
    • 该插槽插入什么内容取决于父组件如何使用;
  • 案例:

    • MySlotCpn.vue:该组件中有一个插槽,可以在插槽中放入需要显示的内容;

    • App.vue中使用它们:可以插入普通的内容、html元素、组件元素等;

      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
      <!-- MySlotCpn.vue -->
      <template>
      <div>
      <h2>MySlotCpn开始</h2>
      <slot></slot>
      <h2>MySlotCpn结束</h2>
      </div>
      </template>



      <!-- App.vue -->
      <template>
      <div>
      <my-slot-cpn>
      <button>按钮</button>
      <h2>App</h2>
      </my-slot-cpn>
      </div>
      </template>

      <script>
      import MySlotCpn from './MySlotCpn.vue';

      export default {
      components: {
      MySlotCpn
      }
      }
      </script>

5.3. 插槽的默认内容

没有插入对应的内容,显示一个默认的内容

1
2
3
4
5
6
7
8
9
<template>
<div>
<h2>MySlotCpn开始</h2>
<slot>
<h2>默认显示内容</h2>
</slot>
<h2>MySlotCpn结束</h2>
</div>
</template>

5.4. 多个插槽的效果

  • 如果一个组件中含有多个插槽,插入多个内容时是什么效果?
    • 默认情况下每个插槽都会获取到插入的内容来显示;

MySlotCpn.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
<template>
<div class="navbar">
<div class="left">
<h2>MySlotCpn开始</h2>
<slot></slot>
<h2>MySlotCpn结束</h2>
</div>
<div class="center">
<h2>MySlotCpn开始</h2>
<slot></slot>
<h2>MySlotCpn结束</h2>
</div>
<div class="right">
<h2>MySlotCpn开始</h2>
<slot></slot>
<h2>MySlotCpn结束</h2>
</div>
</div>
</template>

<script>
export default {

}
</script>

<style scoped>
.navbar{
width: 500px;
display: flex;
justify-content: space-between;
}
</style>

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<my-slot-cpn>
<button>按钮</button>
<h2>App</h2>
</my-slot-cpn>
</div>
</template>

<script>
import MySlotCpn from './MySlotCpn.vue';

export default {
components: {
MySlotCpn
}
}
</script>

<style scoped>

</style>

效果

5.5. 具名插槽的使用

  • 具名插槽顾名思义就是给插槽起一个名字,<slot> 元素有一个特殊的属性 name;

    • 一个不带 name 的slot,会带有隐含的名字 default;
  • 匹配采用 v-slot:name

MySlotCpn.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
<template>
<div class="navbar">
<div class="left">
<h2>MySlotCpn left</h2>
<slot name="left"></slot>
<h2>MySlotCpn结束</h2>
</div>
<div class="center">
<h2>MySlotCpn center</h2>
<slot name="center"></slot>
<h2>MySlotCpn结束</h2>
</div>
<div class="right">
<h2>MySlotCpn right</h2>
<slot name="right"></slot>
<h2>MySlotCpn结束</h2>
</div>
</div>
</template>

<script>
export default {

}
</script>

<style scoped>
.navbar{
width: 500px;
display: flex;
justify-content: space-between;
}
h2{
font-size: 14px;
}
</style>

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
<template>
<div>
<my-slot-cpn>
<template v-slot:left>
<button>按钮</button>
</template>
<template v-slot:right>
<h2>App</h2>
</template>
</my-slot-cpn>
</div>
</template>

<script>
import MySlotCpn from './MySlotCpn.vue';

export default {
components: {
MySlotCpn
}
}
</script>

<style scoped>

</style>

5.6. 动态插槽名

  • 什么是动态插槽名呢?
    • 比如 v-slot:left、v-slot:center等等;
    • 可以通过 v-slot:[dynamicSlotName]方式动态绑定一个名称;

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
<template>
<div>
<my-slot-cpn>
<template v-slot:[name]>
<button>按钮</button>
</template>
</my-slot-cpn>
</div>
</template>

<script>
import MySlotCpn from './MySlotCpn.vue';

export default {
components: {
MySlotCpn
},
data(){
return {
name: "left"
}
}
}
</script>

5.7. 具名插槽使用的时候缩写

即把参数之前的所有内容 (v-slot:) 替换为字符 #;

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
<template>
<div>
<my-slot-cpn>
<template #[name]>
<button>按钮</button>
</template>
<template #right>
<h2>App</h2>
</template>
</my-slot-cpn>
</div>
</template>

<script>
import MySlotCpn from './MySlotCpn.vue';

export default {
components: {
MySlotCpn
},
data(){
return {
name: "left"
}
}
}
</script>

5.8. 渲染作用域

  • 在Vue中有渲染作用域的概念:

    • 父级模板里的所有内容都是在父级作用域中编译的;
    • 子模板里的所有内容都是在子作用域中编译的;
  • 如何理解这句话呢?

    • 来看一个案例
    • 在案例中 MySlotCpn 自然是可以让问自己作用域中的title内容的;
    • 但是在App中,是访问不了 MySlotCpn 中的内容的,因为它们是跨作用域的访问;
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
<!-- MySlotCpn.vue -->
<template>
<div class="navbar">
<h2>{{title}}</h2>
<slot></slot>
</div>
</template>

<script>
export default {
data(){
return {
title: 'Hello MySlotCpn'
}
}
}
</script>

<!-- App.vue -->
<template>
<div>
<my-slot-cpn>
<!-- 只能访问App作用域中的内容 -->
<h2>{{title}}</h2>
</my-slot-cpn>
</div>
</template>

<script>
import MySlotCpn from './MySlotCpn.vue';

export default {
components: {
MySlotCpn
}
}
</script>

浏览器警告

1
[Vue warn]: Property "title" was accessed during render but is not defined on instance. 

5.8.1. 认识作用域插槽

  • 希望插槽可以访问到子组件中的内容是非常重要的:

    • 当一个组件被用来渲染一个数组元素时,使用插槽,并且希望插槽中没有显示每项的内容;
    • Vue给我们提供了作用域插槽;
  • 案例

    1. 定义数据
    2. 遍历names数据
    3. 定义插槽的prop
    4. 通过 v-slot:default 的方式获取到slot的props
    5. 使用slotProps中的item和index

MySlotCpn.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<!-- 2.遍历names数据 -->
<template v-for="(item,index) in names">
<!-- 3.定义插槽的prop -->
<slot :item="item" :index="index"></slot>
</template>
</div>
</template>

<script>
export default {
data(){
return {
// 1. 定义数据
names: ["why","kobe","james","curry"]
}
}
}
</script>

<style scoped>
</style>

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
<template>
<div>
<my-slot-cpn>
<!-- 4.通过 v-slot:default 的方式获取到slot的props -->
<template v-slot:default="slotProps">
<!-- 5.使用slotProps中的item和index -->
<span>{{slotProps.item}}-{{slotProps.index}}</span><br>
</template>
</my-slot-cpn>
</div>
</template>

<script>
import MySlotCpn from './MySlotCpn.vue';

export default {
components: {
MySlotCpn
}
}
</script>

<style scoped>

</style>

5.8.2. 独占默认插槽的缩写

  • v-slot:default=”slotProps”可以简写为v- slot=”slotProps”:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <template>
    <div>
    <my-slot-cpn>
    <template v-slot="slotProps">
    <span>{{slotProps.item}}-{{slotProps.index}}</span><br>
    </template>
    </my-slot-cpn>
    </div>
    </template>
  • 并且如果插槽只有默认插槽时,组件的标签可以被当做插槽的模板来使用,可以将 v-slot 直接用在组件上:

    1
    2
    3
    4
    5
    6
    7
    <template>
    <div>
    <my-slot-cpn v-slot="slotProps">
    <span>{{slotProps.item}}-{{slotProps.index}}</span><br>
    </my-slot-cpn>
    </div>
    </template>

5.8.3. 默认插槽和具名插槽混合

  • 如果有默认插槽和具名插槽,那么按照完整的template来编写

    1
    2
    3
    4
    5
    6
    <my-slot-cpn v-slot="slotProps">
    <span>{{slotProps.item}}-{{slotProps.index}}</span><br>
    <template v-slot:why>
    <h2>why</h2>
    </template>
    </my-slot-cpn>

    1
    2
    [vue/valid-v-slot]
    Default slot must use '<template>' on a custom element when there are other named slots.
  • 只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法:

    1
    2
    3
    4
    5
    6
    7
    8
    <my-slot-cpn>
    <template v-slot="slotProps">
    <span>{{slotProps.item}}-{{slotProps.index}}</span><br>
    </template>
    <template v-slot:why>
    <h2>why</h2>
    </template>
    </my-slot-cpn>

6. 动态组件

  • 点击一个tab-bar,切换不同的组件显示;
  • 这个案例可以通过两种不同的实现思路来实现:
    • 方式一:通过v-if来判断,显示不同的组件;
    • 方式二:动态组件的方式;

6.1. v-if显示不同的组件

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
43
44
45
46
47
48
49
50
51
52
<template>
<div>
<button
v-for="(item) in tabs" :key="item"
:class="{active: currentTab==item}"
@click="itemClick(item)"
>
{{item}}
</button>
<template v-if="currentTab=='home'">
<home></home>
</template>
<template v-else-if="currentTab=='about'">
<about></about>
</template>
<template v-else>
<category></category>
</template>
</div>
</template>

<script>
import Home from './pages/Home.vue';
import About from './pages/About.vue';
import Category from './pages/Category.vue';

export default {
components:{
Home,
About,
Category
},
data(){
return {
tabs: ['home','about','category'],
currentTab: 'home'
}
},
methods: {
itemClick(item){
this.currentTab=item;
}
}

}
</script>

<style scoped>
.active{
color: red;
}
</style>

6.2. 动态组件方式

  • 动态组件是使用 component 组件,通过一个 特殊的attribute is 来实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <template>
    <div>
    <button
    v-for="(item) in tabs" :key="item"
    :class="{active: currentTab==item}"
    @click="itemClick(item)"
    >
    {{item}}
    </button>

    <component :is="currentTab"></component>
    </div>
    </template>
  • 这个currentTab的值需要是什么内容呢?

    • 可以是通过component函数注册的组件;
    • 在一个组件对象的components对象中注册的组件;

6.3. 动态组件的传值

可以将属性和监听事件放到component上来使用

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
43
44
45
46
47
48
49
50
51
52
<template>
<div>
<button
v-for="(item,index) in tabs" :key="item"
:class="{active: currentTab==item}"
@click="itemClick(item,index)"
>
{{item}}
</button>
<component :is="currentTab"
:name="tabs[currentIndex]"
@pageClick = "pageClick"
></component>
</div>
</template>

<script>
import Home from './pages/Home.vue';
import About from './pages/About.vue';
import Category from './pages/Category.vue';

export default {
components:{
Home,
About,
Category
},
data(){
return {
tabs: ['home','about','category'],
currentTab: 'home',
currentIndex: 0
}
},
methods: {
itemClick(item,index){
this.currentTab = item;
this.currentIndex = index;
},
pageClick(item){
console.log("got "+item);
}
}

}
</script>

<style scoped>
.active{
color: red;
}
</style>

Home.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<button @click="pageClick">{{name}}</button>
</div>
</template>

<script>
export default {
props: ['name'],
emits: ['pageClick'],
methods: {
pageClick(){
this.$emit("pageClick",this.name);
}
}
}
</script>

<style scoped>

</style>

6.4. keep-alive

  • 先对之前的案例中About组件进行改造:

    • 在其中增加了一个按钮,点击可以递增的功能;
  • 将counter点到10,那么在切换到home再切换回来about时,状态是否可以保持呢?

    • 答案是否定的;
    • 这是因为默认情况下,在切换组件后,about组件会被销毁掉,再次回来时会重新创建组件;
  • 但是,在开发中希望继续保持组件的状态,而不是销毁掉,这个时候就可以使用一个内置组件: keep-alive

6.4.1. 属性

  • keep-alive有一些属性:
    • include - string | RegExp | Array。只有名称匹配的组件会被缓存;
      • 组件的值通过 name属性指定
    • exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存;
    • max - number | string。最多可以缓存多少组件实例,一旦达 到这个数字,那么缓存组件中最近没有被访问的实例会被销毁;
  • include 和 exclude prop 允许组件有条件地缓存:
    • 二者都可以用逗号分隔字符串、正则表达式或一个数组来表示;
    • 匹配首先检查组件自身的 name 选项;

6.4.2. include

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
<component :is="currentTab"></component>
</keep-alive>

<!-- regex(使用v-bind) -->
<keep-alive :include="/a|b/">
<component :is="currentTab"></component>
</keep-alive>

<!-- Array(使用v-bind) -->
<keep-alive :include="['a','b']">
<component :is="currentTab"></component>
</keep-alive>

6.4.3. 案例

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
<template>
<div>
<button @click="pageClick">{{name}}</button>
<button @click="counter++">{{counter}}</button>
</div>
</template>

<script>


export default {
// 确定include中的值
name: 'about',
props: ['name'],
emits: ['pageClick'],
methods: {
pageClick(){
this.$emit("pageClick",this.name);
}
},
data(){
return {
counter: 0
}
}
}
</script>

<style scoped>

</style>

App.vue:keep-alive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<button
v-for="(item,index) in tabs" :key="item"
:class="{active: currentTab==item}"
@click="itemClick(item,index)"
>
{{item}}
</button>

<keep-alive include="about">
<component :is="currentTab"
:name="tabs[currentIndex]"
@pageClick = "pageClick"
></component>
</keep-alive>

</div>
</template>

7. 异步组件

  • 如果项目过大了,对于某些组件希望通过异步的方式来进行加载(目的是可以对其进行分包处理),那么Vue中提供了一个函数:defineAsyncComponent

  • defineAsyncComponent接受两种类型的参数:

    • 类型一:工厂函数,该工厂函数需要返回一个Promise对象;
    • 类型二:接受一个对象类型,对异步函数进行配置;

7.1. 工厂函数写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<async-home>

</async-home>
</div>
</template>

<script>
import { defineAsyncComponent } from 'vue';
const AsyncHome = defineAsyncComponent(()=>import("./AsyncHome.vue"));

export default {
components: {
AsyncHome
}
}
</script>

<style scoped>

</style>

7.2. 对象类型写法二

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
<template>
<div>
<async-home>

</async-home>
</div>
</template>

<script>
import Loading from './Loading.vue';
import Error from './Error.vue';

import { defineAsyncComponent } from 'vue';
// const AsyncHome = defineAsyncComponent(()=>import("./AsyncHome.vue"));

const AsyncHome = defineAsyncComponent({
// 工厂函数
loader: ()=> import("./AsyncHome.vue"),
// 加载过程中显示的组件
loadingComponent: Loading,
// 加载失败时显示的组件
errorComponent: Error,
// 在显示loadingComponent之前的延迟 | 默认值:200ms
delay: 2000,
// 如果提供了timeout,并且加载组件的时间超过了设定值,将显示错误组件
// 默认值: Infinity 单位ms
// timeout: 0
// 定义组件是否可挂起 | 默认值true
suspensible: true
})

export default {
components: {
AsyncHome
}
}
</script>

<style scoped>

</style>

7.3. Suspense

  • Suspense是一个内置的全局组件,该组件有两个插槽:
    • default:如果default可以显示,那么显示default的内容;
    • fallback:如果default无法显示,那么会显示fallback插槽的内容;
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
<template>
<div>
<suspense>
<template #default>
<async-home></async-home>
</template>
<template #fallback>
<loading></loading>
</template>
</suspense>
</div>
</template>

<script>
import Loading from './Loading.vue';

import { defineAsyncComponent } from 'vue';
const AsyncHome = defineAsyncComponent(()=>import("./AsyncHome.vue"));

export default {
components: {
AsyncHome,
Loading
}
}
</script>

<style scoped>

</style>

8. 获取元素

8.1. $refs

  • 某些情况下,想要直接获取到元素对象或者子组件实例:

    • 在Vue开发中不推荐进行DOM操作的;
    • 可以给元素或者组件绑定一个ref 属性;
  • 组件实例有一个$refs属性:

    • 一个对象Object,持有注册过 ref 的所有 DOM 元素和组件实例
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
<template>
<div>
<h2 ref="title" @click="domClick">refs</h2>

<nav-bar ref="NavBar"></nav-bar>
</div>
</template>

<script>
import NavBar from './NavBar.vue';

export default {
components: {
NavBar
},
methods: {
domClick(){
// 访问元素
console.log(this.$refs.title);
// 访问所有元素和组件
console.log(this.$refs);

// 访问组件实例
console.log(this.$refs.NavBar);

// 调用组件方法
console.log(this.$refs.NavBar.showMessage());
}
}
}
</script>

<style scoped>
h2{
cursor: pointer;
}
</style>

8.2. $parent

通过$parent来访问父元素

NavBar.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<h2>NarBar</h2>
</div>
</template>

<script>
export default {
methods: {
showMessage(){
return 'Hello '+this.$parent.message;
}
}
}
</script>

<style scoped>

</style>

8.3. $root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<h2>NarBar</h2>
</div>
</template>

<script>
export default {
methods: {
showMessage(){
return 'Hello '+this.$root.message;
}
}
}
</script>

<style scoped>

</style>

9. 生命周期

9.1. 认识生命周期

  • 什么是生命周期呢?

    • 每个组件都可能会经历从 创建、挂载、更新、卸载 等一系列的过程;
    • 在这个过程中的某一个阶段,用于可能会想要添加一些属于自己的代码逻辑(比如组件创建完后就请求一些服务器数据);
    • 但是如何可以知道目前组件正在哪一个过程呢?
    • Vue提供了组件的生命周期函数;
  • 生命周期函数:

    • 生命周期函数是一些钩子函数,在某个时间会被Vue源码内部进行回调;
    • 通过对生命周期函数的回调,可以知道目前组件正在经历什么阶段;
    • 就可以在该生命周期中编写属于自己的逻辑代码了

9.2. 生命周期的流程

https://vuejs.org/guide/essentials/lifecycle.html#lifecycle-diagram

9.3. 案例

Home.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
43
44
<template>
<div>
<h2>Home</h2>
<button @click="counter++">{{counter}}</button>
</div>
</template>

<script>
export default {
beforeCreate(){
console.log("beforeCreate");
},
created(){
console.log("Created - 创建");
},
beforeMount(){
console.log("beforeMount");
},
mounted(){
console.log("mounted - 挂载");
},
beforeUnmout(){
console.log("beforeUnmount");
},
unmounted(){
console.log("unmounted - 卸载");
},
beforeUpdate(){
console.log("beforeUpdate");
},
updated(){
console.log("updated - 更新");
},
data(){
return {
counter: 0
}
}
}
</script>

<style scoped>

</style>

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
<template>
<div>
<button @click="show=!show">操作</button>
<home v-if="show"></home>
</div>
</template>

<script>
import Home from './Home.vue';

export default {
components: {
Home
},
data(){
return {
show: true
}
}
}
</script>

<style scoped>

</style>

浏览器控制台

1
2
3
4
5
6
7
Home.vue?ec5c:26 unmounted - 卸载
Home.vue?ec5c:11 beforeCreate
Home.vue?ec5c:14 Created - 创建
Home.vue?ec5c:17 beforeMount
Home.vue?ec5c:20 mounted - 挂载
Home.vue?ec5c:29 beforeUpdate
Home.vue?ec5c:32 updated - 更新

9.4. 缓存组件的生命周期

  • 对于缓存的组件来说,再次进入时,是不会执行created或者mounted等生命周期函数的:
    • 但是有时候确实希望监听到何时重新进入到了组件,何时离开了组件;
    • 这个时候可以使用 activateddeactivated 这两个生命周期钩子函数来监听;

10. 组件的v-model

  • 在 input 中可以使用 v-model 来完成双向绑定:

    • v-model默认完成了两件事;
    • v-bind:value的数据绑定和 @input 的事件监听;
  • 如果封装了一个组件,在使用这个组件时,是否也可以使用v-model来同时完成这两个功能呢?

    • 也是可以的,vue也支持在组件上使用v-model;
  • 在组件上使用的时候,等价于如下的操作:

  • input元素不同的只是属性的名称和事件触发的名称而已;

10.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
<template>
<div>
<input type="text" v-model="message">
<nav-bar v-model="message"></nav-bar>
<!-- 等价于 -->
<!-- <nav-bar :model-value="message" @update:model-value="message=$event"></nav-bar> -->
<input-bar v-model="message2"></input-bar>
<h2>{{message2}}</h2>
</div>
</template>

<script>
import NavBar from './NavBar.vue';
import InputBar from './InputBar.vue';

export default {
components: {
NavBar,
InputBar
},
data(){
return {
message: "",
message2:""
}
}
}
</script>

<style scoped>

</style>

NavBar.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<h2>{{modelValue}}</h2>
</div>
</template>

<script>
export default {
props: {
modelValue: String
},
emits: ['update:model-value'],
methods:{
btnClick(){
this.$emit('update:model-value');
}
}
}
</script>

<style scoped>

</style>

InputBar.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<input type="text" :value="modelValue" @input="inputChange">
</div>
</template>

<script>
export default {
props: {
modelValue: String
},
emits: ['update:modelValue'],
methods:{
inputChange(event){
this.$emit("update:modelValue",event.target.value);
}
}
}
</script>

<style scoped>

</style>

10.2. computed实现

在组件内部按照双向绑定的做法去完成,可以使用计算属性的 setter 和 getter 完成

input.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
<template>
<div>
<!-- <input type="text" :value="modelValue" @input="inputChange"> -->
<input type="text" v-model="value">
</div>
</template>

<script>
export default {
props: {
modelValue: String
},
emits: ['update:modelValue'],

computed: {
value: {
get(){
return this.modelValue;
},
set(value){
this.$emit('update:modelValue',value);
}
}
}
}
</script>

<style scoped>

</style>

10.3. 绑定多个属性

  • 通过v-model是直接绑定了一个属性,如果 希望绑定多个属性 呢?

    • 在一个组件上使用多个v-model是否可以实现呢?
    • 默认情况下的v-model其实是绑定了 modelValue 属性和 @update:modelValue 的事件;
    • 如果希望绑定更多,可以给v-model传入一个参数,那么这个参数的名称就是绑定属性的名称;
  • 注意:这里绑定了两个属性的

    1
    <input-bar v-model="message2" v-model:title="title"></input-bar>
  • v-model:title 相当于做了两件事:

    • 绑定了title属性;
    • 监听了 @update:title的事件;
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
43
44
45
<template>
<div>
<label for="value">
value:
<input type="text" v-model="value">
</label>

<label for="title">
title:
<input type="text" v-model="title">
</label>
</div>
</template>

<script>
export default {
props: {
modelValue: String,
title: String
},
emits: ['update:modelValue','update:title'],
computed: {
value: {
get(){
return this.modelValue;
},
set(value){
this.$emit('update:modelValue',value);
}
},
title: {
get(){
return this.title;
},
set(title){
this.$emit('update:title',title);
}
}
}
}
</script>

<style scoped>

</style>
本文结束  感谢您的阅读