Vue


前言

写在开篇

这是个人的学习笔记,用于自己复习的同时也希望能帮助到有需要的人
由于是初学,加上Vue的知识也不少,难免有错误和不足,希望大家谅解,也欢迎批评指正

Vue Router、VueX在其他几篇博客上

更新日志

2022/01/08
本博客最初于2021/08/20发布在我的C站博客,现在做了微小的调整后搬运到此处

2022/05/15
修改和补充关于vue-cli和vue3.x的部分内容

2022/05/17
继续查漏补缺

2022/05/18
修改Vue2.x部分目录结构,并记录Vue3.x的内容

2022/05/19
补充

完善的准备

认识Vue框架

为了便于理解,我将Vue概括为:
1.Vue是一个主要关注视图层的渐进式框架,即Vue可以只运用到局部代码。

2.并且Vue实现了MVVM双向绑定模式
M:模型层,指js对象
V:视图层,指DOM
VM:链接视图和数据的中间件

3.而且兼顾了React的虚拟DOM和Angular的模块化开发

至于什么是框架,我的理解是:
自动生成各种文件配置及相关代码的,并且提供了许多更简洁高效的开发方式的东西

另外,Vue读作view,而不是 微优易
================================================

需要安装下载的东西都写在这里了,根据需要自行节选阅读

Vue 引入

两种引入方式:
本地引入:去官网复制一份代码到本地然后js引入即可
CDN引入:加上以下代码

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>

接下来的内容是暂时不会用到的,不过提前准备好呢也没有坏处(要是觉得麻烦就先跳过)

npm 安装

Node.js的包管理工具,经常用到

1.搜索Node.js去它的官网下载,然后安装即可(自动配置环境变量)

2.win+r输入cmd打开命令提示符(或者打开编译器终端)输入:

node -v
npm -v

检测版本号
如果出现版本号则说明安装成功

cnpm镜像加速器 安装

由于npm下载国外资源的时候可能会比较慢,所以我们还得专门弄一个“中国版”(镜像加速器)的:

npm install cnpm -g

以后每当遇到npm下载卡顿的时候,就可以用ctrl+c终止下载,然后用cnpm替代npm即可体验极速下载

vue-cli 安装

cli是command line interface,命令行接口

使得我们可以通过命令行使用vue的一些功能

除了上述功能之外,还自动做好了一个工程所需的很多配置,所以可以说:

vue只是一个JS的库,vue-cli才是我们所说的框架(脚手架)

vue-cli3.x只能创建vue2.x的项目,而vue-cli4.x及以上版本可以创建vue2.x和vue3.x的项目

# 安装vue-cli3.x
npm install vue-cli -g
# 安装vue-cli4+
npm install -g @vue/cli

vue-cli创建项目:

vue create your_project_name

webpack 安装

代码打包工具,自动将工程代码转为ES5并且压成一行代码

cnpm install webpack -g

webpack-cli 安装

webpack都装了,那webpack顺便一起吧

npm install webpack-cli -g

axios 安装

其实也就是Vue版的ajax

npm install axios

vue-router 安装

路由
这个要在项目目录下安装
进入项目目录下,输入

npm install vue-router --save-dev

devtools 安装

这是一个浏览器插件,用于vue代码的调试,只需要在浏览器上安装就行了

Vue2.x

基本语法

创建Vue对象 以及 mustache插值语法

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
 let vm = new Vue({
        //绑定id为app的DOM元素,这也确定了Vue对象的作用范围
        el:"#app",
        //以键值对的形式存放数据
        data:{
            mes:"HelloWorld!"
        },
        //类似的,存放方法
        methods: {
 		    helloWorld: function(){
 		    	//访问对象的属性
                alert(this.mes);
            }
        }
    });
</script>

相应的html部分为:
其中嵌套的大括号就是mustache插值语法,括号里面可以写多个变量,也可以填表达式(不推荐这么做)
mustache语法不能用于标签的内部!
(不能出现<div {{name}}></div>这种或者其他类似的形式)

<div id="app">
	<!-- {{}}会动态的去接收值
这就是mustache语法 -->
    <h1>{{mes}}</h1>
    <!-- 使用v-on:绑定事件,也可以简写为@ -->
    <div v-on:click="helloWorld">点击我试试</div>
</div> 

运行效果:

在这里插入图片描述
另外,vue3.0之后,建议将Vue对象中的data写为函数的形式:

data(){
    return{
    	mes:"HelloWorld!"
	}
},

这样写的话,多个与之关联的html元素都会有自己独立的作用域,避免了污染

循环v-for

记得每次都要引入Vue的js文件
为了方便阅读,后面就不再提醒了

<script>
    let vm2 = new Vue({
        el: "#app2",
        data(){
            return{
                items: ["牛逼","天秀","妙绝"]
            }
        }
    })
</script>
<ul id="app2">
    <li v-for="i in items">{{i}}</li>
</ul>

运行结果,可以看到动态的创建了三个
在这里插入图片描述
v-for还可以支持多个参数

<div id="app">
    <div v-for="(i, index) in arr">{{index}}----{{i}}</div>
    <div v-for="(i, index, key) in arr">{{index}}----{{i}}----{{key}}</div>
    <div v-for="(i, index, key) in obj">{{index}}----{{i}}----{{key}}</div>
</div>

在这里插入图片描述
从结果上看,第一个参数是值,第二个参数是键,在对象中还会有第三个参数是下标

此外,还可以写作 i of arr 这样的形式,这更接近原生JS迭代器的语法

分支v-if

<script>
    let vm1 = new Vue({
        el:"#app1",
        data:{
            num: 3
        }
    });
</script>
<div id="app1">
    <div v-if="num == 1">num是1</div>
    <div v-else-if="num === 2">num是2</div>
    <div v-else>num为其他值{{num}}</div>
</div>

运行结果
在这里插入图片描述

单次变化v-once

<script>
    let vm = new Vue({
        el: '#app',
        data(){
            return{
                msg: "hhhh"
            }
        },
        methods: {}
    })
</script>
<div id="app">
    <div>变化:{{msg}}</div>
    <div v-once>不会变化:{{msg}}</div>
</div>

什么叫做单次变化呢?
首先来看看结果
在这里插入图片描述
我们在浏览器的控制台上对值进行一个修改,此时我们就发现:
在这里插入图片描述

