VUE 笔记

1. vue基础

1.1 vue简介

1.1.1 官网

英语官网

中文官网

1.1.2 介绍与描述
  • 是什么? Vue 是一套用来动态构建用户界面的渐进式JavaScript框架 ,简单来讲就是将一堆数据 ==> 人可以看的页面
    • 构建用户界面:把数据通过某种办法变成用户界面
    • 渐进式:Vue可以自底向上逐层的应用,简单应用只需要一个轻量小巧的核心库,复杂应用可以引入各式各样的Vue插件
  • 作者:尤雨溪
1.1.3 vue特点
  1. 采用组件化模式,提高代码复用率、且让代码更好维护(一个.vue就是一个组件,一个组件包含了大致三个部分,样式,结构以及交互)

  2. 声明式编码,让编码人员无需直接操作DOM,提高开发效率

    • 如下图中的声明式编码中的 v-for就是vue里的一个指令
    • 与之对应的是原生的命令式编码,也就是一行代码做一步,每一步都要显示出来

  3. 使用虚拟DOM 和 Diff算法,尽量复用DOM节点

    • 原生js中没有虚拟dom这个说法,每次数据更新都只是数据的替换,不管更新前后数据是否重复
    • vue中如下图:
      • 通过diff算法,最后呈现的结果就是对前三个真实dom进行复用,最后一个新建即可

  1. 遵循MVVM模式
  2. 编码简洁,体积小,运行效率高,适合移动/PC端开发
  3. 它本身只关注 UI,可以引入其它第三方库开发项目

1.2 初始vue

前置工作:

  1. 给浏览器安装 Vue Devtools 插件

  2. 标签引入Vue包

  3. (可选)阻止vue在启动时生成生产提示Vue.config.productionTip = false

  4. favicon 需要将页签图标放在项目根路径,重新打开就有了(shfit+F5 强制刷新)

一个hello小案例:

  1. 首先要创建一个容器

    • 原因: 使用vue是为了帮忙构建页面,但vue构建的页面要摆放在什么位置呢? ===> 所以需要一个容器(也就是div)
  2. 如果直接写死的话,vue并不会工作,且浏览器默认请求是页签图标,也会出现404(favicon.ico)

  3. 要启用vue就必须先创建vue实例,且要传入一个参数,是个对象,被称为配置对象

    • const x = new Vue({}) 其中 const x可以省去
  4. 配置对象

  • new Vue({
           el:'#root',  // el ==> element, 这个是为了让vue实例与容器进行练习
           data:{
               name:'vue'
           }  // data用于存储数据,供el所指定的容器,值暂时写成一个对象
       })
     
    /*
    css里的选择器: 
    id选择器: #id,
    类选择器: .class
    
    */
    
    
<body>
    <div id="root">
        <h1>hello , {{name}}</h1>
    </div>
</body>

</html>
<script src="./vue.js"></script>
<script>
    Vue.config.productionTip = false //阻止 vue 在启动时产生提示

    //创建vue示例 ,只传一个参数,配置对象
    new Vue({
        el:'#root', // el用于指定当前vue实例为哪个容器服务,通常为css选择器字符串
        data:{ //存储数据,供 el 所指定的容器使用
            name:'vue'
        }
    })
</script>

总结:

  1. 想让Vue工作,就必须创建一个Vue实例,且要传入一个配置对象
  2. demo容器里的代码依然符合html规范,只不过混入了一些特殊的Vue语法
  3. demo容器里的代码被称为【Vue模板】
  4. Vue实例和容器是一一对应的
  5. 真实开发中只有一个Vue实例,并且会配合着组件一起使用
  6. {{xxx}}是Vue的语法:插值表达式,{{xxx}}可以读取到data中的所有属性
    • 注意区分js 表达式 和 js代码(语句)
    • a. 表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方
      a a+b demo(1) x === y ? ‘a’ : ‘b’
    • b. js代码(语句)
      if(){} for(){}
  7. 一旦data中的数据发生改变,那么页面中用到该数据的地方也会自动更新(Vue实现的响应式)

1.3 模板语法

Vue模板语法包括两大类

  1. 插值语法

    • 功能:用于解析标签体内容

    • 写法:{{xxx}},xxx 是 js 表达式,可以直接读取到 data 中的所有区域

    • 里面的js语法可以写vm里的所有属性,用于解析标签体内容

  2. **指令语法 **

    • 功能:用于解析标签(包括:标签属性、标签体内容、绑定事件…)

    • 举例:或简写为,xxx 同样要写 js 表达式,可以直接读取到 data 中的所有属性(此时双引号里面的不再是字符串,而是js语句)

    • 用于解析标签(标签属性,标签体内容,绑定事件等)

备注:**Vue**中有很多的指令,且形式都是 v-xxx,此处只是拿v-bind举例

<div id="root">   root容器
	<h1>插值语法</h1>
	<h3>你好,{{name}}</h3>
	<hr/>
	<h1>指令语法</h1>
    <!-- 这里是展示被Vue指令绑定的属性,引号内写的是js表达式 -->
	<a :href="school.url.toUpperCase()" x="hello">点我去{{school.name}}学习1</a>
	<a :href="school.url" x="hello">点我去{{school.name}}学习2</a>
</div>

<script>
    new Vue({  vue实例
		el:'#root',
		data:{
			name:'jack',
			school:{
				name:'百度',
				url:'http://www.baidu.com',
			}
        }
	})
</script>

1.4 数据绑定 v-bind, v-model

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

  • 单向绑定v-bind数据只能从 data 流向页面

    • <--只是将vue数据name绑定给了页面,当页面数据发生改变时,vue数据不变-->
      <input type="text" v-bind:value="name">                                             
      
  • 双向绑定v-model数据不仅能从 data 流向页面,还可以从页面流向 data

备注:

  1. 双向绑定(v-model)一般都应用在表单类元素上,如 等

  2. v-model:value可以简写为v-model,因为v-model默认收集的就是value值

<div id="root">
	<!-- 普通写法 单向数据绑定 -->
    单向数据绑定:<input type="text" v-bind:value="name"><br/>
    双向数据绑定:<input type="text" v-model:value="name"><br/>
    
    <!-- 简写 v-model:value 可以简写为 v-model,因为v-model默认收集的就是value值-->
    单向数据绑定:<input type="text" :value="name"><br/>
    双向数据绑定:<input type="text" v-model="name"><br/>
</div>

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

1.5 el和data的两种写法

el有2种写法:

    1. 创建Vue实例对象的时候配置el属性
    1. 先创建Vue实例,随后再通过vm.$mount('#root')指定el的值

      <script>
         	// 第一种 
      	const vm = new Vue({
      		el:'#root',  //第一种直接在实例对象里面配置即可
      		data:{
      			name:'jack'
              }
      	})
          
          // 第二种,将容器与Vue建立联系
          vm.$mount('#root') //不在实例对象里面进行配置,这种方式更加灵活
      </script>
      
      

data有2种写法

    1. 对象式:data: { }
    1. 函数式:data() { return { } }
    <script>
        new Vue({
    		el:'#root',
            // 第一种
    		data:{
    			name:'jack',
            }
            
            // 第二种,函数式,简写形式
    
            data() {
            	return {
                    name: 'jack'
                }
        	}
        
             /* 完整形式,正常情况下调用这个函数的是Vue实例,this应该是vm才对,一定不要写成箭头函数,否则this将会变成window
            data:function(){
            return {
                name:'jack'
            }
        }*/
    	})
    </script>
    
    

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

一个重要的原则:

Vue管理的函数,一定不要写箭头函数,否则 this 就不再是Vue实例了

1.6 MVVM 模型

  • MVVM模型

    • M:模型 Model,data中的数据

    • V:视图 View,模板代码(Dom页面)

    • VM:视图模型 ViewModelVue实例对象

  • 观察发现

    • data中所有的属性,最后都出现在了vm身上

    • vm身上所有的属性 及Vue原型身上所有的属性,在 Vue模板中都可以直接使用

  • MVVM 本质上是 MVC (Model-View- Controller)的改进版。即模型-视图-视图模型。

  • 模型model指的是后端传递的数据,视图view指的是所看到的页面。

  • 视图模型viewModel是 mvvm 模式的核心,它是连接 view 和 model 的桥梁。它有两个方向:

    • 将模型转化成视图,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定 v-bind

    • 将视图转化成模型,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听

  • 这两个方向都实现的,我们称之为数据的双向绑定

1.7 数据代理

回顾Object.defineproperty方法:

  • 目的:给对象添加属性

  • Object.defineproperty(要添加属性的对象,属性名,{配置项})

  • 配置项

    {	
        value:18
        writable :true, //控制该属性是否可以被修改,默认false
    	configurable :true, //控制该属性是否可以被删除,默认false
     	enumerable:true, //控制该属性是否可以枚举,默认false
        get:function(){ //当有人获取该属性时,get函数(getter)就会被调用
            return number; //时刻与number保持一致
        }set(val){ //简写形式,当修改该属性时,set函数(setter)就会被调用
            ...操作
        }
    }
    
    

数据代理:

数据代理:通过一个对象代理另一个对象中属性的操作(读/写),只需借用上面函数的set和get即可

let obj = {
    x: 100
}

let obj2 = {
    y: 200
}

Object.defineProperty(obj2, 'x', {
    get() {
        return obj.x;
    },
    set(value) {
        obj.x = value;
    }
})
//要求: 给obj2添加一个x属性,要求obj2.x === obj.x, 且当obj2.x修改时obj.x也进行修改
//所以给obj2添加了访问器属性(getter和setter),当要输出obj2.x时就会调用这个get函数,
//当要修改obj2.x时就会调用set函数,同时修改了obj.x,这就是数据代理

vue里的数据代理:

  1. Vue中的数据代理通过vm对象来代理data对象中属性的操作(读/写)

  2. Vue中数据代理的好处:更加方便的操作data中的数据

  3. 基本原理

    • 通过object.defineProperty()把data对象中所有属性添加到vm

    • 为每一个添加到vm上的属性,都指定一个 getter setter

    • getter setter内部去操作(读/写)data中对应的属

