Vue学习笔记
本次Vue学习基于尚硅谷天禹老师的视频课程。
官方文档:https://cn.vuejs.org/v2/guide/
1、初识Vue
Vue是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
Vue的特点:
1、采用组件化模式,提高代码的复用率,让代码更好维护;
2、声明式编码,让编码人员无需直接操作DOM,提高开发效率;
3、使用虚拟DOM和优秀的Diff算法,尽量复用DOM节点;
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>初识Vue</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="js/vue.js"></script> 8 <link rel="shortcut icon" type="image/x-icon" href="favicon.ico"/> 9 </head> 10 <body> 11 <!-- 12 前置: 13 1、强制刷新shift+F5 14 2、解决网页标签favicon.ico缺失的问题,内部服务器 15 3、new Vue({这里是配置对象,要用专有名词,如el,methods}) 16 初识Vue: 17 1.想让Vue工作,就必须创建一个Vue实例,且要传入一个配置对象; 18 2.app容器里的代码依然符合html规范,只不过混入了一些特殊的Vue语法; 19 3.app容器里的代码被称为【Vue模板】,模板会被vue实例解析成正常的html然后挂载到容器里; 20 4.Vue实例和容器是一一对应的,一对一的关系; 21 5.真实开发中只有一个Vue实例,并且会配合着组件一起使用; 22 6.{{xxx}}中的xxx要写js表达式,且xxx可以自动读取到data中的所有属性; 23 7.一旦data中的数据发生改变,那么页面中用到该数据的地方也会自动更新; 24 25 注意区分:js表达式 和 js代码(语句) 26 1.表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方: 27 (1). a 28 (2). a+b 29 (3). demo(1) 30 (4). x === y ? 'a' : 'b' 31 2.js代码(语句) 32 (1). if(){} 33 (2). for(){} 34 --> 35 36 <!-- 准备好一个容器 --> 37 <div id="app"> 38 <h1>Hello,{{name.toUpperCase()}},{{address}}</h1> 39 </div> 40 41 <script type="text/javascript" > 42 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 43 44 //创建Vue实例 45 //new不可少,Vue本身是个构造函数,不能直接调用,必须通过实例来调用 46 new Vue({ //通过构造函数创建实例 47 el:'#app', //el用于指定当前Vue实例为哪个容器服务,值通常为css选择器字符串。 48 data:{ //data中用于存储数据,数据供el所指定的容器去使用,值我们暂时先写成一个对象。 49 name:'waylon', 50 address:'上海' 51 } 52 }) 53 54 </script> 55 </body> 56 </html>
2、模板语法
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>模板语法</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 11 Vue模板语法有2大类: 12 1.插值语法: 13 功能:用于解析 标签体 内容。 14 写法:{{xxx}},xxx是js表达式,且可以直接读取到data中的所有属性。 15 2.指令语法: 16 功能:用于解析标签(包括:标签属性、标签体内容、绑定事件.....)。 17 举例:v-bind:href="xxx" 或 简写为 :href="xxx",xxx同样要写js表达式, 18 且可以直接读取到data中的所有属性。 19 备注:Vue中有很多的指令,且形式都是:v-????,此处我们只是拿v-bind举个例子。 20 21 --> 22 <!-- 准备好一个容器--> 23 <div id="root"> 24 <h1>插值语法</h1> 25 <h3>你好,{{name}}</h3> 26 <hr/> 27 <h1>指令语法</h1> 28 <!-- 添加v-bind:之后就会把=右边的字符串当成js表达式去执行 --> 29 <a v-bind:href="school.url.toUpperCase()" x="hello">点我去{{school.name}}学习1</a> 30 <a :href="school.url" :x="hello">点我去{{school.name}}学习2</a> 31 </div> 32 </body> 33 34 <script type="text/javascript"> 35 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 36 37 new Vue({ 38 el:'#root', 39 data:{ 40 hello:'嘻嘻', 41 name:'jack', 42 school:{ 43 name:'尚硅谷', //虽然同名,但不同级 44 url:'http://www.atguigu.com', 45 } 46 } 47 }) 48 </script> 49 </html>
3、数据绑定
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>数据绑定</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 11 Vue中有2种数据绑定的方式: 12 1.单向绑定(v-bind):数据只能从data流向页面。 13 2.双向绑定(v-model):数据不仅能从data流向页面,还可以从页面流向data。 14 备注: 15 1.双向绑定一般都应用在表单类元素上(如:input、select等) 16 2.v-model:value 可以简写为 v-model,因为v-model默认收集的就是value值。 17 --> 18 <!-- 准备好一个容器--> 19 <div id="root"> 20 <!-- 普通写法: --> 21 <!-- 22 单向数据绑定:<input type="text" v-bind:value="name"><br/> 23 双向数据绑定:<input type="text" v-model:value="name"><br/> 24 --> 25 26 <!-- 简写: --> 27 单向数据绑定:<input type="text" :value="name"><br/> 28 双向数据绑定:<input type="text" v-model="name"><br/> 29 30 <!-- 如下代码是错误的,因为v-model只能应用在表单类元素(输入类元素)上 --> 31 <!-- <h2 v-model:x="name">你好啊</h2> --> 32 </div> 33 </body> 34 35 <script type="text/javascript"> 36 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 37 38 new Vue({ 39 el:'#root', 40 data:{ 41 name:'尚硅谷' 42 } 43 }) 44 </script> 45 </html>
4、el与data的两种写法
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>el与data的两种写法</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 11 data与el的2种写法 12 1.el有2种写法 13 (1).new Vue时候配置el属性。 14 (2).先创建Vue实例,随后再通过vm.$mount('#root')指定el的值。 15 如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可 16 以使用 vm.$mount() 手动地挂载一个未挂载的实例。 17 2.data有2种写法 18 (1).对象式 19 (2).函数式 20 如何选择:目前哪种写法都可以,以后学习到组件时,data必须使用函数式,否则会报错。 21 3.一个重要的原则: 22 由Vue管理的函数,如data(),一定不要写箭头函数,一旦写了箭头函数,this就不再是Vue实例了,而是Window 23 --> 24 <!-- 准备好一个容器--> 25 <div id="root"> 26 <h1>你好,{{name}}</h1> 27 </div> 28 </body> 29 30 <script type="text/javascript"> 31 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 32 33 //el的两种写法 34 /* 35 const vm = new Vue({ 36 // el:'#root', //第一种写法 37 data:{ 38 name:'尚硅谷' 39 } 40 }) 41 console.log(vm) 42 vm.$mount('#root') //第二种写法,mount意为:挂载 43 */ 44 45 // data的两种写法 46 const vm = new Vue({ 47 el:'#root', 48 //data的第一种写法:对象式 49 /* data:{ 50 name:'尚硅谷' 51 } */ 52 53 //data的第二种写法:函数式 54 data(){ //即data:function(){}的简写 55 console.log('@@@',this) //此处的this是Vue实例对象 56 return{ 57 name:'尚硅谷' 58 } 59 } 60 }) 61 </script> 62 </html>
5、Vue中的MVVM模型
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>理解MVVM</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 11 MVVM模型 12 1. M:模型(Model) :data中的数据 ————>js对象 13 2. V:视图(View) :模板代码 ————>DOM 14 3. VM:视图模型(ViewModel):Vue实例 15 观察发现: 16 1.data中所有的属性,最后都出现在了vm身上。 17 2.vm身上所有的属性 及 Vue原型[Prototype]上所有属性,在Vue模板中都可以直接使用。 18 --> 19 <div id="root"> 20 <h1>学校名称:{{name}}</h1> 21 <h1>学校地址:{{address}}</h1> 22 <!-- <h1>测试一下1:{{1+1}}</h1> 23 <h1>测试一下2:{{$options}}</h1> 24 <h1>测试一下3:{{$emit}}</h1> 25 <h1>测试一下4:{{_c}}</h1> --> 26 </div> 27 </body> 28 29 <script type="text/javascript"> 30 31 const vm = new Vue({ 32 el:'#root', 33 data:{ 34 name:'尚硅谷', 35 address:'北京', 36 } 37 }) 38 console.log(vm) 39 </script> 40 </html>
Vue的实例对象vm:
MVVM在vue中的体现:
6、数据代理
1、关于Object.defineproperty方法
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>关于Object.defineproperty方法</title> 6 </head> 7 <body> 8 <script type="text/javascript"> 9 let number = 18 10 let person = { 11 name:'张三', 12 sex:'男', 13 } 14 15 Object.defineProperty(person,'age',{ 16 // value:18, 17 // enumerable:true, //控制属性是否可以枚举,默认值是false 18 // writable:true, //控制属性是否可以被修改,默认值是false 19 // configurable:true //控制属性是否可以被删除,默认值是false 20 21 //当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值 22 get(){ 23 console.log('有人读取age属性了') 24 return number 25 }, 26 27 //当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值 28 set(value){ 29 console.log('有人修改了age属性,且值是',value) 30 number = value 31 } 32 //getter和setter将person和number两者产生了关联 33 }) 34 // console.log(Object.keys(person)) //将对象的属性名提取出来组成一个数组 35 36 console.log(person) 37 52 </script> 53 </body> 54 </html>
2、何为数据代理?
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>何为数据代理</title> 6 </head> 7 <body> 8 9 <!-- 数据代理:通过一个对象实现对另一个对象中属性的(读/写)操作 --> 10 <script type="text/javascript" > 11 let obj = {x:100} 12 let obj2 = {y:200} 13 14 Object.defineProperty(obj2,'x',{ 15 get(){ 16 return obj.x 17 }, 18 set(value){ 19 obj.x = value 20 } 21 }) 22 </script> 23 </body> 24 </html>
3、在Vue中的数据代理如何实现?
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>Vue中的数据代理</title> 6 <script type="text/javascript" src="js/vue.js"></script> 7 </head> 8 <body> 9 <div id="app"> 10 <h1>个人姓名:{{name}}</h1> 11 <h1>个人年龄:{{age}}</h1> 12 </div> 13 <script type="text/javascript"> 14 const vm = new Vue({ 15 el:"#app", 16 data:{ 17 name:'天青色', 18 age:18 19 } 20 }) 21 </script> 22 </body> 23 </html>
在上面这段代码中,data属性是个对象,可以通过实例对象vm去操作data中的键值对,如下:
这不就是数据代理吗?通过一个对象实现对另一个对象中属性的(读/写)操作 。这点可以从vm实例对象下面的name属性看出来,将鼠标光标移到上面显示 Invoke property getter,表示读取属性值时要通过getter实现。
name和age都是有自己的getter和setter:
其实在vm实例对象下面有个_data属性,里面保存了data里的键值对,可以通过vm._data来进行读写操作:
vm下面的name和age就是通过数据代理与_data中的name和age建立了联系,如果没有这层联系,那模板中{{name}}就不能直接拿到_data中的name了,而应该是{{ _data.name }}才可以。而_data数据又来自于data,所以vm就很方便的代理操作了data。
尚硅谷老师的图也讲得很清楚:
补充一点,_data里还实现了数据劫持。当我们通过vm.name更改值时,页面上用到name的地方也会自动响应式更新了。这就是_data里数据劫持的作用。
总结:
1.Vue中的数据代理:
通过vm对象来代理data对象中属性的操作(读/写)
2.Vue中数据代理的好处:
更加方便的操作data中的数据
3.基本原理:
通过Object.defineProperty()把data对象中所有属性添加到vm上。
为每一个添加到vm上的属性,都指定一个getter/setter。
在getter/setter内部去操作(读/写)data中对应的属性。
7、事件处理
8、计算属性
先实现一个小案例:
输入框默认有张和三,手动输入后全名也会跟着改变。
1、通过插值语法实现案例
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>姓名案例_插值语法实现</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 准备好一个容器--> 11 <div id="root"> 12 姓:<input type="text" v-model="firstName"> <br/><br/> 13 名:<input type="text" v-model="lastName"> <br/><br/> 14 全名:<span>{{firstName}}-{{lastName}}</span> 15 </div> 16 </body> 17 18 <script type="text/javascript"> 19 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 20 21 new Vue({ 22 el:'#root', 23 data:{ 24 firstName:'张', 25 lastName:'三' 26 } 27 }) 28 </script> 29 </html>
2、通过methods实现案例
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>姓名案例_methods实现</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 准备好一个容器--> 11 <div id="root"> 12 姓:<input type="text" v-model="firstName"> <br/><br/> 13 名:<input type="text" v-model="lastName"> <br/><br/> 14 全名:<span>{{fullName()}}</span> 15 </div> 16 </body> 17 18 <script type="text/javascript"> 19 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 20 21 new Vue({ 22 el:'#root', 23 data:{ 24 firstName:'张', 25 lastName:'三' 26 }, 27 methods: { 28 fullName(){ 29 console.log('@---fullName') 30 return this.firstName + '-' + this.lastName 31 } 32 }, 33 }) 34 </script> 35 </html>
注意14行的{{fullName()}}是要加()的。
另外,由于是双向数据绑定,每次输入都会导致data对象里面的属性值发生改变,从而会使得模板代码被Vue重新渲染,fullName方法也会被重新调用。如下:
3、通过计算属性实现案例
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>姓名案例_计算属性实现</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="js/vue.js"></script> 8 </head> 9 <body> 21 <!-- 准备好一个容器--> 22 <div id="root"> 23 姓:<input type="text" v-model="firstName"> <br/><br/> 24 名:<input type="text" v-model="lastName"> <br/><br/> 25 测试:<input type="text" v-model="x"> <br/><br/> 26 全名:<span>{{fullName}}</span> <br/><br/> 27 <!-- 全名:<span>{{fullName}}</span> <br/><br/> 28 全名:<span>{{fullName}}</span> <br/><br/> 29 全名:<span>{{fullName}}</span> --> 30 </div> 31 </body> 32 33 <script type="text/javascript"> 34 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 35 36 const vm = new Vue({ 37 el:'#root', 38 data:{ 39 firstName:'张', 40 lastName:'三', 41 x:'你好' 42 }, 43 methods: { 44 demo(){ 45 46 } 47 }, 48 computed:{ 49 fullName:{ 50 //get有什么作用?当有人读取fullName时,get就会被调用,且返回值就作为fullName的值 51 //get什么时候调用?1.初次读取fullName时。2.所依赖的数据发生变化时。 52 get(){ 53 console.log('get被调用了') 54 // console.log(this) //经过Vue的处理后此处的this是vm 55 return this.firstName + '-' + this.lastName 56 }, 57 //set什么时候调用? 当fullName被修改时。 58 set(value){ 59 console.log('set',value) 60 const arr = value.split('-') 61 this.firstName = arr[0] 62 this.lastName = arr[1] 63 } 64 } 65 } 66 }) 67 </script> 68 </html>
总结:
1.定义:要用的属性不存在,要通过已有属性(如data对象里的属性)计算得来。它也是vm下面的一个属性。
2.原理:底层借助了Objcet.defineproperty方法提供的getter和setter。
3.get函数什么时候执行?
(1).初次读取时会执行一次。
(2).当依赖的数据发生改变时会被再次调用。
4.优势:与methods实现相比,内部有缓存机制(复用),效率更高,调试方便。
5.计算属性最终会出现在vm上,直接插值法读取使用即可。
6.如果计算属性要被修改,那必须写set函数去响应修改,且set中要引起计算时依赖的数据发生改变。
4、计算属性的简写
当只考虑计算属性的读取不考虑修改的时候,就可以简写:
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>姓名案例_计算属性实现</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 准备好一个容器--> 11 <div id="root"> 12 姓:<input type="text" v-model="firstName"> <br/><br/> 13 名:<input type="text" v-model="lastName"> <br/><br/> 14 全名:<span>{{fullName}}</span> <br/><br/> 15 </div> 16 </body> 17 18 <script type="text/javascript"> 19 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 20 21 const vm = new Vue({ 22 el:'#root', 23 data:{ 24 firstName:'张', 25 lastName:'三', 26 }, 27 computed:{ 28 //完整写法 29 /* fullName:{ 30 get(){ 31 console.log('get被调用了') 32 return this.firstName + '-' + this.lastName 33 }, 34 set(value){ 35 console.log('set',value) 36 const arr = value.split('-') 37 this.firstName = arr[0] 38 this.lastName = arr[1] 39 } 40 } */ 41 //简写 42 fullName(){ 43 console.log('get被调用了') 44 return this.firstName + '-' + this.lastName 45 } 46 } 47 }) 48 </script> 49 </html>
读取的时候依然是 {{计算属性}} ,不可加() 。
9、监视属性
实现天气小案例:
默认炎热,点击后切换为凉爽,再点击切换为炎热。
1、实现案例
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title></title> 6 <script type="text/javascript" src="js/vue.js"></script> 7 </head> 8 <body> 9 <div id="app"> 10 <h1>今天的天气很{{weather}}</h1> 11 <button type="button" @click="changeWeather">点我切换天气</button> 12 <!-- <button type="button" @click="ishot=!ishot;x其他代码x">点我切换天气</button> --> 13 <!-- <button type="button" @click="window.alert('点击成功')">点我切换天气</button> --> 14 <!-- 在绑定事件的时候,@xxx='yyy',这里的yyy可以写一些简单的语句,复杂的话就交给methods --> 15 </div> 16 <script type="text/javascript"> 17 const vm = new Vue({ 18 el:'#app', 19 data:{ 20 ishot:true, 21 // window 点击事件里的window在vm里找不到,所以要在这添加window,不然会报错 22 }, 23 methods:{ 24 changeWeather(){ 25 this.ishot = !this.ishot 26 } 27 }, 28 computed:{ 29 weather(){ 30 return this.ishot ? '炎热' : '凉爽' 31 } 32 } 33 }) 34 </script> 35 </body> 36 </html>
2、监视属性watch
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>天气案例_监视属性</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 准备好一个容器--> 11 <div id="root"> 12 <h2>今天天气很{{info}}</h2> 13 <button @click="changeWeather">切换天气</button> 14 </div> 15 </body> 16 17 <script type="text/javascript"> 18 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 19 20 const vm = new Vue({ 21 el:'#root', 22 data:{ 23 isHot:true, 24 }, 25 computed:{ 26 info(){ 27 return this.isHot ? '炎热' : '凉爽' 28 } 29 }, 30 methods: { 31 changeWeather(){ 32 this.isHot = !this.isHot 33 } 34 }, 35 /* watch:{ 36 isHot:{ 37 immediate:true, //初始化时让handler调用一下 38 //handler什么时候调用?当isHot发生改变时。 39 handler(newValue,oldValue){ 40 console.log('isHot被修改了',newValue,oldValue) 41 } 42 } 43 } */ 44 }) 45 46 vm.$watch('isHot',{ 47 immediate:true, //初始化时让handler调用一下 48 //handler什么时候调用?当isHot发生改变时。 49 handler(newValue,oldValue){ 50 console.log('isHot被修改了',newValue,oldValue) 51 } 52 }) 53 </script> 54 </html>
总结:
监视属性watch:
1.当被监视的属性变化时, 回调函数自动调用, 进行相关操作
2.监视的属性必须存在,才能进行监视!可以使data里的属性,也可以是计算属性
3.监视的两种写法:
(1).new Vue时传入watch配置(初期明确知道需要监视)
(2).通过vm.$watch监视(后期需求添加的监视)
3、深度监视deep
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>天气案例_深度监视</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 准备好一个容器--> 11 <div id="root"> 12 <h2>今天天气很{{info}}</h2> 13 <button @click="changeWeather">切换天气</button> 14 <hr/> 15 <h3>a的值是:{{numbers.a}}</h3> 16 <button @click="numbers.a++">点我让a+1</button> 17 <h3>b的值是:{{numbers.b}}</h3> 18 <button @click="numbers.b++">点我让b+1</button> 19 <button @click="numbers = {a:666,b:888}">彻底替换掉numbers</button> 20 <!-- {{numbers.c.d.e}} --> 21 </div> 22 </body> 23 24 <script type="text/javascript"> 25 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 26 27 const vm = new Vue({ 28 el:'#root', 29 data:{ 30 isHot:true, 31 numbers:{ 32 a:1, 33 b:1, 34 c:{ 35 d:{ 36 e:100 37 } 38 } 39 } 40 }, 41 computed:{ 42 info(){ 43 return this.isHot ? '炎热' : '凉爽' 44 } 45 }, 46 methods: { 47 changeWeather(){ 48 this.isHot = !this.isHot 49 } 50 }, 51 watch:{ 52 isHot:{ 53 // immediate:true, //初始化时让handler调用一下 54 //handler什么时候调用?当isHot发生改变时。 55 handler(newValue,oldValue){ 56 console.log('isHot被修改了',newValue,oldValue) 57 } 58 }, 59 //监视多级结构中某个属性的变化,注意要用引号,之前isHot是简写 60 /* 'numbers.a':{ 61 handler(){ 62 console.log('a被改变了') 63 } 64 } */ 65 //监视多级结构中所有属性的变化 66 numbers:{ 67 deep:true, //默认为false,开启深度监视 68 handler(){ 69 console.log('numbers改变了') 70 } 71 } 72 } 73 }) 74 75 </script> 76 </html>
总结:
深度监视:
(1).Vue中的watch默认不监测对象内部值的改变(一层)。
(2).配置deep:true可以监测对象内部值改变(多层)。
(3).Vue自身可以监测对象内部值的改变,无论有多少层,如numbers.c.d.e,但Vue提供的watch默认不可以!
(4).考虑到效率性,使用watch时根据数据的具体结构,决定是否采用深度监视。
4、监视属性简写
当监视某个属性而不需要配置immediate和deep时,可以使用简写形式,类似于计算属性的简写:
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>天气案例_监视属性_简写</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 准备好一个容器--> 11 <div id="root"> 12 <h2>今天天气很{{info}}</h2> 13 <button @click="changeWeather">切换天气</button> 14 </div> 15 </body> 16 17 <script type="text/javascript"> 18 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 19 20 const vm = new Vue({ 21 el:'#root', 22 data:{ 23 isHot:true, 24 }, 25 computed:{ 26 info(){ 27 return this.isHot ? '炎热' : '凉爽' 28 } 29 }, 30 methods: { 31 changeWeather(){ 32 this.isHot = !this.isHot 33 } 34 }, 35 watch:{ 36 //正常写法 37 /* isHot:{ 38 // immediate:true, //初始化时让handler调用一下 39 // deep:true,//深度监视 40 handler(newValue,oldValue){ 41 console.log('isHot被修改了',newValue,oldValue) 42 } 43 }, */ 44 //简写 45 /* isHot(newValue,oldValue){ 46 console.log('isHot被修改了',newValue,oldValue,this) 47 } */ 48 } 49 }) 50 51 //正常写法 52 /* vm.$watch('isHot',{ 53 immediate:true, //初始化时让handler调用一下 54 deep:true,//深度监视 55 handler(newValue,oldValue){ 56 console.log('isHot被修改了',newValue,oldValue) 57 } 58 }) */ 59 60 //简写 61 /* vm.$watch('isHot',(newValue,oldValue)=>{ 62 console.log('isHot被修改了',newValue,oldValue,this) 63 }) */ 64 65 </script> 66 </html>
5、watch实现天气案例
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>姓名案例_watch实现</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="../js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 准备好一个容器--> 11 <div id="root"> 12 姓:<input type="text" v-model="firstName"> <br/><br/> 13 名:<input type="text" v-model="lastName"> <br/><br/> 14 全名:<span>{{fullName}}</span> <br/><br/> 15 </div> 16 </body> 17 18 <script type="text/javascript"> 19 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 20 21 const vm = new Vue({ 22 el:'#root', 23 data:{ 24 firstName:'张', 25 lastName:'三', 26 fullName:'张-三' 27 }, 28 watch:{ 29 firstName(val){ 30 setTimeout(()=>{ //此处如果写普通函数则this为window 31 console.log(this) 32 this.fullName = val + '-' + this.lastName 33 },1000); 34 }, 35 lastName(val){ 36 this.fullName = this.firstName + '-' + val 37 } 38 } 39 }) 40 </script> 41 </html>
总结:
computed和watch之间的区别:
1.computed能完成的功能,watch都可以完成。谁简单用谁。
2.watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作,比如延迟,而计算属性却不能延迟得到返回值
两个重要的小原则:
1.所被Vue管理的函数,最好写成普通函数,这样this的指向才是vm 或 组件实例对象。
2.所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数等、Promise的回调函数),最好写成箭头函数,自身无this,向外找,这样this的指向才是vm 或 组件实例对象。
12、列表渲染
1、基本列表
使用v-for指令来渲染列表:
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>基本列表</title> 6 <script type="text/javascript" src="../js/vue.js"></script> 7 </head> 8 <body> 9 <!-- 10 v-for指令: 11 1.用于展示列表数据 12 2.语法:v-for="(item, index) in xxx" :key="yyy" 13 3.可遍历:数组、对象、字符串(用的很少)、指定次数(用的很少) 14 --> 15 <!-- 准备好一个容器--> 16 <div id="root"> 17 <!-- 遍历数组 --> 18 <h2>人员列表(遍历数组)</h2> 19 <ul> 20 <li v-for="(p,index) of persons" :key="index"> 21 {{p.name}}-{{p.age}} 22 </li> 23 </ul> 24 25 <!-- 遍历对象 --> 26 <h2>汽车信息(遍历对象)</h2> 27 <ul> 28 <li v-for="(value,k) of car" :key="k"> 29 {{k}}-{{value}} 30 </li> 31 </ul> 32 <h2>汽车信息(遍历对象)</h2> 33 <ul> 34 <li v-for="a of car" > 35 只有值,无key:{{a}} 36 </li> 37 </ul> 38 39 <!-- 遍历字符串 --> 40 <h2>测试遍历字符串(用得少)</h2> 41 <ul> 42 <li v-for="(char,index) of str" :key="index"> 43 {{char}}-{{index}} 44 </li> 45 </ul> 46 47 <!-- 遍历指定次数 --> 48 <h2>测试遍历指定次数(用得少)</h2> 49 <ul> 50 <li v-for="(number,index) of 5" :key="index"> 51 {{index}}-{{number}} 52 </li> 53 </ul> 54 </div> 55 56 <script type="text/javascript"> 57 Vue.config.productionTip = false 58 59 new Vue({ 60 el:'#root', 61 data:{ 62 persons:[ 63 {id:'001',name:'张三',age:18}, 64 {id:'002',name:'李四',age:19}, 65 {id:'003',name:'王五',age:20} 66 ], 67 car:{ 68 name:'奥迪A8', 69 price:'70万', 70 color:'黑色' 71 }, 72 str:'hello' 73 } 74 }) 75 </script> 76 </html>
2、key的原理
3、列表过滤
4、列表排序
5、更新数据出现的问题
更新数据时出现了一个问题。
需求:点击修改按钮,就能修改第一行的马冬梅的信息。
第一种方式:通过修改对象属性的方式去修改,vue能成功监测到数据修改,并且更新到页面显示。
第二种方式:通过修改整个对象的方式去修改,当点击按钮后,页面并未更新显示最新的马冬梅的信息,而通过控制台确实发现马冬梅的信息已经被修改,打开vue开发者工具,发现vue也监测到数据的改变:
但如果先打开vue开发者工具,再去点击更新按钮,会发现vue甚至都没监测到数据的改变,当然页面也不会更新显示最新的马冬梅信息。
这个问题涉及到vue监测数据的原理。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>更新时的一个问题</title> 6 <script type="text/javascript" src="../js/vue.js"></script> 7 </head> 8 <body> 9 <!-- 准备好一个容器--> 10 <div id="root"> 11 <h2>人员列表</h2> 12 <button @click="updateMei">更新马冬梅的信息</button> 13 <ul> 14 <li v-for="(p,index) of persons" :key="p.id"> 15 {{p.name}}-{{p.age}}-{{p.sex}} 16 </li> 17 </ul> 18 </div> 19 20 <script type="text/javascript"> 21 Vue.config.productionTip = false 22 23 const vm = new Vue({ 24 el:'#root', 25 data:{ 26 persons:[ 27 {id:'001',name:'马冬梅',age:30,sex:'女'}, 28 {id:'002',name:'周冬雨',age:31,sex:'女'}, 29 {id:'003',name:'周杰伦',age:18,sex:'男'}, 30 {id:'004',name:'温兆伦',age:19,sex:'男'} 31 ] 32 }, 33 methods: { 34 updateMei(){ 35 // this.persons[0].name = '马老师' //奏效 36 // this.persons[0].age = 50 //奏效 37 // this.persons[0].sex = '男' //奏效 38 this.persons[0] = {id:'001',name:'马老师',age:50,sex:'男'} //不奏效 39 40 } 41 } 42 }) 43 44 </script> 45 </html>
6、Vue监测数据的原理
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>数据监视原理总结</title> 6 <style> 7 button{ 8 margin-top: 10px; 9 } 10 </style> 11 <!-- 引入Vue --> 12 <script type="text/javascript" src="../js/vue.js"></script> 13 </head> 14 <body> 15 <!-- 准备好一个容器--> 16 <div id="root"> 17 <h1>学生信息</h1> 18 <button @click="student.age++">年龄+1岁</button> <br/> 19 <button @click="addSex">添加性别属性,默认值:男</button> <br/> 20 <button @click="student.sex = '未知' ">修改性别</button> <br/> 21 <button @click="addFriend">在列表首位添加一个朋友</button> <br/> 22 <button @click="updateFirstFriendName">修改第一个朋友的名字为:张三</button> <br/> 23 <button @click="updateSecondFriendName">修改第二个朋友的名字为:天青色,年龄为18</button> <br/> 24 <button @click="addHobby">添加一个爱好</button> <br/> 25 <button @click="updateHobby">修改第一个爱好为:开车</button> <br/> 26 <button @click="removeSmoke">过滤掉爱好中的抽烟</button> <br/> 27 <h3>姓名:{{student.name}}</h3> 28 <h3>年龄:{{student.age}}</h3> 29 <h3 v-if="student.sex">性别:{{student.sex}}</h3> 30 <h3>爱好:</h3> 31 <ul> 32 <li v-for="(h,index) in student.hobby" :key="index"> 33 {{h}} 34 </li> 35 </ul> 36 <h3>朋友们:</h3> 37 <ul> 38 <li v-for="(f,index) in student.friends" :key="index"> 39 {{f.name}}--{{f.age}} 40 </li> 41 </ul> 42 </div> 43 </body> 44 45 <script type="text/javascript"> 46 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 47 48 const vm = new Vue({ 49 el:'#root', 50 data:{ 51 student:{ 52 name:'tom', 53 age:18, 54 hobby:['抽烟','喝酒','烫头'], 55 friends:[ 56 {name:'jerry',age:35}, 57 {name:'tony',age:36} 58 ] 59 } 60 }, 61 methods: { 62 addSex(){ 63 // Vue.set(this.student,'sex','男') 64 this.$set(this.student,'sex','男') 65 }, 66 addFriend(){ 67 this.student.friends.unshift({name:'jack',age:70}) 68 }, 69 70 // 数组里的对象不是响应式的,而数组里的对象的属性却是响应式属性,有setter/getter 71 updateFirstFriendName(){ 72 this.student.friends[0].name = '张三' 73 }, 74 updateSecondFriendName(){ 75 // Vue.set(this.student.friends,1,{name:'天青色',age:18}) 76 // this.$set(this.student.friends,1,{name:'天青色',age:18}) 77 this.student.friends.splice(1,1,{name:'天青色',age:18}) 78 // this.student.friends[1] = {name:'天青色',age:18} //vue检测不到,不会响应式视图更新 79 }, 80 81 addHobby(){ 82 this.student.hobby.push('学习') 83 }, 84 updateHobby(){ 85 // this.student.hobby.splice(0,1,'开车') 86 // Vue.set(this.student.hobby,0,'开车') 87 this.$set(this.student.hobby,0,'开车') 88 }, 89 removeSmoke(){ 90 this.student.hobby = this.student.hobby.filter((h)=>{ 91 return h !== '抽烟' 92 }) 93 } 94 } 95 }) 96 </script> 97 </html>
Vue监视数据的原理:
1、 vue会监视data中所有层次的数据。递归处理,有多少层都能监测。
2、监测data中的对象类型的数据:
通过setter实现监视,且要在new Vue时就传入要监测的数据。如果当时没有具体的数据,就留空。对象中后追加的属性,Vue默认不做响应式处理。
3、如需给后添加的属性(非根级别)做响应式,可以使用如下API:
Vue.set(target,propertyName/index,value) 或
vm.$set(target,propertyName/index,value)
注意:Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象添加属性!!!
4、 监测data中数组类型的数据:
涉及到元素更改的方法有七个:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
这七个Vue在内部都对他们进行了重写,当我们调用这些方法时,首先vue会调用JS原生对应的方法对数组进行更新,然后重新解析模板,进而更新页面。这样就实现了数据监测。
这也是数组和对象监测的不同点。从vue实例里的_data中也可以看出,对象类型的数据都有对应的Setter和Getter,而数组类型的数据却没有。没有则不能直接进行监测。
Vue.set() 或 vm.$set()也能对数组数据进行修改并且vue能监视到。
这些在Vue文档的深入响应式原理这一节都有所说明:
13、收集表单数据
注意,在checkbox类型的输入框中,指定任意checked的值,chrome浏览器都会将其变为checked="checked",即选中状态。
通过js将checked的值变为布尔值会更改选中状态。
14、过滤器
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>过滤器</title> 6 <script type="text/javascript" src="../js/vue.js"></script> 7 <script type="text/javascript" src="../js/dayjs.min.js"></script> 8 </head> 9 <body> 10 <!-- 11 过滤器: 12 定义:对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)。 13 语法: 14 1.注册过滤器: 15 全局过滤器Vue.filter(name,callback) 16 局部过滤器:new Vue{filters:{}} 17 2.使用过滤器:{{ xxx | 过滤器名}} 或 v-bind:属性 = "xxx | 过滤器名" 18 备注: 19 1.过滤器也可以接收额外参数、多个过滤器也可以串联 20 2.并没有改变原本的数据, 是产生新的对应的数据 21 --> 22 <!-- 准备好一个容器--> 23 <div id="root"> 24 <h2>显示格式化后的时间</h2> 25 <!-- 计算属性实现 --> 26 <h3>1现在是:{{fmtTime}}</h3> 27 <!-- methods实现 --> 28 <h3>2现在是:{{getFmtTime()}}</h3> 29 <!-- 过滤器实现 --> 30 <h3>3现在是:{{time | timeFormater}}</h3> 31 <!-- 过滤器实现(传参) --> 32 <h3>4现在是:{{time | timeFormater('YYYY_MM_DD')}}</h3> 33 <!-- 过滤器串联 --> 34 <h3>5现在是:{{time | timeFormater('YYYY_MM_DD') | mySlice}}</h3> 35 <h3 :x="msg | mySlice">尚硅谷</h3> 36 </div> 37 38 <div id="root2"> 39 <h2>{{msg | mySlice}}</h2> 40 </div> 41 </body> 42 43 <script type="text/javascript"> 44 Vue.config.productionTip = false 45 //全局过滤器 46 Vue.filter('mySlice',function(value){ 47 return value.slice(0,4) 48 }) 49 50 new Vue({ 51 el:'#root', 52 data:{ 53 time:1621561377603, //时间戳 54 msg:'你好,尚硅谷' 55 }, 56 computed: { 57 fmtTime(){ 58 return dayjs(this.time).format('YYYY年MM月DD日 HH:mm:ss') 59 } 60 }, 61 methods: { 62 getFmtTime(){ 63 return dayjs(this.time).format('YYYY年MM月DD日 HH:mm:ss') 64 } 65 }, 66 //局部过滤器 67 filters:{ 68 timeFormater(value,str='YYYY年MM月DD日 HH:mm:ss'){ 69 // console.log('@',value) 70 return dayjs(value).format(str) 71 } 72 } 73 }) 74 75 new Vue({ 76 el:'#root2', 77 data:{ 78 msg:'hello,atguigu!' 79 } 80 }) 81 </script> 82 </html>
dayjs.js库来自于BootCDN:
15、内置指令
16、自定义指令
Vue内置指令满足不了需求时,可以自定义指令,包含全局的和局部的,类似于过滤器。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>自定义指令</title> 6 <script type="text/javascript" src="../js/vue.js"></script> 7 </head> 8 <body> 9 <!-- 10 需求1:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍。 11 需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点。 12 自定义指令总结: 13 一、定义语法: 14 (1).局部指令: 15 new Vue({ new Vue({ 16 directives:{指令名:配置对象} 或 directives{指令名:回调函数} 17 }) }) 18 (2).全局指令: 19 Vue.directive(指令名,配置对象) 或 Vue.directive(指令名,回调函数) 20 21 二、配置对象中常用的3个回调: 22 (1).bind:指令与元素成功绑定时调用。 23 (2).inserted:指令所在元素被插入页面时调用。 24 (3).update:指令所在模板结构被重新解析时调用。 25 26 三、备注: 27 1.指令定义时不加v-,但使用时要加v-; 28 2.如果指令名由多个单词组成,定义的时候就用字符串形式“-”连接,不能写简写形式,如'big-number',引用则v-big-number 29 --> 30 <!-- 准备好一个容器--> 31 <div id="root"> 32 <h2>{{name}}</h2> 33 <h2>当前的n值是:<span v-text="n"></span> </h2> 34 <!-- <h2>放大10倍后的n值是:<span v-big-number="n"></span> </h2> --> 35 <h2>放大10倍后的n值是:<span v-big="n"></span> </h2> 36 <button @click="n++">点我n+1</button> 37 <hr/> 38 <input type="text" v-fbind:value="n"> 39 </div> 40 </body> 41 42 <script type="text/javascript"> 43 Vue.config.productionTip = false 44 45 //定义全局指令 46 47 // 全局指令对象形式,第二个参数是对象 48 /* Vue.directive('fbind',{ 49 //指令与元素成功绑定时(一上来) 50 bind(element,binding){ 51 element.value = binding.value 52 }, 53 //指令所在元素被插入页面时 54 inserted(element,binding){ 55 element.focus() 56 }, 57 //指令所在的模板被重新解析时 58 update(element,binding){ 59 element.value = binding.value 60 } 61 }) */ 62 63 //全局指令函数形式,第二个参数是函数 64 /* Vue.directive('big',function(element,binding){ 65 console.log(element,binding) 66 console.log('big',this) //注意此处的this是window 67 // console.log('big') 68 element.innerText = binding.value * 10)} */ 69 70 new Vue({ 71 el:'#root', 72 data:{ 73 name:'尚硅谷', 74 n:1 75 }, 76 directives:{ 77 //big函数何时会被调用?1.指令与元素成功绑定时(一上来)。2.指令所在的模板被重新解析时。 78 /* 'big-number'(element,binding){ //指令名太长就用字符串形式“-”连接,不能写简写形式 79 // console.log('big') 80 element.innerText = binding.value * 10 81 }, */ 82 83 //函数形式的 84 big(element,binding){ 85 console.log(element,binding) 86 console.log('big',this) //注意此处的this是window 87 // console.log('big') 88 element.innerText = binding.value * 10 89 }, 90 /* 对象形式的,可添加特定的回调函数(钩子函数),实现特殊的功能,而上面函数形式的 91 简单形式就默认只有两个钩子函数bing和update,这两个里面的功能通常是一样的*/ 92 fbind:{ 93 //指令与元素成功绑定时(一上来) 94 bind(element,binding){ 95 element.value = binding.value 96 }, 97 //指令所在元素被插入页面时 98 inserted(element,binding){ 99 element.focus() 100 }, 101 //指令所在的模板被重新解析时 102 update(element,binding){ 103 element.value = binding.value 104 } 105 } 106 } 107 }) 108 109 </script> 110 </html>
对于剩下的两个钩子函数和钩子函数的参数:
17、Vue生命周期
1、什么是Vue生命周期
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>引出生命周期</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="../js/vue.js"></script> 8 </head> 9 <body> 17 <!-- 准备好一个容器--> 18 <div id="root"> 19 <h2 v-if="a">你好啊</h2> 20 <h2 :style="{opacity}">欢迎学习Vue</h2> 21 </div> 22 </body> 23 24 <script type="text/javascript"> 25 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 26 27 new Vue({ 28 el:'#root', 29 data:{ 30 a:false, //页面加载后更改a的值为true,验证初始的DOM挂载完毕才会调用一次mounted 31 opacity:1 32 }, 33 methods: { 34 35 }, 36 //Vue完成模板的解析并把初始的真实DOM元素放入页面后(挂载完毕)调用mounted 37 mounted(){ 38 console.log('mounted',this) 39 setInterval(() => { 40 this.opacity -= 0.01 41 if(this.opacity <= 0) this.opacity = 1 42 },16) 43 }, 44 }) 45 46 //通过外部的定时器实现(不推荐) 47 /* setInterval(() => { 48 vm.opacity -= 0.01 49 if(vm.opacity <= 0) vm.opacity = 1 50 },16) */ 51 </script> 52 </html>
生命周期:
1.又名:生命周期回调函数、生命周期函数、生命周期钩子。
2.是什么:Vue在关键时刻帮我们调用的一些特殊名称的函数。
3.生命周期函数的名字不可更改,但函数的具体内容是程序员根据需求编写的。
4.生命周期函数中的this指向是vm 或 组件实例对象。
2、分析生命周期
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>分析生命周期</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="../js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 准备好一个容器--> 11 <div id="root" :x="n"> 12 <h2 v-text="n"></h2> 13 <h2>当前的n值是:{{n}}</h2> 14 <button @click="add">点我n+1</button> 15 <button @click="bye">点我销毁vm</button> 16 </div> 17 </body> 18 19 <script type="text/javascript"> 20 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 21 22 new Vue({ 23 el:'#root', //没有el则不会挂载,可以手动调用实例的$mount('#root') 24 // template:` // 如果有模板,会替换掉body内的容器,innerHTML,不再有id为root的div 25 // <div> 26 // <h2>当前的n值是:{{n}}</h2> 27 // <button @click="add">点我n+1</button> 28 // </div> 29 // `, 30 data:{ 31 n:1 32 }, 33 methods: { 34 add(){ 35 console.log('add') 36 this.n++ 37 }, 38 bye(){ 39 console.log('bye') 40 this.$destroy() 41 } 42 }, 43 watch:{ 44 n(){ 45 console.log('n变了') 46 } 47 }, 48 beforeCreate() { 49 console.log('beforeCreate') 50 }, 51 created() { 52 console.log('created') 53 }, 54 beforeMount() { 55 console.log('beforeMount') 56 }, 57 mounted() { 58 console.log('mounted') 59 console.log(this.$el) 60 console.log(this.$el instanceof HTMLElement) 61 }, 62 beforeUpdate() { 63 console.log('beforeUpdate') 64 console.log(this.n) 65 debugger //修改n后页面还没更新修改 66 }, 67 updated() { 68 console.log('updated') 69 }, 70 beforeDestroy() { 71 console.log('beforeDestroy') 72 }, 73 destroyed() { 74 console.log('destroyed') 75 }, 76 }) 77 </script> 78 </html>
生命周期图:
模板到视图的转化过程:
更新时,通过diff算法将新旧虚拟dom进行对比,即对比新旧虚拟节点,vnode和oldVnode,找出真正需要更新的节点,在真实DOM上进行针对性修改,从而实现视图的更新。
关于虚拟DOM可以参考这个文章:https://www.cnblogs.com/fundebug/p/vue-virtual-dom.html
3、总结生命周期
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>引出生命周期</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="../js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 11 常用的生命周期钩子: 12 1.mounted: 发送ajax请求、启动定时器、绑定自定义事件、订阅消息等【初始化操作】。 13 2.beforeDestroy: 清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】。 14 15 关于销毁Vue实例 16 1.销毁后借助Vue开发者工具看不到任何信息。 17 2.销毁后自定义事件会失效,但原生DOM事件依然有效。 18 3.一般不会在beforeDestroy操作数据,因为即便操作数据,也不会再触发更新流程了。 19 --> 20 <!-- 准备好一个容器--> 21 <div id="root"> 22 <h2 :style="{opacity}">欢迎学习Vue</h2> 23 <button @click="opacity = 1">透明度设置为1</button> 24 <button @click="stop">点我停止变换</button> 25 </div> 26 </body> 27 28 <script type="text/javascript"> 29 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 30 31 new Vue({ 32 el:'#root', 33 data:{ 34 opacity:1 35 }, 36 methods: { 37 stop(){ 38 //不要在这里关闭定时器 39 this.$destroy() 40 } 41 }, 42 //Vue完成模板的解析并把初始的真实DOM元素放入页面后(挂载完毕)调用mounted 43 mounted(){ 44 console.log('mounted',this) 45 this.timer = setInterval(() => { 46 console.log('setInterval') 47 this.opacity -= 0.01 48 if(this.opacity <= 0) this.opacity = 1 49 },16) 50 }, 51 beforeDestroy() { 52 clearInterval(this.timer) 53 console.log('vm即将驾鹤西游了') 54 }, 55 }) 56 57 </script> 58 </html>
18、非单文件组件
1、组件的基本使用
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>基本使用</title> 6 <script type="text/javascript" src="../js/vue.js"></script> 7 </head> 8 <body> 9 --> 10 <!-- 准备好一个容器--> 11 <div id="root"> 12 <hello></hello> 13 <hr> 14 <h1>{{msg}}</h1> 15 <hr> 16 <!-- 第三步:编写组件标签 --> 17 <school></school> 18 <hr> 19 <!-- 第三步:编写组件标签 --> 20 <student></student> 21 </div> 22 23 <div id="root2"> 24 <hello></hello> 25 </div> 26 </body> 27 28 <script type="text/javascript"> 29 Vue.config.productionTip = false 30 31 //第一步:创建school组件 32 const school = Vue.extend({ 33 template:` 34 <div class="demo"> 35 <h2>学校名称:{{schoolName}}</h2> 36 <h2>学校地址:{{address}}</h2> 37 <button @click="showName">点我提示学校名</button> 38 </div> 39 `, 40 // el:'#root', //组件定义时,一定不要写el配置项,因为最终所有的组件都要被一个vm管理,由vm决定服务于哪个容器。 41 data(){ 42 return { 43 schoolName:'尚硅谷', 44 address:'北京昌平' 45 } 46 }, 47 methods: { 48 showName(){ 49 alert(this.schoolName) 50 } 51 }, 52 }) 53 54 //第一步:创建student组件 55 const student = Vue.extend({ 56 template:` 57 <div> 58 <h2>学生姓名:{{studentName}}</h2> 59 <h2>学生年龄:{{age}}</h2> 60 </div> 61 `, 62 data(){ 63 return { 64 studentName:'张三', 65 age:18 66 } 67 } 68 }) 69 70 //第一步:创建hello组件 71 const hello = Vue.extend({ 72 template:` 73 <div> 74 <h2>你好啊!{{name}}</h2> 75 </div> 76 `, 77 data(){ 78 return { 79 name:'Tom' 80 } 81 } 82 }) 83 84 //第二步:全局注册组件 85 Vue.component('hello',hello) 86 87 //创建vm 88 new Vue({ 89 el:'#root', 90 data:{ 91 msg:'你好啊!' 92 }, 93 //第二步:注册组件(局部注册) 94 components:{ 95 school, //简写,school:school,key为组件名 96 student 97 } 98 }) 99 new Vue({ 100 el:'#root2', 101 }) 102 </script> 103 </html>
Vue中使用组件的三大步骤:
1、定义组件(创建组件)
2、注册组件
3、使用组件(写组件标签)
一、如何定义一个组件?
使用Vue.extend(options)创建,其中options和new Vue(options)时传入的那个options几乎一样,但也有点区别;
区别如下:
1.el不要写,为什么? ——— 最终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器。
2.data必须写成函数,为什么? ———— 避免组件被复用时,数据存在引用关系。
备注:使用template可以配置组件结构。
二、如何注册组件?
1.局部注册:靠new Vue的时候传入components选项
2.全局注册:靠Vue.component('组件名',组件)
三、编写组件标签:
<school></school>
几个注意点:
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 ,系统会自动调用extend方法
2、组件嵌套
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>组件的嵌套</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="../js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 准备好一个容器--> 11 <div id="root"> 12 13 </div> 14 </body> 15 16 <script type="text/javascript"> 17 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 18 19 //定义student组件 20 const student = Vue.extend({ 21 name:'student', 22 template:` 23 <div> 24 <h2>学生姓名:{{name}}</h2> 25 <h2>学生年龄:{{age}}</h2> 26 </div> 27 `, 28 data(){ 29 return { 30 name:'尚硅谷', 31 age:18 32 } 33 } 34 }) 35 36 //定义school组件 37 const school = Vue.extend({ 38 name:'school', 39 template:` 40 <div> 41 <h2>学校名称:{{name}}</h2> 42 <h2>学校地址:{{address}}</h2> 43 <student></student> 44 </div> 45 `, 46 data(){ 47 return { 48 name:'尚硅谷', 49 address:'北京' 50 } 51 }, 52 //注册组件(局部) 53 components:{ 54 student 55 } 56 }) 57 58 //定义hello组件 59 const hello = Vue.extend({ 60 template:`<h1>{{msg}}</h1>`, 61 data(){ 62 return { 63 msg:'欢迎来到尚硅谷学习!' 64 } 65 } 66 }) 67 68 //定义app组件,一人之下万人之上 69 const app = Vue.extend({ 70 template:` 71 <div> 72 <hello></hello> 73 <school></school> 74 </div> 75 `, 76 components:{ 77 school, 78 hello 79 } 80 }) 81 82 //创建vm 83 new Vue({ 84 template:'<app></app>', 85 el:'#root', 86 //注册组件(局部) 87 components:{app} 88 }) 89 </script> 90 </html>
3、关于VueComponent
1.school组件本质是一个构造函数VueComponent,且不是程序员定义的,是Vue.extend生成的。
2.我们只需要写<school/>或<school></school>,Vue解析时会帮我们创建school组件的实例对象,即Vue帮我们执行的:new VueComponent(options)。
3.特别注意:每次调用Vue.extend,返回的都是一个全新的VueComponent!!!!
4.关于this指向:
(1).组件配置中:
data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】。
(2).new Vue(options)配置中:
data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】。
5.VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)。Vue的实例对象,以后简称vm。
4、组件原型对象
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>一个重要的内置关系</title> 6 <!-- 引入Vue --> 7 <script type="text/javascript" src="../js/vue.js"></script> 8 </head> 9 <body> 10 <!-- 11 1.一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype 12 2.为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue原型上的属性、方法。 13 --> 14 <!-- 准备好一个容器--> 15 <div id="root"> 16 <school></school> 17 </div> 18 </body> 19 20 <script type="text/javascript"> 21 Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 22 Vue.prototype.x = 99 23 24 //定义school组件 25 const school = Vue.extend({ 26 name:'school', 27 template:` 28 <div> 29 <h2>学校名称:{{name}}</h2> 30 <h2>学校地址:{{address}}</h2> 31 <button @click="showX">点我输出x</button> 32 </div> 33 `, 34 data(){ 35 return { 36 name:'尚硅谷', 37 address:'北京' 38 } 39 }, 40 methods: { 41 showX(){ 42 console.log(this.x) 43 } 44 }, 45 }) 46 47 //创建一个vm 48 const vm = new Vue({ 49 el:'#root', 50 data:{ 51 msg:'你好' 52 }, 53 components:{school} 54 }) 55 56 //定义一个构造函数 57 /* function Demo(){ 58 this.a = 1 59 this.b = 2 60 } 61 //创建一个Demo的实例对象 62 const d = new Demo() 63 64 console.log(Demo.prototype) //显示原型属性 65 66 console.log(d.__proto__) //隐式原型属性 67 68 console.log(Demo.prototype === d.__proto__) 69 70 //程序员通过显示原型属性操作原型对象,追加一个x属性,值为99 71 Demo.prototype.x = 99 72 73 console.log('@',d) */ 74 75 </script> 76 </html>
让组件实例的原型对象的原型对象指向Vue的原型对象而不是直接指向Object,使得组件实例对象(vc)可以访问到 Vue原型上的属性、方法。
19、单文件组件
非单文件组件的弊病就是样式不能跟着组件走,而单文件组件把模板样式和交互都集合到一起了。
20、Vue CLI
1、创建Vue脚手架
Vue CLI即Vue 脚手架,它是 Vue 官方提供的标准化开发工具(开发平台)。
下载安装使用:
如出现下载缓慢请配置 npm 淘宝镜像:npm config set registry https://registry.npm.taobao.org
第一步(仅第一次执行):全局安装@vue/cli。
npm install -g @vue/cli
第二步:切换到你要创建项目的目录,然后使用命令创建项目
vue create xxxx
出现Vue版本选择提示
babel将ES6转为ES5,eslint是代码语法检查。
我这里选择Vue2.
Vue CLI的版本是4.x
第三步:启动项目
在项目目录下:npm run serve
项目就创建完成。
2、分析脚手架结构
我的第一个项目结构:
├── node_modules: 各种模块
├── public
│ ├── favicon.ico: 页签图标
│ └── index.html: 主页面
├── src
│ ├── assets: 存放静态资源
│ │ └── logo.png
│ │── component: 存放组件
│ │ └── School.vue: 组件
│ │── App.vue: 汇总所有组件
│ │── main.js: 入口文件
├── .gitignore: git 版本管制忽略的配置
├── babel.config.js: babel 的配置文件
├── package.json: 应用包配置文件
├── README.md: 应用描述文件
├── package-lock.json: 包版本控制文件
main.js:
App.vue :
School.vue:
Student.vue:
index.html:
启动运行后访问页面:
3、关于render函数
如果将实例构建写成常规形式而不用render,如下:
则会报错:正在使用没有模板解析器的Vue。
为何如此?
因为import Vue from 'vue'引入的Vue,是vue.runtime.esm.js,残缺版的vue,没有模板解析器的Vue。生产阶段不需要模板解析器,这样做可以精简项目体量。
render: h => h(App)其实是下面render函数的简写形式:
render(createElement) {
return createElement(App)
}
createElement也是一个函数。
总结
关于不同版本的Vue:
1.vue.js与vue.runtime.xxx.js的区别:
(1).vue.js是完整版的Vue,包含:核心功能+模板解析器。
(2).vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。
2.因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用
render函数接收到的createElement函数去指定具体内容。
4、修改配置文件
https://cli.vuejs.org/zh/config/#vue-config-js
在项目根目录下新建vue.config.js,里面去配置一些允许被更改的配置项
如page的入口,模板的来源:
或者关闭语法检查:lintOnSave:false
注意,跟pages:{}是平级的。
5、ref属性
ref 被用来给元素或子组件注册引用信息(id的替代者)。应用在html标签上获取的是真实DOM元素,应用在组件标签上获取的是组件实例对象(vc)
使用方式:
1. 打标识:```<h1 ref="xxx">.....</h1>``` 或 ```<School ref="xxx"></School>```
2. 获取:```this.$refs.xxx```
如:
6、props配置
1. 功能:让组件接收外部传过来的数据
2. 传递数据:```<Demo name="xxx"/>``` ,传表达式记得用单项数据绑定 v-bind:name="obj"
3. 接收数据:
1. 第一种方式(只接收):```props:['name'] ```
2. 第二种方式(限制类型):```props:{name:String}```
3. 第三种方式(限制类型、限制必要性、指定默认值):
```js
props:{
name:{
type:String, //类型
required:false, //必要性
default:'老王' //默认值
}
}
```
注意:
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
主要是因为在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。对于对象而言,改变对象里的部分属性的值又不会被认为是改变了对象,主要是地址没变,这样又不会报错警告,浅层的监视,而不是类似于watch里的深度监测deep。
若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。
尽量不要直接修改prop!
1 <template> 2 <div> 3 <h1>{{msg}}</h1> 4 <h2>学生姓名:{{name}}</h2> 5 <h2>学生性别:{{sex}}</h2> 6 <h2>学生年龄:{{myAge+1}}</h2> 7 <button @click="updateAge">尝试修改收到的年龄</button> 8 </div> 9 </template> 10 11 <script> 12 export default { 13 name:'Student', 14 data() { 15 console.log(this) 16 return { 17 msg:'我是一个尚硅谷的学生', 18 myAge:this.age 19 } 20 }, 21 methods: { 22 updateAge(){ 23 this.myAge++ 24 } 25 }, 26 //简单声明接收 27 // props:['name','age','sex'] 28 29 //接收的同时对数据进行类型限制 30 /* props:{ 31 name:String, 32 age:Number, 33 sex:String 34 } */ 35 36 //接收的同时对数据:进行类型限制+默认值的指定+必要性的限制 37 props:{ 38 name:{ 39 type:String, //name的类型是字符串 40 required:true, //name是必要的 41 }, 42 age:{ 43 type:Number, 44 default:99 //默认值 45 }, 46 sex:{ 47 type:String, 48 required:true 49 } 50 } 51 } 52 </script>
7、mixin混入
功能:可以把多个组件共用的配置提取成一个混入对象。
使用方式:
第一步定义混合:新建一个mixin.js文件,写入混入对象
```
{
data(){....},
methods:{....}
....
}
```
第二步使用混入:
全局混入:```Vue.mixin(xxx)```
局部混入:```mixins:['xxx'] ```
如mixin.js :
组件Student:
组件School:
8、插件
1. 功能:用于增强Vue
2. 本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据。
3. 定义插件:在src目录下创建plugins.js :对象包含install方法
1 export default { 2 install(Vue,x,y,z){ 3 console.log(x,y,z) 4 //全局过滤器 5 Vue.filter('mySlice',function(value){ 6 return value.slice(0,4) 7 }) 8 9 //定义全局指令 10 Vue.directive('fbind',{ 11 //指令与元素成功绑定时(一上来) 12 bind(element,binding){ 13 element.value = binding.value 14 }, 15 //指令所在元素被插入页面时 16 inserted(element,binding){ 17 element.focus() 18 }, 19 //指令所在的模板被重新解析时 20 update(element,binding){ 21 element.value = binding.value 22 } 23 }) 24 25 //定义混入 26 Vue.mixin({ 27 data() { 28 return { 29 x:100, 30 y:200 31 } 32 }, 33 }) 34 35 //给Vue原型上添加一个方法(vm和vc就都能用了) 36 Vue.prototype.hello = ()=>{alert('你好啊')} 37 } 38 }
4. 使用插件:先引入再应用
然后就能在组件上使用了。
9、scoped样式
1. 作用:为组件css指定作用域,让样式在组件内部生效,防止多个组件样式名称冲突。
2. 写法:
10、todolist案例
实现一个todolist功能的案例,能够新增任务,也能够删除任务,标记已完成任务:
用组件来实现,初步分析分为三大组件,组件2的每个Item仍然可以分为一个子组件,具体代码实现:
App.vue
1 <template> 2 <div id="root"> 3 <div class="todo-container"> 4 <div class="todo-wrap"> 5 <MyHeader :addTodo="addTodo"/> 6 <MyList :todos="todos" :checkTodo='checkTodo' :deleteTodo='deleteTodo'></MyList> 7 <MyFooter :todos="todos" :checkAll='checkAll' :clearAllTodo='clearAllTodo'></MyFooter> 8 </div> 9 </div> 10 </div> 11 12 </template> 13 14 <script> 15 import MyHeader from './components/MyHeader.vue' 16 import MyFooter from './components/MyFooter.vue' 17 import MyList from './components/MyList.vue' 18 export default { 19 name: 'App', 20 data(){ 21 return{ 22 todos:[ 23 // 一般id都是哈希值字符串,不要用数字 24 {id:'001',title:'吃饭',done:false}, 25 {id:'002',title:'睡觉',done:true}, 26 {id:'003',title:'玩游戏',done:false}, 27 {id:'005',title:'购物',done:true}, 28 ] 29 } 30 }, 31 methods:{ 32 // 为了实现子组件向父组件传递数据,父组件先向子组件传个函数,通过形参来接收子组件传递进来的实参数据 33 //添加一个todo 34 addTodo(todoObj){ 35 // console.log("我是APP组件,我收到的参数是:",todoObj) 36 this.todos.unshift(todoObj) 37 }, 38 //勾选状态变更时 39 checkTodo(id){ 40 this.todos.forEach((todo)=>{ 41 if(todo.id === id) todo.done=!todo.done 42 }) 43 }, 44 //删除一个todo 45 deleteTodo(id){ 46 // 方法一:循环遍历 47 // this.todos.forEach((todo,index,todos)=>{ 48 // if(todo.id === id) todos.splice(index,1) 49 // }) 50 51 //方法二:过滤替换为新数组 52 this.todos = this.todos.filter((todo)=>{ 53 return todo.id !== id 54 }) 55 // this.todos = this.todos.filter(todo=>todo.id !==id) 简写模式 56 }, 57 //全选操作 58 checkAll(isAll){ 59 this.todos.forEach((todo)=>{ 60 todo.done=isAll 61 }) 62 }, 63 //删除所有已经完成的todo 64 clearAllTodo(){ 65 this.todos = this.todos.filter(todo=>!todo.done) 66 } 67 }, 68 components: { 69 MyFooter, 70 MyHeader, 71 MyList 72 } 73 } 74 </script> 75 76 <style> 77 /* body */ 78 body { 79 background: #fff; 80 } 81 .btn { 82 display: inline-block; 83 padding: 4px 12px; 84 margin-bottom: 0; 85 font-size: 14px; 86 line-height: 20px; 87 text-align: center; 88 vertical-align: middle; 89 cursor: pointer; 90 box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 91 border-radius: 4px; 92 } 93 .btn-danger { 94 color: #fff; 95 background-color: #da4f49; 96 border: 1px solid #bd362f; 97 } 98 .btn-danger:hover { 99 color: #fff; 100 background-color: #bd362f; 101 } 102 .btn:focus { 103 outline: none; 104 } 105 .todo-container { 106 width: 600px; 107 margin: 0 auto; 108 } 109 .todo-container .todo-wrap { 110 padding: 10px; 111 border: 1px solid #ddd; 112 border-radius: 5px; 113 } 114 115 116 </style>
组件1:MyHeader:
1 <template> 2 <div class="todo-header"> 3 <label><input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model.trim="title" @keyup.enter="add"/></label> 4 <button class="btn btn-danger" style="margin-top: 20px;" @click="add">添加任务</button> 5 </div> 6 </template> 7 8 <script> 9 import {nanoid} from 'nanoid' 10 export default{ 11 name:'MyHeader', 12 data(){ 13 return{ 14 title:'' 15 } 16 }, 17 props:['addTodo'], 18 methods:{ 19 add(){ 20 if(!this.title) return alert('输入内容不能为空!') 21 // console.log(event.target.value) 22 const todoObj = {id:nanoid(),title:this.title,done:false} 23 this.addTodo(todoObj) 24 this.title='' 25 26 } 27 } 28 } 29 </script> 30 31 <style scoped> 32 /* header */ 33 .todo-header input { 34 width: 560px; 35 height: 28px; 36 font-size: 14px; 37 border: 1px solid #ccc; 38 border-radius: 4px; 39 padding: 4px 7px; 40 } 41 42 .todo-header input:focus { 43 outline: none; 44 border-color: rgba(82, 168, 236, 0.8); 45 box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); 46 } 47 </style>
组件3:MyFooter:
1 <template> 2 <div class="todo-footer"> 3 <label> 4 <!-- <input type="checkbox" :checked="isAll" @change="checkAllOrNo"/> --> 5 <!-- checked有初始值并且会变化,直接用v-model --> 6 <input type="checkbox" v-model="isAll"/> 7 </label> 8 <span> 9 <span>已完成{{doneTotal}}</span> / 全部{{total}} 10 </span> 11 <button class="btn btn-danger" @click="clearAll">清除已完成任务</button> 12 </div> 13 </template> 14 15 <script> 16 export default{ 17 name:'MyFooter', 18 props:['todos','checkAll','clearAllTodo'], 19 computed:{ 20 total(){ 21 return this.todos.length 22 }, 23 //计算已经完成的todo项 24 doneTotal(){ 25 // 简单实现方法: 26 // return this.todos.filter(todo=>todo.done).length 27 // 高级实现方法:reduce() 28 // reduce()方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。 29 return this.todos.reduce((pre,todo)=>pre+ (todo.done?1:0),0) 30 }, 31 // 所有todo全部完成则勾选 32 // 计算属性简写,只读取 33 // isAll(){ 34 // return this.total === this.doneTotal && this.doneTotal>0 35 // // return this.doneTotal > 0?this.total === this.doneTotal:false 36 // } 37 38 //计算属性全写,有setter,适用于v-model 39 isAll:{ 40 get(){ 41 return this.total === this.doneTotal && this.doneTotal>0 42 }, 43 set(value){ 44 this.checkAll(value) 45 } 46 } 47 }, 48 //用v-model之后就不用方法了 49 // methods:{ 50 // checkAllOrNo(e){ 51 // this.checkAll(e.target.checked) 52 // } 53 // } 54 methods:{ 55 clearAll(){ 56 if (confirm('确定要清除所有的已完成任务吗?')) this.clearAllTodo() 57 } 58 } 59 } 60 </script> 61 62 <style scoped> 63 /*footer*/ 64 .todo-footer { 65 height: 40px; 66 line-height: 40px; 67 padding-left: 6px; 68 margin-top: 5px; 69 } 70 71 .todo-footer label { 72 display: inline-block; 73 margin-right: 20px; 74 cursor: pointer; 75 } 76 77 .todo-footer label input { 78 position: relative; 79 top: -1px; 80 vertical-align: middle; 81 margin-right: 5px; 82 } 83 84 .todo-footer button { 85 float: right; 86 margin-top: 5px; 87 } 88 </style>
组件2:MyList:
1 <template> 2 <ul class="todo-main"> 3 <MyItem 4 v-for="todoObj in todos" 5 :key="todoObj.id" 6 :todo='todoObj' 7 :checkTodo='checkTodo' 8 :deleteTodo='deleteTodo'></MyItem> 9 10 </ul> 11 </template> 12 13 <script> 14 import MyItem from './MyItem.vue' 15 export default{ 16 name:'MyList', 17 components:{MyItem}, 18 props:['todos','checkTodo','deleteTodo'], 19 } 20 </script> 21 22 <style scoped> 23 /*main*/ 24 .todo-main { 25 margin-left: 0px; 26 border: 1px solid #ddd; 27 border-radius: 2px; 28 padding: 0px; 29 } 30 31 .todo-empty { 32 height: 40px; 33 line-height: 40px; 34 border: 1px solid #ddd; 35 border-radius: 2px; 36 padding-left: 5px; 37 margin-top: 10px; 38 } 39 </style>
组件2-1:MyItem:
1 <template> 2 <div > 3 <li> 4 <label> 5 <!-- 这里不推荐用v-model,因为其会改变父组件的数据 --> 6 <!-- <input type="checkbox" v-model='todo.done'/> --> 7 <input type="checkbox" :checked='todo.done' @change="handleCheck(todo.id)"/> 8 <!-- 这里不推荐用todo.done=!todo.done来实现勾选状态的交互,同样是因为其会改变父组件的数据 --> 9 <!-- <input type="checkbox" :checked='todo.done' @click="todo.done=!todo.done"/> --> 10 11 <span>{{todo.title}}</span> 12 </label> 13 <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button> 14 </li> 15 </div> 16 </template> 17 18 <script> 19 export default{ 20 name:'MyItem', 21 // 声明接收todo对象 22 props:['todo','checkTodo','deleteTodo'], 23 methods:{ 24 handleCheck(id){ 25 // 通知App组件将对应的todo对象的done值取反 26 this.checkTodo(id) 27 }, 28 handleDelete(id){ 29 if(confirm("确认删除吗?")) this.deleteTodo(id) 30 } 31 }, 32 mounted() { 33 // MyList组件prop传对象表达式记得用单项数据绑定,不然传的就是字符串了 34 // console.log(this.todo) 35 // console.log(typeof(this.todo)) 36 } 37 } 38 </script> 39 40 <style scoped> 41 /*item*/ 42 li { 43 list-style: none; 44 height: 36px; 45 line-height: 36px; 46 padding: 0 5px; 47 border-bottom: 1px solid #ddd; 48 } 49 50 li label { 51 float: left; 52 cursor: pointer; 53 } 54 55 li label li input { 56 vertical-align: middle; 57 margin-right: 6px; 58 position: relative; 59 top: -1px; 60 } 61 62 li button { 63 float: right; 64 display: none; 65 margin-top: 3px; 66 } 67 68 li:before { 69 content: initial; 70 } 71 72 li:last-child { 73 border-bottom: none; 74 } 75 76 li:hover{ 77 background-color: #ddd; 78 } 79 80 li:hover button{ 81 display: block; 82 } 83 </style>
main.js:
1 //引入Vue 2 import Vue from 'vue' 3 //引入App 4 import App from './App.vue' 5 //关闭Vue的生产提示 6 Vue.config.productionTip = false 7 8 //创建vm 9 new Vue({ 10 el:'#app', 11 render: h => h(App) 12 })
案例总结:
组件化编码流程:
(1).拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。
(2).实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
1).一个组件在用:放在组件自身即可。
2). 一些组件在用:放在他们共同的父组件上(状态提升)。
(3).实现交互:从绑定事件开始。
props适用于:
(1).父组件 ==> 子组件 通信
(2).子组件 ==> 父组件 通信(要求父先给子一个函数)
使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!
props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。
11、组件的自定义事件
区别于js的给元素用的内置事件,组件的自定义事件是给组件用的。
App.vue:
1 <template> 2 <div class="app"> 3 <h1>{{msg}}</h1> 4 <!-- 通过父组件给子组件传递函数类型的props来实现:子给父传递数据 --> 5 <School :getSchoolName="getSchoolName"/> 6 <!-- 通过给子组件的实例对象VC绑定自定义事件来实现:子给父传递数据 ,难点在于如何触发该事件--> 7 <Student v-on:atguigu="getStudentName"/> 8 <!-- 一次性 --> 9 <!-- <Student v-on:atguigu.once="getStudentName"/> --> 10 11 <!-- 上面绑定事件的另一种实现方法:ref ,$refs.student就是VC--> 12 <!-- <Student ref="student"/> --> 13 14 <!-- 给组件实例绑定原生的点击事件:要加native,不然就被认为是自定义事件click --> 15 <!-- <Student @click.native='show'/> --> 16 </div> 17 </template> 18 19 <script> 20 import Student from './components/Student' 21 import School from './components/School' 22 23 export default { 24 name:'App', 25 components:{School,Student}, 26 data(){ 27 return{ 28 msg:'天青色!' 29 } 30 }, 31 methods:{ 32 getSchoolName(name){ 33 console.log('APP收到了school的名字了',name) 34 }, 35 /* getStudentName(name,){ 36 console.log('APP收到了student的名字了',name) 37 }, */ 38 39 // 多个参数的情形: 40 getStudentName(name,...params){ 41 console.log('APP收到了student的名字了',name,params) 42 }, 43 44 show(){ 45 alert(666) 46 } 47 }, 48 mounted() { 49 // 当student VC身上的自定义事件atguigu被触发的时候执行回调函数getStudentName 50 // this.$refs.student.$on('atguigu',this.getStudentName) 51 // this.$refs.student.$once('atguigu',this.getStudentName) 52 53 // 为何用这种方法实现呢?因为它比较灵活,比如要等3秒后才绑定atguigu事件: 54 // setTimeout(()=>{this.$refs.student.$on('atguigu',this.getStudentName)}, 3000); 55 56 } 57 } 58 </script> 59 60 <style scoped> 61 .app{ 62 background-color: gray; 63 } 64 </style>
school.vue:
1 <template> 2 <div class="school"> 3 <h2>学校名称:{{name}}</h2> 4 <h2>学校地址:{{address}}</h2> 5 <button @click="sendSchoolName">把学校名给App</button> 6 </div> 7 </template> 8 9 <script> 10 export default { 11 name:'School', 12 props:['getSchoolName'], 13 data() { 14 return { 15 name:'尚硅谷', 16 address:'北京', 17 } 18 }, 19 methods:{ 20 sendSchoolName(){ 21 this.getSchoolName(this.name) 22 } 23 } 24 } 25 </script> 26 27 <style scoped> 28 .school{ 29 background-color: skyblue; 30 padding: 10px; 31 } 32 </style>
student.vue:
1 <template> 2 <div class="school"> 3 <h2>学生姓名:{{name}}</h2> 4 <h2>学生性别:{{sex}}</h2> 5 <button @click="sendStudentName">把学生名传给App</button> 6 <button @click="unbind">解除绑定的自定义事件</button> 7 <button @click="death">销毁组件实例vc</button> 8 </div> 9 </template> 10 11 <script> 12 export default { 13 name:'Student', 14 data() { 15 return { 16 name:'张三', 17 sex:'男' 18 } 19 }, 20 methods:{ 21 sendStudentName(){ 22 // 触发自定义事件并传参 23 // this.$emit('atguigu',this.name) 24 25 // 传递多个参数的情形: 26 this.$emit('atguigu',this.name,111,222,333) 27 }, 28 //解绑自定义事件 29 unbind(){ 30 this.$off('atguigu') 31 // this.$off(['atguigu',xxx]) 解绑多个 32 // this.$off() 解绑全部 33 }, 34 death(){ 35 this.$destroy() //销毁组件实例则其绑定的自定义事件都没了 36 } 37 } 38 } 39 </script> 40 41 <style scoped> 42 .school{ 43 background-color: pink; 44 padding: 10px; 45 margin-top: 1.25rem; 46 } 47 </style>
在上一节todolist案例中,子组件向父组件传递数据是通过props传递回调函数实现的,现在可以用自定义组件来实现。通过给子组件的实例对象VC绑定自定义事件来实现子给父传递数据。
.
总结:
1、一种组件间通信的方式,适用于:子组件 ===> 父组件
2、使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)。
3、绑定自定义事件:
第一种方式,在父组件中:<Demo @atguigu="test"/>
或 <Demo v-on:atguigu="test"/>
第二种方式,在父组件中:
<Demo ref="demo"/> ...... mounted(){ this.$refs.xxx.$on('atguigu',this.test) }
4、若想让自定义事件只能触发一次,可以使用once
修饰符,或$once
方法。
5、触发自定义事件:this.$emit('atguigu',数据)
6、解绑自定义事件this.$off('atguigu')
7、组件上也可以绑定原生DOM事件,需要使用native
修饰符。
注意:通过this.$refs.xxx.$on('atguigu',回调)
绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,否则this指向会出问题!
12、全局事件总线
通过设置全局事件总线,可以实现任意组件间数据通信。
本来应该在vc的原型对象上添加总线bus,但这样一来每个vc都要这么做,所以去vm的原型对象上添加,因为vc在自己的原型对象上找不到bus就会去vm的原型对象上寻找(详见组件那一节)。
例如要实现兄弟组件school和student之间的通信:
在school组件上绑定总线事件,在student组件上触发该事件,传入数据,然后执行school的回调函数,接收数据。
谁要接收数据,谁就在挂载后绑定事件。
点击按钮,运行结果:
在组件销毁时记得去解绑事件。
13、todolist案例补充编辑功能
在组件MyItem中增加编辑按钮和输入框,点击后输入框自动获取焦点,修改完毕后按enter或者失去焦点则保存更改,显示更改后的数据。在编辑模式中编辑按钮隐藏,且输入不能为空。
在App里面给事件总线绑定一个自定义事件,添加回调函数,实行数据的修改。
14、配置代理
如果前端应用和后端 API 服务器没有运行在同一个主机上,你需要在开发环境下将 API 请求代理到 API 服务器。不配置的话跨域请求是不可以的。协议名、域名和端口号都相同才算同一个主机。
方法一
在vue.config.js中添加如下配置:
devServer:{
proxy:"http://localhost:5000"
}
说明:
- 优点:配置简单,请求资源时直接发给前端(8080)即可。
- 缺点:不能配置多个代理,不能灵活的控制请求是否走代理。
- 工作方式:若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器 (优先匹配前端资源)
方法二
编写vue.config.js配置具体代理规则:
module.exports = {
devServer: {
proxy: {
'/api1': {// 匹配所有以 '/api1'开头的请求路径
target: 'http://localhost:5000',// 代理目标的基础路径
changeOrigin: true,
pathRewrite: {'^/api1': ''} // 访问所代理的目标服务器时去除/api1
},
'/api2': {// 匹配所有以 '/api2'开头的请求路径
target: 'http://localhost:5001',// 代理目标的基础路径
changeOrigin: true,
pathRewrite: {'^/api2': ''}
}
}
}
}
/*
changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:8080
changeOrigin默认值为true
*/
说明:
- 优点:可以配置多个代理,且可以灵活的控制请求是否走代理。
-
缺点:配置略微繁琐,请求资源时必须加前缀。
Vue CLI 的配置参考里也有提及:
也可以在后端通过cors解决跨域问题。
15、插槽
作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于 父组件 ===> 子组件 。
分类:默认插槽、具名插槽、作用域插槽
使用方式:
1、默认插槽
父组件中:
<Category>
<div>html结构1</div>
</Category>
子组件中:
<template>
<div>
<!-- 定义插槽 -->
<slot>插槽默认内容...</slot>
</div>
</template>
2、具名插槽
父组件中:
<Category>
<template slot="center">
<div>html结构1</div>
</template>
<template v-slot:footer>
<div>html结构2</div>
</template>
</Category>
子组件中:
<template>
<div>
<!-- 定义插槽 -->
<slot name="center">插槽默认内容...</slot>
<slot name="footer">插槽默认内容...</slot>
</div>
</template>
3、作用域插槽
理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(games数据在Category组件中,但使用数据所遍历出来的结构由App组件决定)
父组件中:
<Category>
<template scope="scopeData">
<!-- 生成的是ul列表 -->
<ul>
<li v-for="g in scopeData.games" :key="g">{{g}}</li>
</ul>
</template>
</Category>
<Category>
<template slot-scope="scopeData">
<!-- 生成的是h4标题 -->
<h4 v-for="g in scopeData.games" :key="g">{{g}}</h4>
</template>
</Category>
子组件中:
<template>
<div>
<slot :games="games"></slot>
</div>
</template>
<script>
export default {
name:'Category',
props:['title'],
//数据在子组件自身
data() {
return {
games:['红色警戒','穿越火线','劲舞团','超级玛丽']
}
},
}
</script>
21、Vuex
22、路由
1、什么是路由?
一个路由(route)就是一组映射关系(key - value),多个路由需要路由器(router)进行管理。前端路由:key是路径,value是组件。
2、安装路由器
安装:npm i vue-router@3 (学的是vue2,所以安装版本3的vue-router ,vue3则直接npm i vue-router)
3、基本使用
在应用中创建路由器:src/router/index.js
-
编写router配置项:
//引入VueRouter import VueRouter from 'vue-router' //引入路由组件 import About from '../components/About' import Home from '../components/Home' //创建router实例对象,去管理一组一组的路由规则 const router = new VueRouter({ routes:[ { path:'/about', component:About }, { path:'/home', component:Home } ] }) //暴露router export default router
-
实现切换(active-class可配置高亮样式)
<router-link active-class="active" to="/about">About</router-link>
-
指定展示位置
<router-view></router-view>
main.js :
注意:
路由组件通常存放在pages
文件夹,一般组件通常存放在components
文件夹:
通过切换,“隐藏”了的路由组件,默认是被销毁掉的,需要的时候再去挂载。
vc身上都有route和router:
不同的组件实例身上的$route属性是不同的,里面储存这自己的路由信息,但$router是相同的,整个应用只有一个$router:
4、多级路由
-
配置路由规则,使用children配置项:
routes:[ { path:'/about', component:About, }, { path:'/home', component:Home, children:[ //通过children配置子级路由 { path:'news', //此处一定不要写:/news component:News }, { path:'message',//此处一定不要写:/message component:Message } ] } ]
-
跳转(要写完整路径):
<router-link to="/home/news">News</router-link>
5、路由query传参
直接在to中追加是不可以的,都当字符串处理:
跳转路由并携带query参数的正确写法:
1、to的字符串写法:
2、to的对象写法:
建议使用第二种写法,看得舒服些。
传递的参数都在vc上$route属性的query里面:
拿到参数:
6、命名路由
给路由添加一个name属性,如:
跳转的时候就可以直接写名字,而不用写长长的三级路径:
7、路由params传参
1、占位
2、跳转
注意写法,以‘/’ 来拼接的。
另一种对象写法:
注意:不能使用path配置项,必须使用name配置!
具体用哪种写法,随意。
3、用参
8、路由props配置
之前在组件内使用参数时是通过$route来获取的,但是这种方法会导致组件和路由的紧密耦合,这会限制组件的灵活性。官方文档(4x版本)中也有说明:
并且对于消息详情页,如果除了消息编号和标题,还有其他的消息属性,那就要写很多,也不太方便
这个时候就需要用到路由的props配置。
作用:让路由组件更方便的收到参数,并且让组件和路由之间不产生耦合。
{
name:'xiangqing',
path:'detail/:id',
component:Detail,
//第一种写法:props值为对象,该对象中所有的key-value的组合最终都会通过props传给Detail组件
// props:{a:900}
//第二种写法:props值为布尔值,布尔值为true,则把路由收到的所有params参数通过props传给Detail组件
// props:true
//第三种写法:props值为函数,该函数返回的对象中每一组key-value都会通过props传给Detail组件
props(route){
return {
id:route.query.id,
title:route.query.title
}
}
}
推荐使用函数形式的props值:
简写:
接收并使用参数:
9、<router-link>的replace属性
作用:控制路由跳转时操作浏览器历史记录的模式
浏览器的历史记录有两种写入方式:分别为push
和replace
,push
是追加历史记录,类似于压栈,replace
是替换当前记录。路由跳转时候默认为push。
如何开启replace
模式:<router-link replace .......>News</router-link>
以下是replace:
10、编程式路由导航
-
作用:不借助
<router-link>
实现路由跳转,让路由跳转更加灵活 -
具体编码:
//$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() //可前进也可后退,正数前进,负数后退
如果连续点击上面的push查看按钮或者replace查看按钮会报错:
报的是重复路由的错误。
解决:可在route下面的index.js中写入以下代码,修改原型对象中的push方法:
11、缓存路由组件
如下,在输入框输入数据然后切换到其他组件,再切回来会发现数据丢失,因为切出去后原来的组件就被销毁了:
有些信息如个人身份信息,客户填了半天切出去再切回来发现全没了,太不友好了!那么怎么才能让它不销毁呢?
News组件是展示在Home组件内部的,可以将Home组件的展示区用keep-alive标签包起来,这样就不会销毁了,include属性指定不被销毁的组件名,如果没有,则Home组件下的所有组件都不会被销毁。
缓存多个组件::include="['News','Message']"
12、两个新的生命周期钩子
在组件消息列表下面添加一行,实现汉字天青色渐变:
但是这样有一个问题,当切出组件News,因为有缓存,News就不会被销毁,定时器也就不会被销毁,定时器就一直在执行。
解决办法:用到两个新的钩子函数activated和deactivated
展示某个组件时即被激活,切出去则失活。
13、全局前置路由守卫
当我们需要给news和message页面添加权限验证:
可以在路由器中使用全局前置路由守卫:
初始化时或者每次路由切换之前都会执行beforeEach()方法,通过if判断满足条件后才能访问,即next()
这里的条件是本地存储中有name=tqs。
不过如果有多个条件判断则写起来比较麻烦,可以在路由规则里的meta对象中添加鉴权标识isAuth,值为true则表示要鉴权。
14、全局后置路由守卫
如果要实现每次切换组件时标题跟着切换,则需要用到后置路由守卫。
路由规则添加title:
后置路由守卫设置页面title:
15、独享路由守卫
直接写在某个路由规则里面
16
未完待续