Vue全家桶系~2.Vue3开篇(过渡)

发布时间 2023-08-11 18:39:20作者: 鲲逸鹏

Vue全家桶

先贴一下Vue3的官方文档:https://cn.vuejs.org/guide/introduction.html

官方API文档:https://cn.vuejs.org/api/

1.前言:新旧时代交替

1.1.开发变化

1.网络模型的变化

  1. 以前网页大多是b/s,服务端代码混合在页面里;

  2. 现在是c/s,前后端分离,通过js api(类似ajax的方式)获取json数据,把数据绑定在页面上渲染。

2.文件类型变化

  1. 以前是.html文件,开发也是html,运行也是html。

  2. 现在是.vue文件,开发是vue,经过编译后,运行时已经变成了js文件。 现代前端开发,很少直接使用HTML,基本都是开发、编译、运行

3.外部文件引用方式变化

  1. 以前通过script srclink href引入外部的js和css;

  2. 现在是es6的写法,import引入外部的js模块(注意不是文件)或css

4.开发方式变化

  1. 以前是Ajax获取数据,然后DOM操作
  2. 现在是Vue的MVVM模式(程序员不用操作DOM了,只关心逻辑和数据即可)

我们延续这种演变进行vue3的学习吧,先从传统html文件混合开始,再到单vue文件选项APIOption API,最后再到vu3新的组合APIComposition API


1.2.环境配置

1.2.1.VSCode

官网下载地址:https://code.visualstudio.com/

1.必装插件

VSCode官方插件Vue Language Features (Volar)

TypeScript支持:TypeScript Vue Plugin (Volar)

Vue快速开发Vue VSCode Snippets(输入缩写快速生成代码段)

错误高亮提示Error Lens

Git管理插件

2.扩展插件

这个插件主要就是高亮TODO: 部分

JavaScript快速开发:输入缩写快速生成js的代码段

1.2.2.NodeJS

NodeJS下载:https://nodejs.org (一般都是下载LTS版本)

npm镜像:https://npmmirror.com/

NodeJS安装后设置下npm的国内镜像:(cnpm 需要全局安装,它是你为数不多的需要全局安装的命令行之一)

安装好后,以后只要是npm xxx的命令,都可以使用cnpm xxx

1.cnpm window

打开Git Bash,输入:npm i cnpm -g --registry=https://registry.npmmirror.com

PS:如果失败,可以先安装npminstall,然后通过它来安装cnpm

# 安装npminstall
npm i npminstall -g --registry=https://registry.npmmirror.com
# 通过npminstall安装cnpm
npminstall -c -g cnpm
# 升级cnpm
cnpm i cnpm -g
# 查看版本号:
cnpm -v

2.cnpm linux

不使用sudo的root权限安装,如果该命令npm i cnpm -g --registry=https://registry.npmmirror.com不行在尝试:

# 看看 npm 全局目录的路径
npm prefix -g

# 把 npm 的全局目录换个位置
mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
npm bin -g

# 把 ~/.npm-global/bin 加到 PATH
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

# 全新安装,请勿使用 sudo
npm i cnpm -g --registry=https://registry.npmmirror.com

# 升级新版本
cnpm i cnpm -g

# 检查版本号
cnpm -v

如果还不行可以尝试把npm全局目录权限改成当前用户

# 把 npm 的全局目录权限 owner 改为当前用户
sudo chown -R `whoami` `npm prefix -g`
sudo chown -R `whoami` ~/.npminstall_tarball

安装完毕如果找不到cnpm,看看是不是没加环境变量里面:

which cnpm
echo $PATH

1.2.3.Vue DevTools ★

开发过程中再安装一个vuejs的开发者工具:如果是edge浏览器直接打开应用市场搜索并安装即可

官网https://devtools.vuejs.org/guide/installation.html 源码https://github.com/vuejs/devtools

配置下:

之后F12后浏览器会有个vue选项,来方便我们开发和观察

2.第一个程序

先以我们熟悉的脚本引入写个demo:

<div id="app"></div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
    const app = Vue.createApp({
        template: '<h2>hello vue3</h2>'
    })
    app.mount('#app'); // 挂载到div#app上
</script>

3.VSCode配置默认终端

默认不是git bash,而是win的PowersShell,我们改下,这样可以方便使用cnpmshell:windows

4.VsCode代码片段(含光标位置)

在线配置:https://snippet-generator.app,开源备份地址:https://github.com/lotapp/snippet-generator

4.1.HTML混合开发阶段

比如这段代码每个页面都要输入,那就可以设置一个代码片段:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">
        
    </div>
    <!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
    <script src="../assets/vue3.js"></script>
    <script>
        const app = Vue.createApp({
            
        });
        app.mount('#app');
    </script>
</body>

</html>

把内容复制到左边文本框,设置下快捷输入的命令,以及简单描述下

代码复制到这个地方:首选项 > 配置用户代码片段 > 文件选html

粘贴到这个里面,最外面的两个大括号保留({}

如果网站挂了也可以自己手动改写代码片段,示例我贴下:prefix:编辑器快捷命令,body:模板内容,description:描述

{
	"create vue app": {
		"prefix": "createvue",
		"body": [
			"<!DOCTYPE html>",
			"<html>",
			"",
			"<head>",
			"    <meta charset=\"UTF-8\">",
			"    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">",
			"    <title>Document</title>",
			"</head>",
			"",
			"<body>",
			"    <div id=\"app\">",
			"        ",
			"    </div>",
			"    <!-- <script src=\"https://unpkg.com/vue@3/dist/vue.global.js\"></script> -->",
			"    <script src=\"../assets/vue3.js\"></script>",
			"    <script>",
			"        const app = Vue.createApp({",
			"            $0",
			"        });",
			"        app.mount('#app');",
			"    </script>",
			"</body>",
			"",
			"</html>"
		],
		"description": "create vue app"
	}
}

以后新建html的时候,输入createvue,就可以快速生成代码了

要指定生成后的光标可以设置下$0,代码生成后鼠标就自动停在那块

4.2.Vue单文件-选项API

vue文件也配置下:

{
	"vue file init": {
		"prefix": "vue3",
		"body": [
			"<template>",
			"    <div>",
			"        $1",
			"    </div>",
			"</template>",
			"",
			"<script>",
			"$2",
			"export default {",
			"    $3",
			"}",
			"</script>",
			"",
			"<style scoped>",
			"$0",
			"</style>"
		],
		"description": "vue file init"
	}
}

4.3.Vue单文件-组合API

{
	"vue file init": {
		"prefix": "vue3",
		"body": [
			"<script setup>",
			"$1",
			"</script>",
			"",
			"<template>",
			"    <div>",
			"        $2",
			"    </div>",
			"</template>",
			"",
			"<style scoped>",
			"$0",
			"</style>"
		],
		"description": "vue file init"
	}
}

其实类似这种vue的代码段,Vue VSCode Snippets中封装的很多,可以在开发中逐步熟悉:vxxx基本上就看到了

2.Vue3语法基础(引入)★

这块上一篇介绍Vue基础的时候基本上都说了,这边快速说下:文本插值、列表、函数、v-model......

文档代码:https://gitee.com/lotapp/BaseCode/tree/master/javascript/3.Vue/3vue3base

2.1.文本插值 {{xxx}}

变量定义在data之中,通过{{title}}显示在页面中,当变量改变时页面中会实时渲染(可以通过控制台app.title=xxx修改查看)

const app = Vue.createApp({
    data() {
        return {
            title: '你好,vue3' // 定义并赋值
        }
    },
    template: `<h2>{{title}}</h2>`
})
app.mount('#app')

PS:template里面的内容,相当于是写在<div id="app">中(如果div#app里面有东西,template里面也有东西,渲染出来的会是template内容)

<div id="app">
    <h2>{{title}}</h2>
</div>

官方文档:https://cn.vuejs.org/guide/essentials/template-syntax.html

隐藏渲染前的元素:v-cloak

有些JS加载或者Ajax操作比较耗时,页面会显示{{xxx}}

这种不太友好,Vue提供了一种v-cloak的方式来隐藏渲染前的dom,我们通过设置style里面的[v-cloak]来控制渲染前的样式

<!-- dom元素中添加v-cloak -->
<div v-cloak>{{title}}</div>

然后样式里面设置一下即可

[v-cloak] {
    display: none;
}

完整案例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        [v-cloak] {
            display: none;
        }
    </style>
</head>
<body>
    <div id="app">
        <!-- dom元素中添加v-cloak -->
        <div v-cloak>{{title}}</div>
    </div>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    title: 'demo'
                }
            }
        });
        app.mount('#app');
    </script>
</body>
</html>

尚未加载成功前不显示{{title}}而是被隐藏掉了。渲染成功后正常显示(v-cloak被vue移除掉了)

官方API:https://cn.vuejs.org/api/built-in-directives.html#v-cloak

2.2.属性绑定 v-bind

官方文档:https://cn.vuejs.org/api/built-in-directives.html#v-bind

API文档:https://cn.vuejs.org/api/built-in-directives.html#v-bind