原理: Vue将data中的数据拷贝了一份到_data属性中,又将_data里面的属性提到Vue实例中(如name),通过Object.defineProperty(vm,'name',{get(){return data.name},实现数据代理,这时在vm对象中会新出现一个属性’name’, 这样通过geter/setter操作 name,进而操作_data中的 name。而_data又对data进行数据劫持,实现响应式

  • 上图中的最右边的图中橙色和紫色的那两条线就是数据代理
  • 如果不加这个数据代理的话,模板语法就该写成{{_data.name}},加上数据代理后就可以写成{{name}}了

1.8 事件处理 v-on

1.8.1 事件的基本使用
  1. 使用v-on:xxx@xxx绑定事件,其中 xxx 是事件名
  2. 事件的回调需要配置在methods对象中,最终会在vm
  3. methods中配置的函数,不要用箭头函数,否则 this 就不是**vm**了
  4. methods中配置的函数,都是被 Vue所管理的函数,注意:methods被配置在哪里,this 的指向是哪个实例对象 (vm或组件实例对象)
  5. @click="demo"@click="demo($event)"效果一致,但后者可以传参
<!-- 准备好一个容器-->
<div id="root">
    <h2>欢迎来到{{name}}学习</h2>
    <!-- <button v-on:click="showInfo">点我提示信息</button> -->
    <button @click="showInfo1">点我提示信息1(不传参)</button>
    <!-- 主动传事件本身 -->
    <button @click="showInfo2($event,66)">点我提示信息2(传参)</button>
</div>

<script>
	const vm = new Vue({
        el:'#root',
        data:{
            name:'vue',
        },
        methods:{
            // 如果vue模板没有写event,会自动传 event 给函数
            showInfo1(event){
                // console.log(event.target.innerText)
                // console.log(this) //此处的this是vm
                alert('同学你好!')
            },
            showInfo2(event,number){
                console.log(event,number)
                // console.log(event.target.innerText)
                // console.log(this) //此处的this是vm
                alert('同学你好!!')
            }
        }
	});
</script>

1.8.2 事件修饰符

Vue中的事件修饰符:

  1. prevent 阻止默认事件(常用)
  2. stop 阻止事件冒泡(常用)
  3. once 事件只触发一次(常用)
  4. capture 使用事件的捕获模式
  5. self 只有event.target是当前操作的元素时才触发事件
  6. passive 事件的默认行为立即执行,无需等待事件回调执行完毕

修饰符可以连续写,比如可以这么用:@click.prevent.stop="showInfo"

<!-- 准备好一个容器-->
<div id="root">
    <h2>欢迎来到{{name}}学习</h2>
    <!-- 阻止默认事件(常用) -->
	<a href="http://www.baidu.com" @click.prevent="showInfo">点我提示信息</a>
    <!-- 阻止事件冒泡(常用) -->
    <div class="demo1" @click="showInfo">
        <button @click.stop="showInfo">点我提示信息</button>
        <!-- 修饰符可以连续写 -->
        <!-- <a href="http://www.atguigu.com" @click.prevent.stop="showInfo">点我提示信息</a> -->
    </div>
    <!-- 事件只触发一次(常用) -->
    <button @click.once="showInfo">点我提示信息</button>
</div>


1.8.3 键盘事件

键盘上的每个按键都有自己的名称和编码,例如:Enter(13)。而Vue还对一些常用按键起了别名方便使用

  1. Vue中常用的按键别名

​ 回车enter

​ 删除delete捕获“删除”和“退格”键

​ 退出esc

​ 空格space

​ 换行tab特殊,必须配合keydown去使用

​ 上up

​ 下down

​ 左left

​ 右right

  1. Vue未提供别名的按键,可以使用按键原始的key值去绑定,但注意要转为kebab-case(多单词小写短横线写法)

  2. 系统修饰键(用法特殊)ctrl alt shift meta (meta就是win键)

​ a配合keyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发
指定 ctr+y 使用 @keyup.ctr.y

​ b配合keydown使用:正常触发事件

  1. 也可以使用keyCode去指定具体的按键(不推荐)

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

v-on:click='xxx'
v-on:keyup='xxx(参数)'
v-on:keyup.enter='xxx'

//简写
@click='xxx'
@keyup='xxx'
@keyup.enter='xxx'

1.9 计算属性

  1. 差值语法实现

    <!-- 准备好一个容器-->
    <div id="root">
        由于页面上的变化要影响实例,所以使用v-model,如果要对组合进行处理,违背了要求模板语法尽量简单,所以舍去
        姓:<input type="text" v-model="firstName">
        名:<input type="text" v-model="lastName"> 
        全名:<span>{{firstName}}-{{lastName}}</span>
    </div>
    
    <script>
    	const vm = new Vue({
            el:'#root',
            data:{
                firstName:'张',
                lastName:'三',
            }
    
        })
    </script>
    

    1. method实现

      使用插值语法来调用函数,当data数据发生变化时,vue会重新解析模板,如果里面是函数则会重新调用

      <!-- 准备好一个容器-->
      <div id="root">
          姓:<input type="text" v-model="firstName">
          名:<input type="text" v-model="lastName"> 
          全名:<span>{{fullName()}}</span>
      </div>
      
      <script>
      	const vm = new Vue({
              el:'#root',
              data:{
                  firstName:'张',
                  lastName:'三',
              }
             methods(){
              fullName(){
                  return this.firstName + '-' + lastName;
              }
          }
          })
      </script>
      
      
    2. 使用计算属性实现

      vue认为data里面的都是属性,经过属性加工后被称为计算属性

      1. 定义:要用的属性不存在,需要通过已有属性计算得来

      2. 原理:底层借助了Objcet.defineproperty()方法提供的getter和setter

      3. get函数什么时候执行?

        • 初次读取时会执行一次

        • 当依赖的数据发生改变时会被再次调用

      4. 优势:与methods实现相比,内部有缓存机制(复用),效率更高,调试方便

      5. 备注

        • 计算属性最终会出现在vm上,直接读取使用即可

        • 如果计算属性要被修改,那必须写set函数去响应修改,且set中要引起计算时依赖的数据发生改变

        • 如果计算属性确定不考虑修改,可以使用计算属性的简写形式

      <!-- 准备好一个容器-->
      <div id="root">
          姓:<input type="text" v-model="firstName">
          名:<input type="text" v-model="lastName"> 
          全名:<span>{{fullName}}</span>
      </div>
      
      <script>
      	const vm = new Vue({
              el:'#root',
              data:{
                  firstName:'张',
                  lastName:'三',
              }
              computed:{
                  fullName:{
                      //get有什么作用?当有人读取fullName时,get就会被调用,且返回值就作为fullName的值
                      //get什么时候调用?1.初次读取fullName时。2.所依赖的数据发生变化时。
                      get(){
                          console.log('get被调用了')
                          return this.firstName + '-' + this.lastName
                      },
                      //set什么时候调用? 当fullName被修改时。
                      // 可以主动在控制台修改fullName来查看情况
                      set(value){
                          console.log('set',value)
                          const arr = value.split('-')
                          this.firstName = arr[0]
                          this.lastName = arr[1]
                      }
                  }
              }
          })
      </script>
      
      

1.10 侦听属性(监视)

1.10.1 侦听属性基本用法

watch监视属性:

  • 当被监视的属性变化时,回调函数自动调用,进行相关操作

  • 监视的属性必须存在,才能进行监视,既可以监视data,也可以监视计算属性

  • 配置项属性immediate:false,改为 true,则初始化时调用一次 handler(newValue,oldValue)

  • 监视有两种写法

    • 创建Vue时传入watch: {}配置

    • 通过vm.$watch()监视

<!-- 准备好一个容器-->
<div id="root">
    <h2>今天天气很{{ info }}</h2>
    <button @click="changeWeather">切换天气</button>
</div>


<script>
	const vm = new Vue({
        el:'#root',
        data:{
            isHot:true,
        },
        computed:{
            info(){
                return this.isHot ? '炎热' : '凉爽'
            }
        },
        methods: {
            changeWeather(){
                this.isHot = !this.isHot
            }
        },
        /* 方式一:
        watch:{
            isHot:{
                immediate: true, // 初始化时让handler调用一下
                // handler什么时候调用?当isHot发生改变时。
                handler(newValue, oldValue){
                    console.log('isHot被修改了',newValue,oldValue)
                }
            }
        } */
        
         //方式二
        vm.$watch('isHot',{
        immediate:true, //初始化时让handler调用一下
        //handler什么时候调用?当isHot发生改变时。
        handler(newValue,oldValue){
            console.log('isHot被修改了',newValue,oldValue)
        }

    })
    
   
</script>

1.10.2 深度侦听
  1. Vue中的watch默认不监测对象内部值的改变(一层)
  2. 在watch中配置deep:true可以监测对象内部值的改变(多层)

注意:

  • Vue自身可以监测对象内部值的改变,但Vue提供的watch默认不可以
  • 使用watch时根据监视数据的具体结构,决定是否采用深度监视
<!-- 准备好一个容器-->
<div id="root">
    {{numbers.c.d.e}}
</div>

<script type="text/javascript">
    Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
    const vm = new Vue({
        el:'#root',
        data:{
            numbers:{
                c:{
                    d:{
                        e:100
                    }
                }
            }
        },
        watch:{
            //监视多级结构中某个属性的变化
            /* 'numbers.a':{
					handler(){
						console.log('a被改变了')
					}
				} */
            //监视多级结构中所有属性的变化
            numbers:{
                deep:true,
                handler(){
                    console.log('numbers改变了')
                }
            }
        }
    });
</script>

1.10.3 侦听属性简写

如果监视属性除了handler没有其他配置项的话,可以进行简写

//正常写法
isHot:{
	// immediate:true, //初始化时让handler调用一下
	// deep:true,//深度监视
	handler(newValue,oldValue){
		console.log('isHot被修改了',newValue,oldValue)
	}
}


//简写
isHot(newValue,oldValue){
	console.log('isHot被修改了',newValue,oldValue,this)
} 

//正常写法
 vm.$watch('isHot',{
        immediate:true, //初始化时让handler调用一下
        //handler什么时候调用?当isHot发生改变时。
        handler(newValue,oldValue){
            console.log('isHot被修改了',newValue,oldValue)
        }
 //简写
  vm.$watch('isHot',(newValue,oldValue)=>{
     console.log('isHot被修改了',newValue,oldValue)
 } )  
1.10.4 计算属性 VS 侦听属性

computed和watch之间的区别:

  • computed能完成的功能,watch都可以完成

  • watch能完成的功能,computed不一定能完成,例如watch可以进行异步操作

两个重要的小原则

  • 所有被Vue管理的函数,最好写成普通函数,这样 this 的指向才是vm或组件实例对象

  • 所有不被Vue所管理的函数(定时器的回调函数、ajax 的回调函数等、Promise 的回调函数),最好写成箭头函数,这样 this 的指向才是vm或组件实例对象

    <!-- 准备好一个容器-->
    <div id="root">
        姓:<input type="text" v-model="firstName"> <br/><br/>
        名:<input type="text" v-model="lastName"> <br/><br/>
        全名:<span>{{fullName}}</span> <br/><br/>
    </div>
    
    <script>
    	const vm = new Vue({
            el:'#root',
            data:{
                firstName:'张',
                lastName:'三',
                fullName:'张-三'
            },
            watch:{
                // watch 监视器里可以写 异步函数
                firstName(val){
                    setTimeout(()=>{
                        console.log(this)
                        this.fullName = val + '-' + this.lastName
                    },1000);
                },
                lastName(val){
                    this.fullName = this.firstName + '-' + val
                }
            }
        })
    </script>
    
    

1.11 绑定样式

class样式

  • 写法::class="xxx",xxx 可以是字符串、数组、对象

  • :style="[a,b]"其中a、b是样式对象

  • :style="{fontSize: xxx}"其中 xxx 是动态值

    • 字符串写法适用于:类名不确定,要动态获取

    • 数组写法适用于:要绑定多个样式,个数不确定,名字也不确定

    • 对象写法适用于:要绑定多个样式,个数确定,名字也确定,但不确定用不用

    <style>
    	.normal{
            background-color: skyblue;
        }
          .atguigu1{
            background-color: yellowgreen;
        }
        .atguigu2{
            font-size: 30px;
            text-shadow:2px 2px 10px red;
        }
        .atguigu3{
            border-radius: 20px;
        }
    
    </style>
    
    <!-- 准备好一个容器-->
    <div id="root">
        <!-- 绑定class样式--字符串写法,适用于:样式的类名不确定,需要动态指定 -->
        <div class="basic" :class="mood" @click="changeMood">{{name}}</div>
         <!-- 绑定class样式--数组写法,适用于:要绑定的样式个数不确定、名字也不确定 -->
    	<div class="basic" :class="classArr">{{name}}</div>
        <!-- 绑定class样式--对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用 -->
    	<div class="basic" :class="classObj">{{name}}</div>
        <!-- 绑定style样式--对象写法 -->
    	<div class="basic" :style="styleObj">{{name}}</div>
         <!-- 绑定style样式--数组写法 -->
    	<div class="basic" :style="styleArr">{{name}}</div>
    </div>
    
    <script>
    	const vm = new Vue({
            el:'#root',
            data:{
                mood:'normal',
                classArr: ['atguigu1','atguigu2','atguigu3'],
                 classObj:{
                    atguigu1:false,
                    atguigu2:false,
    			},
                styleObj:{
                    fontSize: '40px',
                    color:'red',
    			},
                styleArr:[
                    {
                        fontSize: '40px',
                        color:'blue',
                    },
                    {
                        backgroundColor:'gray'
                    }
                ]
    
                
            }
        })
    </script>
    
    

1.12 条件渲染 v-if,v-show

v-if:

  • 写法:

    (1).v-if=“表达式”

    (2).v-else-if=“表达式”

    (3).v-else=“表达式”

  • 适用于:切换频率较低的场景

  • 特点:不展示的DOM元素直接被移除

  • 注意:v-if可以和:v-else-if、v-else一起使用,但要求结构不能被“打断”

v-show:

  • 写法:v-show=“表达式”
  • 适用于:切换频率较高的场景
  • 特点:不展示的DOM元素未被移除,仅仅是使用样式隐藏掉(display:none)

注意:使用v-if的时,元素可能无法获取到,而使用v-show一定可以获取到
template 标签不影响结构,页面html中不会有此标签,但只能配合v-if,不能配合v-show

v-if 是实打实地改变dom元素,v-show 是隐藏或显示dom元素

<!-- 准备好一个容器-->
<div id="root">
    <!-- 使用v-if做条件渲染 -->
    <h2 v-if="false">欢迎来到{{name}}</h2>
    <h2 v-if="1 === 1">欢迎来到{{name}}</h2>
    
    
    <!-- v-else和v-else-if -->
    <div v-if="n === 1">Angular</div>
    <div v-else-if="n === 2">React</div>
    <div v-else-if="n === 3">Vue</div>
    <div v-else>哈哈</div>
    
    
    <!-- v-if与template的配合使用 -->
    <!-- 就不需要写好多个判断,写一个就行 -->
    <!-- 这里的思想就像事件代理的使用 -->
    <template v-if="n === 1">
        <h2>你好</h2>
        <h2>尚硅谷</h2>
        <h2>北京</h2>
    </template>
    
    <!-- 使用v-show做条件渲染 -->
    <h2 v-show="false">欢迎来到{{name}}</h2>
    <h2 v-show="1 === 1">欢迎来到{{name}}</h2>
    
</div>

<script>
	const vm = new Vue({
        el:'#root',
        data:{
            styleArr:[
                {
                    fontSize: '40px',
                    color:'blue',
                },
                {
                    backgroundColor:'gray'
                }
            ]
        }
    })
</script>

1.13 列表渲染 v-for

1.13.1 基本列表

v-for指令

  • 用于展示列表数据
  • 语法:
  • ,这里key可以是index,更好的是遍历对象的唯一标识
  • 可遍历:数组、对象、字符串(用的少)、指定次数(用的少)
<div id="root">
    <!-- 遍历数组 -->
    <h2>人员列表(遍历数组)</h2>
    <ul>
        <li v-for="(p,index) of persons" :key="index">
            {{p.name}}-{{p.age}}
        </li>
    </ul>

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

    <!-- 遍历字符串 -->
    <h2>测试遍历字符串(用得少)</h2>
    <ul>
        <li v-for="(char,index) of str" :key="index">
            {{char}}-{{index}}
        </li>
    </ul>

    <!-- 遍历指定次数 -->
    <h2>测试遍历指定次数(用得少)</h2>
    <ul>
        <li v-for="(number,index) of 5" :key="index">
            {{index}}-{{number}}
        </li>
    </ul>
</div>

<script>
	const vm = new Vue({
        el:'#root',
        data: {
			persons: [
				{ id: '001', name: '张三', age: 18 },
				{ id: '002', name: '李四', age: 19 },
				{ id: '003', name: '王五', age: 20 }
			],
			car: {
				name: '奥迪A8',
				price: '70万',
				color: '黑色'
			},
			str: 'hello'
		}
    })
</script>

1.13.2 key的作用和原理

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

  1. 虚拟DOM中key的作用:key是虚拟DOM中对象的标识,当数据发生变化时,Vue会根据新数据生成新的虚拟DOM,随后Vue进行新虚拟DOM与旧虚拟DOM的差异比较,比较规则如下

  2. 对比规则

    • 旧虚拟DOM中找到了与新虚拟DOM相同的key

      • ⅰ若虚拟DOM中内容没变, 直接使用之前的真实DOM

      • ⅱ若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM

    • 旧虚拟DOM中未找到与新虚拟DOM相同的key

​ 创建新的真实DOM,随后渲染到到页面

  1. 用index作为key可能会引发的问题

    • 若对数据进行逆序添加、逆序删除等破坏顺序操作,会产生没有必要的真实DOM更新 ==> 界面效果没问题,但效率低

    • 若结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题

  2. 开发中如何选择key?

    • 最好使用每条数据的唯一标识作为key,比如 id、手机号、身份证号、学号等唯一值

    • 如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于渲染列表,使用index作为key是没有问题的

<!-- 准备好一个容器-->
<div id="root">
    <!-- 遍历数组 -->
    <h2>人员列表(遍历数组)</h2>
    <button @click.once="add">添加一个老刘</button>
    <ul>
        <li v-for="(p,index) of persons" :key="index">
            {{p.name}}-{{p.age}}
            <input type="text">
        </li>
    </ul>
</div>

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

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

1.13.3 列表过滤

两种方式进行过滤:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>列表过滤</title>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h2>人员列表</h2>
			<input type="text" placeholder="请输入名字" v-model="keyWord">
			<ul>
                //只需要修改filPersons数组的数据信息即可
				<li v-for="(p,index) of filPerons" :key="index">
					{{p.name}}-{{p.age}}-{{p.sex}}
				</li>
			</ul>
		</div>

		<script type="text/javascript">
			Vue.config.productionTip = false
			
			//用watch实现
			//#region 
			/* new Vue({
				el:'#root',
				data:{
					keyWord:'',
					persons:[
						{id:'001',name:'马冬梅',age:19,sex:'女'},
						{id:'002',name:'周冬雨',age:20,sex:'女'},
						{id:'003',name:'周杰伦',age:21,sex:'男'},
						{id:'004',name:'温兆伦',age:22,sex:'男'}
					],
					filPerons:[]
				},
				watch:{
					keyWord:{
						immediate:true,
						handler(val){
							this.filPerons = this.persons.filter((p)=>{
								return p.name.indexOf(val) !== -1
							})
						}
					}
				}
			}) */
			//#endregion
			
			//用computed实现
			new Vue({
				el:'#root',
				data:{
					keyWord:'',
					persons:[
						{id:'001',name:'马冬梅',age:19,sex:'女'},
						{id:'002',name:'周冬雨',age:20,sex:'女'},
						{id:'003',name:'周杰伦',age:21,sex:'男'},
						{id:'004',name:'温兆伦',age:22,sex:'男'}
					]
				},
				computed:{
					filPerons(){
                        //数组的filter函数要复习
						return this.persons.filter((p)=>{
							return p.name.indexOf(this.keyWord) !== -1
						})
					}
				}
			}) 
		</script>
</html>

1.13.4 列表排序
data:{
    sortType:0 // 0原顺序,1降序,2升序
}

computed:{
	filPerons(){
		const arr = this.persons.filter((p)=>{
			return p.name.indexOf(this.keyWord) !== -1
		})
		//判断一下是否需要排序
		if(this.sortType){
			arr.sort((p1,p2)=>{
				return this.sortType === 1 ? p2.age-p1.age : p1.age-p2.age
			})
		}
		return arr
	}
}

1.13.5 Vue 数据监视

更新时的一个问题

this.persons[0] = {id:'001',name:'马老师',age:50,sex:'男'} 更改data数据,Vue不监听,模板不改变

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

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

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

</script>

控制台上的数据发生了改变,说明,这个更改的数据并没有被 vue 监测到。(因为没有重新解析模板)

所以我们来研究一下 Vue 监测的原理。

我们先研究 Vue 如何监测 对象里的数据

模拟一个数据监测

<script type="text/javascript" >

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

    //创建一个监视的实例对象,用于监视data中属性的变化
    const obs = new Observer(data)		
    console.log(obs)	

    //准备一个vm实例对象
    let vm = {}
    vm._data = data = obs

    function Observer(obj){
        //汇总对象中所有的属性形成一个数组
        const keys = Object.keys(obj)
        //遍历
        keys.forEach((k) => {
            //给data里的属性都加上数据代理,一但数据被改变,就会调用set方法,一旦调用set方法被调用就是说data里的属性发生了改变,就要让模板重新解析,这是vue检测数据的实现方式
            Object.defineProperty(this, k, {
                get() {
                    return obj[k]
                },
                set(val) {
                    console.log(`${k}被改了,我要去解析模板,生成虚拟DOM.....我要开始忙了`)
                    obj[k] = val
                }
            })
        })
    }
</script>

原理:

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

  2. 如何监测对象中的数据? 通过setter实现监视,且要在new Vue()时就传入要监测的数据

    • 对象创建后追加的属性,Vue默认不做响应式处理

    • 如需给后添加的属性做响应式,请使用如下API

    • Vue.set(target,propertyName/index,value)

    • vm.$set(target,propertyName/index,value)

  3. 如何监测数组中的数据? 通过包裹数组更新元素的方法实现,本质就是做了两件事

    • 调用原生对应的方法对数组进行更新

    • 重新解析模板,进而更新页面

  4. 在Vue修改数组中的某个元素一定要用如下方法

    • push() pop() unshift() shift() splice() sort() reverse()这几个方法被Vue重写了

    • Vue.set()vm.$set()

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

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>总结数据监视</title>
		<style>
			button{
				margin-top: 10px;
			}
		</style>
		<!-- 引入Vue -->
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<!--
 
		-->
		<!-- 准备好一个容器-->
		<div id="root">
			<h1>学生信息</h1>
			<button @click="student.age++">年龄+1岁</button> <br/>
			<button @click="addSex">添加性别属性,默认值:男</button> <br/>
			<button @click="student.sex = '未知' ">修改性别</button> <br/>
			<button @click="addFriend">在列表首位添加一个朋友</button> <br/>
			<button @click="updateFirstFriendName">修改第一个朋友的名字为:张三</button> <br/>
			<button @click="addHobby">添加一个爱好</button> <br/>
			<button @click="updateHobby">修改第一个爱好为:开车</button> <br/>
			<button @click="removeSmoke">过滤掉爱好中的抽烟</button> <br/>
			<h3>姓名:{{student.name}}</h3>
			<h3>年龄:{{student.age}}</h3>
			<h3 v-if="student.sex">性别:{{student.sex}}</h3>
			<h3>爱好:</h3>
			<ul>
				<li v-for="(h,index) in student.hobby" :key="index">
					{{h}}
				</li>
			</ul>
			<h3>朋友们:</h3>
			<ul>
				<li v-for="(f,index) in student.friends" :key="index">
					{{f.name}}--{{f.age}}
				</li>
			</ul>
		</div>
	</body>

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

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

2. vue组件化编程

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

(1) 传统方式编写应用

  • 一个页面分为如图四个部分,使用一个html页面(只要在里面引入css,js即可)即可完成,但为方便维护,每个部分都有各自的css样式以及js文件(用于交互)
  • 如果又有一个页面,顶部和底部样式及交互都一致,所以只需进行复制上面的顶部及底部的css 和 js代码写入到新的html页面里,不一样的再自己进行编写即可
  • 存在两个问题
    • 依赖关系混乱,不好维护(可以通过js模块化进行解决js的依赖问题)
    • 代码复用率不高
(2) 使用组件方式编写应用

  • 一个组件就相当于一个大鞋盒子,包含了某一块对应的样式,结构和交互
  • 优势
    • 最直观的小优势就是在改某一组件的css,html或者是js时,只会改变这一组件,不会影响其他组件,便于维护
    • 提升复用率,此时的不再是复制而是复用,直接引入已经创建好的组件即可
    • 封装
(3) 组件

该图可以体现

  • 所有的组件都必须听一个大哥的,也就是vm
  • 树状结构体现了页面的分布格局,组件可以进行嵌套

​ a. 定义:用来实现局部功能的代码资源的集合(html/css/js/image…)

​ b. 为什么:一个界面的功能很复杂

​ c. 作用:复用编码,简化项目编码,提高运行效率

(4)模块

​ a. 理解:向外提供特定功能的 js 程序,一般一个模块就是一个 js 文件

​ b. 为什么:js 文件很多很复杂

​ c. 作用:复用、简化 js 的编写,提高js运行效率

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

​ 也就是说将一个很大的js分成很多个小的js文件 , 但分开后的效果与一个大的没有区别

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

2.2 非单文件组件

非单文件组件:一个文件中包含有 n 个组件 (a.html这种文件可以有多个组件 )

单文件组件 :一个文件中只包含有 1 个组件 (a.vue 这个文件只能含有一个组件, 实际上一个组件就是一个.vue)

2.2.1 基本使用

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

  1. 定义组件

    • 使用Vue.extend(options)创建,其中optionsnew Vue(options)时传入的options几乎一样,但也有点区别
      • el不要写,因为最终所有的组件都要经过一个vm的管理,由vm中的el才决定服务哪个容器
      • data必须写成函数,避免组件被复用时,数据存在引用关系
      • 备注: 使用template可以配置组件结构
  2. 注册组件

    • 局部注册:new Vue()的时候options传入components选项
    • 全局注册:Vue.component('组件名',组件)
  3. 使用组件

    • 编写组件标签如
<!DOCTYPE html>
<html>

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

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

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

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

  //第一步:创建school组件 ,组件在创建时不需要指定为谁服务,就是一块砖,哪里需要哪里搬
  const school = Vue.extend({ //新的api,(延伸,延长)需要传入一个配置对象,这个配置对象和创建vue实例时的配置对象几乎一致
    template: `                              
				<div class="demo">
					<h2>学校名称:{{schoolName}}</h2>
					<h2>学校地址:{{address}}</h2>
					<button @click="showName">点我提示学校名</button>	
				</div>
			`,   //组件的结构
    // el:'#root', //组件定义时,一定不要写el配置项,因为最终所有的组件都要被一个vm管理,由vm决定服务于哪个容器。
    data() { //在组件里data不能写成对象,因为组件是可以给很多地方进行使用的,如果使用对象,一个变化会使其他使用这个组件的data也进行变化,所以只能使用函数式,每一次返回的都是一个新的对象
      return {
        schoolName: '尚硅谷',
        address: '北京昌平'
      }
    },
    methods: {
      showName() {
        alert(this.schoolName)
      }
    },
  })

  //第一步:创建student组件
  const student = Vue.extend({
    template: `
				<div>
					<h2>学生姓名:{{studentName}}</h2>
					<h2>学生年龄:{{age}}</h2>
				</div>
			`,
    data() {
      return {
        studentName: '张三',
        age: 18
      }
    }
  })

  //第一步:创建hello组件
  const hello = Vue.extend({  
    template: `
				<div>	
					<h2>你好啊!{{name}}</h2>
				</div>
			`,
    data() {
      return {
        name: 'Tom'
      }
    }
  })

  //第二步:全局注册组件  ,所有的vm都可以使用这个组件
  Vue.component('hello', hello)

  //创建vm
  new Vue({
    el: '#root',
    data: {
      msg: '你好啊!'
    },
    //第二步:注册组件(局部注册)
    components: {  key(最终真正的组件名,与html页面的上面组件标签保持一致):value(是通过Vue.extend() 赋值的变量名,必须是上面定义的才行) ,但一般两个名字一致,可以进行简写
      school,  //  school:school   也可以写成  xuexiao:school
      student
    }
  })

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

</html>

2.2.2 组件的几个注意点
  • 关于组件名

    • 一个单词组成

      • 第一种写法(首字母小写):school

      • 第二种写法(首字母大写):School

    • 多个单词组成

      • 第一种写法(kebab-case命名):my-school

      • 第二种写法(CamelCase命名):MySchool(需要Vue脚手架支持)

备注
① 组件名尽可能回避HTML中已有的元素名称,例如h2、H2

② 可以使用name配置项指定组件在开发者工具中呈现的名字,在定义组件中添加一个name配置项

  • 关于组件标签

    • 第一种写法:
    • 第二种写法: (不使用脚手架会导致后续组件不能渲染) 自闭和

简写方式
const school = Vue.extend(options) 可以简写成 const school = options (不写时会自动调用Vue.extend)

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <title>几个注意点</title>
  <script type="text/javascript" src="../js/vue.js"></script>
</head>

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

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

  //定义组件  定义
  const s = Vue.extend({
    name: 'atguigu',  //新加的
    template: `
				<div>
					<h2>学校名称:{{name}}</h2>	
					<h2>学校地址:{{address}}</h2>	
				</div>
			`,
    data() {
      return {
        name: '尚硅谷',
        address: '北京'
      }
    }
  })

  new Vue({
    el: '#root',
    data: {
      msg: '欢迎学习Vue!'
        
    },
      //注册使用
    components: {
      school:s,
     /*"my-school":s
        MySchool:s */
    }
  })
</script>

</html>

2.2.3 组件的嵌套
要求: 1. 让student组件成为school的子组件。
	本质上school注册给了vm,所以<school></school>是写在整个html里面的
	也就是student注册给了school ,所以<student></student>是要写在school的html片段里面的
2. 再创建一个hello组件,让hello组件与school平级,
	再创建一个app组件,让它管理所有父组件,也就是将hello和school组件册给app,再将app组件注册给vm


<!DOCTYPE html>
<html>

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

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

  </div>
</body>

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

  //定义student组件
  const student = Vue.extend({
    name: 'student',
    template: `
				<div>
					<h2>学生姓名:{{name}}</h2>	
					<h2>学生年龄:{{age}}</h2>	
				</div>
			`,
    data() {
      return {
        name: '尚硅谷',
        age: 18
      }
    }
  })

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

  //定义hello组件
  const hello = Vue.extend({
    template: `<h1>{{msg}}</h1>`,
    data() {
      return {
        msg: '欢迎来到尚硅谷学习!'
      }
    }
  })

  //定义app组件
  const app = Vue.extend({
    template: `
				<div>	
					<hello></hello>
					<school></school>
				</div>
			`,
    components: {
      school,
      hello
    }
  })

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

</html>

2.2.4 VueComponent构造函数
  1. app组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的,底层源码中调用Vue.extend就返回了一个函数,就是VueComponent函数

    Vue.extend = function(exop){
        var sub = function VueComponent(op){省略};  //每次调用extend返回的都是一个新的VueComponent,使用变量接收了一下,对应4
        return sub;
    }
    
  2. 我们只需要写或,Vue解析时会帮我们创建app组件的实例对象,即Vue帮我们执行new VueComponent(options),也就是说,一个标签就是调用了一次new VueComponent(options)

  3. 特别注意:每次调用Vue.extend,返回的都是一个全新的VueComponent

  4. 关于this指向
    ① 组件配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】
    ② new Vue(options)配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象vm

  5. VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象),与vm一致

  6. Vue的实例对象,以后简称为vm,组件本质上就是一个构造函数,组件对应的标签就是其组件的实例对象

<!DOCTYPE html>
<html>

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

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

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

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

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

  //定义hello组件
  const hello = Vue.extend({
    template: `
				<div>
					<h2>{{msg}}</h2>
					<test></test>	
				</div>
			`,
    data() {
      return {
        msg: '你好啊!'
      }
    },
    components: {
      test
    }
  })


  // console.log('@',school)  输出看一看组件到底是什么,输出后其本质是一个函数(function VueComponent(){}),其首字母大写,是构造函数
  // console.log('#',hello)  它们两个输出函数名和函数体是一致的,但它们是两个不同的VueComponent(函数)

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

</html>

2.2.5 一个重要的内置关系
  • 通过观察,发现vm里面有的 vc上面都有,所以有人就将两个画等号,但是不恰当的。

    • vc是通过new VueComponent() 出来的,不需要指定el

    • vm则是通过new Vue() 出来的,里面需要指定el为某个容器服务,还有很多区别

结论:vc有的功能vm都有,但vm有的vc并不一定全有

原型链

  • 每一个构造函数都有一个prototype属性,被称为显示原型属性,程序员可以操作的,通过构造函数.prototype.xxx = function(){}来 放东西(可以是属性或者方法) 用于放实例对象公用的属性或方法
  • 每一个实例对象都有一个__proto__属性,被称为隐式原型属性,且实例对象.__proto__ === 构造函数.prototype每一个实例对象都可以通过__proto__ 用于取属性或方法
  • 所以在寻找实例对象的属性xxx时,实例对象.xxx ==> 实例对象.__proto__ .xxx === 构造函数.prototype.xxx ,就可以找到了

内置关系总结

  1. 一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype

在这里插入图片描述

  1. Vue 没有让VueComponent的原型对象的__proto__直接指向 应该的 Object的原型对象

  2. 而是让其指向Vue的原型对象,这是Vue做的一个操作,

  3. 为什么要有这个关系:让组件实例对象vc就可以访问到Vue原型上的属性和方法,vc就是一个小型的vm

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <title>一个重要的内置关系</title>
  <!-- 引入Vue -->
  <script type="text/javascript" src="../js/vue.js"></script>
</head>

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

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

  //定义school组件
  const school = Vue.extend({
    name: 'school',
    template: `
				<div>
					<h2>学校名称:{{name}}</h2>	
					<h2>学校地址:{{address}}</h2>	
					<button @click="showX">点我输出x</button>
				</div>
			`,
    data() {
      return {
        name: '尚硅谷',
        address: '北京'
      }
    },
    methods: {
      showX() {
        console.log(this.x)
      }
    },
  })

  //创建一个vm
  const vm = new Vue({
    el: '#root',
    data: {
      msg: '你好'
    },
    components: {
      school
    }
  })


</script>

</html>

2.3 单文件组件

  • 不再是将所有的组件都写在一个html页面

  • 而是一个组件写成一个.vue页面

    • 一个.vue的三个组成部分

      • 模板页面

        <template>
            <!--组件的结构-->
        </template>
        
      • JS 模块对象

        <script>
            ∥组件交互相关的代码(数据、方法等等)
            /*
            原本是:
            const school = Vue.extend({...})
            但因为当别人引入.vue是需要暴露给别人,所以
            1. export default school  默认暴露,推荐使用这种方法,比较简单
            2. export const school = Vue.extend({}) 分别暴露
            3. exprot {school} 统一暴露
            
            */
            
            
            //export default Vue.extend({})
            //简写:
            export default {
                name:'xxx', //组件名最好和文件名保持一致
                data(){},
                methods:{}
            }
        </script>
        
      • 样式

        <style>
            /*组件的样式*/
        </sty1e>
        

        es6模块化(需要了解)

老大组件** App.vue**: 快捷键’<v’

<template>
	<div>  // template必须要有一个根元素<div>
        <School/>
        <Student/>
    </div>
</template>
<script>
    ∥引入组件
    import School from './School' //文件后缀可省略
    import Student from './Student'
    export default {
       name:'App',
       components:{
           School,
           Student
       }
    }  //还需要将App注册到vm中
</script>
<style>
</sty1e>

main.js

//与 App.vue 对话
import App from './App.vue'

new Vue({
    el:'#root', //容器在哪呢? 所以还要有一个html页面
    template:`<App></App>` //也可以写在html中的容器里面
    components:{App} //注册组件
})

index.html

....
<div id="root"></div> <!--准备一个容器-->
 //先让容器准备好再进行引入
//浏览器不支持ES6的 import 语法,一下引入只为思想,实际需要脚手架去工作
//<script type="text/javascript" src="../js/vue.js"></script> //引入vue
//<script type="text/javascript" src="./main.js"></script> //**入口文件**

3. 使用vue脚手架

3.1 初始化脚手架

3.1.1. 说明
  • Vue脚手架是Vue官方提供的标准化开发工具(开发平台)
  • Vue脚手架的最新的版本是 4.x
  • 文档 [Vue CLI](https://cli.vuejs.org/zh/) command line interface 命令行接口工具
3.1.2. 具体步骤
  1. 如果下载缓慢请配置npm淘宝镜像npm config set registry http://registry.npm.taobao.org 在命令行里面敲一遍即可

  2. 全局安装(仅第一次执行) @vue/cli npm install -g @vue/cli

  3. 切换到创建项目的目录,使用命令创建项目vue create xxx

  4. 选择使用vue的版本

  5. 启动项目npm run serve

  6. 打包项目npm run build

  7. 暂停项目 Ctrl+C

Vue脚手架隐藏了所有webpack相关的配置,若想查看具体的webpack配置,请执行
vue inspect > output.js 组件命名使用双驼峰体

3.1.3 脚手架文件结构

├── node_modules 
├── public
│   ├── favicon.ico: 页签图标
│   └── index.html: 主页面
├── src
│   ├── assets: 存放静态资源,png,video等
│   │   └── logo.png
│   │── components: 存放App能够管理的所有组件
│   │   └── HelloWorld.vue
│   │── App.vue: 汇总所有组件
│   │── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── package.json: 应用包配置文件 
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件

重点分析了两个文件,main.js 和 index.html

main.js:

/* 该文件是整个项目的入口文件 */

//1. 引入Vue
import Vue from 'vue'
//2.引入App组件,它是所有组件的父组件,一人之下,万人之上
import App from './App.vue'

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

//4.创建Vue实例对象 ---vm
new Vue({
  //5.下面这行代码一会解释,完成了这个功能:将App组件放入容器中
  render: h => h(App),
}).$mount('#app')   //6.这个.$mount('#app') 可以替换成 el:"#app"

index.html

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
<!--    针对IE浏览器的一个特殊配置,含义是让IE浏览器以最高的显然级别渲染页面  -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--    开启移动端的理想视口  -->
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
<!--    图标  <%= BASE_URL %> 代表的就是public这个文件夹,不写./ 或者是 ../ -->
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<!--    配置网站标题,向package.json里面的name进行查找  ,webpack-->
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
<!--  当浏览器不支持js时noscript标签中的元素就会被渲染 -->
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
<!--容器-->
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

流程图

脚手架执行的流程图分析如下:

3.1.4 render函数

render函数出现在main.js里出现过,用于将app组将绑定到容器中

之前main.js创建vm的写法:

import App from './App.vue'
new Vue({
    el:'#root', //容器在哪呢? 所以还要有一个html页面
    template:`<App></App>` //也可以写在html中的容器里面,template模板,其实是因为缺少模板解析器所以这里错误
    components:{App} //注册组件
})

脚手架里创建vm的写法:

import App from './App.vue'
//4.创建Vue实例对象 ---vm
new Vue({
   el:"#app"
  //5.下面这行代码一会解释,完成了这个功能:将App组件放入容器中
  render: h => h(App), //不需要注册组件
}) 

  • 试过之后,发现第一个自己写的会出现错误,错误为使用了残缺版的的Vue,缺少了模板解析器,给出了两个方案

    • 要么 引入完整版的vue

      • 所以引入完整版的vue

        import Vue from 'vue/dist/vue'
        
    • 要么把模板交给render函数(这个不需要引入完整版的vue)

      • render(){
            console.log('render')
            return null;
        }  //1. 首先,render是一个函数,且render是由vue帮助调用的,必须要有返回值,否则会报错
        
      • render(createElement){ //传入的是一个形参而已
            console.log(typeof createElement)  ==>输出的是function,说明createElement是一个函数,很重要
            return null;
        }  //2. render可以接收参数
        
      • render(createElement){ //传入的是一个形参而已
            return createElement('h1','你好啊') // createElement('创建的元素名','内容')  也就是<h1>你好啊</h1>,并返回
        }  //3. 通过createElement这个函数来渲染具体内容
        
        
        改进,render中没有用到this,所以可以写成箭头函数
        render:createElement=> createElement('h1','你好啊') 
        render:q=> q('h1','你好啊')  //换一个形参名,后面传两个参数是因为h1标签是内置的,里面需要内容,但如果只是传入一个组件就只需传入一个参数即可
        最终版:
        render:q=> q(App) //传入一个组件
        

    总结

    1. vue.jsvue.runtime.xxx.js的区别

      • vue.js 是完整版的Vue,包含:核心功能+模板解析器

      • vue.runtime.esm.js 是运行版的Vue,只包含核心功能,没有模板解析器

        esm 就是 ES6 module

    2. 因为 vue.runtime.esm.js 没有模板解析器,所以不能使用template配置项,需要使用render函数接收到的createElement函数去指定具体内容

3.1.5 脚手架配置
  1. 使用 vue inspect > output.js可以查看到Vue脚手架的默认配置。会出现一个新的 output.js文件,如果在软件中运行时报错就直接到cmd里面执行即可,为了让其不爆红,将这个大对象使用const a = {} 接收一下

  2. 脚手架的配置文件都是在output.js(只读,更改无效)里面可以找到的

  3. 使用vue.config.js可以对脚手架配置进行个性化定制,详情见:https://cli.vuejs.org/zh/config(现用现查)

    //是写在vue.config.js里的内容
    const { defineConfig } = require('@vue/cli-service')
    module.exports = defineConfig({
      transpileDependencies: true,//前面就是默认就有的
      pages:{
        index:{
          //入口
          entry:'src/main.js'  //修改入口文件路径
        }
      },
      lintOnSave:false  //关闭语法检查
    })
    
  4. 如果使用脚手架的默认配置,有五个地方的文件名不能进行更改

    • 首先是public 和 src 两个文件夹的名字不能改

    • 然后是public文件夹里面的页签图标名以及index.html文件名都不可以改

    • 最后就是入口文件main.js这个文件名也不可以进行更改

    • ├── .gitignore: git版本管制忽略的配置
      ├── babel.config.js: babel的配置文件
      ├── package.json: 应用包配置文件 
      ├── README.md: 应用描述文件
      ├── package-lock.json:包版本控制文件   // 这些文件名也不要随便更改
      

3.2 ref属性

ref 被用来给元素或子组件注册引用信息(id的替代者)

  • 应用在html标签上获取的是真实DOM元素,应用在组件标签上获取的是组件实例对象vc

  • 使用方式

    • 打标识:

    • 获取:this.$refs.xxx

App.vue:(组件正常写即可,render出现在main.js)

<template>
  <div>
    <h1 v-text="msg" ref="title"></h1> // dom中使用ref来代替html里的id作为获取dom元素的标识
    <button ref="btn" @click="showDOM">点我输出上方的DOM元素</button>
    <School ref="sch" />
  </div>
</template>


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

export default {
  name: "App",
  components: { School }, //子组件
  data() {
    return {
      msg: "欢迎学习Vue!",
    };
  },
  methods: {
    showDOM() {
      console.log(this.$refs.title); //真实DOM元素
      console.log(this.$refs.btn); //真实DOM元素
      console.log(this.$refs.sch); //School组件的实例对象(vc)
    },
  },
};
</script>

3.3 props 配置项

  • 功能: 让组件接收外部传过来的数据

  • 传递数据 这里age前加:,通过v-bind使得里面的18是数字,Student是要传数据的子组件名, 注意: v-bind是指令语法,其引号当中的是简单的js表达式,这样进行传递age就是一个Number; 不加:,age就是一个String

  • 接收数据

    • 第一种方式(只接收)props:[‘name’, ‘age’]

    • 第二种方式(限制类型)props:{name:String, age:Number}

    • 第三种方式(限制类型、限制必要性、指定默认值)

  • 从外界接收到的数据正常情况下无法更改,要改看下面的注意

注意:props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。因为props的优先级更高,vc先接收props里的数据,然后才接收data里的数据

...
data() {
    console.log(this);
    return {
      msg: "我是一个尚硅谷的学生",
****  myAge: this.age,  //所以可以在data里拿到从外界接收的数据,写在data里是可以被更改的********
    };
  },
...

student.vue : 子组件

<template>
  <div>
    <h1>{{ msg }}</h1>
    <h2>学生姓名:{{ name }}</h2>
    <h2>学生性别:{{ sex }}</h2>
    <h2>学生年龄:{{ myAge + 1 }}</h2>
    <button @click="updateAge">尝试修改收到的年龄</button>
  </div>
</template>

<script>
export default {
  name: "Student",
  data() {
    console.log(this);
    return {
      msg: "我是一个尚硅谷的学生",
      myAge: this.age,
    };
  },
  methods: {
    updateAge() {
      this.myAge++;
    },
  },
  //1.简单声明接收
  // props:['name','age','sex']

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

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

3.4 mix in(混入)

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

  2. 使用方式:

    • 第一步定义混合:

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

}


- 第二步使用混入:

  - ​	 全局混入:```Vue.mixin(xxx)```所有的`vc`和`vm`都进行使用,都加上这个功能

  - ​	局部混入:```mixins:['xxx']	```

**mixin.js**  :共用的配置

```js
export const hunhe = { //将对象暴露出去,这个公用的是一个函数
methods: {
 showName() {
   alert(this.name);
 },
},
mounted() { //钩子,挂载完后直接加载
 console.log("你好啊!");
},
};
export const hunhe2 = {
data() {
 return {
   x: 100,
   y: 200,
 };
},
};

main.js:

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
import {hunhe,hunhe2} from './mixin'
//关闭Vue的生产提示
Vue.config.productionTip = false

Vue.mixin(hunhe)
Vue.mixin(hunhe2)


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

子组件

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

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

export default {
  name: "Student",
  data() {
    return {
      name: "张三",
      sex: "男",
    };
  },
  // mixins:[hunhe,hunhe2]
};
</script>

注意

  1. 组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”,在发生冲突时以组件优先
  2. 同名生命周期钩子将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用

3.5 插件

  1. 功能:用于增强Vue

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

  3. 定义插件:

    对象.install = function (Vue, options) {
        // 1. 添加全局过滤器
        Vue.filter(....)
    
        // 2. 添加全局指令
        Vue.directive(....)
    
        // 3. 配置全局混入(合)
        Vue.mixin(....)
    
        // 4. 添加实例方法
        Vue.prototype.$myMethod = function () {...}
        Vue.prototype.$myProperty = xxxx
    }
    
  4. 使用插件:Vue.use(),一个优秀的插件就是一个对象,里面有一个install方法,加载了很多强大的功能,通过插件可以在自己的页面中使用这些已经写好的强大功能

plugins.js:

export default {
    //1.定义插件
  install(Vue, x, y, z) { //Vue帮助调用install(),且传入的第一个参数是vm的构造函数Vue
    console.log(x, y, z);
    //全局过滤器
    Vue.filter("mySlice", function (value) {
      return value.slice(0, 4);
    });

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

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

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


main.js:

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

//2. 应用(使用)插件
Vue.use(plugins, 1, 2, 3);
//创建vm
new Vue({
  el: "#app",
  render: (h) => h(App),
});

子组件

<template>
  <div>
    <h2>学校名称:{{ name | mySlice }}</h2>
    <h2>学校地址:{{ address }}</h2>
    <button @click="test">点我测试一个hello方法</button>
  </div>
</template>

<script>
export default {
  name: "School",
  data() {
    return {
      name: "尚硅谷atguigu",
      address: "北京",
    };
  },
  methods: {
    test() {
      this.hello();
    },
  },
};
</script>

3.6 scope样式

  1. 作用:让样式在局部生效,防止冲突

  2. 写法:

在使用less语言时才用的到: Vue中的webpack并没有安装最新版,导致有些插件也不能默认安装最新版,如 npm i less-loader@7,而不是最新版

//写在定义vc里面即可,和data平级,可以写在任意的vue里面
<style scoped>
scoped修饰,样式只作用域当前组件
    .test{
        background-coloar:orange;  //css语法
    }
</style>

因为所有子组件的style都回汇总到app.vue里面,当各个组件的选择器样式存在冲突时,App.vue引入的会取代App.vue引入的(import),如何解决:

    1. 将样式冲突的选择器的名字进行更换(在开发中不太现实)
    1. <style>
      样式
      </style>
      
      换成
      <style lang = "css/less" scoped>  //lang是language语言的简称,不写lang默认是css
      样式
      </style>  实际上是加上了一个随机值用于区分
      
    1. app.vue里面一般不加scoped,为了使全局使用

3.7 Todo-list案例

  1. 组件化编码流程:

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

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

      • 一个组件在用:放在组件自身即可。
      • 一些组件在用:放在他们共同的父组件上(状态提升)。
    • 实现交互:从绑定事件开始。

  2. props适用于:

    • 父组件 ==> 子组件 通信

    • 子组件 ==> 父组件 通信(要求父先给子一个函数)

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

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

3.7.1 拆分静态组件
  1. 一个input组件,用于添加要做的事情 ===================>header.vue ,但因为可能会与html元素冲突,所以改成MyHeader.vue

  2. 一个列表里的元素组件,很多的元素组成了一个列表=====> item

  3. 一个展示列表组件,用于展示全部列表===================>list

    • item组件是list组件的子组件
  4. 一个用于统计列表的数量的组件,已完成1/全部30==========>footer 同上,改成MyFooter

  5. 一个app组件,肯定是要有的===========================>app

定义完组件后,直接在app组件内进行引入和注册

//只有html页面和css文件,将其整合成组件?
/*
1. 首先将html页面body元素全部放到app的template模板中,将css文件全部放到app的style中
2. 先拆html :按照结构进行拆分,拿走一个组件的结构就将代表结构的组件标签给写上,假如拿走了MyHeader的结构,就要在相应位置上加上<MyHeader/>标签
3. 再拆style
*/
App.vue
<template>
<div class="todo-container">
    <div class="todo-wrap">

      <MyHeader />
      <List />
      <MyFooter />

    </div>
  </div>

</template>

<script>
import MyHeader from './components/MyHeader.vue'
import MyFooter from './components/MyFooter.vue'
import List from './components/List.vue'

export default {
  name: 'App',
  //注册组件
  components:{
    MyHeader,
    MyFooter,
    List
  }

}
</script>

<style>
/*app*/
.todo-container {
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

</style>

MyHeader.vue
<template>
<div class="todo-header">
    <input type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
</template>

<script>
export default {
  name: "Header"
}
</script>

<style scoped>
/*header*/
.todo-header input {
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
}

.todo-header input:focus {
  outline: none;
  border-color: rgba(82, 168, 236, 0.8);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}

</style>
MyFooter.vue
<template>
<div class="todo-footer">
        <label>
          <input type="checkbox"/>
        </label>
        <span>
          <span>已完成0</span> / 全部2
        </span>
        <button class="btn btn-danger">清除已完成任务</button>
      </div>

</template>

<script>
export default {
  name: "Footer"
}
</script>

<style scoped>
/*footer*/
.todo-footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

.todo-footer label input {
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}

.todo-footer button {
  float: right;
  margin-top: 5px;
}

</style>
List.vue
<template>
<ul class="todo-main">
    <Item/>
    <Item/>
    <Item/>
    <Item/>

  </ul>

</template>

<script>
import Item from './Item.vue'

export default {
  name: "List",
  components:{Item}
}
</script>

<style scoped>
/*list*/
.todo-main {
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}

.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}


</style>
Item.vue
<template>
    <li>
      <label>
        <input type="checkbox"/>
        <span>xxxxx</span>
      </label>
      <button class="btn btn-danger" style="display:none">删除</button>
    </li>
</template>

<script>
export default {
  name: "Item"
}
</script>

<style scoped>
/*item*/
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

li button {
  float: right;
  display: none;
  margin-top: 3px;
}

li:before {
  content: initial;
}

li:last-child {
  border-bottom: none;
}

</style>
3.7.2 展示动态数据
(1) 确定数据的类型,名称
  • 由于要存的数据是一堆数据,在js中只有两个容器可以保存,保存完之后通过循环遍历可以展示出来
    • 数组
    • 对象
  • 通过分析可知,此次案例要存的数据个数不定(数组),且这一堆数据的单个个体又有很多的性质可以操作(对象),所以存成数组包对象的格式
    • [ { },{ },…{ } ]
(2) 确定数据保存在哪个组件

哪个组件要展示数组? 反正第一个想到的就是List.vue

List.vue:

<template>
<ul class="todo-main">
    <!--  2.根据数据来确定遍历了多少遍,并发送数据到Item-->
    <!--  2.1 v-for 用于从数据todos里拿到数组里的一个个对象,有几个对象就循环几次-->
    <!--  2.2 :key  使用v-bind:key将对象里面的id动态绑定给key-->
    <!--  2.3 不仅仅要循环还要将循环出来的对象传给Item组件,在Item组件中进行数据的处理,怎么传给Item??
              直接定义一个变量todo并将其赋值为拿出来的对象即可,需要使用v-bind:,这样传进去的才是对象(简单的js语句),不加':',传的仅仅只是一个字符串-->
    <Item v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj"/>

  </ul>
</template>

<script>
import Item from './Item.vue'

export default {
  name: "List",
  components:{Item},
   //1.准备数据
  data(){
    return{
      todos:[{id:'001',title:'吃饭',done:true},//done后面要写成bool值,不能是字符串,为了在Item解析时与:checked = "true"对应:checked="xx.done"
          {id:'002',title:'睡觉',done:false},
          {id:'003',title:'开车',done:true}  //id用字符串,因为Number是有尽头的,一般是hash值,这里为方便
      ]
    }
  }
}
</script>

<style>
    省去

Item.vue:

<template>
    <li>
      <label>
<!--     5.如何动态的让标签里的属性存在或者消失?  :属性:"true/false" -->
        <input type="checkbox" :checked="todo.done"/>
<!--    4. 更改span标签内的内容-->
        <span>{{todo.title}}</span>
      </label>
      <button class="btn btn-danger" style="display:none">删除</button>
    </li>
</template>

<script>
export default {
  name: "Item",
  //3. 声明接收todo对象
  props:['todo']
}
</script>

<style scoped>
省去
3.7.3 交互——从绑定事件监听开始
  • 第一个交互:按下回车添加一个todo (MyHeader.vue)

  • 按照代码的注释进行复习即可

    • MyHeader.vue:

    • <template>
      <div class="todo-header">
      <!--  1.绑定键盘事件-->
          <input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
      </div>
      </template>
      
      <script>
      import {nanoid} from 'nanoid'  //使用了分别暴露的形式,所以写成{}
      export default {
        name: "Header",
        //6.3 接收App传入的rece数据,是一个方法
        props:['addtodo'],
        methods:{
          //2. 定义函数功能
          add(envent){
            //6.8 当输入框为空时不能添加todolist(校验数据)
            if(!envent.target.value.trim()) return  //如果为空直接返回
          //  2.1 拿到输入的数据
            //  2.1.1会默认传来一个事件对象,根据对象拿到数据
            console.log(envent.target.value)
            //2.1.2 还可以使用数据双向绑定 v-model可以看看前面的
          //2.2 将输入包装成对象,uuid的变种nanoid(简单),由于是单机版的,所以要自己生成id
           const todoObj = {id:nanoid(), title:envent.target.value, done:false}
          //2.3 接下来就是要将包装成的对象从MyHeader组件发送到List组件的todos数组===>怎么办?以目前的知识储备无法发送过去,传送数据只能是父传子(通过标签)
            //因为传送数据要引入子便签就如在List组件中引入Item组件<Item a="111"/> 要发送的数据是a
            //要想发送只能将数据放在App.vue里面,List组件可以接收App组件发送的数据(父传子),而Header组件向App组件发送数据也是有解决办法的(子传父)
      
            //6. 解决完将数据移入app和app向List发送数据这两个问题后,接下来要解决Header向app发送数据的问题
            //6.4.调用传给vc的方法
            this.addtodo(todoObj)
            //6.7 为了完善,将输入框的内容清空
            envent.target.value = ''
          }
        }
      }
      </script>
      
      
    • app.vue:

    • <template>
      <div class="todo-container">
          <div class="todo-wrap">
      <!--6.2 将准备好的函数传给MyHeader-->
            <MyHeader :addtodo="addTodo"/>
      <!--      4.将数据(数组)传给List组件-->
            <List :todos="todos"/>
            <MyFooter />
      
          </div>
        </div>
      
      </template>
      
      <script>
      import MyHeader from './components/MyHeader.vue'
      import MyFooter from './components/MyFooter.vue'
      import List from './components/List.vue'
      
      export default {
        name: 'App',
        //注册组件
        components:{
          MyHeader,
          MyFooter,
          List
        },
        //3.将数据从List组件移到App组件
        data(){
          return{
            todos:[{id:'001',title:'吃饭',done:true},//done后面要写成bool值,不能是字符串,为了在Item解析时与:checked = "true"对应:checked="xx.done"
                {id:'002',title:'睡觉',done:false},
                {id:'003',title:'开车',done:true}  //id用字符串,因为Number是有尽头的,一般是hash值,这里为方便
            ]
          }
        },
      
        methods:{
            //6.1 首先在App组件中准备一个函数,函数名随便起即可
          addTodo(x){
            console.log('我是app组件,我收到了数据',x)
            //6.5 收到Header传来的新数据后,将新数据整合到数组中即可
            this.todos.unshift(x) //在数组前进行添加 当vue发现数据变化后就会重新解析模板,就会导致列表的变化
          }
        }
      
      }
      </script>
      
    • List.vue:部分

    • <script>
      import Item from './Item.vue'
      
      export default {
        name: "List",
        components:{Item},
        //5. 接收从app传入的数据
        props:['todos']
      
      }
      </script>
      

注意:data , props, methods ,computed里面的东西全部会出现在实例对象vc里面,一定不能同名

3.7.4 交互——勾选
  • 勾选即为真true,不勾选即为假false

  • 要操作的为Item.vue组件的checkbox(勾选框)

  • 步骤:

    • 首先要拿到勾选那件事的id
    • Item.vue:
    <template>
        <li>
          <label>
    <!--    1. 对勾选框进行绑定点击事件或者是change事件(只需将click改成change事件即可)-->
            <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>
            <span>{{todo.title}}</span>
          </label>
          <button class="btn btn-danger" style="display:none">删除</button>
        </li>
    </template>
    
    <script>
    export default {
      name: "Item",
      props:['todo','checkTodo'],
      //2. 定义handleCheck,让其传入id即可拿到id
      methods:{
        handleCheck(id){
          // console.log(id)
          //3.通知app组件将对应的todo对象的done值取反(数据在哪儿,对数据的操作就应该在哪儿),所以应该将数据(id)发送给app组件,让app组件根据id查找数据并修改
          //7. 在接收到App经过List传来的checktodo函数之后,将id传给App即可
          this.checkTodo(id)
    
        }
    
      }
    }
    </script>
    
    • app.vue:(部分)

    • <template></template>
            <!--    5. 逐层传递checktodo函数-->
            <List :todos="todos" :checkTodo="checkTodo"/>
      </template>
      
      <script>
      
      export default {
        name: 'App',
        components:{
          MyHeader,
          MyFooter,
          List
        },
        data(){
          return{
            todos:[{id:'001',title:'吃饭',done:true},//done后面要写成bool值,不能是字符串,为了在Item解析时与:checked = "true"对应:checked="xx.done"
                {id:'002',title:'睡觉',done:false},
                {id:'003',title:'开车',done:true}  //id用字符串,因为Number是有尽头的,一般是hash值,这里为方便
            ]
          }
        },
      
        methods:{ 
          // 4.勾选or取消勾选一个todo
          checkTodo(id){
            //4.1 循环遍历todos
            this.todos.forEach((todo)=>{
              //函数体
              if(todo.id === id){ //找到对应的id之后将done取反
                todo.done = !todo.done //取反
              }
      
            })
          }
        }
      
      }
      </script>
      
    • List.vue:(部分) 只是传递函数的作用

    • <!--  6.List组件接收过后再将这个函数传给Item.vue即可-->
       <Item v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" :checkTodo="checkTodo"/> //接收不再写
      
    • over!!

3.7.5 中间删除操作+选中变色
  • 选中变色 鼠标移动事件处理 改变背景颜色,图标显示隐藏

就只需在item.vue里面绑定

  • 标签的style即可
  • li:hover{
        background:#ddd
    }
    
    • 删除操作

      • 和上面的勾选操作大致一样,首先对选中的按钮绑定点击事件,传入按钮的id

      • 将id发送给App组件,根据id找到对象,进行删除即可

      • 如何将数据从孙子发送到爷爷呢?

          1. 首先在app(爷爷)组件中定义一个函数
          1. 然后把这个函数经过List(父)组件发送给Item(子)组件中
          1. 在Item组件刚刚定义的点击事件函数中调用这个app传来的函数,传入id
          1. 就这样就可以将id传入给app组件了,在app组件中的这个函数进行删除操作即可
        • //app组件的函数如下:
          //删除 3.删除一个todo
          deleteTodo(id){
              //filter不更改原数组,所以要接收一下,对数组进行过滤,传入一个函数返回的是过滤条件,满足即过滤,不满足就删去,最终得到的是被过滤好的数据
              this.todos = this.todos.filter((todo)=>{
                  return todo.id !== id
              })
              console.log(this.todos)
          }
          
      • 其他的代码与上面修改的都一致

    3.7.6 底部统计和全部删除操作

    底部统计

    1. 首先将app组件里的todos传给footer进行全部统计

    2. Footer组件接收数据,并将模板中的总数进行更改todos.length即可

      //接收
        props:['todos']
      //更改模板
      <span>已完成0</span> / 全部{{todos.length}}
      
      
    3. 已完成的那个标签需要进行遍历计算,所以用到计算属性

    //计算属性 
    computed:{
        doneTotal(){
          //数组里面有一个reduce,是用于数据统计的,传入两个参数,第一个为函数(传入两个参数,pre第一次输出是我们传入的初始值,
          // 第二次被调用输出是第一次被调用的返回值,依次类推 ,第二个参数current为循环遍历拿到的单个元素),
          // 第二个参数用于统计的初始值,函数最终的返回值就是最后一次被调用后pre值,注意:数组的长度为几第一个函数就会被调用几次
          const x = this.todos.reduce((pre,current)=>{
            // console.log('@',pre)
            return pre + (current.done? 1: 0) //后面是一个三元表达式,如果元素对象的done为真就计入统计加1,为假不计入统计即可
          },0)
    
          return x
    //简化可写成: return this.todos.reduce((pre,current)=>pre + (current.done?1:0),0)
        }
      }
     
    更改模板:
       <span>已完成{{doneTotal}}</span> / 全部{{todos.length}}
    
    
    1. 前面的那个勾选框勾选

      • 只需要看已完成和全部是否相等即可
      • 相等就勾选,不相等就不勾选
    2. 清除已经完成的列表

      MyFooter.vue

      <template>
      <!--  3. 当列表为空时,不显示footer-->
      <div class="todo-footer" v-show="todos.length">
              <label>
      <!--          1. 当已完成和全部不相等时就不勾选,checked为false,反之为true-->
      <!--          4. 当人为地选中勾选框时,也得让上面展示的列表全部选中,所以要绑定一个点击或change事件-->
                <input type="checkbox" :checked="isAll" @click="checkAll"/>
              </label>
              <span>
                <span>已完成{{doneTotal}}</span> / 全部{{todos.length}}
              </span>
      <!--  7. 要设置点击事件-->
              <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
            </div>
      
      </template>
      
      <script>
      export default {
        name: "Footer",
        props:['todos','checkAllTodo','clearAllTodo'],
        computed:{
          doneTotal(){
            //数组里面有一个reduce,是用于数据统计的,传入两个参数,第一个为函数(传入两个参数,pre第一次输出是我们传入的初始值,
            // 第二次被调用输出是第一次被调用的返回值,依次类推 ,第二个参数current为循环遍历拿到的单个元素),
            // 第二个参数用于统计的初始值,函数最终的返回值就是最后一次被调用后pre值,注意:数组的长度为几第一个函数就会被调用几次
            const x = this.todos.reduce((pre,current)=>{
              // console.log('@',pre)
              return pre + (current.done? 1: 0) //后面是一个三元表达式,如果元素对象的done为真就计入统计加1,为假不计入统计即可
            },0)
      
            return x
      
          },
          isAll(){
            //2. 要考虑列表为空时的情况,列表为空要返回false
            return this.doneTotal === this.todos.length && this.todos.length > 0
          }
        },
        methods:{
          //5. 定义函数
          checkAll(event){
            // console.log(event.target.checked) //拿到是全选还是不全选
            //6.拿到全选或不全选的信息后,将信息传给app组件,对列表进行更改,过程基本一致,不再赘述
            this.checkAllTodo(event.target.checked)
          },
          clearAll(){
            console.log(222)
            this.clearAllTodo()
          }
        }
      }
      </script>
      
      

      App.vue:

          // 全选or全不选
          checkAllTodo(done){
            //数组的forEach函数,传入的一个函数,数组个数有几个,传入的函数就会被自动调用几次,被调用的函数会传入一个参数是数组的单个元素
            this.todos.forEach((todo)=>{
              todo.done = done
            })
          },
          //清除所有已经完成的todo
          clearAllTodo(){
            console.log(111)
            this.todos = this.todos.filter((todo)=>{
              return !todo.done //条件为false被过滤,返回被过滤的数组
            })
          }
      
      //新增了两个函数,并将函数传给MyFooter
      

    案例over!! 2023/2/8 18:56

    3.8 webStorage(js本地存储)

    场景:

    当打开浏览器的任意一个购物页面时,不进行登录账号的前提下在搜索框中输入商品名并搜索后关闭页面,关闭浏览器,再次打开浏览器页面后发现会有一个搜索历史,里面存放的就是我们刚刚搜索的商品名,那么这个商品名是存放在哪里的呢?

    其实就是应用了浏览器的本地存储,存到了自己的硬盘上

    1. 在哪个网站打开的控制台,application中的Localstorage里面的网站就是那个网站的东西,不会出现其他网站的东西
    2. localstorage 浏览器关闭后存储的内容也不会消失
    3. sessionstorage 在浏览器窗口关闭后存储的内容会消失
    • webStorage包含了localStoragesessionStorage

    • 存储内容大小一般支持 5MB 左右(不同浏览器可能还不一样)

    • 浏览器端通过Window.sessionStorageWindow.localStorage属性来实现本地存储机制

    • 相关API

      • xxxStorage.setItem('key', 'value')该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值

      • xxxStorage.getItem('key')该方法接受一个键名作为参数,返回键名对应的值

      • xxxStorage.removeItem('key')该方法接受一个键名作为参数,并把该键名从存储中删除

      • xxxStorage.clear()该方法会清空存储中的所有数据

    备注

    SessionStorage存储的内容会随着浏览器窗口关闭而消失

    LocalStorage存储的内容,需要手动清除才会消失

    xxxStorage.getItem(xxx)如果 xxx 对应的 value 获取不到,那么getItem()的返回值是null

    JSON.parse(null)的结果依然是null

    localstorage:

    • 有些网站有换肤的功能,这时候就可以将换肤的信息存储在本地的LocalStorage中,当需要换肤的时候,直接操作LocalStorage即可
    • 在网站中的用户浏览信息也会存储在LocalStorage中,还有网站的一些不常变动的个人信息等也可以存储在本地的LocalStorage中
    <!DOCTYPE html>
    <html>
    	<head>
    		<meta charset="UTF-8" />
    		<title>localStorage</title>
    	</head>
    	<body>
    		<h2>localStorage</h2>
            //创建几个按钮用于操作localstoarge的数据,增删查
    		<button onclick="saveData()">点我保存一个数据</button>
    		<button onclick="readData()">点我读取一个数据</button>
    		<button onclick="deleteData()">点我删除一个数据</button>
    		<button onclick="deleteAllData()">点我清空一个数据</button>
    
    		<script type="text/javascript" >
    			let p = {name:'张三',age:18}
    
    			function saveData(){
                    // 传入两个参数,key,value,且key和value都必须是字符串,传入的不是,会自动调用xxx.toString()
    				localStorage.setItem('msg','hello!!!')
    				localStorage.setItem('msg2',666) // 666存到浏览器中也是字符串
                    // 转成 JSON 对象存进去,特殊的是对象.toString() = [object]不会显示数据,所以不能直接将对象传进去,要经过以下处理才行
    				localStorage.setItem('person',JSON.stringify(p))
    			}
    			function readData(){
    				console.log(localStorage.getItem('msg'))
    				console.log(localStorage.getItem('msg2'))
    
    				const result = localStorage.getItem('person')
    				console.log(JSON.parse(result))
    
    				// console.log(localStorage.getItem('msg3'))
    			}
    			function deleteData(){
    				localStorage.removeItem('msg2')
    			}
    			function deleteAllData(){
    				localStorage.clear()
    			}
    		</script>
    	</body>
    </html>
    
    

    sessionstorage:

    由于SessionStorage具有时效性,所以可以用来存储一些网站的游客登录的信息,还有临时的浏览记录的信息。当关闭网站之后,这些信息也就随之消除了

    和上方基本一致。。。。。。

    3.8.1使用本地存储优化Todo-List

    之前的todo-list有几个问题:

    • 页面刚打开时理应是空的任务列表才对,所以代码中todos应该是空的
    • 页面刷新手动添加的数据全部都消失了,所以数据应该在localstorage里面存储一份才对,当删除列表元素时应同步更新localstorage里面的数据,怎么办呢?
      • 对todos添加一个监视事件,一旦todos发生改变,立即将新的todos重新写入到浏览器的localstorage里面
      • 在初始化时将数据从localstorage取到todos里面进行渲染,并要注意在本地存储也没有时要给todos一个初始值[],否则会报错

    app.vue:

     data(){
        //{id:'001',title:'吃饭',done:true},//done后面要写成bool值,不能是字符串,为了在Item解析时与:checked = "true"对应:checked="xx.done"
        //       {id:'002',title:'睡觉',done:false},
        //       {id:'003',title:'开车',done:true}  //id用字符串,因为Number是有尽头的,一般是hash值,这里为方便
        return{
          //要考虑初始化时,最后得到的是null,取不到其他组件的todos.性质就会报错,所以加上一个空数组即可
          todos: JSON.parse(localStorage.getItem('todos')) || []//页面刷新后读取存在localstorage里的数据,就可以达到一个数据不会消失的效果了
        }
      },
    
    //监视事件
      watch:{
        //来监视todos这个数据,由于监视默认只监视数据的第一层,也就是说只监视数组里面的对象是否变化,并不监视对象里面的属性是否发生变化,所以要使用深度监视
        todos:{
          deep:true,
          handler(newtodos){
                  localStorage.setItem('todos',JSON.stringify(newtodos)) //将监视得来的新的数组存入到localstorage里
          }
        }
      }
    

    3.9 组件的自定义事件

    • 组件自定义事件是一种组件间通信的方式,适用于:子组件 ===> 父组件

    • 使用场景

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

      • 第一种方式,在父组件中:<Demo @atguigu=“test”/>或
      • 第二种方式,在父组件中:使用 this.$refs.xxx.$on() 这样写起来更灵活,比如可以加定时器啥的。this.$refs.xxx只是为了拿到组件实例对象,实际上只需要vc.$on()就可绑定事件
    • 解绑自定义事件this.$off('atguigu') 在被绑定的组件中去解绑自定义事件,在此处是在Student组件中解绑

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

    <!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,使用ref) -->
    <Student ref="student" @click.native="show"/> 组件绑定原生的事件之后就会将v-on:click事件交给了Student组件模板中最外层的标签<div @click="show"></div>
    

    注意:通过this.$refs.xxx.$on('atguigu',回调)绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,否则this指向会出问题! 定义在methods,mounted(还有其他的)里面的普通函数(不使用箭头函数),里面的函数体的this硬绑定给了定义时的组件实例对象,而不是调用时的组件实例对象,这也是为什么在没有学习组建的自定义事件时子组件调用父组件里的函数,父组件可以收到子组件的数据的原因

    第一种方式:

    app.vue:

    <template>
    	<div class="app">
    		<!-- 1.通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,使用@或v-on)
    				v-on是给Student组件的实例对象vc绑定事件,配置方法啊getStudentName,如何触发atguigu这个事件??
    	绑定给谁就去找谁去触发事件,所以在Student组件触发事件。。。-->
    		<Student @atguigu="getStudentName"/> 
    	</div>
    </template>
    
    <script>
    	import Student from './components/Student'
    
    	export default {
    		name:'App',
    		components:{Student},
    		data() {
    			return {
    				msg:'你好啊!',
    				studentName:''
    			}
    		},
    		methods: {
                //2.定义组件函数
    			getStudentName(name,...params){
    				console.log('App收到了学生名:',name,params)
    				this.studentName = name
    			}
    		}
    	}
    </script>
    
    <style scoped>
    	.app{
    		background-color: gray;
    		padding: 5px;
    	}
    </style>
    
    

    student.vue:

    <template>
    	<div class="student">
            //3. 绑定点击事件
    		<button @click="sendStudentlName">把学生名给App</button>
    	</div>
    </template>
    
    <script>
    	export default {
    		name:'Student',
    		data() {
    			return {
    				name:'张三',
    			}
    		},
    		methods: {
                //4. 点击之后触发自定义的组件
    			sendStudentlName(){
    				//5. 触发Student组件实例身上的atguigu事件,借助$emit
    				this.$emit('atguigu',this.name,666,888,900)
    			}
    		},
    	}
    </script>
    
    <style lang="less" scoped>
    	.student{
    		background-color: pink;
    		padding: 5px;
    		margin-top: 30px;
    	}
    </style>
    
    
    

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

    使用 this.$refs.xxx.$on() 这样写起来更灵活,比如可以加定时器啥的。

    app.vue:

    <template>
    	<div class="app">
    		<!-- 1.通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,使用ref) -->
    		<Student ref="student"/>  this.$refs.student就可以拿到student组件实例对象
    	</div>
    </template>
    
    <script>
    	import Student from './components/Student'
    
    	export default {
    		name:'App',
    		components:{Student},
    		data() {
    			return {
    				studentName:''
    			}
    		},
    		methods: {
    			getStudentName(name,...params){
    				console.log('App收到了学生名:',name,params)
    				this.studentName = name
    			},
    		},
    		mounted() {
                //2.当app组件挂载完毕,给组件的实例对象绑定事件atguigu,调用改事件后会执行回调函数this.getStudentName
    			this.$refs.student.$on('atguigu',this.getStudentName) //绑定自定义事件
    			// this.$refs.student.$once('atguigu',this.getStudentName) //绑定自定义事件(一次性)
                //3.等一段时间后再绑定事件
                setTimeput(()=>{
                    this.$refs.student.$on('atguigu',this.getStudentName)
                },3000)
    		},   //等3s后再绑定事件
    	}
    </script>
    
    <style scoped>
    	.app{
    		background-color: gray;
    		padding: 5px;
    	}
    </style>
    
    
    

    student.vue:

    <template>
    	<div class="student">
    		<button @click="sendStudentlName">把学生名给App</button>
    	</div>
    </template>
    
    <script>
    	export default {
    		name:'Student',
    		data() {
    			return {
    				name:'张三',
    			}
    		},
    		methods: {
    			sendStudentlName(){
    				//触发Student组件实例身上的atguigu事件
    				this.$emit('atguigu',this.name,666,888,900)
    			}
    		},
    	}
    </script>
    
    <style lang="less" scoped>
    	.student{
    		background-color: pink;
    		padding: 5px;
    		margin-top: 30px;
    	}
    </style>
    
    
    

    若想让自定义事件只能触发一次,可以使用once修饰符,或$once方法。

    触发自定义事件:this.$emit('atguigu',数据)

    使用 this.$emit() 就可以子组件向父组件传数据

    3.10 全局事件总线(GlobalEventBus)

    • 一种可以在任意组件间通信的方式,本质上就是一个对象,它必须满足以下条件

        1. 所有的组件对象都必须能看见他
        1. 这个对象必须能够使用$on, $emit, o f f 方法去绑定、触发和解绑事件 ( 谁有 off方法去绑定、触发和解绑事件(谁有 off方法去绑定、触发和解绑事件(谁有on, e m i t , emit, emit,off呢?组件的实例对象有,

          实际上这些是在Vue的原型对象上的 Vue.prototype = { $on,$emit,$off},所以这个对象应该是vc或者vm)

    • 使用步骤:

      1. 定义全局事件总线

        new Vue({
        	......
            //就相当于原理图中的app外部的x,就是一个全局的东西,任何组件都能访问
        	beforeCreate() {
        		Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm,就是一个傀儡而已
        	},
            ......
        }) 
        
      2. 使用事件总线

        • 接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身

          methods(){
            demo(data){......}
          }
          ......
          mounted() {
              //给this.$bus(vm)绑定自定义事件
            this.$bus.$on('xxxx',this.demo)
          }
          
        • b提供数据:this. b u s . bus. bus.emit(‘xxx’,data) (在另一个组件b中调用这个事件,传入数据,就可把数据传给组件a)

      3. 子组件中最好在beforeDestroy钩子中,用$off()去解绑当前组件所用到的事件(当组件被销毁前最好手动解绑一下当前组件所用到的事件)

    school.vue:

    <template>
    	<div class="school">
    		<h2>学校名称:{{name}}</h2>
    		<h2>学校地址:{{address}}</h2>
    	</div>
    </template>
    
    <script>
    	export default {
    		name:'School',
    		data() {
    			return {
    				name:'尚硅谷',
    				address:'北京',
    			}
    		},
            methods: {
                demo(data) {
                    console.log('我是School组件,收到了数据',data)
                }
            }
    		mounted() {
    			// console.log('School',this)
    			this.$bus.$on('hello',this.demo)
    		},
    		beforeDestroy() {
    			this.$bus.$off('hello')
    		},
    	}
    </script>
    
    <style scoped>
    	.school{
    		background-color: skyblue;
    		padding: 5px;
    	}
    </style>
    
    

    student.vue:

    <template>
    	<div class="student">
    		<h2>学生姓名:{{name}}</h2>
    		<h2>学生性别:{{sex}}</h2>
    		<button @click="sendStudentName">把学生名给School组件</button>
    	</div>
    </template>
    
    <script>
    	export default {
    		name:'Student',
    		data() {
    			return {
    				name:'张三',
    				sex:'男',
    			}
    		},
    		mounted() {
    			// console.log('Student',this.x)
    		},
    		methods: {
    			sendStudentName(){
    				this.$bus.$emit('hello',this.name)
    			}
    		},
    	}
    </script>
    
    <style lang="less" scoped>
    	.student{
    		background-color: pink;
    		padding: 5px;
    		margin-top: 30px;
    	}
    </style>
    
    

    3.11 消息的订阅与发布(基本不用) 没有看

    • 消息订阅与发布(pubsub)消息订阅与发布是一种组件间通信的方式,适用于任意组件间通信

    • 使用步骤

        1. 安装pubsub:npm i pubsub-js
        1. 引入:import pubsub from 'pubsub-js'
        1. 接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身
        methods:{
          demo(data){......}
        }
        ......
        mounted() {
          this.pid = pubsub.subscribe('xxx',this.demo) //订阅消息
        }
        
        
        1. 提供数据:pubsub.publish('xxx',数据)
        1. 最好在beforeDestroy钩子中,用PubSub.unsubscribe(pid)去取消订阅。

    3.12 $nextTick

    1. 语法:this.$nextTick(回调函数)

    2. 作用:在下一次 DOM 更新结束后执行其指定的回调。

    3. 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行。

    4. 如何理解要基于更新后的新DOM?

      • 在本次函数中模板数据发生了改变,但vue正常来讲会将整个函数执行完毕之后才会重新更新模板
      • 但当遇到this.$nextTick(回调函数)函数后,vue就会立即重新更新模板,然后再返回函数执行这个回调函数
      • 所以是说基于更新后的新DOM
      this.$nextTick(function({
      	this.$refs.inputTitle.focus()
      }
      

    3.13 过渡与动画(略)

    4. Ajax 配置代理 和slot插槽

    发送ajax请求在这里推荐使用axios, 需要先发送请求 npm i axios,以下是axios发送ajax请求的格式

     <button @click="sendAjax">点我发送ajax请求</button>
    
    //1. 引入axios用于发送ajax请求
    import axios from 'axios'
    export default {
      name: 'App',
      methods:{
          //2.封装发送ajax的函数
        sendAjax(){
          axios.get('http://localhost:5000/student').then(  //请求的是目标服务器
              //发送get形式的ajax,成功调用response这个函数,返回的信息实际上是response.data
              response =>{
                console.log('请求成功了',response.data)
              },
              //失败则调用error这个函数
              error =>{
                console.log('请求失败了',error.message)
              }
          )
        }
      }
    }
    
    • 注意:1. Ajax不允许跨域请求,也就是说不允许通过ajax来请求其他网站上的资源

      1. 只能请求和本页面同协议,同域名,同端口的路径
      1. 跨域请求得不到请求的资源,但实际上是已经向目标服务器发送了请求
      1. 目标服务器也已经返回了资源,只是返回后自己的浏览器发现是跨域请求,不给显示给我们自己而已
    4.1 脚手架配置代理

    要解决上述ajax不能跨域的问题,有如下几个解决方法:

    1. 设置响应头(cors) (是最根本的解决方式)

      • 核心原理:跨域访问的资源允许你跨域访问。(是写在后端被访问的资源的,资源允许访问才行)

      • 只需要被请求资源的服务器在返回数据时加上一个特殊的响应头即可

      • 实现:

        • response.setHeader("Access-Control-Allow-Origin", "http://localhost:8080"); // 允许某个,只允许后面的访问
          response.setHeader("Access-Control-Allow-Origin", "*"); // 允许所有
          
    2. 配置代理服务器(用的最多的)

      • 原理:

        img

      • 方法一

        • 在vue.config.js中添加如下配置:

          • devServer:{
              proxy:"http://localhost:5000"  //这里配置的是要转发的目标路径,也就是目标服务器,只写到端口即可
            }
            
        • 在发送ajax请求时就要将目标指向代理服务器:(要写到资源路径)

          • sendAjax(){
                  axios.get('http://localhost:8080/student').then()
            }
            
        • 优点:配置简单,请求资源时直接发给前端(8080)即可。

        • 缺点:不能配置多个代理,不能灵活的控制请求是否走代理。

        • 工作方式:若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器 (优先匹配前端资源)

      • 方式二:

        • 编写vue.config.js配置具体代理规则:

          • module.exports = { //目前所在的路径是http://localhost:8080
            	devServer: {
                  proxy: {
                  '/api1': {//1.请求前缀,匹配所有以 '/api1'开头的请求路径,以此开头即使用代理
                    target: 'http://localhost:5000',// 2.代理目标的基础路径
                    changeOrigin: true, //3. 当目标服务器问代理服务器来自于哪里,为true时会撒谎称为目标服务器的路径
                    pathRewrite: {'^/api1': ''}//4.正则,代理服务器将请求地址转给真实服务器时会将 /api1 去掉
                   //   ws:ture 5.用于支持websocket
                  },
                  '/api2': {
                    target: 'http://localhost:5001',
                    changeOrigin: true,
                    pathRewrite: {'^/api2': ''}
                  }
                }
              }
            }
            /*3.
               changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
               changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:8080
               changeOrigin默认值为true
            */
            
            
          • 优点:可以配置多个代理,且可以灵活的控制请求是否走代理。

          • 缺点:配置略微繁琐,请求资源时必须加前缀

    4.2 slot插槽
    1. 作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于 父组件 ===> 子组件
    2. 分类:默认插槽、具名插槽、作用域插槽
    3. 使用方式:
    默认插槽
      • 默认插槽:
    父组件中:app.vue
    	 <template>
            <Category>
               <div>html结构1</div>  //在组件标签里面添加内容
            </Category>
    	 </template>
    
    子组件中:Category.vue
            <template>
                <div>
                   <!-- 定义插槽 -->
                   <slot>插槽默认内容...</slot>
                </div>
            </template>
    
    

    案例

    app.vue:

    <template>
    	<div class="container">
    		<Category title="美食" >
                //img标签在被解析后放到了组件Category里的<slot></slot>标签里面
    			<img src="https://s3.ax1x.com/2021/01/16/srJlq0.jpg" alt="">  //新的添加标签体
    		</Category>
    
    		<Category title="游戏" >
    			<ul>
    				<li v-for="(g,index) in games" :key="index">{{g}}</li>
    			</ul>
    		</Category>
    
    		<Category title="电影">
    			<video controls src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"></video>
    		</Category>
    	</div>
    </template>
    
    <script>
    	import Category from './components/Category'
    	export default {
    		name:'App',
    		components:{Category},
    		data() {
    			return {
    				foods:['火锅','烧烤','小龙虾','牛排'],
    				games:['红色警戒','穿越火线','劲舞团','超级玛丽'],
    				films:['《教父》','《拆弹专家》','《你好,李焕英》','《尚硅谷》']
    			}
    		},
    	}
    </script>
    
    <style scoped>
    	.container{
    		display: flex;
    		justify-content: space-around;
    	}
    </style>
    

    Category:

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

    案例

    app.vue

    <template>
    	<div class="container">
    		<Category title="美食" >
    			<img slot="center" src="https://s3.ax1x.com/2021/01/16/srJlq0.jpg" alt="">
    			<a slot="footer" href="http://www.atguigu.com">更多美食</a>
    		</Category>
    
    		<Category title="游戏" >
    			<ul slot="center">
    				<li v-for="(g,index) in games" :key="index">{{g}}</li>
    			</ul>
    			<div class="foot" slot="footer">
    				<a href="http://www.atguigu.com">单机游戏</a>
    				<a href="http://www.atguigu.com">网络游戏</a>
    			</div>
    		</Category>
    
    		<Category title="电影">
    			<video slot="center" controls src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"></video>
    			<template v-slot:footer>//在遇到template时的新写法,只有template有
    				<div class="foot">
    					<a href="http://www.atguigu.com">经典</a>
    					<a href="http://www.atguigu.com">热门</a>
    					<a href="http://www.atguigu.com">推荐</a>
    				</div>
    				<h4>欢迎前来观影</h4>
    			</template>
    		</Category>
    	</div>
    </template>
    
    <script>
    	import Category from './components/Category'
    	export default {
    		name:'App',
    		components:{Category},
    		data() {
    			return {
    				foods:['火锅','烧烤','小龙虾','牛排'],
    				games:['红色警戒','穿越火线','劲舞团','超级玛丽'],
    				films:['《教父》','《拆弹专家》','《你好,李焕英》','《尚硅谷》']
    			}
    		},
    	}
    </script>
    
    <style scoped>
    	.container,.foot{
    		display: flex;
    		justify-content: space-around;
    	}
    	h4{
    		text-align: center;
    	}
    </style>
    

    Category.vue

    <template>
    	<div class="category">
    		<h3>{{title}}分类</h3>
    		<!-- 定义一个插槽(挖个坑,等着组件的使用者进行填充) -->
    		<slot name="center">我是一些默认值,当使用者没有传递具体结构时,我会出现1</slot>
    		<slot name="footer">我是一些默认值,当使用者没有传递具体结构时,我会出现2</slot>
    	</div>
    </template>
    
    <script>
    	export default {
    		name:'Category',
    		props:['title']
    	}
    </script>
    
    <style scoped>
    	.category{
    		background-color: skyblue;
    		width: 200px;
    		height: 300px;
    	}
    	h3{
    		text-align: center;
    		background-color: orange;
    	}
    	video{
    		width: 100%;
    	}
    	img{
    		width: 100%;
    	}
    </style>
    
    
    作用域插槽
    1. 作用域插槽
      • 理解:数据在组件的自身(子组件),但根据数据生成的结构需要组件的使用者(父组件)来决定。(games数据在Category(子)组件中,但使用数据所遍历出来的结构由App(父)组件决定)
      • 具体编码:
      父组件中:
      		<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>
      
      

    案例

    <template>
    	<div class="container">
    
    		<Category title="游戏">
    			<template scope="atguigu">// 要想收到插槽传来的数据就必须就要写template,scope是为了接收传来的数据的,可以随便命名,传来的是一个对象,数据在对象里面
    				<ul>
    					<li v-for="(g,index) in atguigu.games" :key="index">{{g}}</li>
    				</ul>
    			</template>
    		</Category>
    
    		<Category title="游戏">
    			<template scope="{games}">//解析赋值写法
    				<ol>
    					<li style="color:red" v-for="(g,index) in games" :key="index">{{g}}</li>
    				</ol>
    			</template>
    		</Category>
    
    		<Category title="游戏">
    			<template slot-scope="{games}">
    				<h4 v-for="(g,index) in games" :key="index">{{g}}</h4>
    			</template>
    		</Category>
    
    	</div>
    </template>
    
    <script>
    	import Category from './components/Category'
    	export default {
    		name:'App',
    		components:{Category},
    	}
    </script>
    
    <style scoped>
    	.container,.foot{
    		display: flex;
    		justify-content: space-around;
    	}
    	h4{
    		text-align: center;
    	}
    </style>
    

    Category.vue

    <template>
    	<div class="category">
    		<h3>{{title}}分类</h3>
    		<slot :games="games" msg="hello">我是默认的一些内容</slot>//在默认插槽的基础上加上了属性,将属相传给了插槽的使用者,也就是App.vue
    	</div>
    </template>
    
    <script>
    	export default {
    		name:'Category',
    		props:['title'],
    		data() {
    			return {
    				games:['红色警戒','穿越火线','劲舞团','超级玛丽'], //把数据放到了Category里面
    			}
    		},
    	}
    </script>
    
    <style scoped>
    	.category{
    		background-color: skyblue;
    		width: 200px;
    		height: 300px;
    	}
    	h3{
    		text-align: center;
    		background-color: orange;
    	}
    	video{
    		width: 100%;
    	}
    	img{
    		width: 100%;
    	}
    </style>
    

    5. Vuex

    本质就是多个组件共享数据的一个功能,其中若有任意一个组件对vuex里的数据进行了修改,其他组件取数据时同步进行了修改

    5.1 理解 Vuex

    • 概念:专门在Vue中实现集中式状态(数据)管理的一个Vue插件,对Vue应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信

    • 什么时候使用 Vuex

      • 多个组件依赖于同一状态(也就是数据)
      • 来自不同组件的行为需要变更同一状态
    • Vuex 工作原理图:

      • 绿色的虚线区组成了Vuex,三大组成部分,这三个组成部分需要受到 store 的管理:
        • Actions :动作,行为 { }
        • Mutations: 修改,加工,维护 { }
        • State: 状态(数据) ,所以实际上数据就是交给了Vuex里的state对象进行管理的 { }
      • dispatch: 分发,派遣,接口api, 将从组件中获取的数据进行处理,dispatch('jia',2)
      • Actions: 调用完dispatch之后,就会来到Actions, 同样也是一个对象 { ..., jia: function(){}}, 将数据2传入到对应的函数内,如果操作和数据都存在,也不需要什么业务逻辑就可以跳过Actions,直接从组件通过调用commit到达Mutations
      • commit: 提交,在对应的函数内部,会手动调用(需要自己去写)commit函数, commit('jia',2)
      • Mutations: 提交完之后会走到这个里面,也是一个对象,{..., jia:function(){ 会拿到state和传过来的2 调用state.sum += 2}}, 就会自动调用mutate让State里的数据进行了改变
      • Render:渲染 ,当state发生了改变就会重新渲染,更新

    5.2 求和案例

    5.2.1 使用纯vue进行编写

    Count.vue:

    在这个组件中,将sum数据放到了Count.vue这个组件里面,在使用vuex时,我们要试着将sum返到vuex里面才行

    <template>
    <div>
      <h1>当前求和为{{sum}}</h1>
    <!--  需要获取用户输入的数据-->
    <!--  注意:select框里面的value值默认都是字符串,只有通过v-bind绑定之后引号里面的才按照js表达式进行,所以要加上:-->
    <!--  或者是将收集到的数据强转成number  <select v-model.number="n"> ,里面的option不再加上:-->
      <select v-model="n">
        <option :value="1">1</option>
        <option :value="2">2</option>
        <option :value="3">3</option>
      </select>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
      <button @click="incrementOdd">当前求和为奇数再加</button>
      <button @click="incrementWait">等一等再加</button>
    </div>
    </template>
    
    <script>
    export default {
      name: "Count",
    
      data(){
        return {
          n:1, // 用户选择的数字,初始化时为1
          sum:0  // 要保存求和的数据才行
        }
      },
      methods:{
        increment(){
          this.sum += this.n
        },
        decrement(){
          this.sum -= this.n
        },
        incrementOdd(){
          if(this.sum % 2){
            this.sum += this.n
          }
        },
        incrementWait(){
          setTimeout(()=>{
            this.sum += this.n
          },500)
        }
      }
    }
    </script>
    
    <style scoped>
      button{
        margin-left: 5px;
      }
    </style>
    
    5.2.2 搭建vuex环境
    • 安装vuex ,npm i vuex(版本4) ,npm i vuex@3 (vue2使用vuex3)
    • 因为是个插件,所以 Vue.use(Vuex)
    • store
    • vc ===> store , 让所有的组件都能看到store

    创建文件:src/store/index.js

    //该文件用于创建Vuex中最为核心的store
    //引入Vue核心库
    import Vue from 'vue'
    //引入Vuex
    import Vuex from 'vuex'
    //应用Vuex插件,在创建store实例前必须要应用插件
    Vue.use(Vuex)
    
    //准备actions对象——响应组件中用户的动作
    const actions = {}
    //准备mutations对象——修改state中的数据
    const mutations = {}
    //准备state对象——保存具体的数据
    const state = {}
    
    //const store = new Vuex.Store({配置对象})
    //创建并暴露store
    export default new Vuex.Store({
    	actions, //简写形式
    	mutations,
    	state
    })
    
    

    main.js中创建vm时传入store配置项

    ......
    //引入store
    import store from './store'  //默认引入index.js
    ......
    
    //创建vm
    new Vue({
    	el:'#app',
    	render: h => h(App),
    	store  //直接在这里配置,就可以在vm和vc中同时出现store
    })
    
    
    5.2.3 使用vuex进行编写
    • Vuex的基本使用

      • 1初始化数据state,配置actions、mutations,操作文件store.js

        //引入Vue核心库
        import Vue from 'vue'
        //引入Vuex
        import Vuex from 'vuex'
        //引用Vuex
        Vue.use(Vuex)
        
        const actions = {
            //响应组件中加的动作
        	jia(context,value){
        		// console.log('actions中的jia被调用了',miniStore,value)
        		context.commit('JIA',value)
        	},
        }
        
        const mutations = {
            //执行加
        	JIA(state,value){
        		// console.log('mutations中的JIA被调用了',state,value)
        		state.sum += value
        	}
        }
        
        //初始化数据
        const state = {
           sum:0
        }
        
        //创建并暴露store
        export default new Vuex.Store({
        	actions,
        	mutations,
        	state,
        })
        
        
      • 2组件中读取vuex中的数据$store.state.数据

      • 3组件中修改vuex中的数据$store.dispatch('action中的方法名',数据)

    ​ 或$store.commit('mutations中的方法名',数据)

    若没有网络请求或其他业务逻辑,组件中也可越过actions,即不写dispatch,直接编写commit

    index.js:

    //该文件用于创建Vuex中最为核心的store
    import Vue from 'vue'
    //引入Vuex
    import Vuex from 'vuex'
    //应用Vuex插件
    Vue.use(Vuex)
    
    //准备actions——用于响应组件中的动作
    const actions = {
    	/* jia(context,value){ //在组件中只是传入了一个value,放到了第二个参数的位置,而第一个则是默认传来的ministore,里面存放了可能需要的一些函数
    		console.log('actions中的jia被调用了')
    		context.commit('JIA',value)
    	},
    	jian(context,value){
    		console.log('actions中的jian被调用了')
    		context.commit('JIAN',value)
    	}, */
    	jiaOdd(context,value){
    		console.log('actions中的jiaOdd被调用了')
    		if(context.state.sum % 2){
    			context.commit('JIA',value)
    		}
    	},
    	jiaWait(context,value){
    		console.log('actions中的jiaWait被调用了')
    		setTimeout(()=>{
    			context.commit('JIA',value)
    		},500)
    	}
    }
    //准备mutations——用于操作数据(state)
    const mutations = {
    	JIA(state,value){ //第一个是默认的state对象,第二个才是传来的值
    		console.log('mutations中的JIA被调用了')
    		state.sum += value
    	},
    	JIAN(state,value){
    		console.log('mutations中的JIAN被调用了')
    		state.sum -= value
    	}
    }
    //准备state——用于存储数据
    const state = {
    	sum:0 //当前的和
    }
    
    //创建并暴露store
    export default new Vuex.Store({
    	actions,
    	mutations,
    	state,
    })
    
    

    Count.vue:

    <template>
    	<div>
    		<h1>当前求和为:{{$store.state.sum}}</h1>
    		<select v-model.number="n">
    			<option value="1">1</option>
    			<option value="2">2</option>
    			<option value="3">3</option>
    		</select>
    		<button @click="increment">+</button>
    		<button @click="decrement">-</button>
    		<button @click="incrementOdd">当前求和为奇数再加</button>
    		<button @click="incrementWait">等一等再加</button>
    	</div>
    </template>
    
    <script>
    	export default {
    		name:'Count',
    		data() {
    			return {
    				n:1, //用户选择的数字
    			}
    		},
    		methods: {
    			increment(){ //$store是在vc或vm身上
                    // commit 是操作 mutations
    				this.$store.commit('JIA',this.n)
    			},
    			decrement(){
                    // commit 是操作 mutations
    				this.$store.commit('JIAN',this.n)
    			},
    			incrementOdd(){
                    // dispatch 是操作 actions
    				this.$store.dispatch('jiaOdd',this.n)
    			},
    			incrementWait(){
                    // dispatch 是操作 actions
    				this.$store.dispatch('jiaWait',this.n)
    			},
    		},
    		mounted() {
    			console.log('Count',this)
    		},
    	}
    </script>
    
    <style lang="css">
    	button{
    		margin-left: 5px;
    	}
    </style>
    
    

    5.3 getters 配置项

    • 概念:当state中的数据需要经过加工后再使用时,可以使用getters加工,相当于全局计算属性

    • 在store.js中追加getters配置

      ......
      
      const getters = {
      	bigSum(state){
      		return state.sum * 10
      	}
      }
      
      //创建并暴露store
      export default new Vuex.Store({
      	......
      	getters
      })
      
      
    • 组件中读取数据:$store.getters.bigSum即可

    5.4 四个map方法的使用

    • 导入

      import {mapState, mapGetters, mapActions, mapMutations} from 'vuex'
      
    • **mapState方法:**用于帮助我们映射state中的数据为计算属性

      computed: {
          //借助mapState生成计算属性:sum、school、subject(对象写法)
           ...mapState({sum:'sum',school:'school',subject:'subject'}),
               
          //借助mapState生成计算属性:sum、school、subject(数组写法)
          ...mapState(['sum','school','subject']),
      },
      
    • **mapGetters方法:**用于帮助我们映射getters中的数据为计算属性

      computed: {
          //借助mapGetters生成计算属性:bigSum(对象写法)
          ...mapGetters({bigSum:'bigSum'}),
      
          //借助mapGetters生成计算属性:bigSum(数组写法)
          ...mapGetters(['bigSum'])
      },
      
    • **mapActions方法:**用于帮助我们生成与actions对话的方法,即:包含$store.dispatch(xxx)的函数

      methods:{
          //靠mapActions生成:incrementOdd、incrementWait(对象形式)
          ...mapActions({incrementOdd:'jiaOdd',incrementWait:'jiaWait'})
      
          //靠mapActions生成:incrementOdd、incrementWait(数组形式)
          ...mapActions(['jiaOdd','jiaWait'])
      }
      
    • **mapMutations方法:**用于帮助我们生成与mutations对话的方法,即:包含$store.commit(xxx)的函数

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

    备注:mapActionsmapMutations使用时,若需要传递参数需要:**在模板中绑定事件时传递好参数,**否则传的参数是事件对象(event)。

    具体案例:

    <template>
      <div>
        <h1>当前求和为:{{ sum }}</h1>
        <h3>当前求和放大10倍为:{{ bigSum }}</h3>
        <h3>年龄:{{ age }}</h3>
        <h3>姓名:{{name}}</h3>
        <select v-model.number="n">
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
        </select>
    	<!-- 用了mapActions 和 mapMutations 的话要主动传参 -->
        <button @click="increment(n)">+</button>
        <button @click="decrement(n)">-</button>
        <button @click="incrementOdd(n)">当前求和为奇数再加</button>
        <button @click="incrementWait(n)">等一等再加</button>
      </div>
    </template>
    
    <script>
    import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
    export default {
      name: "Count",
      data() {
        return {
          n: 1, //用户选择的数字
        };
      },
      computed: {
    	...mapState(['sum', 'age', 'name']),
    	...mapGetters(['bigSum'])  
      },
      methods: {
        ...mapActions({incrementOdd: 'sumOdd', incrementWait: 'sumWait'}),
        ...mapMutations({increment: 'sum', decrement: 'reduce'})
      },
      mounted() {
        console.log("Count", this);
      },
    };
    </script>
    
    <style lang="css">
    button {
      margin-left: 5px;
    }
    </style>
    
    
    

    5.5 模块化+命名空间

    1. 目的:让代码更好维护,让多种数据分类更加明确。

    2. 修改store.js
      为了解决不同模块命名冲突的问题,将不同模块的namespaced: true,之后在不同页面中引入getter actions `mutations 时,需要加上所属的模块名

      const countAbout = {
        namespaced:true,//开启命名空间
        state:{x:1},
        mutations: { ... },
        actions: { ... },
        getters: {
          bigSum(state){
             return state.sum * 10
          }
        }
      }
      
      const personAbout = {
        namespaced:true,//开启命名空间
        state:{ ... },
        mutations: { ... },
        actions: { ... }
      }
      
      const store = new Vuex.Store({
        modules: {
          countAbout,
          personAbout
        }
      })
      
      
    3. 开启命名空间后,组件中读取state数据

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

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

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

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

    案例

    count.js

    //求和相关的配置
    export default {
    	namespaced:true,
    	actions:{
    		jiaOdd(context,value){
    			console.log('actions中的jiaOdd被调用了')
    			if(context.state.sum % 2){
    				context.commit('JIA',value)
    			}
    		},
    		jiaWait(context,value){
    			console.log('actions中的jiaWait被调用了')
    			setTimeout(()=>{
    				context.commit('JIA',value)
    			},500)
    		}
    	},
    	mutations:{
    		JIA(state,value){
    			console.log('mutations中的JIA被调用了')
    			state.sum += value
    		},
    		JIAN(state,value){
    			console.log('mutations中的JIAN被调用了')
    			state.sum -= value
    		},
    	},
    	state:{
    		sum:0, //当前的和
    		school:'尚硅谷',
    		subject:'前端',
    	},
    	getters:{
    		bigSum(state){
    			return state.sum*10
    		}
    	},
    }
    
    

    person.js

    //人员管理相关的配置
    import axios from 'axios'
    import { nanoid } from 'nanoid'
    export default {
    	namespaced:true,
    	actions:{
    		addPersonWang(context,value){
    			if(value.name.indexOf('王') === 0){
    				context.commit('ADD_PERSON',value)
    			}else{
    				alert('添加的人必须姓王!')
    			}
    		},
    		addPersonServer(context){
    			axios.get('https://api.uixsj.cn/hitokoto/get?type=social').then(
    				response => {
    					context.commit('ADD_PERSON',{id:nanoid(),name:response.data})
    				},
    				error => {
    					alert(error.message)
    				}
    			)
    		}
    	},
    	mutations:{
    		ADD_PERSON(state,value){
    			console.log('mutations中的ADD_PERSON被调用了')
    			state.personList.unshift(value)
    		}
    	},
    	state:{
    		personList:[
    			{id:'001',name:'张三'}
    		]
    	},
    	getters:{
    		firstPersonName(state){
    			return state.personList[0].name
    		}
    	},
    }
    

    index.js:

    //该文件用于创建Vuex中最为核心的store
    import Vue from 'vue'
    //引入Vuex
    import Vuex from 'vuex'
    import countOptions from './count'
    import personOptions from './person'
    //应用Vuex插件
    Vue.use(Vuex)
    
    //创建并暴露store
    export default new Vuex.Store({
    	modules:{
    		countAbout:countOptions,
    		personAbout:personOptions
    	}
    })
    

    count.vue

    <template>
    	<div>
    		<h1>当前求和为:{{sum}}</h1>
    		<h3>当前求和放大10倍为:{{bigSum}}</h3>
    		<h3>我在{{school}},学习{{subject}}</h3>
    		<h3 style="color:red">Person组件的总人数是:{{personList.length}}</h3>
    		<select v-model.number="n">
    			<option value="1">1</option>
    			<option value="2">2</option>
    			<option value="3">3</option>
    		</select>
    		<button @click="increment(n)">+</button>
    		<button @click="decrement(n)">-</button>
    		<button @click="incrementOdd(n)">当前求和为奇数再加</button>
    		<button @click="incrementWait(n)">等一等再加</button>
    	</div>
    </template>
    
    <script>
    	import {mapState,mapGetters,mapMutations,mapActions} from 'vuex'
    	export default {
    		name:'Count',
    		data() {
    			return {
    				n:1, //用户选择的数字
    			}
    		},
    		computed:{
    			//借助mapState生成计算属性,从state中读取数据。(数组写法)
    			...mapState('countAbout',['sum','school','subject']),
    			...mapState('personAbout',['personList']),
    			//借助mapGetters生成计算属性,从getters中读取数据。(数组写法)
    			...mapGetters('countAbout',['bigSum'])
    		},
    		methods: {
    			//借助mapMutations生成对应的方法,方法中会调用commit去联系mutations(对象写法)
    			...mapMutations('countAbout',{increment:'JIA',decrement:'JIAN'}),
    			//借助mapActions生成对应的方法,方法中会调用dispatch去联系actions(对象写法)
    			...mapActions('countAbout',{incrementOdd:'jiaOdd',incrementWait:'jiaWait'})
    		},
    		mounted() {
    			console.log(this.$store)
    		},
    	}
    </script>
    
    <style lang="css">
    	button{
    		margin-left: 5px;
    	}
    </style>
    
    
    

    person.vue:

    <template>
    	<div>
    		<h1>人员列表</h1>
    		<h3 style="color:red">Count组件求和为:{{sum}}</h3>
    		<h3>列表中第一个人的名字是:{{firstPersonName}}</h3>
    		<input type="text" placeholder="请输入名字" v-model="name">
    		<button @click="add">添加</button>
    		<button @click="addWang">添加一个姓王的人</button>
    		<button @click="addPersonServer">添加一个人,名字随机</button>
    		<ul>
    			<li v-for="p in personList" :key="p.id">{{p.name}}</li>
    		</ul>
    	</div>
    </template>
    
    <script>
    	import {nanoid} from 'nanoid'
    	export default {
    		name:'Person',
    		data() {
    			return {
    				name:''
    			}
    		},
    		computed:{
    			personList(){
    				return this.$store.state.personAbout.personList
    			},
    			sum(){
    				return this.$store.state.countAbout.sum
    			},
    			firstPersonName(){
    				return this.$store.getters['personAbout/firstPersonName']
    			}
    		},
    		methods: {
    			add(){
    				const personObj = {id:nanoid(),name:this.name}
    				this.$store.commit('personAbout/ADD_PERSON',personObj)
    				this.name = ''
    			},
    			addWang(){
    				const personObj = {id:nanoid(),name:this.name}
    				this.$store.dispatch('personAbout/addPersonWang',personObj)
    				this.name = ''
    			},
    			addPersonServer(){
    				this.$store.dispatch('personAbout/addPersonServer')
    			}
    		},
    	}
    </script>
    

    反正笔记也就整理到这里,比较遗憾的就是至今为止还没有使用vue做一个完整的项目。。。等以后有机会再写吧

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值