v-text和v-html

v-text其实还没有mustache语法{{}}`好用... 但是还是要了解 相比之下,v-html就显得有用多了

<script>
    let vm = new Vue({
        el: '#app',
        data(){
            return{
                msg:"<div style=\"color: red;\">123</div>"
            }
        },
    })
</script>
<div id="app">
    <div>{{msg}}</div>
    <div v-text="msg"></div>
    <div v-html="msg"></div>
</div>
运行结果也可想而知 ![在这里插入图片描述](https://img-blog.csdnimg.cn/ffcc95d216da40198a47ec82ab3dde81.png) #### 预设v-pre 这个说实话应该猜都能猜到作用了,其实就是\
\
的vue版本 这里我也不给vue对象的代码了(因为给了也没用)
<div id="app">
    <div v-pre>预设不处理:{{msg}}</div>
</div>
![在这里插入图片描述](https://img-blog.csdnimg.cn/4b025bb7343b47fe9ac67190ae2df55c.png) 有什么用呢?大概就是...基本上没什么用.... #### 解析判定v-cloak vue对象的data数据可能会来自服务器,那么网络不畅的时候就可能导致数据没有及时渲染到页面,就会出现满屏的`{{}}
这种mustache语法,让用户看了觉得十分诡异,为了判定Vue对象到底有没有解析完毕,就出现了v-cloak

<script>
    let vm = new Vue({
        el: 'app',
        data(){
            return{
                msg:"hhhh"
            }
        }
    })
</script>
<div id="app">
    <div v-cloak>啊哈!{{msg}}</div>
</div>

请求成功和请求失败的状态:
在这里插入图片描述
在这里插入图片描述
没错,v-cloak就是在成功时消失,失败时保留
那么我们就可以利用css的属性选择器搞一手操作了

<style>
    [v-cloak]{
        display: none;
    }
</style>

这样一来,失败时就不会显示mustache语法了

动态绑定v-bind

之前说过了,mustache不能用在标签的内部,也就是不能用在左右箭头的里面,那么问题来了:
对于这样的vue对象

<script>
    let vm = new Vue({
        el: '#app',
        data(){
            return{
                myColor: "color:red;",
                someOnesColor: "blue"
            }
        }
    })
</script>

我却偏要这么写html(甚至还报错了):
在这里插入图片描述
很明显这个mycolor是没有解析成color:red;的(被当作了css)
此时我们就需要用到v-bind了(第二种写法是个语法糖)

<div id="app">
    <div v-bind:style="myColor">啊哈</div>
    <div :style="myColor">啊哈</div>
    <div :style="{color: someOnesColor,}">啊哈</div>
</div>

在这里插入图片描述

这里提一句,如果返回多个值,也可以考虑用数组或者对象的形式发送/接受,

中间可以利用函数进行处理(当然不用也行)

diff算法

指的是新旧虚拟DOM对比、更新的过程

vue或react中,一个dom对象默认有一个key值,需要用v-bind绑定一个唯一值(不要用下标,因为下标会在排序等变化值发生改变并不是唯一的),
这样一来在基于diff算法的动态变化(增删改排序)时,能够做到正确匹配每个元素

key值具体的作用是:

  1. 配合虚拟DOM进行比较,判断是否更新
  2. 如果不使用key值,那么Vue会使用一种 最大限度减少动态元素尽可能尝试修改/复用相同元素 的算法;
    使用key值则会基于key值进行元素排序,并移除key不存在的元素

分析

首先看看它要解决的一种问题情境:

在一个数组ABDEFG中插入一个C

传统(暴力)做法就是,DEFG全部往后挪,然后插入一个C

无疑这种做法很多时候的效率很低

diff算法根据无key和有key分情况讨论

无key:diff-patchUnkeyedChildren

在进行一次变动之后触发,将新旧两个虚拟DOM进行对比,然后一个for循环遍历较短那个,根据新的数据更改旧的数据

diff-patchUnkeyedChildren

其实非常简单…
这里比较的是虚拟DOM,这样就直接操作DOM所引起的巨大开销
这里判定是否一样的标准是虚拟DOM的type和key是否都一样——这就涉及到有key的情况了

有key:diff-patchKeyedChildren

因为这个是针对单次操作的,所以一次操作通常只会有一组或者一个数据的更新,

有key的话就是,如果遍历的过程中发现key的值不同了,那么跳出循环,然后从两者尾部开始遍历,然后继续开始比较更新,最后又到了冲突的位置,那么,这里发生一次mount就好了(也就是双端比较)

事件绑定v-on

<script>
    let vm = new Vue({
        el: '#app',
        data(){
            return{}
        },
        methods: {
            sayhi(){
                alert("好啊你真敢点我");
            },
            saywow(){
                alert("这么说你很勇哦");
            }
        }
    })
</script>

其中@是一个语法糖

<div id="app">
    <div v-on:click="sayhi">点我一下你试试</div>
    <div @click="saywow">这个彬彬就是逊啦</div>
</div>

运行结果
在这里插入图片描述
在这里插入图片描述

事件对象参数

另外,涉及到参数的一种情况

<script>
    let vm = new Vue({
        el: '#app',
        methods:{
            clickEvent1(args){
                console.log(args);
            },
            clickEvent2(args){
                console.log(args);
            }
        }
    })
</script>

注意绑定的事件,都不传参数,但是一个加了括号一个没加括号

<div id="app">
    <button @click="clickEvent1()">按钮1号</button>
    <button @click="clickEvent2">按钮2号</button>
</div>

可见分别输出undefined事件对象
所以如果不加括号时会自动传一个事件对象
在这里插入图片描述
那么问题来了,如果需要传参数的同时有需要事件对象该如何操作呢?

<script>
    let vm = new Vue({
        el: '#app',
        methods:{
            clickEvent3(num1, num2, event){
                console.log(num1);
                console.log(num2);
                console.log(event);
            }
        }
    })
</script>

注意,第三个参数前面加了一个$符号,表示传递事件对象,这样一来我们就可以在传递其他参数的通时传递事件对象了

<div id="app">
    <button @click="clickEvent3(123, 456, $event)">按钮3号</button>
</div>

当然,事件对象参数的位置可以随意,但是约定俗成还是放末尾比较好