设置dom的属性(eg:src、href、class),可以通过v-bind:xxx="XX"来设置,简写为:xxx="XX"

PS:还可以向另一个组件传递props

1.简单属性案例

<div id="app">
    <h2 :title="msg">鼠标放我身上看看</h2>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app=Vue.createApp({
        data() {
            return {
                msg: '我是一个属性值'
            }
        },
    })
    app.mount('#app')
</script>

效果如下:

扩展说明:属性值不能使用之前的{{xxx}}来绑定,需要使用特定的v-bind

错误写法:<button title="{{msg}}">{{msg}}</button>

微调下就生效了:<button :title="msg">{{msg}}</button>

2.样式绑定 :class★

在dom中写上:class="{'类名':bool}",bool可以是一个变量,也可以是一个bool结果的表达式

解析下代码:

  1. :class="{active:selectedStudent==name}给li设置了一个类active,这个类显示不显示就看selectedStudent变量是否和当前name相同
  2. @click="selectedStudent=name"当我单击li的时候,把当前name赋值给selectedStudent
    1. 这就意味着,我只要单击li,li的active类就生效了
<div id="app">
    <ul>
        <li v-for="name in students" :key="name" :class="{active:selectedStudent==name}"
            @click="selectedStudent=name">
            {{ name }}
        </li>
    </ul>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                selectedStudent: '',
                students: ['张三', '李四', '王二']
            }
        },
    })
    app.mount('#app') // 挂载
</script>

效果也是一样的:

3.多样式绑定

案例简单贴下::class="{'btn-bgcolor':bool,'active':bool}

dom自带的class:class可以同时使用,最后会把:class追加到class里面

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .btn-bgcolor {
            border: none;
            background-color: rgb(14, 230, 190);
        }
        .btn {
            padding: 10px 10px;
        }
        .active {
            color: white;
        }
    </style>
</head>
<body>
    <div id="app">
        <button class="btn" :class="{'btn-bgcolor':true,'active':isActive}" @click="change">我是一个按钮</button>
    </div>
    <!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
    <script src="../assets/vue3.js"></script>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    isActive: false
                }
            },
            methods: {
                change() {
                    this.isActive = !this.isActive;
                }
            },
        });
        app.mount('#app');
    </script>
</body>
</html>

效果:打开后是这样的,然后每次点击字体颜色在黑白之间切换

样式逻辑比较复杂的话也可以通过函数来实现上面的效果:就:class这边变换下

<button class="btn" :class="getClass()" @click="change">我是一个按钮</button>

然后method里面新增个getClass的方法,其他都一样

getClass() {
    return { 'btn-bgcolor': true, 'active': this.isActive }
}

之前的样式绑定都是属于对象绑定{},也可以数组绑定,eg:

<div :class="[{ 'active': isActive }, errorClass]"></div>

更多参考官方文档:https://cn.vuejs.org/guide/essentials/class-and-style.html

4.动态绑定属性值

语法::[xx]="xxx",比如:<img :[name]="value">,然后通过name控制属性名,value控制属性值

这边举一个常用案例v-bind="xxx":把学生信息以属性的方式绑定到span中:

<div id="app">
    <ul>
        <li v-for="student in students" :key="student.id">
            <!-- <span v-bind="student">{{student.name}}</span> -->
            <span :="student">{{student.name}}</span>
        </li>
    </ul>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                students: [
                    { id: 1, name: '张三', age: 22, gender: '男' },
                    { id: 2, name: '李四', age: 29, gender: '女' },
                    { id: 3, name: '王二', age: 32, gender: '男' }
                ]
            }
        },
    });
    app.mount('#app');
</script>

效果:

2.3.列表渲染 v-for

通过v-for进行循环遍历students数组中的内容

<div id="app">
    <ul>
        <li v-for="name in students" :key="name">
            {{ name }}
        </li>
    </ul>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                students: ['张三', '李四', '王二']
            }
        },
    })
    app.mount('#app')
</script>

效果:

官方文档:https://cn.vuejs.org/guide/essentials/list.html

官方API:https://cn.vuejs.org/api/built-in-directives.html#v-for

2.4.条件渲染 v-if

有个students数组,当里面没有数据的时候需要显示一下提示语,有数据则正常显示

<div id="app">
    <template v-if="students.length==0">
        <h3>暂时没有看到学生</h3>
    </template>
    <template v-else>
        <ul>
            <li v-for="name in students" :key="name">
                {{ name }}
            </li>
        </ul>
    </template>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                students: ['小明', '小张', '王二', 'goudan']
            }
        },
    });
    app.mount('#app');
</script>

当students数组里面没有数据时,显示v-if里面的内容,否则就显示v-else里面的内容(template不会显示)

条件渲染:https://cn.vuejs.org/guide/essentials/conditional.html

v-show和v-if最大的不同主要就是:v-show是通过css控制显示与否,不管显示不显示都会有dom,且不支持v-else、template

v-if只要不满足条件直接不渲染,如果一个元素频繁的显示和隐藏可以考虑v-show

官方API文档:https://cn.vuejs.org/api/built-in-directives.html#v-if

2.5.事件处理 v-on

通过method来定义函数,dom通过v-on:事件名="调用函数" 来调用对应的方法,简写:@事件名="调用函数"

1.简单案例

来个案例:通过两个按钮来控制count数值的加和减

<div id="app">
    <h3>计数:{{count}}</h3>
    <button @click="addCount">++</button>&nbsp;
    <button @click="subCount">--</button>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                count: 0
            }
        },
        methods: {
            addCount() {
                this.count++;
            },
            subCount() {
                this.count--;
            }
        },
    })
    app.mount('#app')
</script>

2.事件传参案例

如果@click后面方法没有参数,默认就是传event回来,被调用的函数可以是无参,也可以接收这个event

PS:通过 event.target 获取的是 <li> 元素本身,而不是student对象

<div id="app">
    <ul>
        <li v-for="student in students" :key="student.id" :title="student.name" @click="show">
            {{student.id}}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
        </li>
    </ul>
</div>
<script src=" ../assets/vue3.js">
</script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                students: [
                    { id: 1, name: '张三', age: 22, gender: '男' },
                    { id: 2, name: '李四', age: 29, gender: '女' },
                    { id: 3, name: '王二', age: 32, gender: '男' }
                ]
            }
        },
        methods: {
            show(event) {
                //如果@click后面方法没有参数,默认就是传event回来
                console.log(event.target.title); // event.target获取的是li元素本身,不是student哦~
            }
        },
    });
    app.mount('#app');
</script>

效果:单击就显示li的title

如果是多个参数,还需要event,可以通过$event传过来

PS:为什么event要加个$ ==> 你不加他到底是字符串,还是data中的变量呢?vue就不知道了

案例代码:

<div id="app">
    <ul>
        <li v-for="student in students" @click="show(student,$event)" :title="student.name" :key="student.id">
            {{student.id}}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
        </li>
    </ul>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                students: [
                    { id: 1, name: '张三', age: 22, gender: '男' },
                    { id: 2, name: '李四', age: 29, gender: '女' },
                    { id: 3, name: '王二', age: 32, gender: '男' }
                ]
            }
        },
        methods: {
            show(student, event) {
                console.log(student.id, student.name, student.age, student.gender);
                console.log(event.target.title);
            }
        },
    });
    app.mount('#app');
</script>

效果:

3.函数为什么不用箭头函数★

为什么不用箭头函数?==> 函数中的this不一样

<script>
    const app = Vue.createApp({
        methods: {
            test1: function () {
                console.log(this);
            },
            test2() { // es6写法,本质和test1的一样
                console.log(this);
            },
            test3: () => {
                console.log(this); // 这种虽然方便,但是this不一样了
            }
        },
    })
    app.mount('#app')
</script>

看下输出示意图:前面两个this都是app对象,而箭头函数里面的this就变成windows了

官方文档:https://cn.vuejs.org/guide/essentials/event-handling.html

https://cn.vuejs.org/api/built-in-directives.html#v-on

事件修饰符:https://cn.vuejs.org/guide/essentials/event-handling.html#event-modifiers

2.6.双向绑定 v-model

1.简单小案例

可以在表单 <input> <textarea><select> 元素上使用v-model来创建双向数据绑定

<div id="app">
    <input v-model="name" type="text" />
    <div>{{ name }}</div>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                name: '小明',
            }
        },
    })
    app.mount('#app')
</script>

文本框内容一改变,文字就改变

再看个综合的案例:列表显示学生信息 + 通过文本框输入内容,回车后添加到列表中

<div id="app">
    <input v-model="name" type="text" @keyup.enter="addStudent" />
    <div v-if="students.length==0">
        <h3>暂时没有学生</h3>
    </div>
    <ul v-else>
        <li v-for="name in students" :key="name">
            {{ name }}
        </li>
    </ul>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                name: '',
                students: ['张三', '李四', '王二']
            }
        },
        methods: {
            addStudent() {
                // 把文本框内容加到数组中
                this.students.push(this.name);
                this.name = '';// 清空文本框
            }
        },
    })
    app.mount('#app')
</script>

2.修饰符案例

  • .lazy:监听 change 事件而不是 input
    • 默认是刚输入就改变model,现在是回车、提交、事件改变后才修改model
  • .number: 将输入的合法字符串转为数字
  • .trim:移除输入内容两端空格
    <div id="app">
        <input type="text" v-model.lazy="name">
        <div>{{name}}</div>
    </div>
    <script src="../assets/vue3.js"></script>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    name: '小明'
                }
            },
        });
        app.mount('#app');
    </script>

三个合起来的案例:

<div id="app">
    <input v-model.lazy="name" type="text">
    <input v-model.number="age" type="text" />
    <input v-model.trim="rmark" type="text" />
    <div>{{name}}-{{age}}-{{rmark}}</div>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                name: '小明',
                age: 22,
                rmark: '暂无'
            }
        },
    });
    app.mount('#app');
</script>

更多可以查看文档:https://cn.vuejs.org/guide/essentials/forms.html

https://cn.vuejs.org/api/built-in-directives.html#v-model

2.7.性能优化 v-memo(new)

这个可以理解为一种缓存,只有设定的内容修改后才会出现渲染列表 ==> 用于性能优化(有时候数据一样,再重新渲染 大列表 就太浪费了

下面做个测试:v-memo="[name,age,gender]当name、age、gender改变时会出现渲染。rmark更新后不会重新渲染列表

<div id="app">
    <!-- 三个属性有一个改变都重新渲染 -->
    <div v-memo="[name,age,gender]">
        name:{{name}},age:{{age}},gender:{{gender}},rmark:{{rmark}}
    </div>
    <div>
        <button @click="updateName">修改name</button>
        <button @click="updateRMark">修改rmark</button>
        <button @click="updateInfo">修改所有</button>
    </div>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                name: '张三',
                age: 23,
                gender: '男',
                rmark: '暂无'
            }
        },
        methods: {
            updateName() {
                this.name = '李四';
            },
            updateRMark() {
                // 修改不在v-memo列表中的内容,页面不会出现渲染
                this.rmark = '修改';
            },
            updateInfo() {
                this.name = '测试';
                this.age = 44;
                this.gender = '中';
                this.rmark = '没有';
            }
        },
    });
    app.mount('#app');
</script>

官方文档:https://cn.vuejs.org/api/built-in-directives.html#v-memo

2.8.计算属性 computed

有复杂计算的时候直接在双括号{{}}里面写复杂表达式,是不合适的,可以通过computed来处理

<div id="app">
    <p v-for="i in 3" :key="i">
        {{ name }}-{{genderStr}}-{{dataTime}}
    </p>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                name: '张三',
                gender: 1,
                time: 1670944665
            }
        },
        computed: {
            genderStr() {
                console.log('gender')
                return this.gender == 1 ? '男' : '女';
            },
            dataTime() {
                console.log('dataTime')
                const date = new Date(this.time)
                return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
            }
        },
    });
    app.mount('#app');
</script>

computed比method多了个缓存:

官方文档:https://cn.vuejs.org/guide/essentials/computed.html

2.9.侦听器 watch

watch监听数据的案例:title修改可以直接监听到,而student只修改name则监听不到了(student中三个属性全部被修改才能监听到

<div id="app">
    <h2>{{title}}</h2>
    <ul>
        <li v-for="item in student" :key="item">
            {{ item }}
        </li>
    </ul>
    <div><button @click="changeInfo">修改内容</button></div>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                title: '欢迎光临~',
                student: { id: 1, name: '张三', age: 32 },
            }
        },
        methods: {
            changeInfo() {
                this.title = '网站已经被黑!';
                this.student.name = '德行';
                this.student.age = 32;
            }
        },
        watch: {
            title(newValue, oldValue) {
                console.log(`【${oldValue}】被修改为:【${newValue}】`)
            },
            student(newValue, oldValue) {
                console.log(newValue);
            }
        },
    });
    app.mount('#app');
</script>

如果想student对象一变化就侦听到,可以使用deep: true(慎重使用,数据太多太深会影响性能)如果一开始就触发监听就使用immediate: true

如果只是想检测student里面某个值的改变可以这样写:

官方文档:https://cn.vuejs.org/guide/essentials/watchers.html

2.10.综合案例

模拟一个购物车,有这些要求:

  1. 如果购物车没有数据要提示下
    1. v-if and v-else
  2. 通过加减可以设置书籍的数量,当数量<=1的时候,减按钮不能点
    1. :disabled="disable=item.count<2"
  3. 点击移除书籍可以删掉该图书
    1. 先获取当前book的index(注意不是id)v-for="(item,index) in books"
    2. 然后通过array.splice(index, 1);删掉对应对象
  4. 底部有个价格的汇总
    1. 通过computed,遍历books,价格*数量并累加

样式

<style>
    .border {
        border-collapse: collapse;
    }

    td {
        border: 1px solid #aaa;
        padding: 10px;
    }
</style>

HTML

<div id="app">
    <table class="border">
        <tr v-for="(item,index) in books" :key="index">
            <td>{{ item.id }}</td>
            <td>{{ item.name }}</td>
            <td>{{ item.price }}</td>
            <td>
                <button :disabled="disable=item.count<2" @click="subtract(item)">-</button>
                {{ item.count }}
                <button @click="add(item)">+</button>
            </td>
            <td><button @click="del(index)">移除书籍</button></td>
        </tr>
    </table>
    <h3 v-if="books.length>0">结算金额:{{totalCompute}}</h3>
    <h3 v-else>购物车中暂时没有书籍数据!</h3>
</div>

Script:

const app = Vue.createApp({
    data() {
        return {
            books: [
                { id: 1, name: 'Net安全', price: 44.5, count: 1 },
                { id: 2, name: 'Web安全', price: 64, count: 1 },
                { id: 3, name: 'Net基础', price: 38, count: 1 },
                { id: 4, name: 'Vue学习', price: 50.1, count: 1 },
            ]
        }
    },
    methods: {
        subtract(item) {
            item.count--;
        },
        add(item) {
            // this.books[id - 1].count++;
            item.count++;
        },
        del(index) {
            this.books.splice(index, 1); //删除数组序号为index的对象
        }
    },
    computed: {
        totalCompute() {
            // let total = 0;
            // // 遍历一下books,并把每一项价格统计下
            // this.books.forEach(book => {
            //     total += book.price * book.count;
            // });
            // return total;
     		return this.books.reduce((total, book) => total + book.count * book.price, 0);
        }
    },
});
app.mount('#app');

效果


3.Vue3组件入门(过渡)

目前这一块以选项APIOptions API)为主,后面会逐步过渡到组合APIComposition API

官方文档:https://cn.vuejs.org/guide/essentials/component-basics.html

深入组件:https://cn.vuejs.org/guide/components/registration.html

3.1.组件注册

  1. 全局组件:在任何其他的组件中都可以使用的组件
    1. app.component()
  2. 局部组件:局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用
    1. components:{}

1.app根组件

先看下全局组件的案例:我们平时创建的app对象,其实就是一个全局组件:

<div id="app">
    {{title}}
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                title: 'this is component test'
            }
        },
    });
    app.mount('#app');
</script>

我们换个写法和这个也一样的效果:

<div id="app">
    {{title}}
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    // vueApp对象{}
    const vueApp = {
        data() {
            return {
                title: 'this is component test'
            }
        },
    };
    const app = Vue.createApp(vueApp);
    app.mount('#app');
</script>

效果:

2.全局组件

全局组件:每个组件都可以在组件内部调用全局组件

PS:组件本身是可以有自己的代码逻辑的,比如data、computed、methods等等

我们自定义一个全局组件:app.component('组件名',{})

组件名最好是xxx-xxx的格式,Vue文件中也可以是AbcDemo的这种格式

const app = Vue.createApp({});
// 创建自定义的全局组件
app.component('my-title', {
    data() {
        return {
            title: 'this is component test'
        }
    },
    template: `<h2>{{title}}</h2>`
})
app.mount('#app');

HTML部分:

<div id="app">
    <my-title></my-title>
</div>

效果:template不出现在dom中