修饰符

还记得事件冒泡事件捕获

<script>
    let vm = new Vue({
        el: '#app',
        methods:{
            fatherClick(){
                console.log("我是父元素");
            },
            sonClick(){
                console.log("我是子元素");
            }
        }
    })
</script>

写了点style方便观察(懒得写class)

<div id="app" @click="fatherClick" style="background-color: red;height: 300px;text-align: center;">
    <button @click="sonClick" style="margin-top:150px;">冒泡:OoOoOooOOOo</button>
</div>

当我们点击子元素时,触发子元素事件之后会触发父元素事件
这是因为子元素时父元素的一部分,所以点击子元素也相当于点击了父元素的一部分,也就触发了事件冒泡(事件触发自底向上
在这里插入图片描述
在以前我们会采用在子元素的函数中利用事件对象的内置函数stopPropagation来阻止冒泡

sonClick(event){
    	event.stopPropagation();
        console.log("我是子元素");
}

但是现在我们有了修饰符,允许我们在标签中写:

<button @click.stop="sonClick" style="margin-top:150px;">冒泡:OoOoOooOOOo</button>

另外,不只有事件绑定v-on才有修饰符,其他vue关键词也有

懂了概念和作用,那么使用修饰符也是得心应手,下面是一些常用的事件修饰符,这里不再赘述:

修饰符 作用
.trim 清除首尾空格
.stop 阻止事件冒泡
.prevent 阻止默认行为
.self 只有元素本身能触发自己的事件
.once 只能触发一次
.capture 将事件冒泡调整为事件捕获(自顶向下触发)
.keycode 监听键盘
.enter 监听键盘是否按下enter键
.up(上下左右空格之类依葫芦画瓢) 监听键盘是否按下↑

计算属性computed

虽然说mustache语法中的{{}}内可以进行一些如同字符串拼接的简单操作,但是遇到更多的计算时,不仅是视觉上会显得不美观,而且还存在无法正确运算的可能。
我们可以通过methods来解决这些问题,不过methods内的方法是动态的从而很容易被修改,存在被意外地污染的风险
这时候,为了防止这种事情发生,就出现了computed计算属性

最简单的使用大概就像以下这样

<script>
    let vm = new Vue({
        el: '#app',
        data(){
            return{
                msg1: "hello",
                msg2: "world"
            }
        },
        computed:{
            msgStrcat(){
                return this.msg1 + " " + this.msg2
            }
        }  
    })
</script>
<div id="app">
    {{msgStrcat}}
</div>

和函数有什么区别?
在mustache语法调用的时候不加(),因为这是一个属性而不是函数——这是形式上的区别,
那还有更多本质上的区别吗?
各位看官莫急,且看以下分析:
1.computed内的实际形式:

computed:{
    msgStrcat:{
        set(){
            console.log("这是computed的set函数");
        },
        get(){
            console.log("这是computed的get函数");
        }
    }
}  

其中get和set也就是ES6中增加的内容,可以参考我的ES6学习笔记
2.被调用的时候:

<script>
    let vm = new Vue({
        el: '#app',
        methods:{
            strcat(){
                console.log("这是methods的方法");
            }
        },
        computed:{
            msgStrcat:{
                set(newValue){
                    console.log("这是computed的set");
                },
                get(){
                    console.log("这是computed的get函数");
                }
            }
        }  
    })
</script>

然后是三次同时调用computed和methods

<div id="app">
    <div>{{msgStrcat}}{{strcat()}}</div>
    <div>{{msgStrcat}}{{strcat()}}</div>
    <div>{{msgStrcat}}{{strcat()}}</div>
</div>

可以看见的是,computed只被触发了一次,这源于set的性质
在这里插入图片描述
没错!computed计算后的结果是放在缓存里面的,这样一来不仅避免了大量重复计算从而大大地提高了页面响应式加载的效率!
所以以后遇到那种几乎不变的值时,就可以考虑computed了(由于几乎不变,set也没什么用了…)
当然也别管那么多,以后遇到属性直接先莽computed!

显示v-show

本质是修改css中display的值

<script>
    let vm = new Vue({
        el: '#app',
        data(){
            return{
                isShowed:true
            }
        }
    })
</script>
<div id="app">
    <div v-show="isShowed">显示</div>
    <div v-show="!isShowed">没有显示</div></div>
</div>

在这里插入图片描述
和v-if的区别
v-show纵使不显示,那么标签也还在,只不过隐藏了
在这里插入图片描述
但是v-if是 移除或创建 标签
想一下vue为什么出现,不就是为了响应式动态加载避免频繁操作DOM吗,所以这里也是一个道理——在涉及到频繁切换的时候,我们就优先考虑v-show而不是v-if

双向绑定v-model

<script>
    let vm = new Vue({
        el: '#app',
        data(){
            return{
                inputStr:""
            }
        }
    })
</script>
<div id="app">
    <input type="text" v-model="inputStr">
    <div>您输入的是:{{inputStr}}</div>
</div>

双向绑定之后,两边就可以同步变化了
在这里插入图片描述

v-model的底层实现

<script>
    let vm = new Vue({
        el: '#app',
        data(){
            return{
                msg: 666,
            }
        },
        methods: {
            valueChanged(event){
                this.msg = event.target.value;
            }
        }
    })
</script>
<div id="app">
    <input type="text" :value="msg" @input="valueChanged">
    <div>您输入的是:{{msg}}</div>
</div>

​ 也就是拆分为两个个部分:

  1. 通过input时间监听输入值,并将不断维护这个输入值
  2. 动态绑定这个值

懂了这个原理,你就可以这么写:

<div id="app">
    <input type="text" :value="msg" @input="msg = $event.target.value">
    <div>您输入的是:{{msg}}</div>
</div>

lazy修饰符

根据上文,我们可以看到v-model底层绑定的是input事件,我们可以使用v-model.lazy来切换为change事件,效果就是会等待输入完成后才开始变化。

trim修饰符

去除输入内容首尾多余的空白

template标签

这个标签最大的不同就是不会被渲染,经常配合v-if和v-for之类的做一些特定场景渲染

等等,这个东西,好像是原生HTML的吧…

<template v-if="true">
    <h1>
        如果需要判断条件,然后又有很多标签
    </h1>
    <div>
        还不希望有一个奇怪的父级元素的话,
    </div>
    <div>
        就可以这样做
    </div>
</template>

组件化component

开始组件化开发

低耦合高内聚嘛
先来看看最简单的使用方式

<script>
    // 创建
    let vm = Vue.createApp({
        data(){
            return{}
        }
    });
    // 定义全局组件
    vm.component('button-count',{
        data(){
            return {
                count: 0
            }
        },
        template:`
            <button @click="count ++;">你点击了这个按钮{{count}}次</button>
        `
    });
    // 绑定
    vm.mount('#app');
</script>

这里我们不再采用Vue2中使用new来创建Vue对象的形式,而是采用了Vue3中的createApp方法。
这里需要注意的是,引入的文件需要更改为:

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

接下来我们就可以这样写了
(组件化中标签的写法有一定的讲究)
组件命名要么像buttonCount,要么像button-count

<div id="app">
    <button-count></button-count><br><br>
    <button-count></button-count><br><br>
    <button-count></button-count>
</div>

效果如下:
在这里插入图片描述
由于data部分我们使用的函数形式,所以能够返回独立的函数作用域,因此每个点击按钮的数值可以相互独立
反之,如果不采用函数形式的data,那么所有值都会同步。

组件命名

kebab

就是蛇形命名法,像这样: my-name

Pascal

就是大驼峰命名法,像这样: myName

虽然Pascal命名法也能直接使用,但是实际上是被自动转换为了kebab

全局组件

我们之前使用的都是全局组件
在介绍它有什么特点之前,先来看看以下代码:

<script>
    // 创建
    let vm = Vue.createApp({
        data(){
            return{}
        }
    });
    // 定义全局组件
    vm.component('cool-title',{
        template:`
            <h1 style="color:red;background-color:pink;">炫酷的标题</h1>
        `
    })
    vm.component('button-count',{
        template:`
            <cool-title></cool-title>
            <button>嘿,兄贵快来点我</button>
        `
    });
    // 绑定
    vm.mount('#app');
</script>

在一个Vue对象内定义多个组件的时候,它们之间可以相互调用
上述代码中,第二个组件内就调用了第一个组件
效果如下:
在这里插入图片描述

局部组件

先看代码

<script>
    // 定义局部组件
    let title = {
        data(){
            return {
                title: "炫酷的标题" 
            }
        },
        template:`
            <h1 style="color:red;background-color:pink;">{{title}}</h1>
        `
    }
    let button = {
        template:`
            <cool-title></cool-title>
            <button>嘿,兄贵快来点我</button>
        `
    }
    // 创建
    let vm = Vue.createApp({
        data(){
            return{}
        },
        components:{
            "cool-title":title,
            "button-count":button
        }
    });
    vm.mount('#app');
</script>

没错,是声明一个类似于对象的东西,最后在创建Vue对象实例的时候才塞到components内,
这个时候我们还要给他们自定义一个名字构成键值对

效果如下
在这里插入图片描述
没错,如果你仔细看了代码,你会发现第二个组件内调用了第一个组件,但是从效果上来看并没有起到作用。

这就是局部组件,只能在components中引入后才能使用

所以如果硬要使用,就得像这样写:

let button = {
    components:{
        "niubi-title":title
    },
    template:`
        <niubi-title></niubi-title>
        <button>嘿,兄贵快来点我</button>
    `
}

其他形式
这两种形式都是可以的,引入方式都一样

<script type="text/template" id="coolTitle">
    <h1 style="color:red;background-color:pink;">酷炫的标题</h1>
    <button>嘿,兄贵快来点我</button>
</script>

<template id="goodTitle">
    <h1 style="color:red;background-color:pink;">酷炫的标题</h1>
    <button>嘿,兄贵快来点我</button>
</template>

使用:

let button = {
    components: {
    },

    template:`#coolTitle
    `,

}

注意这时候对象中的template中就不能再书写其他东西了(包括前置空格),不然总是会出一些稀奇古怪的问题

组件通信

父组件向子组件传递props

props其实就是参数

<script>
    let box = {
        props: ['p'],
        template:`
            <h1 style="color:red;">{{p}}</h1> `
    }
    let vm = Vue.createApp({
        data(){
            return{}
        },
        components:{
            "great-box":box,
        }
    })
    vm.mount('#app')
</script>
<div id="app">
    <great-box :p="6666"></great-box>
</div>

效果如下
在这里插入图片描述
约束
可以通过如下形式对参数传递的内容进行约束,
当内容不符合约束时,控制台会给出相应的警告

let box = {
    props: {
        p: String
    },
    template:`
        <h1 style="color:red;">{{p}}</h1>
    `
}

当然,也有更完善的约束形式:

let box = {
    props: {
        p: {type:String, required:true, default:'哈哈哈哈'}
    },
    template:`
        <h1 style="color:red;">{{p}}</h1>
    `
}

props命名规范
props中比如写一个

coolBrand,那么在html部分进行绑定时就要写成cool-brand,原因是html不区分大小写

no-props

不使用props传递内容

<script>
        let vm = Vue.createApp({
            data(){
                return{
                    price: 5
                }
            },
            components: {
                "box": {
                    template:`
                    <h1>
                        <div>吴彦祖?哦,是镜子啊</div>    
                    </h1>`
                }
            }
        }).mount("#app");     
    </script>
<div id="app">
    <h1>当前的价格是{{price}}</h1>
    <box ababa="阿巴阿巴" style="width: 200px;height: 200px;background-color: #faa;"></box>
</div>

在这里插入图片描述
课件,最外层的h1继承了模板标签的各项属性

需要注意的是,在以下两种情况中,继承无法完成:
第一种:inheritAttrs:false

components: {
    "box": {
        inheritAttrs:false,
        template:`
        <h1>
            <div>吴彦祖?哦,是镜子啊</div>    
        </h1>`
    }
}

第二种:模板内最大父级元素不唯一 (并且不做处理时)
此处指的两个h1标签,都是最大的父级元素

components: {
    "box": {
        template:`
        <h1>
            <div>吴彦祖?哦,是镜子啊</div>   
        </h1>
        <h1>
            <div>古尔丹?哦,是镜子啊</div>   
        </h1>
        `
    }
}

在第二种情况中,如果还是想要继承
可以使用用 v-bind:=”$attrs”

components: {
    "box": {
        // inheritAttrs:false,
        template:`
        <h1 :="$attrs">
            <div>吴彦祖?哦,是镜子啊</div>   
        </h1>
        <h1 :="$attrs">
            <div>古尔丹?哦,是镜子啊</div>   
        </h1>
        `
    }
}

另外,我们还可以选择其中一部分属性继承

components: {
    "box": {
        // inheritAttrs:false,
        template:`
        <h1 :a="$attrs.ababa">
            <div>吴彦祖?哦,是镜子啊</div>   
        </h1>
        <h1 :style="$attrs.style">
            <div>古尔丹?哦,是镜子啊</div>   
        </h1>
        `
    }
}

需要注意的是,这样具体到某一个属性的时候,v-bind需要加上一个名字(盲猜之前没加的时候都是自动把$attrs解构了

子组件向父组件传递

子组件向父组件传递的方式是通过自定义事件

<script>
    let box = {
        template:`
            <button style="color:red;" @click="boxClick">我是子组件,快来点我</button>
        `,
        methods: {
            boxClick(){
                console.log("子组件发出了信息");
                //1.this.$emit使得boxClick方法可以作为事件触发
                //(注意当做事件使用时,命名规则形如box-click)
                this.$emit('boxClick');
            }
        }
    }

    let vm = Vue.createApp({
        data(){
            return{}
        },
        components:{
            "great-box":box,
        },
        methods: {
            response(){
                console.log("父组件表示收到");
            }
        }
    })
    vm.mount('#app')
</script>
<body>
    <div id="app" style="width: 200px;height: 200px;background-color: #faa;">
        <!-- 2.把方法当事件用,绑定父组件中的一个方法 -->
        <great-box @box-click="response"></great-box>
    </div>

来一步一步分析:
1.子组件自定义一个方法,这个方法内部有一个this.$emit()是用于触发自定义事件的,它的存在使得这个函数可以作为一个事件来使用其中值是作为事件时的名称,可以随意。
2.在模板标签上用1中所构成的自定义事件来绑定一个父组件的方法
3.触发子组件的自定义事件,则父组件的函数也会触发

效果:
在这里插入图片描述

$refs,$parent和$root

我们注意到,vue中是不适合使用DOM操作的,但是我们总是有一些场景需要使用类似的操作,这个时候就该用到这三个东西了

    <script>
//这个box就是子组件对象
        let box = {
            template:`
            <div style="background-color: #faa;width:200px;height:200px">
            </div>
            `
        }

        let vm = Vue.createApp({
            components:{
                "box":box
            },
            methods: {
                getChildComponent(){
                //this.$refs访问到所有具有ref属性的子组件对象
                    console.log('啊哈',this.$refs);
                }
            }
        })
        vm.mount('#app')
    </script>

给父组件下的元素添加一个ref属性,就是给个名字
点击button调用方法,通过this.$refs访问到父组件下的具有ref属性的子组件对象

这就意味着,除了可以访问到被标记的DOM元素,还可以访问到其所在的组件对应的数据、方法等

使用同一个ref值,则以数组形式进行存储——非常方便

<div id="app">
    <box ref="牛蛙牛蛙"></box>
    <box ref="牛蛙牛蛙2"></box>
    <box></box>
    <button @click="getChildComponent">别来点我</button>
</div>

效果:
在这里插入图片描述
由于是对象,所以也可以通过这样的语句去进一步地访问:

console.log(this.$refs.牛蛙牛蛙);

至于$parent和$root,前者是访问最近的父级,后者是访问最远的父级,用法同上,不再赘述

组件进阶

动态组件

嘛…也没啥好说的,还是类似于一个模板

<script>
     let vm = Vue.createApp({
         data(){
             return {
                 boxOne: 'cool-box',
                 boxTwo: 'handsome-box'
             }
         },
         //就是一个组件,名字都写明白了,然后替换掉is属性绑定的那个组件
         template:`
 
             <component :is="boxOne"></component>
         `
     })
     vm.component('cool-box',{
         template: `
             <h1>很炫酷的box</h1>
         `
     })
     vm.component('handsome-box',{
         template: `
             <h1>十分帅气的box</h1>
         `
     })
     vm.mount('#app')
 </script>
异步组件

异步组件实际上也就是大家所想的那样
只是语法上要注意一下就是了

实现方式很多,只要保证结果是“在需要的时候才加载”就行了

<script>
    let vm = Vue.createApp({
        data(){
            return{}
        },
        methods: {}
    });
    vm.component('box',Vue.defineAsyncComponent(()=>{
        return new Promise((resolve, reject)=>{
            resolve({
                template: `
                    <div>hhhh</div>
                `
            })
        })
    }));
    vm.mount('#app')
</script>

在这里插入图片描述
当然还有其他实现的形式:

vm.component('box2',
    () => import('')
);

这种方法简洁一些,但实际上还是一个意思

组件生命周期

钩子函数
为什么叫钩子函数?大概就是说,钩在某个组件上的函数,这个函数会在组件创建到销毁过程中的某个阶段触发
使用方式类似于下面这样:

<script>
    let vm = Vue.createApp({
        beforeCreate(){
            console.log('beforeCreate!!!!');
        }
    });
</script>

beforeCreate顾名思义就是创建之前执行的函数。
其他常用的生命周期钩子函数还有:
| 阶段 | 名称 | 作用 |
| —— | ————- | ————————————– |
| 初始化 | beforeCreate | 创建之前执行 |
| 初始化 | created | 创建之后执行 |
| 初始化 | beforeMount | 渲染(挂载)之前执行 |
| 初始化 | mounted | 渲染(挂载)之后执行 |
| 更新 | beforeUpdate | 数据更新之前执行 |
| 更新 | updated | 数据更新之后执行 |
| 销毁 | beforeUnMount | app.unmount(‘#app’)(DOM销毁)之前执行 |
| 销毁 | unMounted | app.unmount(‘#app’)(DOM销毁)之前执行 |

(unmount方法可以手动结束组件的生命周期,但是不建议操作组建的生命周期)

侦听器watch

侦听数据,当其发生变化时,触发相应的操作

    <script>
        let vm = Vue.createApp({
            data(){
                return{
                    price: 5
                }
            },
            watch:{
            //第一个参数是当前的值,第二个参数是之前的值
                price(cur, pre){
                    console.log("price变化了")
                    console.log("现在的值是",cur)
                    console.log("以前的值是",pre)
                }
            }
        }).mount("#app");
//注意,这里的挂载!
//之前都是单独写一个vm.mount("#app")
//只有这样写才能用vm.price访问
    </script>

在这里插入图片描述

watch可以监听的内容涵盖了基本数据类型和引用数据类型,这意味着,函数也可以被监听

另外,对于对象,除了开启深度监听之外,还可以使用 “obj.prop” 这种形式来具体监听某一个属性
(注意加上双引号)

那么问题来了,这个和computed有什么区别呢?
通俗地说,区别在于:

watch支持异步,不缓存,并且可以接受两个参数;
computed不支持异步,默认缓存;

更多的区别在于:
watch默认为浅度观测(对多层嵌套,只观测其最外层)….
computed在调用时需要在模板中渲染,且默认为深度依赖(对多层嵌套,观测所有)….

分别什么时候用?

计算属性就用computed;
异步、复杂的操作 或者 一个值得变化会引起一堆值变化的情况 就用 watch

插槽slot

简单使用

一个组件在一个项目中可能会被反复用。但是问题是,我们可能会根据使用该组件的环境来对其进行一个调整,可是组件无法更改,我们也不想重新再写一个组件。就这样,插槽出现了。

<script>
    let box = {
        template: `
        <div style="background-color: #faa;width: 250px;height: 250px;float:left">
            <h1>好消息好消息!</h1>
            <slot></slot>
            <p>机会有限</p>
           <p>先到先得</p>
        </div>
        `
    }
    let vm = Vue.createApp({
        data(){
            return{}
        },
        methods: {},
        components: {
            'box': box
        }
    });
	vm.mount('#app');
</script>
<div id="app">
    <box></box>
    <box></box>
    <box></box>
</div>

在这里插入图片描述
如果我们要对其进行修改,那么就在组件标签中填入相应的内容,这些内容会依次匹配slot

<div id="app">
    <box>
        <div style="width: 200px;height: 50px;background-color: #fff;">
            <h1>hhhhh</h1>
        </div>
    </box>
    <box>
        牛逼
    </box>
    <box>
        <div style="font-size: 32px;color: #fff;">6666</div>
    </box>
</div>

插入之后就是这样了
在这里插入图片描述
如果需要默认内容的话,只要在slot标签里面写好就行了

具名插槽

也就是具有名字的插槽的意思。前面提到的都是匿名插槽,内容会自动匹配模板中的slot标签,这也意味着可能出现匹配错误的情况。为了准确的引导匹配,我们可以在slot标签中加上一个name属性,然后在插入的标签上加上v-slot。

let box = {
    template: `
    <div style="background-color: #faa;width: 250px;height: 250px;float:left">
        <h1>好消息好消息!</h1>
        <slot name="dalao"></slot>
        <p>机会有限</p>
       <p>先到先得</p>
    </div>
    `
}

注意v-slot只能运用在template标签或者components中

<template v-slot:dalao>
        <box>
            <div style="font-size: 32px;color: #fff;">6666</div>
        </box>
</template>

注意v-slot的值不加引号。另外,和v-on一样,v-slot可以简写为#

渲染作用域

什么是渲染作用域?看看下面的代码就知道了:

<script>
     let box = {
         data(){
             return {
                 flag: false
             }
         },
         template:`
             <div style="background-color:green;width:200px;height:200px"></div> 
         `
     }

     let vm = Vue.createApp({
         data(){
             return{
                 flag: true
             }
         },
         components: {
             'box': box
         }
     })

     vm.mount('#app')

 </script>

上图中,子组件和父组件都有一个值为flag,但是分别为false和true。
那么假如像下图这样访问flag值,究竟会不会显示呢?

<div id="app">
    <box v-show="flag"></box>
</div>

竟然是会显示的,解释是,在模板外访问flag值时,优先使用父级组件的值。
在这里插入图片描述
那么,什么又是在模板内访问flag值呢?

let box = {
    data(){
        return {
            flag: false
        }
    },
    //template里面使用就是模板内使用
    template:`
        <div v-show="flag" style="background-color:green;width:200px;height:200px"></div> 
    `
}

这样就会只访问组件自身的值
(如果自身没有,并不会向上访问父组件,反而会给出警告)
在这里插入图片描述
总结起来就是,在模板外使用访问父组件的值,内部访问子组件自身的值

那问题来了,如果要在模板外使用,却依旧需要子组件的值呢?

作用域插槽

作用域插槽就是为了解决上述问题而诞生的
实际上就是一个v-bind与slot结合使用的产物,如果想通了v-bind的作用的话可以不用刻意去学作用域插槽

<script>
    let box = {
        data(){
            return {
                datas: ['123','456','789']
            }
        },
        //slot标签通过v-bind绑定data属性,并把datas赋值给data
        //后面通过v-slot:dalao1=""来接收
        template:`
            <div style="background-color:green;width:300px;height:300px">
                <h1>广告位招商啦~</h1>
                
                <slot name="dalao1" :data="datas"></slot>
                
                <h1>买得早的都抱富(婆)了</h1>
            </div> 
        `
    }
    let vm = Vue.createApp({
        data(){
            return{}
        },
        components: {
            'box': box,
        }
    })
    vm.mount('#app')
</script>

这里dataReceiver类似于一个参数,可以自由命名,其data属性(就是在模板里面绑定那个data)保存了传递的值
(#dalao1是v-slot:的简写)

<div id="app">
    <box></box>
    <box>
        <template #dalao1="dataReceiver">
            <div style="font-size: 32px;color: #fff;">
                <ul>
                    <li v-for="i in dataReceiver.data">{{i}}</li>
                </ul>
            </div>
        </template>
    </box>
</div>

效果如下
在这里插入图片描述
另外,如果不给slot一个name值,那么在使用v-slot时,写成v-slot:default=””就好了,这样就会自动匹配

特效动画

(略)
两种使用特效动画的方式,
一种是通过动态修改class等来添加动画,这个就是css3的内容,不在话下;
另一种就是Vue封装的特效动画。
不过这个东西个人感觉相对次要,这里先不做笔记,以后再说

其他API

自定义指令

前面我们用到了v-if等指令,现在我们来自定义指令
比如我们来自定义一个叫做v-tianxiu的指令

  <script>
      let vm = Vue.createApp({
          data(){
              return{}
          },
          methods: {

          },
          directives:{
              tianxiu: {
                  // 里面写生命周期钩子
                  mounted(el){
                      el.focus();
                      console.log('执行成功了');
                  }
              }
          }
      })
//这样写也可以
//也可以写个局部的然后挂载到组件上
      // vm.directive('tianxiu',{
      //     mounted(el){
      //         el.focus()
      //         console.log('执行成功')
      //     }
      // }
      vm.mount("#app")
  </script>

这样一来,在组件挂载后就会执行这个v-tianxiu
也就是 自动聚焦+输出“执行成功了”

<div id="app">
    <input type="text" placeholder="请输入一点东西" v-tianxiu>
</div>

自定义事件最多可以有四个参数
在这里插入图片描述

传送门teleport

就和c/c++的goto差不多一个意思

components: {
    'box': {
        template: `
            <div class="box">这是box</div>
        `
    },
    'container': {
        template: `
            <teleport to=".box">
                <div class="mask">是container组件的部分</div>    
            </teleport>
            <div class="container">这是container</div>
        `
    }
}

这里box和container是两个不相干的标签

<div id="app">
    <box></box>
    <container></container>
</div>

但是通过teleport传送,让container的部分传送到了box内
并且是放到box内原有的DOM元素之后
在这里插入图片描述

Vue3.x

组件式API

之前我们在Vue2.x中的写法:

<script>
    let vm = Vue.createApp({
        data(){},
        methods: {},
        computed: {},
        watch: {},
        components: {},
        directives: {}
    })
vm.mount()
</script>

这样把,不同的内容分类放到不同的选项里面就是选项式API
但是如果后期业务量庞大起来,一个功能的相关内容会分布到各个位置,维护起来就十分麻烦。
所以Vue3.x推出了组件化API,把一个业务的内容放到一个地方

(以下图片来自网络)
这是选项式API
请添加图片描述
这是组件式API
请添加图片描述
接下来我们开始使用组件式API

<script>
    // 使用 createApp创建Vue实例
	let vm = Vue.createApp({
            setup(props, context){
                // 建议先把数据和方法打包再整体返回
                const data = reactive({
                    	msg: 'hello compositionAPI'
                })
                const methods = {
                    sayHi: ()=>{
                        console.log(this,'hhh');
                    }     
                }
                return {
				  data,
                    methods
                }
            },
            template: `
                <div>
                    <h1>{{data.msg}}</h1>  
                    <button @click="methods.sayHi">点击一下</button>  
                </div>
            `
        }).mount("#app")
    
</script>

其中,改动了不少生命周期函数(删了一些又增了一些)

不过仍然较好地兼容Vue2.X中选项式API,甚至可以在一定程度上混用

新的生命周期钩子

setup()
取代了beforeCreate()和created(),
并且不建议再使用data()等(虽然可以并存)

<script>
    let vm = Vue.createApp({
        setup(props, context){
            return {
                msg: 'hello compositionAPI',
                sayHi: ()=>{
                    console.log(this,'hhh');
                }
            }
        },
        template: `
            <div>
                <h1>{{msg}}</h1>  
                <button @click="sayHi">点击一下</button>  
            </div>
        `
    }).mount("#app")
</script>

在这里插入图片描述
这里注意setup是创建之前触发,此时this指向的是window
更多生命周期钩子变化如下:
| 选项式API | 组件式API |
| ———————————————————— | —————– |
| beforeCreate | setup |
| created | setup |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| errorCaptured | onErrorCaptured |
| renderTracked | onRenderTracked |
| renderTriggered | onRenderTriggered |
| 其中onRenderTracked是每次页面刷新后重新收集响应式依赖时, | |
| 然后onRenderTriggered是每次页面重新渲染时 | |

需要注意,除了setup之外,其余的都要写到setup里面,比如:

const app = Vue.createApp({
    setup(props, context){
        let name = '张三丰'

        const {onMounted} = Vue
        onMounted(()=>{
            console.log('挂载成功')
        })
        
        return {
            name
        }
    },
}).mount("#app")

响应式的新使用方式

响应式原理

简单说就是,视图跟随数据发生实时的变化

Vue2.X使用property实现,而Vue3.X使用proxy实现

数组的响应式

在Vue2.X中,数组的一些方法被重写了,所以能够实现响应式,这些方法是:

push、pop、shift、unshift、splice、sort、reverse

ref

这个并非是获取组件对象的那个ref

在过去选项式API里面,data()中的数据会被处理从而能达到响应式的效果,
但是组件式API里面就不会自动处理
在这里插入图片描述
要是没了方便的响应式效果,那么框架也失去了一大亮点
所以我们需要一个东西来实现基础数据的响应式,于是乎ref就诞生了

<script>
    let vm = Vue.createApp({
        setup(props, context){
        //解构赋值,从Vue对象里拿到ref
            const {ref} = Vue
            return {
            //像这样处理基础数据
                msg: ref('hello compositionAPI'),
                sayHi: ()=>{
                    console.log(this,'hhh');
                }
            }
        },
        template: `
            <div>
                <h1>{{msg}}</h1>  
                <button @click="sayHi">点击一下</button>  
            </div>
        `
    }).mount("#app")
</script>

在这里插入图片描述
原理是通过proxy代理,包装成proxy({value:….})的形式
至于多次提到的proxy代理到底是什么,后面再细嗦

另外,需要注意,如果在代码中对msg的值进行修改,需要写成msg.value的形式
(在页面上不用这么写是因为底层会自动帮你转化为msg.value)

reactive

其实作用和ref差不多,就是约定俗成地,在对 数组对象等引用类数据 进行响应式处理地时候,我们不使用ref,而使用reactive
(其实ref也可以,处理引用类数据地时候,ref底层还是会自动使用reactive,不过这样一来效率就低多了,所以不推荐)

唯一需要注意的是,reactive没有value属性,访问值的时候需要这样写:

let {computed, ref, reactive} = Vue
let num1 = reactive({one: 1, two: 2})

let show = ()=>{
	console.log(num1)//proxy代理
	
	console.log(num1.value)//undefined
	
	//把num1作为了一个对象
    console.log(num1.one,num1.two)
}

在这里插入图片描述

用法都一样,这里就不再赘述了

readonly

顾名思义咯,只读
防止数据传递传着传着就变了

const {readonly} = Vue
const myName = readonly(ref('吴彦祖'))

在这里插入图片描述

toRefs

在解构赋值的时候比较常用
比如,解构赋值一个具有响应式能力的对象时,
解构后的数据就成为了普通的数据而不再具有响应式能力
如果我们希望其继续拥有响应式能力,那么就要使用到toRefs()
在这里插入图片描述
为了方便阅读,往后尽量只给出关键的代码

const {ref, toRefs, reactive} = Vue
let person = reactive({name:"古尔丹", job:"大酋长"})
//解构赋值后的name和job也会拥有响应式的能力
let {name, job} = toRefs(person)

原理还是proxy代理,把代理的对象拆开,给属性代理

toRef

本质还是把里面的东西丢给代理
相关语法如下:

const {ref, toRefs, reactive,toRef} = Vue
let age = toRef(person, 'age').value = 233

这里需要两个参数,第一个是对象名称,第二个是该对象的属性名称
toref返回一个proxy代理,也就有value值
和toRefs的差异大概也就和名字意义的差异一般

这里不得不说一下ref(),reactive(),toRef(),toRefs()的区别

ref()深拷贝,获取数据时需要加上.value
reactive()深拷贝,不需要加.value(ref是在reactive的基础上实现的)
toRef()浅拷贝,获取数据时需要加上.value
toRefs()浅拷贝,获取数据时需要加上.value

其他方法和属性

context

context上下文
就是setup的参数

setup(props, context){
	const {ref, toRefs, reactive,toRef} = Vue
	let {attrs, slots, emit} = context
	
	console.log("这是attrs:",attrs)
	console.log("这是slots:", slots)
	console.log("这是emit:", emit)
	console.log("props:",props)
},

attrs在前面no-props提到过,也就是继承的属性,
slots是插槽
emit在前面组件通信提到过,是发射的事件
props是v-bind绑定传过来的参数
在这里插入图片描述

computed

没错就是那个computed,现在又来了
不过这里需要引入一下
作用也没变,都是把东西放到缓存

let {computed, ref} = Vue
let num1 = ref(233)
let num2 = computed(()=>{
    return num1.value * 10;
})

watch

还是侦听器,和以前一样

let vm = Vue.createApp({
    setup(props, context){
        let {computed, ref, reactive,watch} = Vue
        let myName= ref('')
        //监听myName
        watch(myName,(cur, pre)=>{
            console.log("现在的值:", cur)
            console.log("过去的值:",pre)
        })
        return {
            myName
        }
    },
    template:`
        <input type="text" placeholder="请输入你的姓名" v-model="myName">
        <div>我的名字是:{{myName}}</div>
    `,
}).mount("#app")

在这里插入图片描述
如果监听多个值的话,写成如下形式即可

watch([myName,myAge],([curName, curAge], [preName, preAge])=>{
       console.log("现在的值:", curName, "和", curAge)
       console.log("过去的值:",preName, "和", preAge)
})

另外,由于监听的对象不能是一个对象的属性,那么如果要监听对象的属性的话,需要这么写:

watch(()=>person.name,(cur,pre)=>{
     console.log("")
})

用一个函数返回一手,就可以变成一般的数据类型了(当然你可以解构之类的)

watch还有第三个参数,再说这个之前先了解watch的性质

1.默认惰性(浏览器打开不会自动触发,要等到相关组件开始活动才触发,比如输入框要输入后才触发)

2.可调度(可以选择是否有惰性以及是否深度监视)
(tip:浅度监视是只监视如一个对象Obj1,发生如同Obj1 = Obj2这种操作Obj1本身发生的变化,会忽视对象内部的变化;相反地,深度监视就是会监视对象的所有变化

具体语法:

watch(()=>person.name, (cur, pre)=>{
    console.log("现在的值:", cur)
    console.log("过去的值:", pre)
},{
    //是否立即执行(是否取消惰性)
    immediate: true,
    //是否深度监视
    deep: false
})

watchEffect

还是侦听器,不过默认深度监视,没有惰性,并且不能查询变化以前的值

watchEffect(()=>{
    console.log('watchEffect开始监听:')
    console.log('myName:', myName.value)
    console.log('myAge', myAge.value)
})

在这里插入图片描述

provide与inject

由于Vue是单向数据流,孙组件要用父组件内的数据的话,得先经过子组件,也就是:
父组件—>子组件—–>孙组件
如果嵌套层级过多,那么数据传递效率就会相当慢
或者,两个毫不相干的组件之间传递数据的…

除了组件通信中提到的方法,这里还可以使用provide和inject——也就是常说的发布-订阅模式的实现之一

<script>
    const grandson = {
        template:`
            <div style="background-color:red;">我是孙组件</div>
        `
    }
    const son = {
        components: {
            'grandson': grandson 
        },
        template: `
            <div style="background-color:orange">
                我是子组件
                <grandson></grandson>
            </div>      
        `
    }
    const father = Vue.createApp({
        setup(props, context){
            return {
                fatherMsg: '我是father'
            }
        },
        components: {
            'son': son
        },
        template: `
            <div style="background-color:yellow">
                我是父组件
                <son></son>    
            </div>
        `
    }).mount("#app")
</script>

在这里插入图片描述

如果需要跨越性传递数据:

使用provide向所有内部组件提供数据

setup(props, context){
    const {ref, provide} = Vue
    //注意这里是value
    let fatherMsg = ref('我是father').value
    provide('fatherMsg', fatherMsg)
    return {
        fatherMsg
    }

使用inject接收数据

setup(props, context){
    const {inject} = Vue
    let grapaMsg = inject('fatherMsg', '第二个参数:默认值')
    return {
        grapaMsg
    }
},

在这里插入图片描述
在这里插入图片描述

后记

到这里Vue篇大概也算完结了,不过我也会继续修改补充完善内容。
考虑到使用频率,部分没有提及的Vue知识点会在后续的Vue-Cli篇中补充。
感谢各位的阅读。


文章作者: Serio
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Serio !
  目录