换种写法效果和这个一样(组件里面的template没有智能提示,放页面中就有了,这种写法后面会用

<div id="app">
    <my-title></my-title>
</div>

<!-- my-title组件的template-->
<template id="title">
    <h2>{{title}}</h2>
</template>

<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({});
    // 创建自定义的全局组件
    const myTitle = {
        data() {
            return {
                title: 'this is component test'
            }
        },
        template: '#title'
    };
    app.component('my-title', myTitle)
    app.mount('#app');
</script>

显示效果是一样的,template默认是不会显示出来的,但是dom里面是有的


3.局部组件

开发中基本上都是局部组件,还是上面的案例,我们用局部组件注册的方式改写下:

组件名一般都是xxx-xxx的格式,Vue文件中也可以是AbcDemo的这种大驼峰的命名格式

<div id="app">
    <my-title></my-title>
</div>
<script src="../assets/vue3.js"></script>
<script>
    // 根组件
    const App = {
        components: {
            // 局部组件名:对象内容
            'my-title': {
                data() {
                    return {
                        title: 'this is vue component test'
                    }
                },
                template: '<h2>{{title}}</h2>',
            },
        },
    };
    Vue.createApp(App).mount('#app'); //创建并挂载
</script>

展示效果:

进一步分离实现相同效果:

<div id="app">
    <my-title></my-title>
</div>

<template id="title">
    <h2>{{title}}</h2>
</template>

<script src="../assets/vue3.js"></script>
<script>
    // 自定义组件名
    const MyTitle = ('my-title', {
        data() {
            return {
                title: 'this is vue component test'
            }
        },
        template: '#title',
    });
    // 根组件
    const App = {
        components: {
            MyTitle, // 局部组件
        },
    };
    Vue.createApp(App).mount('#app'); //创建并挂载
</script>

展示:

3.2.单文件组件

文档代码:https://gitee.com/lotapp/BaseCode/tree/master/javascript/3.Vue/4vue3component

.vue单文件里面就三块内容:<template>放HTML、<script>放JS、<style>放CSS

PS:xxx.vue浏览器是不认识的,最后是通过webpack、vite这类打包工具进行打包,最后生成了类似我们上面写的代码风格的html

单个Vue组件中可以有更多的支持

  1. 代码高亮
  2. ES6、CommonJS的模块化能力;
  3. 组件作用域的CSS
  4. 预处理器来构建组件
    1. eg:TypeScriptBabelLessSass

说那么多,怎么去使用呢?

  1. 通过Vue CLI创建项目,所有配置都默认配置好了,我们在里面直接使用Vue文件开发即可
  2. 通过webpack、vite这类打包工具进行打包处理

1.脚手架创建项目

vue官方现在推荐使用:cnpm create vue@latest来创建。这个打包默认使用的是vite了

PS:vue-cli默认使用的是webpack,现在已经不更新了

cd vue3-demo切换到新建的目录中,然后输入cnpm install来安装一下依赖。然后运行项目cnpm run dev

PS:vscode终端中运行npm run dev(PowerShell中运行cnpm有点问题)

命令行输入:运行npm run dev编译npm run build预览npm run preview,IDE运行:点下调试选对应选项即可

我们也可以使用图形化操作,bash里面输入:vue ui,然后会自动打开一个UI页面,在里面新建或者选择项目也行

build的话会在原项目下面创建一个dist的发布文件夹,我们写的代码都打包成这里面的文件了,直接部署到服务器即可

2.目录说明

vscode打开后大致这样的感觉,我们平时在src中开发即可,index.html是主页面

main.js是主函数,里面会加载main.css以及挂载vue对象。这个导入进来的createApp方法,相当于是以前写的vue.createApp

PS:从App.vue中导入进来的App对象,相当于之前写的const App={xxx}

每一个xxx.vue都相当于一个独立的组件,里面就三个元素:<template><srcipt><style>


3.根组件案例(含详细说明)

以前我们要创建一个vue组件都是在html中写,比如:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        h2 {
            color: red;
        }
    </style>
</head>
<body>
    <div id="app">
        <h2>{{title}}</h2>
    </div>
    <script src="../../assets/vue3.js"></script>
    <script>
        const App = {
            data() {
                return {
                    title: 'this is vue component test'
                }
            },
        };
        Vue.createApp(App).mount('#app');
    </script>
</body>
</html>

运行后是这样的:


现在用Vue文件开发略有不同,我这边详细说下:

我们看下main.js:导入了vue.createApp方法、也从App.vue中导入了App对象{}

这里的#appindex.html中写了

贴一下App.vue的根组件:如果晕头转向请自己练一遍,然后就清楚了

脚手架创建后可以把不相关的vue文件删掉,只保留App.vue(把main.cssbase.css中的样式先删掉)

<template>
  <h2>{{ title }}</h2>
</template>

<script>
// 有import导入就有export导出
export default {
  data() {
    return {
      title: 'this is vue component test'
    }
  },
}
</script>

<style>
h2 {
  color: red;
}
</style>

运行项目可以手动点vscode里面的调试运行,也可以Ctrl + J打开vscode终端,命令行输入npm run dev

如果不需要调试:开发过程中也可以在项目目录下单独开一个bash,输入npm run dev,然后不关闭它,你修改它也会同步修改的

运行效果:


4.全局组件-vue文件版

这种方式平时用的不多,简单说下:

MyTitle.vue:

<script>
// 有import导入就有export导出
export default {
  data() {
    return {
      title: 'this is vue component test'
    }
  },
}
</script>

<template>
  <h2>{{ title }}</h2>
</template>

<style>
h2 {
  color: red;
}
</style>

App.vue:

<template>
  <MyTitle></MyTitle>
</template>

main.js

import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import MyTitle from './components/MyTitle.vue'

const app = createApp(App)
app.component('MyTitle', MyTitle)
app.mount('#app')

效果:

5.局部组件-vue文件版

平时基本上都是这种局部组件的方式开发,还是上面的案例,贴下代码:

PS:以后我就直接贴组件.vueApp.vue,这边把相关文件都贴下

1MyTitle.vue代码:

<script>
// 有import导入就有export导出
export default {
  data() {
    return {
      title: 'this is vue component test'
    }
  },
}
</script>

<template>
  <h2>{{ title }}</h2>
</template>

<style>
h2 {
  color: red;
}
</style>

2App.vue代码:

<script>
// 1先拿到组件-导入MyTitle
import MyTitle from './components/MyTitle.vue';

export default {
  components: {
    MyTitle, // 2写下使用到的局部组件MyTitle
  },
}
</script>

<template>
  <!-- 3使用自定义组件 -->
  <MyTitle></MyTitle>
</template>

<style></style>

3main.js内容不变:

import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

4index.html内容不变:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

项目运行后结果:

6.样式的作用域

之前代码其实有个问题,比如代码和局部组件一样的情况下,我App.vue里面也有个<h2>,而h2的样式其实是在MyTitle组件中设置的,但也影响到了App.vue

解决也很简单,就是在样式里面设置一个作用域:<style scoped>

这样MyTitle.vue中设置的样式就不会影响到其他组件了(其实本质就是给他设置了一个属性选择器

7.组件嵌套

父组件用了几个子组件就导入几个,不用管子组件里面套了多少层,来个组件树的案例:

App.vue:对于App.vue来说,我的子组件就这四个

AppHeader.vue

AppFlooter.vue

AppMain.vue:就一个子组件AppList

AppSideBar.vue:也只有一个子组件

AppList.vue:里面一个子组件,重复2次

AppItem.vue,来个数据展示

运行后效果:

3.3.组件通信

父子组件间的通信比较简单,父传子是通过props传递,子传父是通过$emit回传

1.父组件发信息给子组件-props★

父组件给子组件传递信息的本质就是:在HTML标签上打上自定义属性,eg:<span name="张三" age="22" gender="男">

然后子组件通过props的配置,eg:props:['name','age','gender'],知道去获取哪些自定义属性,并得到他们的值:

来个案例:StudentInfo.vue

<template>
    {{ id }}.{{ name }}-{{ age }}-{{ gender }}
</template>

<script>
export default {
    // props的作用就是接收父组件传递过来的属性
    props: ['id', 'name', 'age', 'gender']
}
</script>

<style scoped></style>

传递本质就是这种:{{ xxx }}除了在data里面找数据,也会去props里面查找

<template>
	<!-- {{ xxx }}除了在data里面找数据,也会去props里面查找 -->
    <StudentInfo id="1" name="小明" age="22" gender="男"></StudentInfo>
</template>
<script>
// 1.父组件传递信息给子组件
import StudentInfo from './components/StudentInfo.vue'
export default {
    components: {
        StudentInfo,
    },
}
</script>

但vue中有自定义属性,可以更方便处理,我们改写成灵活的写法:

<template>
    <StudentInfo :id="id" :name="name" :age="age" :gender="gender"></StudentInfo>
    <!-- <AppSon :students="objs"></AppSon> -->
</template>

<script>
// 1.父组件传递信息给子组件
import StudentInfo from './components/StudentInfo.vue'
export default {
    components: {
        StudentInfo,
    },
    data() {
        return {
            id: 1,
            name: '小明',
            age: 22,
            gender: '男'
        }
    },
}
</script>

效果:1.小明-22-男


props除了传递列表,还可以传递对象,并在这个对象里面定义数据类型、默认值、是否必传(require默认是false)等,来个案例:

PS:props中的type类型可以是:String、Number、Boolean、Array、Object、Date、Function、Symbol

AppSon.vue

<template>
    <ul>
        <li v-for="student in students" :key="student.id">
            {{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
        </li>
    </ul>
</template>

<script>

export default {
    props: {
        students: {
            type: Array,
            default: []
        },
    },
}
</script>

<style scoped></style>

App.vue

<template>
    <AppSon :students="objs"></AppSon>
</template>

<script>
import AppSon from './components/AppSon.vue';

export default {
    components: {
        AppSon,
    },
    data() {
        return {
            objs: [
                { id: 1, name: '张三', age: 22, gender: '男' },
                { id: 2, name: '李四', age: 29, gender: '女' },
                { id: 3, name: '王二', age: 32, gender: '男' }
            ]
        }
    },
}
</script>

效果:

2.子组件发信息给父组件-$emit★

子组件给父组件传递信息,其实是通过某个事件方法执行了:$emit(自定义事件,传递的值),当自定义事件触发后就会调用父方法进行数值的处理

为了方便理解,我事件名都弄不一样的:

说下流程,用户文本框里面输入内容,回车后触发子组件定义的method;这个method里面执行了$emit并把文本框清空;接着自定义的@addStudent就被触发了,从而调用父组件的方法parentMethod对数据进行进一步处理

AppSon1.vue:

<template>
    <div>
        <input v-model="name" type="text" @keyup.enter="sonMethod" />
    </div>
</template>

<script>
export default {
    data() {
        return {
            name: ''
        }
    },
    emits: ['addStudent'], // 为了代码提示和协同开发
    methods: {
        sonMethod() {
            console.log('into appson1.vue sonMethod');
            this.$emit('addStudent', this.name); // 把name值传递给组件自定义事件addStudent
            this.name = '';
        }
    },
}
</script>

<style scoped></style>

App.vue:

<template>
    <!-- 3.子组件传递信息给父组件 -->
    <span v-for="student in students" :key="student">
        {{ student }}-
    </span>
    <AppSon1 @addStudent="parentMethod"></AppSon1>
</template>

<script>
// 3.子组件传递信息给父组件
import AppSon1 from './components/AppSon1.vue';
export default {
    components: {
        AppSon1
    },
    data() {
        return {
            students: [],
        }
    },
    methods: {
        // 自定义addStudent触发后,会调用对应的parentMethod方法
        parentMethod(name) {
            console.log('into app.vue parentMethod');
            this.students.push(name);
        }
    },
}
</script>

输入小明回车,再输入小华回车,就现在这个效果:

扩展补充下:对于协同开发来说,你可能是开发的父组件,但是子组件是另一个人开发的,emit在逻辑里面,需要找半天,很麻烦。vue3提供了一种emits的参数支持,以后在定义自定义事件的时候把emits列表里面也写下,这样协同开发比较方便,而且vscode也会有对应的提示

比如,我在emits里面定义了这几个自定义事件:emits: ['addStudent','delStudent','updateStudent']

emits也可以是对象,可以对传给父组件的值进行验证,平时列表就够了,扩展可看官网:https://cn.vuejs.org/api/options-state.html#emits

那么我在vscode开发的时候,父组件在写事件的时候就会有提示了

再来个navbar的案例:TabBar.vue

<template>
    <div id="nav">
        <div :id="key" v-for="(value, key, index) in contents" @click="getContent(value)" :key="index">
            <span :class="{ active: currentIndex == index }" @click="currentIndex = index">
                {{ key }}
            </span>
        </div>
    </div>
</template>

<script>
export default {
    data() {
        return {
            currentIndex: -1,
            contents: {
                'left': '我是Left的内容',
                'center': '我是Center的内容',
                'right': '我是Right的内容'
            }
        }
    },
    emits: ['content'],
    methods: {
        getContent(value) {
            this.$emit('content', value)
        }
    },
}
</script>

<style scoped>
.active {
    font-weight: bold;
    padding: 10px;
    border-bottom: solid 2px red;
}

#nav {
    display: flex;
    background-color: rgb(255, 251, 255);
}

#left {
    flex: 1;
    text-align: center;
    padding: 10px;
    /* background-color: rgb(245, 201, 230); */
}

#center {
    flex: 3;
    text-align: center;
    padding: 10px;
    /* background-color: rgb(246, 246, 161); */
}

#right {
    flex: 1;
    text-align: center;
    padding: 10px;
    /* background-color: rgb(87, 243, 178); */
}
</style>

App.vue

<template>
    <div>
        <TabBar @content="getContent"></TabBar>
        <div id="bar-item">{{ content }}</div>
    </div>
</template>

<script>
import TabBar from './components/TabBar.vue';
export default {
    components: {
        TabBar,
    },
    data() {
        return {
            content: ''
        }
    },
    methods: {
        getContent(value) {
            this.content = value;
        }
    },
}
</script>

<style scoped>
#bar-item {
    text-align: center;
    padding: 20px;
    background-color: rgb(249 248 248);
}
</style>

效果:


3.插槽案例☆

插槽通俗讲就是:子组件预留一个坑位,父组件后期来填坑

PS:如果插槽没有放元素,则显示默认内容。如果放了内容,插槽原先的内容会被忽略掉

3.1.默认插槽

每个插槽都有一个具体的名字,当子组件里面就一个插槽的时候,我们不用写(default),vue自动帮我们处理了

SlotSimple.vue

<template>
    <div>
        <h2>slot案例:</h2>
        <slot>
            我是默认的内容
        </slot>
    </div>
</template>

<script>

export default {
    
}
</script>

<style scoped>

</style>

App.vue

<template>
    <div>
        <SlotSimple></SlotSimple>
        <SlotSimple>
            <button>slot one btn</button>
        </SlotSimple>
    </div>
</template>

<script>
import SlotSimple from './components/SlotSimple.vue';
export default {
    components: {
        SlotSimple,
    },
}

</script>

<style scoped></style>

效果:上面的父组件没填坑,就会显示子组件里默认的内容,下面的父组件放了一个按钮,则插槽内容显示自定义的button

3.2.具名插槽☆

子组件里面写下<solt name=xxx>,父组件在插槽内容外包裹一个<template>,eg:<template v-slot=xxx> or <template #xxx>

SlotName.vue:

<template>
    <div>
        <h2>slot案例:</h2>
        <slot name="default">
            我是默认的slot
        </slot>
        <slot name="center">
            我是默认的Center
        </slot>
        <slot name="right">
            我是默认的Right
        </slot>
    </div>
</template>

App.vue<template v-slot:center><template #center>效果一样

<template>
    <div>
        <SlotName>
            <template #default>
                <h3>default</h3>
            </template>
            <template v-slot:center>
                <h3>center</h3>
            </template>
            <template #right>
                <h3>right</h3>
            </template>
        </SlotName>
    </div>
</template>

<script>
import SlotName from './components/SlotName.vue';

export default {
    components: {
        SlotName,
    },
}
</script>

效果:

3.3.动态插槽名

父组件里面的插槽名也可以使用:动态插槽名,这个用的不多就不写案例了,子组件和具名一样定义,就是父组件这边v-slot:[变量名]

  1. 子组件正常定义具名插槽:<solt name=xxx>
  2. 父组件支持动态插槽名的控制:<template v-slot:[变量名]>

3.4.作用域插槽☆

说作用域插槽的时候先看一个编译作用域的概念:

比方说我现在有一个子组件TabBar.vue,这个子组件里面的data数据,父组件App.vue是不能直接访问的(ps:能相互访问还搞啥通信)

反过来也一样,子组件也不能直接访问父组件里面的data(父组件可以直接访问父组件的数据)

现在要说的是:子组件里定义了一个slot插槽,开始内容是在父组件里面书写的,父组件有数据还好,如果父组件没有插槽里面的数据呢?可以直接传过去吗? ==> 可以的,vue提供了一种便捷方法

为什么要从solt中传递参数给父组件? ==> 受编译作用域限制,拿不到子组件的变量,只能通过pros拿到

作用域插槽的本质就是:把子组件的数据传递给父组件的插槽使用。看个案例:

SlotProps.vue:把值绑定到属性上,template的name和上面一样,是唯一的

<template>
    <div>
        <template v-for="(v, k, i) in contents" :key="i">
            <slot :name="k" :k="k" :v="v" :i="i"></slot>
        </template>
    </div>
</template>

<script>

export default {
    data() {
        return {
            contents: {
                'left': '我是Left的内容',
                'center': '我是Center的内容',
                'right': '我是Right的内容'
            }
        }
    },

}
</script>

App.vuev-slot:ID名称="props" ==> 简写:#ID名称="props"

<template>
    <div>
        <SlotProps>
            <template v-slot:left="props">
                <p>{{ props.i }} - {{ props.k }} - {{ props.v }}</p>
            </template>
            <template v-slot:center="props">
                <p>{{ props.i }} - {{ props.k }} - {{ props.v }}</p>
            </template>
            <template #right="props">
                <p>{{ props.i }} - {{ props.k }} - {{ props.v }}</p>
            </template>
        </SlotProps>
    </div>
</template>

<script>
import SlotProps from "./components/SlotProps.vue";
export default {
    components: {
        SlotProps,
    },
}
</script>

效果

4.非父子组件通信

如果深层次嵌套的组件想要传递数据,靠props一层层传递会非常麻烦,vue提供了Provide(父组件提供数据)和Inject(子孙组件进行注入)和全局事件总线来实现非父子组件之间的通信

PS:真实开发中一般都是用诸如:Pinia官方推荐)、Vuex(已不更新)之类的状态管理库

4.1.依赖注入案例

来个案例,点击按钮可以修改信息,这样可以验证一下数据是否一致

先说下下树结构:我们现在不想层层传递数据,想直接把app.vue的数据传给appItem.vue

PS:依赖注入是针对父级组件与子孙级组件之间的数据传递(隔了很多层,我们不想一层层传递数据,就可以使用依赖注入)

AppItem.vueinject是一个列表

<template>
    <div>
        <h2>{{ title }}</h2>
        {{ student.name }}-{{ student.age }}-{{ student.gender }}
    </div>
</template>

<script>
export default {
    inject: ['title', 'student'] // 注入title、student
}
</script>

App.vueprovide是方法的形式(不是方法this就获取不到data里面的数据了)

<template>
    <div>
        <AppMain></AppMain>
        <button @click="changeStudent">改数值</button>
    </div>
</template>
<script>
import AppMain from './components/AppMain.vue';

export default {
    data() {
        return {
            title: '这是一个依赖注入案例',
            student: {
                name: '小明',
                age: 22,
                gender: '男'
            }
        }
    },
    components: {
        AppMain,
    },
    // 要用provide方法,不然this就不对了,获取不到data信息了
    provide() {
        return {
            title: this.title,
            student: this.student
        }
    },
    methods: {
        changeStudent() {
            this.student.name = '长孙无敌';
            this.student.age = 33;
            this.title = '网站已经被黑';
        }
    },
}
</script>

打开后是这样的,看起来貌似没有问题:

但是点击修改数值后发现:title没有被修改,student类型反而被修改了

PS:引用类型这种没有问题,简单类型你传递title: this.title相当于传递了这个:title:'这是一个依赖注入案例'所以要处理下

我们导入响应式API中的computed函数,微微修改下App.vue

<template>
    <div>
        <AppMain></AppMain>
        <button @click="changeStudent">改数值</button>
    </div>
</template>

<script>
// 导入computed函数
import { computed } from 'vue';
    
import AppMain from './components/AppMain.vue';

export default {
    data() {
        return {
            title: '这是一个依赖注入案例',
            student: {
                name: '小明',
                age: 22,
                gender: '男'
            }
        }
    },
    components: {
        AppMain,
    },
    // 要用provide方法,不然this就不对了,获取不到data信息了
    provide() {
        return {
            // 变化点在这,title不是直接复制一个简单的字段,而是计算属性
            title: computed(() => {
                return this.title;
            }),
            student: this.student
        }
    },
    methods: {
        changeStudent() {
            this.student.name = '长孙无敌';
            this.student.age = 33;
            this.title = '网站已经被黑';
        }
    },
}
</script>

再点一下就发现标题也修改了

更多可以查看文档:https://cn.vuejs.org/guide/components/provide-inject.html

4.2.事件总线案例

依赖注入必须是有子孙关系的组件通信,如果来个不相关的组件通信呢?==> 事件总线

PS:这个有点类似后端开发中MQ的订阅与发布

我们来个树结构:现在AppHeader要和AppItem进行通信

Vue2里面是有事件总线,vue3从实例中移除了$on、$off、$once方法,如果要使用事件总线可以使用官方推荐的库:mitttiny-emitter

首先安装一下mitt:cnpm i mitt,然后我们来封装一个事件总线的js库(eventBus.js):

import mitt from 'mitt';
const eventBus = new mitt();
export default eventBus;

AppHeader作为我们数据的发送方:(发布消息

<template>
    <div>
        <button @click="sendMessage">AppHeader send message</button>
    </div>
</template>

<script>
import eventBus from '../util/eventBus'

export default {
    data() {
        return {
            title: '这是一个事件总线的订阅发布案例',
            student: {
                name: '小华',
                age: 32,
                gender: '男'
            }
        }
    },
    methods: {
        sendMessage() {
            console.log('AppHeader.vue sendMessage')
            // 发送事件,传递消息
            eventBus.emit('sendData', {
                title: this.title,
                student: this.student
            });
        }
    },
}
</script>

AppItem作为信息接收方:(订阅消息unmounted一定也写下,组件销毁后事件就没必要继续订阅了

<template>
    <div v-if="title != '' && student != []">
        <h2>{{ title }}</h2>
        {{ student.name }}-{{ student.age }}-{{ student.gender }}
    </div>
</template>

<script>
import eventBus from '../util/eventBus';

export default {
    data() {
        return {
            title: '',
            student: {}
        }
    },
    methods: {
        getMessage(data) {
            console.log('AppItem.vue getMessage');
            this.student = data.student;
            this.title = data.title;
        }
    },
    created() {
        console.log('AppItem.vue created');
        eventBus.on('sendData', this.getMessage); // 订阅事件sendData,触发后调用getMessage
    },
    unmounted() {
        console.log('AppItem.vue mounted');
        eventBus.off('sendData', this.getMessage); // 组件销毁的时候取消事件订阅
    },
}
</script>

运行后的效果:

当我们触发事件发布时:信息正常被发送并接收了

3.4.生命周期★

每个组件都会经历:创建挂载更新卸载等系列的过程,我们可以在对应阶段做一些操作(eg:create阶段可以异步加载数据)

Vue提供了一些生命周期的回调函数(钩子函数)https://cn.vuejs.org/api/composition-api-lifecycle.html

1.简单说明

  1. beforecreate // 执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
    1. eg:App.vue中有个<AppItem>,vue自己初始化完毕,准备去创建<AppItem>实例,但是还没创建
  2. created★ // 组件实例(js对象)创建完毕,各种数据可以使用,常用于异步数据获取、事件监听、this.$watch()
  3. eg:<AppItem>组件实例已经创建了,但是里面<template>还没编译
  4. beforeMounted // 未执行渲染、更新,dom未创建
    1. eg:这时候<AppItem><template>内容已经有了,但是还没挂载到App.vue上(还没挂载到虚拟DOM)
  5. mounted ★// 初始化(挂载)结束,dom已创建,可用于获取访问数据和dom元素
    1. eg:已经挂载到虚拟DOM上,并且生成了真实的DOM(用户可以看到HTML元素了)
  6. beforeupdate // 更新前,可用于获取更新前各种状态
    1. 当我们数据发生改变的时候,会通过DIFF算法,重新渲染和更新DOM
  7. updated // 更新后,所有状态已是最新
    1. 在数据更新完毕,重新进入mounted 挂载之前,会进入updated
  8. beforeUnmount // 销毁前,可用于一些定时器或订阅的取消(组件还在
    1. PS:当组件显示与否通过v-if控制时,不显示的时候就会把组件销毁,在销毁前进入这个函数
  9. unmounted★ // 组件已销毁,可用于一些定时器或订阅的取消(组件不在了
    1. 已经移除掉组件的虚拟DOM了,组件实例会被销毁掉

更多参考官方文档:https://cn.vuejs.org/guide/essentials/lifecycle.html

生命周期钩子:https://cn.vuejs.org/api/composition-api-lifecycle.html

2.案例演示

App.vue:

<template>
    <div>
        <div>
            <button @click="b = !b">是否显示组件</button>
        </div>
        <template v-if="b">
            <AppItem :count="count">
                <button @click="addCount">count++</button>
            </AppItem>
        </template>
    </div>
</template>

<script>
import AppItem from './components/AppItem.vue';

export default {
    components: { AppItem },
    data() {
        return {
            b: false,
            count: 0
        }
    },
    methods: {
        addCount() {
            this.count++;
        }
    }
}
</script>

AppItem.vue

<template>
    <div>
        {{ count }}&nbsp;<slot></slot>
    </div>
</template>

<script>
export default {
    props: ['count'],
    beforeCreate() {
        console.log('AppItem beforeCreate');
    },
    created() {
        console.log('AppItem created');
    },
    beforeMount() {
        console.log('AppItem beforeMount');
    },
    mounted() {
        console.log('AppItem mounted');
    },
    beforeUpdate() {
        console.log('AppItem beforeUpdate');
    },
    updated() {
        console.log('AppItem updated');
    },
    beforeUnmount() {
        console.log('AppItem BeforeUnmount');
    },
    unmounted() {
        console.log('AppItem Unmounted');
    },
}
</script>

运行后因为v-if=false,所以AppItem没有加载:

我们现在点一下按钮:AppItem组件被创建并挂载了

我们点3下count++按钮就会触发三次数据修改函数(AppItem组件销毁前基本上都beforeUpdate、updated中来回反复)

我们再点下是否显示组件的按钮,这时候v-if又变成false了,AppItem组件就被卸载了

3.5.组件ref引用☆

$refs案例

$refs场景:有些时候组件中想要直接获取到元素对象、子组件实例(比如获取元素的宽高)【很少用到】

Vue不推荐进行原生DOM操作 ==> 给元素或者组件绑定一个ref的attribute属性

来个例子:把dom元素里面加个属性:ref="名字",然后就可以通过:this.$refs.名称获取到dom

AppItem.vue

<template>
    <div>
        <input v-model="title" type="text" ref="title" />
        <button ref="btn" @click="getRefs">提交</button>
    </div>
</template>

<script>

export default {
    data() {
        return {
            title: 'title'
        }
    },
    methods: {
        getRefs() {
            console.log(this.$refs.title);
            console.log(this.$refs.btn);
        }
    },
}
</script>

效果:提交的时候就可以获取到input的dom和button的dom对象

如果ref绑定在组件上,可以获取组件的实例,通过这个实例可以调用里面的方法、字段,也可以获取里面的dom($el

获取组件DOMthis.$refs.item.$el获取组件数据this.$refs.item.xxx调用组件方法this.$refs.item.xxx();

AppItem.vue还是上面的代码

<template>
    <div>
        <input v-model="title" type="text" ref="title" />
        <button ref="btn" @click="getRefs">提交</button>
    </div>
</template>

<script>

export default {
    data() {
        return {
            title: 'title'
        }
    },
    methods: {
        getRefs() {
            console.log(this.$refs.title);
            console.log(this.$refs.btn);
        },
    },
}
</script>

App.vue修改了下:

<template>
    <div>
        <button @click="callItem">ref调用AppItem</button>
        <AppItem ref="item"></AppItem>
    </div>
</template>

<script>
import AppItem from './components/AppItem.vue';

export default {
    components: { AppItem },
    methods: {
        callItem() {
            console.log(this.$refs.item.$el);
            console.log(this.$refs.item.title);
            this.$refs.item.getRefs();
        }
    },
}
</script>

效果:

扩展:\(parent、\)root

this.$parent ==> 获取父组件实例this.$root ==> 获取根组件实例(Vue3中已经删掉$children

PS:这个用的不多,和上面ref获取组件差不多,eg:this.$parent.$el

3.6.动态组件

1.切换组件的传统开发

讲动态组件之前先来个案例:现在有三个按钮,按钮名对应着同名的组件,我们点击按钮的时候下面显示对应的组件内容+按钮字变红

PS:我们平时如果要实现多组件切换,基本上都是通过:v-if、v-else-if 、v-else来实现:

AppLeft、AppCenter、AppRight代码差不多,就H2这边名字不一样:

<template>
    <div>
        <h3>AppCenter</h3>
        <ul>
            <li v-for="item in students" :key="item.id">
                {{ item.id }}-{{ item.name }}-{{ item.Age }}-{{ item.gender }}
            </li>
        </ul>
    </div>
</template>

<script>

export default {
    props: ['students']
}
</script>

<style scoped></style>

App.vue

<template>
    <div>
        <button :class="{ active: currName == name }" v-for="name in  names " :key="name" @click="changeTab(name)">
            {{ name }}
        </button>
        <template v-if="currName == 'AppLeft'">
            <AppLeft :students="students"></AppLeft>
        </template>
        <template v-else-if="currName == 'AppCenter'">
            <AppCenter :students="students"></AppCenter>
        </template>
        <template v-else>
            <AppRight :students="students"></AppRight>
        </template>
    </div>
</template>

<script>
import AppCenter from './components/AppCenter.vue';
import AppLeft from './components/AppLeft.vue';
import AppRight from './components/AppRight.vue';

export default {
    data() {
        return {
            names: ['AppLeft', 'AppCenter', 'AppRight'], // 组件名称
            currName: 'AppLeft', // 相当于索引
            students: [ // 传递给子组件的数据
                { id: 1, name: '小明', age: 22, gender: '男' },
                { id: 2, name: '小华', age: 33, gender: '女' },
                { id: 1, name: '小花', age: 28, gender: '男' },
            ]
        };
    },
    methods: {
        changeTab(name) {
            this.currName = name; // 把当前tab名赋予currName,这样active类就生效了
        }
    },
    components: { AppLeft, AppCenter, AppRight } // 子组件局部注册
}
</script>

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

运行效果:点击谁就显示谁

2.切换组件-Vue动态组件

这种if else的方式判断数据少还好,数据多了能写死,vue提供了一种动态组件的方式

语法:<component :is="tabs[currentTab]"></component>

被传给 :is 的值可以是:1.被注册的组件名(全局、局部)2.导入的组件对象

不用改任何代码,就是把一系列的判断改成了:<component :is="currName" :students="students"></component>

App.vue:is="组件名":student="students"是父组件传给子组件的数据

<template>
    <div>
        <button :class="{ active: currName == name }" v-for="name in names" @click="changeTab(name)" :key="name">
            {{ name }}
        </button>
        <component :is="currName" :students="students"></component>
    </div>
</template>

<script>
import AppCenter from './components/AppCenter.vue';
import AppLeft from './components/AppLeft.vue';
import AppRight from './components/AppRight.vue';


export default {
    data() {
        return {
            names: ['AppLeft', 'AppCenter', 'AppRight'],
            currName: 'AppLeft',
            students: [
                { id: 1, name: '小明', age: 22, gender: '男' },
                { id: 2, name: '小华', age: 33, gender: '女' },
                { id: 1, name: '小花', age: 28, gender: '男' },
            ]
        };
    },
    methods: {
        changeTab(name) {
            this.currName = name;
        }
    },
    components: { AppLeft, AppCenter, AppRight }
}
</script>

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

运行效果一样:点击谁就显示谁

3.7.keep-alive☆

1.组件切换后被销毁

keep-alive相当于是一个组件的状态缓存,我们先看个例子,看完案例就知道为什么引入keep-alive

AppLeft、AppCenter、AppRight内容差不多,就名称不一样:我们写两个生命周期的函数来监听

<template>
    <div>
        <h3>Appxxx:{{ count }}</h3>
        <button @click="count++">count++</button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            count: 0
        }
    },
    created() {
        console.log('Appxxx created');
    },
    unmounted() {
        console.log('Appxxx unmounted');
    }
}
</script>

<style scoped></style>

App.vue:用的还是动态组件<component :is="组件名"></component>

<template>
    <div>
        <button :class="{ active: currName == name }" v-for="name in names" @click="changeTab(name)" :key="name">
            {{ name }}
        </button>
        <component :is="currName"></component>
    </div>
</template>
<script>
import AppLeft from './components/AppLeft.vue';
import AppRight from './components/AppRight.vue';
import AppCenter from './components/AppCenter.vue';

export default {
    data() {
        return {
            names: ['AppLeft', 'AppCenter', 'AppRight'],
            currName: 'AppLeft'
        };
    },
    methods: {
        changeTab(name) {
            this.currName = name; // 当前选项卡名称赋值给currName
        }
    },
    components: { AppLeft, AppCenter, AppRight }
}
</script>

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

打开后AppLeft组件已经被创建,我们点3下按钮,AppLeft中的count值为3

切换到AppCenter组件:控制台提示AppLeft已经被卸载了

我们重新切换会AppLeft,发现count=3已经没了,又变成初始的0了。。。

2.keep-alive来缓存

试想一下,我们如果是实际使用中写个复杂表单,写大半了,不小心切换了下标签,内容直接没了,是什么心情?==> keep-live即可解决

上面演示的代码不用修改,就是在原有基础上增加一个<KeepAlive>即可

效果:组件不会被卸载,而且里面的值切换后还在(不会重置,而是被缓存了)

切换过来内容还是在的,且组件都没有被销毁

如果只想要某个选项缓存,其他不缓存,可以使用include - string | RegExp | Array(名字匹配就会被缓存)

  1. 字符串:分割要用,eg:include="名称1,名称2"
  2. 正则include前面要加个:eg::include="/名称1|名称2/"
  3. 数组include前面要加个:eg::include="[名称1,名称2]"

AppVue:动态组件外面包裹一下即可:<KeepAlive include="left,center">动态组件</KeepAlive>

这个名称直接写组件名是没用的,而是在组件里面定义的name:(name与name直接就,间隔,别额外加个空格什么的)

还有几个属性:

  1. exclude - string | RegExp | Array:名字匹配的不缓存,其他都缓存(也是字符串、正则、数组)
  2. max - number | string:最多可以缓存多少组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁

3.缓存组件生命周期

对于缓存的组件来说,再次进入时,我们是不会执行created或者mounted等生命周期函数的,KeepAlive提供了两个钩子:activateddeactivated

在center里面加上两个钩子(其他还是上面的案例,代码不变)

效果

3.8.异步组件

异步组件一般两个用途:第一个:异步加载服务器组件,第二个:把组件和项目分包

官方详细文档:https://cn.vuejs.org/guide/components/async.html

平时我们编译的时候都是整体打一个包,但有些场景下是需要分别打包的

PS:有时候都放一个包里面会导致首页这类内容多的页面特别卡,有分包需求

先看个正常案例:

<!-- App.vue -->
<template>
    <div>
        <AppHeader></AppHeader>
        <AppItem></AppItem>
    </div>
</template>

<script>
import AppItem from './components/AppItem.vue';
import AppHeader from './components/AppHeader.vue';

export default {
    components: {
        AppItem,
        AppHeader
    }
}
</script>

子组件没写什么内容,就分别写了一个文本

build之后:整体打包成一个js文件了

如果我们要把header单独打包呢?==> 这时候异步组件用处就来了

我们先导入defineAsyncComponent,然后处理下导入的对象并在父组件中注册一下

import { defineAsyncComponent } from 'vue';

const appHeaderAsync = defineAsyncComponent(() => {
    // 异步加载服务器的组件、分包打包项目某个组件
   return import('./components/AppHeader.vue');
});

export default {
    components: {
        AppHeader: appHeaderAsync // 注册下
    }
}

贴一下App.vue

<template>
    <div>
        <AppHeader></AppHeader>
        <AppItem></AppItem>
    </div>
</template>

<script>
import AppItem from './components/AppItem.vue';

import { defineAsyncComponent } from 'vue';

const appHeaderAsync = defineAsyncComponent(() => import('./components/AppHeader.vue'));

export default {
    components: {
        AppItem,
        AppHeader: appHeaderAsync
    }
}
</script>

重新npm run build之后:appheader被单独打包了

预览效果:npm run preview

的确变成两个js文件了

异步组件平时项目用的不多(用路由懒加载),还有一些扩展内容可以看官方文档

3.9.组件的v-model★

1.语法糖探究

单独拎出来讲肯定是和之前v-model不一样的,我们先不管他,按照之前使用习惯来做个测试:

App.vue里面对AppItem子组件绑定了一个title

然后我们去appItem中使用:

打开浏览器后发现vue提示我们没在AppItem中定义

其实v-model就是一个语法糖,我们写v-model="title",相当于写成了这样:

<AppItem :modelValue="title" onUpdate:modelValue=回调函数 >

这段语句相当于我们自己通过属性传参,然后定义一个自定义方法用来接收子组件$emit('自定义方法',数据)的值

2.v-model实现的本质

v-model可以双向绑定,那么必然可以相互间的通信,那就可以简化步骤:

  1. 父组件先把数据传递给子组件 ==> 属性传值
    1. eg:父组件::title="title"、子组件:props: ['title']
  2. 子组件得到数据并展示出来,当子组件修改数据后再把数据重新传给父组件,eg:
    1. 父组件:@myEvent="updateTitle"定义一个自定义事件myEvent,并定义一个事件触发的处理函数
    2. 子组件:this.$emit('myEvent', 新数据);(加上:emits: ['myEvent']
  3. 父组件接收到新数据后,对data中的变量重新赋值
    1. eg:updateTitle(data){ this.title = data; }

代码比较简单,截个图:

刚才说v-model是语法糖,相当于vue帮我们写了这个:<AppItem :modelValue="title" Update:modelValue=回调函数 >

PS:相当于上面代码不变的情况下,只要把我们之前定义的:title换成:modelValue,自定义事件@myEvent换成Update:modelValue就行了

还是上面代码,我们改写下:App.vue

<template>
    <div>
        <!-- <AppItem :title="title" @myEvent="updateTitle"></AppItem> -->
        <AppItem :modelValue="title" @update:modelValue="updateTitle"></AppItem>
    </div>
</template>
<script>
import AppItem from './components/AppItem.vue';
export default {
    components: { AppItem },
    data() {
        return {
            title: '我是一个标题'
        }
    },
    methods: {
        updateTitle(data) {
            console.log('App update:', data);
            this.title = data;
        }
    },
}
</script>

AppItem.vue

<template>
    <div>
        <h2> {{ modelValue }}</h2>
        <button @click="updateTitle">修改title</button>
    </div>
</template>

<script>
export default {
    props: ['modelValue'],
    emits: ['update:modelValue'],
    methods: {
        updateTitle() {
            console.log('AppItem update');
            this.$emit('update:modelValue', '我是被AppItem修改过的值');
        }
    },
}
</script>

点击后效果:

上面案例就是v-model的本质了,我们现在可以使用v-model进一步简化App.vue中自定义组件AppItem的写法:

<AppItem :modelValue="title" @update:modelValue="title = $event"></AppItem>

App.vue:基本上没变,就是把:modelValue="title"换成了v-model="title"

回调函数这边我就简写了:title = $event(回调函数上面也可以这么写,我上面只是为了你进一步理解才分开写的)

<template>
    <div>
        <!-- <AppItem :title="title" @myEvent="updateTitle"></AppItem> -->
        <!-- <AppItem :modelValue="title" @update:modelValue="title = $event"></AppItem> -->
        <AppItem v-model="title" @update:modelValue="title = $event"></AppItem>
    </div>
</template>
<script>
import AppItem from './components/AppItem.vue';
export default {
    components: { AppItem },
    data() {
        return {
            title: '我是一个标题'
        }
    },
}
</script>

AppItem还是之前的内容:

3.父组件中多个子组件

再继续深究,如果我父组件中有多个AppItem,我又该怎么写呢?

AppItem无需修改:props中写下modelValue,emit里面写update:modelValue(只要写一份就行了)

我们来看下App.vue

<template>
    <div>
        <AppItem v-model="title" @update:modelValue="title = $event"></AppItem>
        <AppItem v-model="title2" @update:modelValue="title2 = $event"></AppItem>
        <AppItem v-model="title3" @update:modelValue="title3 = $event"></AppItem>
    </div>
</template>
<script>
import AppItem from './components/AppItem.vue';
export default {
    components: { AppItem },
    data() {
        return {
            title: '我是一个标题',
            title2: '我是另一个标题',
            title3: '我是第三个标题'
        }
    },
}
</script>

分别点击都可以生效:

4.自定义v-model名称

如果就要像之前我们自己手写各种自定义名称呢?

vue其实也有提供方法:v-model:自定义名称

PS:现在不奇怪为什么自定义事件是个update:modelValue了吧,默认的v-model,其实就是v-model:modeValue

看案例:AppItem.vueprops: ['title']$emit('update:myEvent', 内容)

<template>
    <div>
        <h2>{{ title }}</h2><button @click="changeTitle">修改标题</button>
    </div>
</template>

<script>
export default {
    props: ['title'],
    emits: ['update:myEvent'],
    methods: {
        changeTitle() {
            this.$emit('update:myEvent', '这个AppItem修改的内容');
        }
    },
}
</script>

App.vue<AppItem v-model:title="title" @update:myEvent="title = $event">

<template>
    <div>
        <AppItem v-model:title="title" @update:myEvent="title = $event"></AppItem>
    </div>
</template>

<script >
import AppItem from './components/AppItem.vue';
export default {
    components: { AppItem },
    data() {
        return {
            title: '这是一个标题'
        }
    },
}
</script>

点击后就可以修改了:

5.子组件中绑定多个v-model

那么已经有了别名了,是不是可以绑定多个v-model呢?==> 可以的 (平时很少这样干,完全可以传递个对象或者数组过去)

AppItem.vue:如果是整体批量处理可以在子组件定义一个自定义事件即可

<template>
    <div>
    {{ name }} - {{ age }} - {{ gender }}
    <button @click="changeData">修改内容</button>
    </div>
</template>

<script>
export default {
    props: ['name','age','gender'],
    emits: ['update:name','update:age','update:gender'],
    methods: {
        changeData(){ // 如果是这种整体修改的,可以用一个自定义事件统一处理
            this.$emit('update:name','王二');
            this.$emit('update:age',33);
            this.$emit('update:gender','女');
        },
    },
}
</script>

App.vue:实际使用中往往是传个对象或者数组过去,很少这么传的

<template>
    <div>
        <AppItem v-model:name="name" v-model:age="age" v-model:gender="gender" 
        	@update:name="name = $event" @update:age="age = $event"
                 @update:gender="gender = $event"></AppItem>
    </div>
</template>

<script >
import AppItem from './components/AppItem.vue';
export default {
    components: { AppItem },
    data() {
        return {
            title: '这是一个标题',
            name: '张三',
            age: 22,
            gender: '男'
        }
    },
}
</script>

3.10.混入Mixins

目前我们是使用组件化的方式在开发整个Vue的应用程序,但是组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。

在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成,Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能(这块以前Vue2中大量使用,Vue3基本不用 ==> 使用setup函数)

  1. 一个Mixin对象可以包含任何组件选项:

  2. 当组件使用Mixin对象时,所有Mixin对象的选项将被混合进入该组件本身的选项中

说明:Mixins 在 Vue3 支持主要是为了兼容Vue2和生态中其他库。在新的应用中应避免使用 mixin,特别是全局mixin

这边简单说下案例:我们先在混入对象中配置一些函数和字段(平时怎么用,这个里面都可以写)

App.vue:注册一下AppItem

AppItem组件里面该怎么样还是怎么用,如果字段和方法和混入里面的重名了,vue会以组件为准

效果:msg我组件里面并没有定义,方法也没有些,但是通过myMixin配置下就都有了

Vue3基本上不用这个,其他内容我一笔带过:

如果mixin里面的内容和组件重名了,组件本身的优先级更高

生命周期函数比较特殊:会都放在一个数组里面,最后mixin中的生命周期函数和组件的都会调用

也可以放全局,让每个组件都混入,但Vue3不推荐这么做,所以就不介绍了

官方文档https://v2.cn.vuejs.org/v2/guide/mixins.html