Vue2核心


Vue2 - 核心

1. 初始Vue

引入Vue.js:你可以下载Vue.js到本地,也可以使用Vue的CDN,不过我还是推荐下载到本地。

下载完毕之后,使用script标签引入即可。

在引入完Vue之后,这时候只是允许你使用Vue,但是页面并没有什么变化,所以,你要使用Vue就需要创建一个Vue实例,并且给出一些配置。

例如,我现在有这么一段代码:

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="../Vue_js/vue.js"></script>
</head>
<body>
<div id="root">
    <h1>hello world</h1>
</div>
<script type="text/javascript">
    Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。
</script>
</body>

我现在的要求是我的h1中的内容不能写死了,必须根据实际得到的数据来展示。

这个时候就需要用到Vue:

创建Vue实例,并且传入配置对象

new Vue({
   key:value 
});

学过axios的我们知道,在使用axios发送请求的时候,也是需要传入配置对象,例如发送的url是什么,请求的方式是什么;这里也是一样,需要传入一个配置对象:

new Vue({
   el:'#root' //el表示element,value为CSS选择器,这里是id选择器。
});

这样就将Vue和上面的div容器进行了关联,现在,你就可以继续设置配置对象,来达到需求:

<div id="root">
    <h1>{{keyword}} world</h1>
</div>
<script type="text/javascript">
    Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。
    //创建Vue实例
    new Vue({
        el:'#root',//el用于指定当前Vue实例为哪个容器服务,值通常为css选择器字符串
        data:{
            keyword:'hello' //data中用于存储数据,数据供e1所指定的容器去使用,值我们暂时先写成一个对象。
        }
    });
</script>

对于上述代码中{{}}表示插值语法,先记住,我通过data配置属性中keyword属性来渲染数据,最终显示到页面上还是hello world。

注意:上述代码中的Vue实例只是通过el配置属性绑定了id为root的div,对于其他的div是没有联系的。容器和实例之间的关系只能是一对一。

例如:

<div id="root">
    <h1>{{keyword}} world</h1>
</div>
<div id="child">
    <h1>{{keyword}} world</h1>
</div>

结果

可见第二个div并没有渲染出数据。

总结:

  1. 想让Vue工作,就必须创建一个Vue实例,且要传入一个配置对象
  2. root容器里的代码依然符合html规范,只不过混入了一些特殊的Vue语法
  3. root容器里的代码被称为【Vue模板】:也就是上面的{{}}
  4. Vue实例和容器是一一对应的,且只能是一一对应的
  5. 真实开发中只有一个Vue实例,并且会配合着组件一起使用
  6. 中的xxx要写js**表达式**,且xxx可以自动读取到data中的所有属性,一旦data中的数据发生改变,那么页面中用到该数据的地方也会自动更新

2. 模板语法

Vue中的模板语法有两大类:

  1. 插值语法:{{}}

    插值语法用于解析标签体内容,例如:<h1>{{keyword}} world</h1>,其中h1中的内容就是标签体。

    语法: {{xxx}} ,xxxx 会作为 js 表达式解,例如:<h1>{{Date.now()}}</h1>,其中{{}}中的内容就是一个js表达式。

    js表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方,例如a、a+b、demo(1)、x===y?'a':'b'

    js语句(代码):if(){}、for(){}

  2. 指令语法:v-xxx

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

    例如我现在有这么一段代码:

    <div id="root">
        <h1>{{keyword}} world</h1>
        <a href="www.baidu.com">点我去百度</a>
    </div>
    <script type="text/javascript">
        Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。
        new Vue({
            el:'#root',//el表示element,value为CSS选择器,这里是id选择器。
            data:{
                keyword:'hello'
            }
        });
    </script>
    

    其中的a标签中,href写了跳转的地址,现在我同时要求使用Vue来渲染这个跳转地址,不能写死了,可能出现下面几种情况:

    <div id="root">
        <h1>{{keyword}} world</h1>
        <a href="url">点我去百度</a>
    </div>
    <script type="text/javascript">
        Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。
        new Vue({
            el:'#root',//el表示element,value为CSS选择器,这里是id选择器。
            data:{
                keyword:'hello',
                url:'www.baidu.com'
            }
        });
    </script>
    

    结果

    或者:

    <div id="root">
        <h1>{{keyword}} world</h1>
        <a href="{{url}}">点我去百度</a>
    </div>
    <script type="text/javascript">
        Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。
        new Vue({
            el:'#root',//el表示element,value为CSS选择器,这里是id选择器。
            data:{
                keyword:'hello',
                url:'www.baidu.com'
            }
        });
    </script>
    

    结果

    可见都没有渲染成功,所以,对于标签相关的内容,我们一般使用指令语法来渲染:

    <div id="root">
        <h1>{{keyword}} world</h1>
        <a v-bind:href="url">点我去百度</a>
    </div>
    <script type="text/javascript">
        Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。
        new Vue({
            el:'#root',//el表示element,value为CSS选择器,这里是id选择器。
            data:{
                keyword:'hello',
                url:'www.baidu.com'
            }
        });
    </script>
    

    结果

    可见这下成功渲染出来了。

    注意:v-bind:href="xxx" 或简写为 :href="xxx"xxx同样要写js表达式,且可以直接读取到data中的所有属性。并且在实际开发中,一般简写使用的较多。

    Vue中有很多的指令,且形式都是:v-xxx,此处我们只是拿v-bind举个例子。

3. 数据绑定

数据绑定分为两种:

  1. 单项数据绑定:v-bind

    数据只能从data流向页面,例如如下代码:

    <div id="root">
        单项数据绑定: <input type="text"  :value="desc">
    </div>
    <script type="text/javascript">
        Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。
        new Vue({
            el:'#root',//el表示element,value为CSS选择器,这里是id选择器。
            data:{
                desc:'我是单项数据绑定'
            }
        });
    </script>
    

    结果

    改变input的值,data没变

  2. 双向数据绑定:v-model

    数据不仅能从 data 流向页面,还能从页面流向data,例如如下代码:

    双项数据绑定: <input type="text"  v-model:value="desc">
    

    v-bind换为v-model即可。

    我不管改变哪边,另外一边都会变。

    注意:双向绑定一般都应用在表单类元素上(如:input、select等) ;v-model:value可以简写为v-model,因为v-model默认收集的就是value值。

4. data和el的两种写法

el的两种写法:

写法一:

new Vue({
    el:'#root'
});

写法二:

const x = new Vue();
x.$mount('#root');

这里有些人可能会疑惑,我将x对象输出给大家看看:

Vue实例

看了上图,并没有看见$mount,这是为什么呢?因为$mount是存放在了原型对象上:

在原型对象上,由于x是Vue的实例,所以x够看见Vue身上的原型对象。

这里的mount就相当于挂载的意思,可以理解为将Vue实例挂载到了容器身上,使二者产生了关系,所以就能通过Vue来渲染数据了。

在实际工作做,el的写法一和写法二都可以,任选一个几个。

data的两种写法:

写法一:对象式

new Vue({
    el:'#root',
    data:{
        desc:'数据'
    }
});

写法一是data后面直接跟上一个对象。

写法二:函数式

new Vue({
    el:'#root',
    data:function (){
        return {
            //数据对象
        }
    }
});

写法二是跟一个函数,不过这个函数必须要有返回值,返回值一般就是一个数据对象

简写:

new Vue({
    el:'#root',
    data(){
        return {
            //数据对象
        }
    }
});

一般我们使用简写形式居多。

注意: 如果考虑到this的问题,那你就要考虑清楚,你的this指向想要指向谁;如果想要指向Vue,那么这里就只能写为普通函数,不能写为箭头函数,如果想要指向window,那么这里就可以使用箭头函数。

在实际开发中,如果设计到了组件,那么你就必须使用函数式,否则就会报错

还有一个重要原则:由Vue管理的函数,一定不要写箭头函数,一旦写了箭头函数,this就不再是Vue实例了。这个其实就是我上面提到的this指向的问题。

5. MVVM模型

MVVM实际上是3部分:

  • M:模型(Model) :对应 data 中的数据
  • V:视图(View) :模板
  • VM:视图模型(ViewModel) : Vue 实例对象

三者关系

在Vue(VM)中,存在一个DOM监听器和Data绑定,当视图(View/DOM)改变的时候,会被Vue给监听到,从而将改变的数据放入model中,当model中的数据改变的时候,也会将数据给Binding到View中,所以Vue相当于一个桥梁,后面我们常将Vue的实例对象命名为vm,表示视图模型。

Vue实例

对于上图中,data中所有的属性都会出现在Vue上,例如desc,并且上诉的所有属性在Vue模板中可以直接使用,不用再使用对象点属性的方式来调用。

6. Object.defineProperty方法

我现在有这样一段代码:

let person = {
name:'念心卓'
}

我现在要给person对象添加一个age属性,可能以前是person.age=value的形式,不过现在我们有更加高级的方式:Object.defineProperty

Object.defineProperty方法是更高级的给对象添加属性方式。比原来的person.age这种方法高级的多,可以配置很多详细信息:

let person = {
    name:'念心卓'
}
//参数1:要改变的对象;参数2:要添加的属性;参数3:配置对象
Object.defineProperties(person,age,{
    value: 18
    //还有一些其他属性...
})

注意:这种方式添加的属性默认是不可枚举的,也就是不能遍历到这个属性。

常用属性:

  • enumerable,布尔类型,默认值是false。如果想让该字段可枚举,就要显式设置为true。

  • configurable,布尔类型,默认值是false。控制字段是否可以被删除

  • writable,布尔类型,默认值是false。控制字段是否可以被修改

  • value,就是赋值,赋字段一个值。

还有两个重要的常用属性:get和set

来看下面这种场景:

let number = 18
let person = {
    name: "大吉",
    sex: "男",
    age: number
}
console.log(person.age)//输出18

number = 22//将number这个变量修改为22
console.log(person.age)//输出仍然是18,而不是22

如果我们想让第二个console.log输出的是22呢?应该怎么操作?

这里就引入了我们强大的get和set功能了!!

let number = 18
let person = {
    name: "大吉",
    sex: "男",
    age: number
}
console.log(person.age)     //输出18

Object.defineProperty(person, "age", {
    get() {
        console.log('有人读取了age属性')
        return number;  //return值很重要
    }
})
number = 22     //将number这个变量修改为22
//输出的是22。只要有人读取age属性,就会调用get方法,get方法return的值就是number这个变量
console.log(person.age)//22

所以我们通过get,实现了只要有人读取age属性,就可以执行get这个方法,至于get这个方法你里面想塞什么都可以。反正只要有人读取age这个属性,就执行get。

我们再来看看set:

let number = 18
let person = {
    name: "大吉",
    sex: "男",
    age: number
}

console.log("number的值是:" + number)  //18
Object.defineProperty(person, "age", {
    //只要有人修改age属性
    //就会调用set方法,且会收到修改age属性的具体值
    set(value) {
        console.log('有人修改了age属性,age:' + value)
         //神奇的操作来了,我们让number这个变量修改
        number = value; 
    }
})
person.age = 22     //修改age属性,从而调用set方法
//非常神奇,我们明明修改了age,却能对number做修改
console.log("number的值是:" + number)  //22

总结:通过get和set,使得操作对象更加灵活了

为什么要学这个Object.defineProperty方法呢?是为了下一节数据代理做准备!

7. 数据代理(数据劫持)

概念:数据代理就是通过一个对象对另一个对象中属性的操作(读/写)

有如下代码:

let obj1 = {x:100};
let obj2 = {y:100};
Object.defineProperty(obj2,'x',{
    get(){
        return obj1.x;
    },
    set(value){
        obj1.x = value;
    }
})

操作情况

可见上图中如果有人动了obj2,相当于通过obj2动了obj1。 这就是最简单的数据代理实例

Vue如何应用数据代理呢?

数据代理例子

无法理解可以查看视频:尚硅谷Vue2.0+Vue3.0全套教程丨vuejs从入门到精通第13个视频。

通过数据代理,我们本应该操作vm._data.属性,现在我们直接操作vm.属性即可。

总结:

  1. Vue中的数据代理:通过vm对象来代理data对象中属性的操作(读/写)
  2. Vue中数据代理的好处:更加方便的操作data中的数据
  3. 基本原理:通过Object.defineProperty()把data对象中所有属性添加到vm上。为每一个添加到vm上的属性,都指定一个getter/setter。在getter/setter内部去操作(读/写)data中对应的属性

8. 事件处理

事件的基本使用:

  1. 使用v-on:x@xxx绑定事件,其中xxx是事件名;
  2. 事件的回调需要配置在methods对象中,最终会在vm上
  3. methods中配置的函数,不要用箭头函数!否则this就不是vm了
  4. methods中配置的函数,都是被Vue所管理的函数,this的指向是vm或组件实例对象;
  5. @click="demo"@c1ick="demo($event)"效果一致,但后者可以传参

例如我有下面一部分代码:

<div id="root">
    <button>点我输出信息</button>
</div>

我现在想要点击这个按钮就输出信息:

<div id="root">
    <button v-on:click="showInfo">点我输出信息</button>
    <button @click="showInfo">点我输出信息(简写形式)</button>
</div>
<script type="text/javascript">
    new Vue({
        el:'#root',
        methods:{
            showInfo(ev){
                console.log(ev); //输出事件对象
                alert('点击成功');
            }
        }
    })
</script>

自行测试即可。

可见,绑定事件使用v-on:事件类型,例如上面我的是点击事件,所以写的是v-on:click,后面跟上回调的函数名称,并且函数名称写在methods配置对象属性中;v-on:click = @click,后者在实际开发中用的较多。并且如果没有传参,就无需写小括号,里面会自动带有一个参数,参数为事件对象。

如果想要传参,则使用@click="showInfo(参数)",这样你写方法的时候也可以接收到参数:

<div id="root">
    <button @click="showInfo2(66)">我是事件传参</button>
</div>
<script type="text/javascript">
    new Vue({
        el:'#root',
        methods:{
            showInfo2(number){
                console.log(number) //输出66
            }
        }
    })
</script>

但是这样虽然可以传递参数,但是会将事件对象给弄丢,所以我们一般会使用$event来占位,这样事件对象也不会丢:

<div id="root">
    <button @click="showInfo2($event,66)">给事件对象占一个位</button>
</div>
<script type="text/javascript">
    new Vue({
        el:'#root',
        methods:{
            showInfo2(e,number){
                console.log(e,number) //输出事件对象和66
            }
        }
    })
</script>

注意:并不是都需要事件对象,如果不需要,你也可以不占位,一切按照实际开发为准。

9. 事件修饰符

Vue中的事件修饰符:

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

    <div id="root">
        <a href="http://www.baidu.com" @click.prevent="showInfo">点我去百度</a>
    </div>
    <script type="text/javascript">
        new Vue({
            el:'#root',
            methods:{
                showInfo(){
                    console.log('阻止事件的默认行为');
                },
            }
        })
    </script>
    

    当我点击链接的时候,事件触发了,但是并没有发送跳转,阻止了a标签的默认行为。

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

    <div id="root">
        <div @click="showInfo">
            <button @click="showInfo">点我冒泡</button>
        </div>
    </div>
    <script type="text/javascript">
        new Vue({
            el:'#root',
            methods:{
                showInfo(){
                    console.log(123);
                },
            }
        })
    </script>
    

    这里会输出两次,这就是事件冒泡导致的,当我点击button的时候,由于外层的div绑定的事件和button一致,所以就会发生事件冒泡,所以输出两次

    不了解事件冒泡的可以去看JavaScript中相关内容,里面有详细介绍。

    现在要阻止冒泡:

    <div id="root">
        <div @click="showInfo">
            <button @click.stop="showInfo">点我冒泡</button>
        </div>
    </div>
    

    给事件后面加上.stop即可。

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

    <div id="root">
        <button @click="showInfo">点我触发</button>
    </div>
    

    正常情况是你点击一次这个案例,这个事件的回调就会执行一次,现在我要求它只有第一次点击的时候执行回调,后面无论点击多少次都不会执行:

    <div id="root">
        <button @click.once="showInfo">点我触发</button>
    </div>
    
  4. capture:使用事件的捕获模式

    同理,不明表什么是捕获的去看JavaScript。

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

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

10. 键盘事件

键盘事件一般用两个:keydownkeyup;常用keydown

对于键盘事件,也有按键修饰符,以下是Vue为我们提供的一些案件别名

  • .enter:回车Enter
  • .tab:换行Tab
  • .delete :(捕获“Delete”和“Backspace”两个按键)
  • .esc:退出Esc
  • .space:空格Space
  • .up:箭头上
  • .down:箭头下
  • .left:箭头左
  • .right:箭头右

例如:

<div id="root">
    <input type="text" @keydown.enter="showInfo">
</div>
<script type="text/javascript">
    new Vue({
        el:'#root',
        methods:{
            showInfo(){
                console.log(123);
            },
        }
    })
</script>

上诉代码中,当我按下enter的时候,就会触发事件。

其他按键自行实现。

注意:

  1. Vue未提供别名的按键,可以使用按键原始的key值去绑定,但注意要转为kebab-case(短横线命名) ,例如CapsLock键,对应的事件修饰符:

    caps-lock

  2. 系统修饰键(用法特殊):ctrlaltshiftmeta

    • 配合keyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发。
    • 配合keydown使用:正常触发事件。
  3. 也可以使用keyCode去指定具体的按键(不推荐

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

    例如:

    <div id="root">
        <input type="text" @keydown.huiche="showInfo">
    </div>
    <script type="text/javascript">
        Vue.config.keyCodes.huiche = 13 //回车Enter对应的键码为13
    </script>
    

    依然触发成功。

  5. 对于按键修饰符还可以连着写,例如:@keydown.ctrl.y;表示同时按下ctrl和y的时候才能触发,同理,之前的事件修饰符也可以连着写:click.stop.prevent;表示先阻止冒泡,在阻止默认行为。

11. 计算属性

模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。比如说,我们有这样一个包含嵌套数组的对象:

new Vue({
    el:'#root',
    data: {
        books: [
            'Vue 2 - Advanced Guide',
            'Vue 3 - Basic Guide',
            'Vue 4 - The Mystery'
        ]
    },
})

然后我想将books拿到,并且要获取第一本书,然后所有字母大写,之后再获取书名:

<div id="root">
    <span>获取到第一本书:{{books[0].toUpperCase().split('-')[0]}}</span>
</div>

可见模板中的表达式十分的臃肿,且难以维护。

计算属性要解决的问题是:插值语法{{}} 中,越来越长的JS表达式,不利于阅读和组件化的问题。我们希望的是在插值语法中就写一个简单的词,不要是一些复杂的表达式。

可能你会想,那么我们是否可以用函数来做呢?其实当然可以:

<div id="root">
    <!--注意:这里调用函数与之前的事件回调函数的调用方法有区别:
    事件回调:调用函数不用写小括号(无参情况)
    模板语法:调用函数必须写小括号,否则就是输出函数体了-->
    <span>获取到第一本书:{{findFirstBookName()}}</span><br/>
    <span>获取到第一本书:{{findFirstBookName()}}</span><br/>
    <span>获取到第一本书:{{findFirstBookName()}}</span><br/>
    <span>获取到第一本书:{{findFirstBookName()}}</span><br/>
    <span>获取到第一本书:{{findFirstBookName()}}</span>
</div>
<script type="text/javascript">
    Vue.config.keyCodes.huiche = 13 //回车Enter对应的键码为13
    new Vue({
        el:'#root',
        data: {
            books: 'Vue 2 - Advanced Guide'
        },
        methods:{
            findFirstBookName(){
                console.log('我被调用了');
                return  this.books[0].toUpperCase().split('-')[0];
            }
        }
    })
</script>

执行结果

可见使用函数来做也是可以的,不过这个函数只要有一个地方使用,就会被调用一次。

使用计算属性:

<div id="root">
    <!--注意:这里调用函数与之前的事件回调函数的调用方法有区别:
    事件回调:调用函数不用写小括号(无参情况)
    模板语法:调用函数必须写小括号,否则就是输出函数体了,但是对于计算属性来说,不用写小括号,计算属性中,它本来就是一个属性-->
    <span>获取到第一本书:{{book}}</span><br/>
    <span>获取到第一本书:{{book}}</span><br/>
    <span>获取到第一本书:{{book}}</span><br/>
    <span>获取到第一本书:{{book}}</span><br/>
    <span>获取到第一本书:{{book}}</span>
</div>
<script type="text/javascript">
    const vm =  new Vue({
        el:'#root',
        data: {
            books: 'Vue 2 - Advanced Guide '
        },
        // methods:{
        //     findFirstBookName(){
        //         console.log('我被调用了');
        //         return  this.books[0].toUpperCase().split('-')[0];
        //     }
        // },
        computed:{//computed配置对象
            book:{ //具体的某个配置对象
                //获取属性时自动被Vue调用
                get(){ //相当于Object.defineProperty中的get
                    console.log(this) //这个this仍然是vue实例对象,因为最终是通过vue来调用这个get
                    console.log('我被调用了');
                    return this.books.split('-')[0];
                },
                //修改属性时自动被Vue调用
                set(value){相当于Object.defineProperty中的set
                    this.books = value;
                    console.log(`当前的books对象的值:${this.books}`); //注意,我这里其实是调用的get方法
                }
            }
        }
    })

执行结果

二者对比:计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 books 还没有发生改变,多次访问 book 计算属性会立即返回之前的计算结果,而不必再次执行函数。而使用函数则不一样,它没有缓存,无论你的值是否改变,只要有多处使用到了,那么他就会渲染多次。

补充:

  1. get有什么作用:当有人读取book时,get就会被调用,且返回值就作为book的值。
  2. get什么时候调用:
    • 初次读取book时。
    • 所依赖的数据发生变化时(此处依赖的数据为books)。
  3. set什么时候调用:当book被改变时。

计算属性还有简写形式

computed:{
    book(){
        console.log(this)
        console.log('我被调用了');
        return this.books.split('-')[0];
    }
}

注意:只有当只读不改的情况下才能使用简写形式,并且此处的book()就相当于之前的get(),由于不能修改,所以就没有set();

12. 监视属性

案例:点击按钮,切换天气显示

<body>
<div id="root">
    <h2>今天天气很{{weather}}</h2>
    <button @click="changeWeather">点我切换天气</button>
</div>
<script type="text/javascript">
    Vue.config.keyCodes.huiche = 13 //回车Enter对应的键码为13
    const vm = new Vue({
        el: '#root',
        data: {
            isHot: true
        },
        methods: {
            changeWeather() {
                this.isHot = !this.isHot;
            }
        },
        computed: {
            weather() {
                return this.isHot ? '炎热' : '凉爽';
            }
        }
    })
</script>
</body>

自行测试即可。

可以发现,我上面的代码是用以前学过的知识来完成,现在我我们要学习一个新的属性:监视属性。

要求:我改变天气的时候,它能够检测到我的改变,并且能够把我改变之前的值输出。

新增Vue配置属性:

watch:{
    isHot:{ //配置对象:需要检测的属性,这里我选择检测isHot,如果检测weather,那么你改为weather即可
        handler(newValue,oldValue){ //这里要写一个handler函数
            console.log('weather被修改了',newValue,oldValue);
        }
    }
}

自行测试即可。

注意:计算属性也可以被监视。

当然,还有另外一种写法,当你再配置Vue对象的时候,你还不知道需要检测哪一个值,这时候,你可以在Vue配置完毕之后,用实例对象来配置也是可以的:

const vm = new Vue({
    el: '#root',
    data: {
        isHot: true
    },
    methods: {
        changeWeather() {
            this.isHot = !this.isHot;
        }
    },
    computed: {
        weather() {
            return this.isHot ? '炎热' : '凉爽';
        }
    },
    /*watch:{
        isHot:{
            handler(newValue,oldValue){
                console.log('weather被修改了',newValue,oldValue);
            }
        }
    }*/
});
vm.$watch('isHot',{
    handler(newValue,oldValue){
        console.log('isHot被修改了',newValue,oldValue);
    }
})

这样,使用$watch追加也是可以的。

适用场景:当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

注意:监视属性中的配置对象中,除了handler配置,还有其他很多配置。

总结:

  1. 当被监视的属性变化时,回调函数自动调用,进行相关操作
  2. 监视的属性必须存在,才能进行监视!
  3. 监视的两种写法:
    • new Vuel时传入watch配置
    • 通过vm.$watch监视

明白了监视属性是怎么一回事,现在我们看看监视属性中的一个属性:deep;它表示深度监视。

我现在的代码如下:

<div id="root">
    <h1>a的值为{{number.a}}</h1>
    <button @click="number.a++">点我a加1</button>
</div>
<script type="text/javascript">
    Vue.config.keyCodes.huiche = 13 //回车Enter对应的键码为13
    const vm = new Vue({
        el: '#root',
        data: {
            number: {
                a: 1,
                b: 1
            }
        } 
    });
</script>
</body>

现在我点击按钮,要求a+1之后,能够监视到a的属性的变化:

new Vue({
    el: '#root',
    data: {
        number: {
            a: 1,
            b: 1
        }
    },
    watch:{
        'number.a':{
            handler(){
                console.log('a的值被改变了');
            }
        }
    }
});

测试发现成功,注意,我监视属性中配置对象写的是'number.a',这是因为直接写number.a回报错,事实上,所有的key-value形式中,key真正的写法都是用单引号包起来的,只不过如果不涉及到多层级结构的,我们一般是直接写key,没有用单引号包起来。

在上面的代码的基础上,我又想监视number中,b值改变,你可能会想到,仿照'number.a'的写法来监视b,这么做是不合适,因为如果我number对象中,还有其他的很多的key-value要被监视,你这样一直写下去就很繁琐了,所以我们这时候用到watch中的deep属性,来实现深度监视

watch:{
    number:{
        deep:true, //表示开启深度监视,默认是false
        handler(){
            console.log('a的值被改变了');
        }
    }
}

这时候,只要number中任意的key改变了,都能够被监视到。


监视属性也有简写形式,例如下面的代码:

<div id="root">
    <h1>选择的宠物为{{pet}}</h1>
</div>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
            pet: 'cat'
        },
        watch: {
            pet(newValue,oldValue) { //简写形式
                console.log(`pet的值被改变了原来为${oldValue},现在为${newValue}`);
            }
        }
    });
</script>

注意:在简写形式中,不可以再写其他的配置属性了(deep、immediate),因为这里根本就没有了配置对象,而是直接的一个函数(pet)。

使用vm.$watch的形式:

vm.$watch('pet',function (newValue,oldValue) {
    console.log(`pet的值被改变了原来为${oldValue},现在为${newValue}`);
})

13. 监视属性和计算属性的区别

computed和watch之间的区别:

  1. computed能完成的功能,watch都可以完成。
  2. watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作

**两个重要的小原则: **

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

其最终目的就是为了让this的指向为vm(Vue实例)。

例如之前的监视属性中的代码:

watch: {
    pet(newValue,oldValue) {
        console.log(`pet的值被改变了原来为${oldValue},现在为${newValue}`);
        setTimeout(()=>{ //过1秒之后再执行,相当于异步任务
            console.log(this); //这里的this就是vue实例vm
        },1000)
    }
}

14. 样式绑定

Vue中样式绑定一共有两种方式:

  1. class样式绑定
  2. style样式绑定

14.1 class样式绑定

我现在有如下样式代码:

.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 dashed rgb(2, 197, 2);
    background-color: gray;
}
.normal{
    background-color: skyblue;
}

.atguigu1{
    background-color: yellowgreen;
}
.atguigu2{
    font-size: 30px;
    text-shadow:2px 2px 10px red;
}
.atguigu3{
    border-radius: 20px;
}

基本的html代码:

<div id="root">
    <div class="basic">{{name}}</div>
</div>

我现在的基本要求:点击div模块,要求将happy、sad、normal任意一个样式加在这个div模块上:

<div id="root">
    <!-- 绑定class样式--字符串写法,适用于:样式的类名不确定,需要动态指定 -->
    <div class="basic" :class="mood" @click="changeMood">{{name}}</div> <br/>
</div>
<script type="text/javascript">
     new Vue({
        el: '#root',
        data: {
            name: 'class样式选择',
            mood:'normal'
        },
        methods:{
            changeMood(){
                const arr = ['happy','sad','normal']
                const index = Math.floor(Math.random()*3)
                this.mood = arr[index]
                console.log(this.mood)
            }
        }
    });
</script>

结果

html源码

以上方法适用于:个数确认(1个),样式类名不确定。


现在我要求换了:要求将atguigu1、atguigu2、atguigu3任意样式加在这个div模块上,可以是多个样式,也可以是单个样式等:

<div id="root">
    <!-- 绑定class样式--数组写法,适用于:要绑定的样式个数不确定、名字也不确定 -->
    <div class="basic" :class="classArr">{{name}}</div> <br/><br/>
</div>
<script type="text/javascript">
     const vm = new Vue({
        el: '#root',
        data: {
            name: 'class样式选择',
            classArr:['atguigu1','atguigu2','atguigu3']
        }
    });
</script>

初始化时,可见是多个类名

可以操作样式的个数

以上方法适用于:个数不确定,样式类名也不确定。


现在我要求换了:要求有atguigu1、atguigu2两个样式在这个div模块上,但是我自己来决定使用1还是用2还是两个都用或都不用:

<div id="root">
    <!-- 绑定class样式--对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用 -->
    <div class="basic" :class="classObj">{{name}}</div> <br/><br/>
</div>
<script type="text/javascript">
     const vm = new Vue({
        el: '#root',
        data: {
            name: 'class样式选择',
            classObj:{
                atguigu1:true,
                atguigu2:false
            }
        }
    });
</script>

结果

以上方法适用于:个数确定,样式类名也确定,但是决定是否使用,或者使用那几个是由自己动态决定的。

14.2 style样式绑定

<div id="root">
    <!-- 绑定style样式--对象写法 -->
    <div class="basic" :style="styleObj">{{name}}</div> <br/><br/>
    <!-- 绑定style样式--数组写法 -->
    <div class="basic" :style="styleArr">{{name}}</div>
</div>
<script type="text/javascript">
     const vm = new Vue({
        el: '#root',
        data: {
            name: 'class样式选择',
            styleObj:{
                fontSize: '40px', //注意:这里的key可不能乱写,因为css中由font-size,所以这里为fontSize
                color:'red',
            },
            styleArr:[
                {
                    fontSize: '40px',
                    color:'blue',
                },
                {
                    backgroundColor:'gray' //因为css中由background-color,所以这里为backgroundColor
                }
            ]
        }
    });
</script>

style样式绑定用的比较少。

15. 条件渲染

15.1 v-show

<div id="root">
    <!-- 使用v-show做条件渲染 -->
    <h2 v-show="false">欢迎{{name}}</h2>
    <h2 v-show="1 === 1">欢迎{{name}}</h2>
</div>

源码

可见v-show底层是通过控制display属性来实现显示与隐藏的

15.2 v-if

<div id="root">
    <!-- 使用v-if做条件渲染 -->
    <h2 v-if="false">欢迎{{name}}</h2>
    <h2 v-if="1 === 1">欢迎{{name}}</h2>
</div>

源码

可见v-if底层是直接将DOM都给干掉了。

既然说了v-if,那么就会有v-else-if,这和if、else是一致的:

<div id="root">
    <button @click="n++">点我n+1</button>
    <!-- v-else和v-else-if -->
    <div v-if="n === 1">Angular</div>
    <div v-else-if="n === 2">React</div>
    <div v-else-if="n === 3">Vue</div>
    <div v-else>哈哈</div>
    <div>此处没有v-if</div>
</div>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
            name: '念心卓',
            n:0
        }
    });
</script>

结果自行测试即可。

这里简单说一下,当data配置对象中的数据发生变化时,Vue会重新渲染整个页面(DOM),所以,最开始页面显示的是:哈哈,此处没有v-if,当n=1的时候,页面上就会显示Angular,当Vue发现已经有一个条件满足的时候,后面的条件就都不会再看了,就跳过了后面的渲染,直接来到了最后一个div。

注意:当你使用v-if、v-else-if的时候,中间不能够被打断,例如:

<div v-if="n === 1">Angular</div>
<div v-else-if="n === 2">React</div>
<div>此处没有v-if</div>
<div v-else-if="n === 3">Vue</div>
<div v-else>哈哈</div>

这样是会报错的。

有时还会遇到这么一种情况:

<h2>你好</h2>
<h2>念心卓</h2>
<h2>哈哈</h2>

我现在的代码如上,我要求当n等于1的时候,上面3个h2同时显示,可能你会这样做:

<h2 v-if="n === 1">你好</h2>
<h2 v-if="n === 1">念心卓</h2>
<h2 v-if="n === 1">哈哈</h2>

但是这样做略显麻烦,那么你可能又会这样做:

<div v-if="n===1">
    <h2>你好</h2>
    <h2>念心卓</h2>
    <h2>哈哈</h2>
</div>

给外面包上一层div,但是这样也会有隐藏的问题,比如当你所有样式都配置好了之后(使用JavaScript配置的),如果你再给外面加上一层div,会导致样式出错,找不到,所以这样这种方法也不是很好。

为了解决这样一种问题,你可以使用新的标签:template

<template v-if="n===1">
    <h2>你好</h2>
    <h2>念心卓</h2>
    <h2>哈哈</h2>
</template>

源码

可见使用template标签之后,源码上并没有template,这样既方便,也不会影响既有的样式。


总结:

  1. v-if

    适用于:切换频率较低的场景。
    特点:不展示的DOM元素直接被移除
    注意:v-if可以和:v-else-if、v-else一起使用,但要求结构不能被“打断”。

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

备注:使用v-if的时,元素可能无法获取到(因为DOM有可能都被干掉),而使用v-show一定可以获取到。

16. 列表渲染

16.1 基本列表

在列表渲染中,我们一般使用v-for指令来进行渲染

我们可以用 v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名

数组渲染代码如下:

<div id="root">
    <ul>
        <li v-for="person in persons">
            {{person.name}}--{{person.age}}
        </li>
    </ul>
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            persons:[
                {id:'001',name:'张三',age:18},
                {id:'002',name:'李四',age:19},
                {id:'003',name:'王五',age:20},
            ]
        }
    });
</script>

v-for 块中,我们可以访问所有父作用域的 property。v-for 还支持一个可选的第二个参数,即当前项的索引

例如:

<div id="root">
    <ul>
        <li v-for="(person,index) in persons">
            {{person.name}}--{{person.age}}--{{index}}
        </li>
    </ul>
</div>

当然,在遍历的时候,你也可以用 of 替代 in 作为分隔符,因为它更接近 JavaScript迭代器的语法:

<div id="root">
    <ul>
        <li v-for="(person,index) of persons">
            {{person.name}}--{{person.age}}--{{index}}
        </li>
    </ul>
</div>

效果也是一样的。

当然除了能够遍历数组,v-for也能够遍历对象

<div id="root">
    <!-- 遍历对象 -->
    <h2>汽车信息(遍历对象)</h2>
    <ul>
        <li v-for="(value,key) of car">
            {{key}}-{{value}}
        </li>
    </ul>
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            car: {
                name: '奥迪A8',
                price: '70万',
                color: '黑色'
            }
        }
    });
</script>

结果

注意:

遍历对象的时候,第一个参数是value,第二个参数才是key,还可以用第三个参数作为索引:

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

除了遍历对象之外,还能够遍历字符串

<div id="root">
    <!-- 遍历字符串 -->
    <h2>测试遍历字符串(用得少)</h2>
    <ul>
        <li v-for="(char,index) of str">
            {{char}}-{{index}}
        </li>
    </ul>
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            str: 'Vue'
        }
    });
</script>

结果

注意:

  1. 上述v-for指令中,我都没有写:key,但是一般我们再写v-for的时候,:key是少不了的,:key指令是十分重要的,在下一小节给出。
  2. v-for可以遍历数组、对象,这两个用的多一点,遍历字符串用的较少

16.2 Key的基本原理

对于上一小节的代码中,我使用v-for的时候并没有使用:key,但是在实际开发中,最好还是写上,否则可能会出现问题,下面我来演示一下如何出现问题。

我现在有这么一段代码:

<div id="root">
    <ul>
        <li v-for="person in persons">
            {{person.name}}--{{person.age}} <input type="text">
        </li>
    </ul>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            persons: [
                {id: '001', name: '张三', age: 18},
                {id: '002', name: '李四', age: 19},
                {id: '003', name: '王五', age: 20}
            ]
        }
    });
</script>

现在我的需求是,也要在张三前面添加一个人:

<div id="root">
    <ul>
        <li v-for="person in persons">
            {{person.name}}--{{person.age}} <input type="text">
        </li>
    </ul>
    <button @click="addPerson">点我添加成员</button>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            persons: [
                {id: '001', name: '张三', age: 18},
                {id: '002', name: '李四', age: 19},
                {id: '003', name: '王五', age: 20}
            ]
        },
        methods:{
            addPerson(){
                const person = {id: '004', name: '赵六', age: 22}
                this.persons.unshift(person);
            }
        }
    });
</script>

当输入框中没有东西的时候,点击添加毫无问题,现在,我在输入框中加入东西:

当我点击添加的时候,注意观察:

可见发生了错位,那么这是为什么呢?难道是我没加:key吗,加上看看:

<li v-for="person in persons" :key="index">

测试发现,还是一样的问题,那我换个试试:

<li v-for="person in persons" :key="person.id">

可见,当:key的值为索引或者不加的时候,都会出差,但是使用person中的唯一表示id就不会报错,这是什么原因呢?

详细解释可看:禹神Vue视频中P30节。(最好理解了)

总结:

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

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

  2. 对比规则:
    旧虚拟DOM中找到了与新虚拟DOM相同的key:

    • 若虚拟DOM中内容没变,直接使用之前的真实DOM!
    • 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM。

    旧虚拟DOM中未找到与新虚拟DOM相同的Key:

    • 创建新的真实DOM,随后渲染到到页面。
  3. 用index作为key可能会引发的问题:
    若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新==>界面效果没问题,但效率低
    如果结构中还包含输入类的DOM:会产生错误DOM更新=>界面有问题

  4. 开发中如何选择key?:
    最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值。
    如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。

16.3 列表过滤

我现在有如下代码:

<div id="root">
    <input type="text" placeholder='请输入筛选的关键字'>
    <ul>
        <li v-for="person in persons" :key="person.id">
            {{person.name}}--{{person.age}}--{{person.sex}}
        </li>
    </ul>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            persons: [
                {id: '001', name: '马冬梅', age: 19, sex: '女'},
                {id: '002', name: '周冬雨', age: 20, sex: '女'},
                {id: '003', name: '周杰伦', age: 21, sex: '男'},
                {id: '004', name: '温兆伦', age: 22, sex: '男'}
            ]
        }
    });
</script>

页面效果:

现在要求我输入什么,就筛选出符合条件的即可。

思路:监视输入的值,然后使用数组过滤

一说到监视,可能就会想到使用vue中的监视属性watch来做:

<div id="root">
    <input type="text" placeholder='请输入筛选的关键字' v-model="keyWords">
    <ul>
        <li v-for="person in filterPersons" :key="person.id">
            {{person.name}}--{{person.age}}--{{person.sex}}
        </li>
    </ul>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            keyWords: '',
            persons: [
                {id: '001', name: '马冬梅', age: 19, sex: '女'},
                {id: '002', name: '周冬雨', age: 20, sex: '女'},
                {id: '003', name: '周杰伦', age: 21, sex: '男'},
                {id: '004', name: '温兆伦', age: 22, sex: '男'}
            ],
            filterPersons: []
        },
        watch: {
            //这种简写形式初始的时候页面没有数据,可以自行测试
            /*keyWords(value) {
                this.filterPersons = this.persons.filter((person) => {
                    return person.name.indexOf(value) !== -1;
                })
            }*/
            keyWords: {
                immediate: true, //立即以表达式的当前值触发回调,也就是初始的时候value='',但是person.name.indexOf(value)却不等于-1
                handler(value) {
                    this.filterPersons = this.persons.filter((person) => {
                        return person.name.indexOf(value) !== -1;
                    })
                }
            }
        }
    });
</script>

测试成功。

但是,最为标准的写法还是计算属性来做:

<div id="root">
    <input type="text" placeholder='请输入筛选的关键字' v-model="keyWords">
    <ul>
        <li v-for="person in filterPersons" :key="person.id">
            {{person.name}}--{{person.age}}--{{person.sex}}
        </li>
    </ul>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            keyWords: '',
            persons: [
                {id: '001', name: '马冬梅', age: 19, sex: '女'},
                {id: '002', name: '周冬雨', age: 20, sex: '女'},
                {id: '003', name: '周杰伦', age: 21, sex: '男'},
                {id: '004', name: '温兆伦', age: 22, sex: '男'}
            ]
        },
        computed:{
            filterPersons(){
                return this.persons.filter((person)=>{
                     //注意这里的this是Vue实例,因为不被Vue管理的函数最好写为箭头函数才是Vue实例
                    return person.name.indexOf(this.keyWords) !== -1;
                })
            }
        }
    });
</script>

可见计算属性完成更加简洁。

16.4 列表排序

在之前列表过滤的基础上,我想要在加上排序,代码实现:

<div id="root">
    <input type="text" placeholder='请输入筛选的关键字' v-model="keyWords">
    <button @click="sortType = 1">升序</button>
    <button @click="sortType = 2">降序</button>
    <button @click="sortType = 0">原序</button>
    <ul>
        <li v-for="person in filterPersons" :key="person.id">
            {{person.name}}--{{person.age}}--{{person.sex}}
        </li>
    </ul>
</div>
<script>
    new Vue({
        el: '#root',
        data: {
            keyWords: '',
            persons: [
                {id: '001', name: '马冬梅', age: 19, sex: '女'},
                {id: '002', name: '周冬雨', age: 20, sex: '女'},
                {id: '003', name: '周杰伦', age: 21, sex: '男'},
                {id: '004', name: '温兆伦', age: 22, sex: '男'}
            ],
            sortType: 0 //0表示原序,1表示升序,2表示降序
        },
        computed: {
            filterPersons() {
                const arr = this.persons.filter((person) => {
                    //注意这里的this是Vue实例,因为不被Vue管理的函数最好写为箭头函数才是Vue实例
                    return person.name.indexOf(this.keyWords) !== -1;
                });
                if (this.sortType) {
                    arr.sort((p1, p2) => {
                        return this.sortType === 1 ? p1.age - p2.age : p2.age - p1.age;
                    })
                }
                return arr;
            }
        }
    });
</script>

17. 监测改变

这里说的监测数据改变和我们之前学的watch是不一样的,watch他是提供我们程序员使用的表层的一个监测,我这里所说的监测是指的Vue底层的数据监测,就是当你改变数据的时候,Vue能够监测到。

17.1 更新时问题

例如如下代码就不能够监测到:

<div id="root">
    <button @click="updateMeiInfo">点我更新马冬梅信息</button>
    <ul>
        <li v-for="person in persons" :key="person.id">
            {{person.name}}--{{person.age}}--{{person.sex}}
        </li>
    </ul>
</div>
<script>
    const vm = new Vue({
        el: '#root',
        data: {
            persons: [
                {id: '001', name: '马冬梅', age: 19, sex: '女'},
                {id: '002', name: '周冬雨', age: 20, sex: '女'},
                {id: '003', name: '周杰伦', age: 21, sex: '男'},
                {id: '004', name: '温兆伦', age: 22, sex: '男'}
            ],
        },
        methods:{
            updateMeiInfo(){
                // this.persons[0].name = '马老师';// 更新奏效
                // this.persons[0].age = 50;// 更新奏效
                // this.persons[0].sex = '男';// 更新奏效

                this.persons[0] = {id: '001', name: '马老师', age: 50, sex: '男'};
            }
        }
    });
</script>

对于上面的代码,可见,单个修改每个对象的属性能够成功,但是修改整个对象数组中的某一项整体修改的时候,却无法被Vue监测到,这是怎么回事呢?

在解释这个现象之前,我们先来聊聊Vue是如何监视一个对象的改变的?

17.2 Vue监视对象

在前面的时候,我们学习了数据代理,粗略的知道了vm中的data其实和vm中的_data是一回事,执行vm._data === vm._data输出true

但是现在,我要说的是,这并不是一回事,用下图讲解:

数据代理例子

在图中,黄色的线属于数据代理,在代理的时候,Vue会按照下面的步骤来完成:

  1. 加工数据data
  2. 加工完data之后,才执行vm._data = vm.data
  3. 最后使用Object.defineProperty来添加属性,提供getter/setter

现在我们来模拟一下对象的数据监测

<script type="text/javascript">
    //定义一份数据,用来模拟vm中的data
    let data = {
        name: '念心卓',
        age: 18,
    }

    //声明一个观察者构造函数
    function Observer(obj) {
        //汇总对象中所有的属性形成一个数组
        const keys = Object.keys(obj)
        //遍历
        keys.forEach((k) => {
            //使用这个来提供getter和setter
            Object.defineProperty(this, k, {
                get() {
                    console.log(`${obj[k]}被访问了`)
                    return obj[k]
                },
                set(val) {
                    console.log(`${k}被改了,我要去解析模板,生成虚拟DOM.....我要开始忙了`)
                    obj[k] = val
                }
            })
        })
    }
    //创建一个监视的实例对象,用于监视data中属性的变化
    const obs = new Observer(data)
    console.log(obs)
    //准备一个vm实例对象
    let vm = {}
    vm._data = data = obs //这里就是Vue中vm._data === vm.data的原因了
</script>

可见这里面提供了get和set,当我访问数据的时候,get就会别调用,当我修改数据的时候,set就会被调用。

可见当我访问和修改都能够被vm给监测到了。

17.3 Vue.set()

知道了Vue中对象底层的监视原理,那么我们现在有新的需求了,我现在有如下代码:

<div id="root">
    <h1>学校信息</h1>
    <h2>学校名称:{{school.name}}</h2>
    <h2>学校地址:{{school.address}}</h2>
    <hr/>
    <h1>学生信息</h1>
    <h2>姓名:{{student.name}}</h2>
    <h2>年龄:真实{{student.age.rAge}},对外{{student.age.sAge}}</h2>
    <h2>朋友们</h2>
    <ul>
        <li v-for="(f,index) in student.friends" :key="index">
            {{f.name}}--{{f.age}}
        </li>
    </ul>
</div>
<script type="text/javascript">
    Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。

    const vm = new Vue({
        el:'#root',
        data:{
            school:{
                name:'尚硅谷',
                address:'北京',
            },
            student:{
                name:'tom',
                age:{
                    rAge:40,
                    sAge:29,
                },
                friends:[
                    {name:'jerry',age:35},
                    {name:'tony',age:36}
                ]
            }
        }
    })
</script>

最新的需求如下:我要给学生信息新增一个属性为性别(sex),要求点击按钮实现性别的添加,你可能会这样做,关键代码:

<h1>学生信息</h1>
<button @click="addSex">添加一个性别属性,默认值是男</button>
<h2>姓名:{{student.name}}</h2>
<h2 v-if="student.sex">性别:{{student.sex}}</h2>
<script type="text/javascript">
    const vm = new Vue({
        el:'#root',
        data:{
            school:{
                name:'尚硅谷',
                address:'北京',
            },
            student:{
                name:'tom',
                age:{
                    rAge:40,
                    sAge:29,
                },
                friends:[
                    {name:'jerry',age:35},
                    {name:'tony',age:36}
                ]
            }
        },
        methods: {
            addSex(){
                this.student.sex = '男' //这样写
            }
        }
    })
</script>

测试的时候发现点击按钮之后,页面并没有展示出来,但是内存中的数据却是发生了改变的:

可见数据是成功添加进去了,但是并未被Vue监测到,在上一小节,我们明白了Vue要想监测到数据的改变,必须要有对应的get和set方法,通过上图我们可以发现,student对象的其他属性都有对应的get和set,但是sex属性却是干巴巴的显示出来了,并且没有对应的get和set,这就是为什么不能够被Vue监测到的原因,对应的,既然无法被Vue监测到,那么就不可能去重现渲染页面了,自然页面也就没有展示。

那么如何让Vue监测到呢?使用Vue.set()API即可监测。

查看官方文档解释如下:

解释:响应式对象就是data中的数据,并且当数据改变的时候,页面要跟着变,这就是响应式

现在我们使用这个API来试试:

methods: {
    addSex(){
        Vue.set(this.student,'sex','男')
        //this.$set(this.student,'sex','男') //另外一种写法
    }
}

注意:还有一种写法:vm.$set(target,propertyName/index,value)

测试发现当我点击的时候,页面重新渲染了,并且展示成功了,这时候我们再来看看vm上的数据:

这时你发现sex都有对应的get和set了,所以能够被Vue监测到,当数据发生变化的时候,页面也就被重新渲染了。

重新回头看官方文档你会发现有一个注意事项:注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。

表达的意思就是如果你直接在data根数据上添加数据是不可以,例如如下代码:

data:{
    school:{
        name:'尚硅谷',
        address:'北京',
    },
    student:{
        name:'tom',
        age:{
            rAge:40,
            sAge:29,
        },
        friends:[
            {name:'jerry',age:35},
            {name:'tony',age:36}
        ]
    }
}

现在你想在school和student同级添加一个其他属性,是不会成功的,会报错的。这里的就是根数据。

好了,现在你知道了对于对象来说,如何添加一个属性能够被Vue监测到(使用Vue.set()API),那么我们再来看看数组的情况

17.4 Vue监视数组

还是之前的代码,我给用户信息中添加一个hobby数组数据:

<!-- 准备好一个容器-->
<div id="root">
    <h1>学生信息</h1>
    <h2>姓名:{{student.name}}</h2>
    <h2>年龄:真实{{student.age.rAge}},对外{{student.age.sAge}}</h2>
    <h2>爱好</h2>
    <ul>
        <li v-for="(h,index) in student.hobby" :key="index">
            {{h}}
        </li>
    </ul>
</div>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data: {
            student: {
                name: 'tom',
                age: {
                    rAge: 40,
                    sAge: 29,
                },
                hobby: ['抽烟', '喝酒', '烫头'], //准备好数组数据
            }
        }
    })
</script>

可见我的hobby数组中并没有提供get和set,这时候你去改变数组其中的一个元素是不生效的,试试:

同样的,内存中的数据改动了,但是Vue并没有监测到他的改变,也就没有重新渲染页面,从而页面也就没有展示出来,这就和我们最开始说的更新时的问题很相似了。

我们使用之前的API - Vue.set()来看看:

可见,这样更改能够被Vue监测到,并且重新渲染页面。

除了这种方式来更改,还有其他方式,官方文档给出:

官方文档的意思就是,数组本身是存在一些API的,这些API会导致原数组更改,只要调用数组本身API导致原数组了,那么就会被Vue监测到,对应的API如上图。

那么对于这种情况Vue又是怎样实现的呢?其实Vue底层做了包装,当我们在Vue中想要更改数组,并且重新渲染,我们调用了使原数组更改的API的时候,例如arr.push(),其实是调用了Vue底层的push()方法,在Vue的这个方法中,首先会调用Array.prototype.push(),之后再去做渲染相关的动作。

同样的,凡是不能够是原数组改变的API,生成新数组的,例如:

如果你不用原来的数组对象去接收,那么也是不会被监测到的。


总结:

Vue监视数据的原理:

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

  2. 如何监测对象中的数据?

    通过setter实现监视,且要在new Vue时就传入要监测的数据,即使说写data配置的时候就要定义好。

    • 对象中后追加的属性,Vue默认不做响应式处理
    • 如需给后添加的属性做响应式,请使用如下API:Vue.set(target,propertyName/index,value)或者vm.$set(target,propertyName/index,value)
  3. 如何监测数组中的数据?

    通过包裹数组更新元素的方法实现,本质就是做了两件事:

    • 调用原生对应的方法对数组进行更新。
    • 重新解析模板,进而更新页面。
  4. 在Vue修改数组中的某个元素一定要用如下方法:

    • 使用这些API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
    • 使用Vue.set()或者vm.$set()
  5. 特别注意:Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象 添加属性!!!

18. 收集表单数据

我现在拥有的表单如下:

现在我想要通过Vue来收集表单中的数据,结果代码:

<!-- 准备好一个容器-->
<div id="root">
    <form @submit.prevent="demo">
        账号:<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/>
        性别:
        男<input type="radio" name="sex" v-model="userInfo.sex" value="male"><input type="radio" name="sex" v-model="userInfo.sex" value="female"> <br/><br/>
        爱好:
        学习<input type="checkbox" v-model="userInfo.hobby" value="study">
        打游戏<input type="checkbox" v-model="userInfo.hobby" value="game">
        吃饭<input type="checkbox" v-model="userInfo.hobby" value="eat">
        <br/><br/>
        所属校区
        <select v-model="userInfo.city">
            <option value="">请选择校区</option>
            <option value="beijing">北京</option>
            <option value="shanghai">上海</option>
            <option value="shenzhen">深圳</option>
            <option value="wuhan">武汉</option>
        </select>
        <br/><br/>
        其他信息:
        <textarea v-model.lazy="userInfo.other"></textarea> <br/><br/>
        <input type="checkbox" v-model="userInfo.agree">阅读并接受<a href="http://www.atguigu.com">《用户协议》</a>
        <button>提交</button>
    </form>
</div>
<script type="text/javascript">
    Vue.config.productionTip = false

    new Vue({
        el:'#root',
        data:{
            userInfo:{
                account:'',
                password:'',
                age:'',
                sex:'female',
                hobby:[],
                city:'beijing',
                other:'',
                agree:''
            }
        },
        methods: {
            demo(){
                console.log(JSON.stringify(this.userInfo))
            }
        }
    })
</script>

解释:

首先我们都知道,收集input框中的数据使用v-model是没有错的,但是,这里面有一些例外,例如:

  • 收集单选框

    收集单选框中的value值的时候,例如上图中的男或者女,必须给input框指定value属性,否则无法收集,例如没有value属性的时候:

    vue初始化数据:

    new Vue({
        el:'#root',
        data:{
            userInfo:{
                sex:'' //用于接收数据,初始化为空
            }
        }
    })
    
    <input type="radio" name="sex" v-model="userInfo.sex"><input type="radio" name="sex" v-model="userInfo.sex"> <br/><br/>
    

    可见点击的时候就为null了,收集不到你选择的值,所以我们需要指定value属性:

    <input type="radio" name="sex" v-model="userInfo.sex" value=""><input type="radio" name="sex" v-model="userInfo.sex" value=""> <br/><br/>
    

    可见收集成功。

  • 收集复选框

    对于复选框,也会存在同样的问题:

    Vue初始化数据:

    new Vue({
        el:'#root',
        data:{
            userInfo:{
                hobby:''
            }
        }
    })
    
    爱好:
    学习<input type="checkbox" v-model="userInfo.hobby" >
    打游戏<input type="checkbox" v-model="userInfo.hobby">
    吃饭<input type="checkbox" v-model="userInfo.hobby">
    

    可见,勾选的时候hobby的值是一个布尔值,并不是学习、打游戏、吃饭中的一种,现在我们按照之前的方法,指定value属性看看:

    爱好:
    学习<input type="checkbox" v-model="userInfo.hobby" value="study">
    打游戏<input type="checkbox" v-model="userInfo.hobby" value="game">
    吃饭<input type="checkbox" v-model="userInfo.hobby" value="eat">
    

    经过测试,仍然是同样的问题。

    解决办法:将初始化数据设置为数组即可解决:

    new Vue({
        el:'#root',
        data:{
            userInfo:{
                hobby:[]
            }
        }
    })
    

    可见现在正常了。

对于最开始的结果代码处,你可能还会看见这部分:

<form @submit.prevent="demo">....</form>

这里有一个@submit.prevent,这里表示表单提交事件,并且阻止了表单点击提交跳转页面的默认行为,这样,你就可以再demo出发出你的Ajax请求来做一些操作了。

同时,你可能还会看见这几处代码:

  • v-model.trim:作用是当你再输入框中输入的内容首尾有空格的时候,Vue是不会帮你收集首尾的空格的。

  • v-model.number:作用是当你再输入框输入数组的时候,Vue默认会收集起来,类型为字符串,但是加上这个之后,就会按照number类型来收集。使用它的时候一般会将输入框的类型改为number,限制只能输入数子,不能输入字符:

    <input type="number" v-model.number="userInfo.age">
    
  • v-model.lazy:作用是Vue不会实时监测你的输入,只会再你输入完毕之后,该输入框失去焦点的时候,你输入的内容才会被收集起来。


总结:

  1. 若:<input type="text"/>,则v-model收集的是value值,用户输入的就是value值。
  2. 若:<input type="radio"/>,则v-model收集的是value值,且要给标签配置value值
  3. 若:<input type="checkbox"/>
    • 没有配置input的value属性,那么收集的就是checked(勾选 or 未勾选,是布尔值)
    • 配置input的value属性:
      • v-model的初始值是非数组,那么收集的就是checked(勾选 or 未勾选,是布尔值)
      • v-model的初始值是数组,那么收集的的就是value组成的数组
  4. 备注:v-model的三个修饰符:
    • lazy:失去焦点再收集数据
    • number:输入字符串转为有效的数字
    • trim:输入首尾空格过滤

19. 过滤器

我目前有一个需求,有一个时间戳,要求生成指定格式的时间:

<div id="root">
    <h2>时间戳:{{timestamp}}</h2>
    <h2>格式化时间戳:{{formatByMethod()}}</h2>
    <h2>格式化时间戳:{{formatByComputed}}</h2>
</div>

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

    new Vue({
        el:'#root',
        data:{
            timestamp: 1704374101424
        },
        methods:{
            formatByMethod(){
                return dayjs(this.timestamp).format('YYYY-MM-DD HH:mm:ss');
            }
        },
        computed:{
            formatByComputed(){
                return dayjs(this.timestamp).format('YYYY-MM-DD HH:mm:ss');
            }
        }
    })
</script>

注意:其中dayjs我是用的第三方库。

对于上面的代码,我是使用的函数,以及计算属性来完成的,现在我要求使用第三种方法来完成,要求使用过滤器来做:

<div id="root">
    <h2>时间戳:{{timestamp}}</h2>
    <h2>格式化时间戳:{{timestamp | timefFormater}}</h2>
</div>

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

    new Vue({
        el:'#root',
        data:{
            timestamp: 1704374101424
        },
        filters:{
            timefFormater(valule){
                return  dayjs(valule).format('YYYY-MM-DD HH:mm:ss');
            }
        }
    })
</script>

可见同样能够实现效果,现在我来解释一下这个代码的意思:

首先对于模板语中:{{timestamp | timefFormater}},第一个timestamp 是data中的属性,|表示管道符,timefFormater就代表一个过滤器方法,但是你在使用timefFormater的时候,要确保vm中有过滤器这个配置,也就是filters配置属性,里面可以写多个过滤器方法。当执行

{{timestamp | timefFormater}}的时候,会将前面的数据(timestamp)传入过滤器方法(timefFormater)中,并且,如果有多个过滤器方法,例如:

{{value| function1 | function2 | function3}},传值也是从左到右依次传递,先将value传递给function1,之后用function1的执行结果传递给function2,依次类推。对于过滤器方法:

timefFormater(valule){
    return  dayjs(valule).format('YYYY-MM-DD HH:mm:ss');
}

这里的value其实就是前面传递过来的值,并且第一个参数永远是前一个的值,后面才是你自定义的值,例如:

<h2>格式化时间戳:{{timestamp | timefFormater('YYYY年MM月DD日')}}</h2>

对应的过滤器方法为:

timefFormater(valule,str){
    return  dayjs(valule).format(str);
}

第一个参数是雷打不动的管道符前面的值,后面的参数才是你自定义的,并且过滤器方法的返回值就代表整个插值语法。


以上过滤器是局部过滤器,即是说多个vm实例之间是不共享的,如果要多个vm实例之间共享,那么你要注册全局过滤器:

// 注册
Vue.filter('my-filter', function (value) {
  // 返回处理后的值
})

参数1为过滤器方法名称,参数2就是具体的方法体了。

总结:

过滤器就是对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)。

语法:
1. 注册过滤器:Vue.filter(name,callback) new Vue{filters:{}}
2. 使用过滤器:{{ xxx | 过滤器名}} v-bind:属性 = "xxx | 过滤器名"

备注:

  1. 过滤器也可以接收额外参数、多个过滤器也可以串联
  2. 并没有改变原本的数据, 是产生新的对应的数据

20. 内置指令

我们已经知道的内置指令:

  • v-bind :单向绑定解析表达式, 可简写为 :xxx,不过只能用于标签属性
  • v-model: 双向数据绑定,只能用于有value属性的input标签上
  • v-for:遍历数组/对象/字符串
  • v-on绑定事件监听, 可简写为@
  • v-if:条件渲染(动态控制节点是否存存在,即DOM是否存在
  • v-else:条件渲染(动态控制节点是否存存在),与v-if连用
  • v-show:条件渲染 (动态控制节点是否展示,DOM仍然存在)

现在又要讲解一些新的内置指令:

  1. v-text

    作用:向其所在的节点中渲染文本内容。例如:

    <div id="root">
        <div>我的名字:{{name}}</div>
        <div v-text="name">彭于晏</div>
        <div v-text="content"></div>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el:'#root',
            data:{
                name: '念心卓',
                content:'<h3>大帅哥</h3>'
            }
        })
    </script>
    

    可见结果中,v-text会直接拿到name的值替换掉div标签中的内容,并且如果它渲染的数据里面包含html标签,他是不会渲染html标签内容的,这一点和JavaScript中innerHTMLinnerText有点像。

    与插值语法的区别:v-text会替换掉节点中的内容,则不会

  2. v-html

    作用:向指定节点中渲染包含html结构的内容。例如:

    <div id="root">
        <div>我的名字:{{name}}</div>
        <div v-text="name">彭于晏</div>
        <div v-html="content"></div>
    </div>
    <script type="text/javascript">
        Vue.config.productionTip = false
        new Vue({
            el:'#root',
            data:{
                name: '念心卓',
                content:'<h3>大帅哥</h3>'
            }
        })
    </script>
    

    可见v-html可以解析标签,这也是直观上和v-text的区别,同理,v-html也会替换掉标签体内的内容,和v-text一致。

    严重注意:v-html有安全性问题!!!!

    • 在网站上动态渲染任意HTML是非常危险的,容易导致XSS攻击
    • 一定要在可信的内容上使用v-html,永不要用在用户提交的内容上!
  3. v-cloak

    看下面一段代码:

    看到上面的这种情况,这时候你可能会想到这样来做:

    对于上面这种情况,貌似可以行通,但是仍然有很大的问题,就是你先解析的标签体,那么,标签体里面所有用到Vue模板语法的地方都会出现{{xxx}}的情况,例如上面{{name}},这样给用户直接展示出来是不友好的,我们希望直接展示的就是:我的名字:念心卓,而不是:我的名字:{{name}}

    所以,最好的解决办法是当Vue还没有渲染完毕的时候,不显示所有涉及到vue解析的模块,我们要将他们隐藏起来,可以使用css来控制,这时候,使用v-cloak来配合css是最好的:

    注意:v-cloak指令(没有值),本质是一个特殊属性,Vue实例创建完毕并接管容器后,会删掉v-cloak属性,使用css配合v-cloak可以解决网速慢时页面展示出的问题

  4. v-once

    这个指令和之前学的事件修饰符有点像:@事件类型.once

    例如下面的代码:

    <div id="root">
        <h2>初始化的n值是:{{n}}</h2>
        <h2>当前的n值是:{{n}}</h2>
        <button @click="n++">点我n+1</button>
    </div>
    <script src="../Vue_js/vue.js"></script>
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el:'#root',
            data:{
                n:1
            }
        })
    </script>
    

    可见当我点击按钮的时候,初始化的n值也跟着变了,我们想要的结果是当前n值改变,而初始化的n值不变。

    这时候你可能会想到,直接使用静态的不行吗:<h2>初始化的n值是:1</h2>,这里行是行,不过为了讲解v-once的作用,这里我们不这样做。

    我们给初始化n值那行标签上加个v-once指令看看:<h2 v-once>初始化的n值是:{{n}}</h2>

    可见这次达到目的了。

    作用:v-once所在节点在初次动态渲染后,就视为静态内容了,以后数据的改变不会引起v-once所在结构的更新,可以用于优化性能。

  5. v-pre

    在我们的代码中,经常有很多代码是写死了的,即没有使用vue的模板语法,绑定事件等等,也就是最纯粹的html的写法,这时候,对于这些代码,你可以给他加上v-pre指令,来提高页面的性能:

    作用:可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译

21. 自定义指令

前面我们讲解了内置指令,现在再详细说说自定义指令。注意:这里可能会涉及到操作DOM,你可能会有疑惑,我们都学习了Vue了,还需要自己来操作DOM,那学习Vue又有什么意义呢?其实Vue给的内置指令中,也有很多操作了DOM,不过这些都被Vue的指令来做了,所以,你一般是直接使用指令,而没有直接去操作DOM。

并且自定义指令有两种定义方式:

  1. 函数式:相当于简写形式。
  2. 对象式:最完整的写法。

21.1 函数式

对于一般简单的需求,不需要涉及到一些细节的时候,我们一般使用函数式就能够解决问题。

现在有一个需求:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍

如何定义指令:我们仍然参考Vue,使用v-xxx,这个xxx就是自定义指令。

定义规则:

  1. 对于自定义指令的xxx,你需要再Vue的配置对象中directives来定义。
  2. 对于多个单词的,不建议使用小驼峰,应该使用短横线分割。

参考代码:

<div id="root">
    <h2>当前的n值是:<span v-text="n"></span> </h2>
    <h2>放大10倍后的n值是:<span v-big="n"></span> </h2>
    <button @click="n++">点我n+1</button>
    <hr/>
</div>
<script type="text/javascript">
    Vue.config.productionTip = false

    new Vue({
        el:'#root',
        data:{
            n:1
        },
        directives:{
            //自定义指令这里接收两个参数:1.正式的元素DOM,2一些绑定的信息
            //big函数何时会被调用?1.指令与元素成功绑定时(一上来)。2.指令所在的模板被重新解析时。
            big(element,binding){ //因为自定义指令是v-big,所以这里要写big
                console.log(element);
                console.log(binding);
                element.innerHTML = binding.value * 10;//操作DOM
            }
        }
    })
</script>

注意上诉代码中说明的自定义指令调用的时机

注意:其实自定义指令中,参数还有一些,这里我就只打印出来了最常用的两个。

具体可参考官网:https://v2.cn.vuejs.org/v2/guide/custom-directive.html#ad

21.2 对象式

有时候,对于一些细节的东西,函数式是无法搞定的,这时候我们就要用对象式

我现在有这样一个需求:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点

我先用函数式实现一遍,看看有什么问题:

<div id="root">
    <!--之前的代码省略-->
    <input type="text" v-fbind:value="n">
</div>
<script type="text/javascript">
    Vue.config.productionTip = false

    new Vue({
        el:'#root',
        data:{
            n:1
        },
        directives:{
            //之前的代码省略
            fbind(el,binding){
                console.log(el);
                console.log(binding);
                el.value = binding.value;
                el.focus(); //获取焦点
            }
        }
    })
</script>

对于执行结果可以看出,效果并没有实现,这是为什么呢?难道是最后获取焦点的时候代码没有执行吗?可以肯定的告诉你,代码是执行了的,那为什么没有成功呢?这里就涉及到一个执行时机的问题,看看下面的代码,我现在有个要求是点击按钮创建一个输入框,要求这个输入框一创建的时候,就要获取焦点:

<button id="btn">点我创建一个输入框</button>
<script type="text/javascript" >
    const btn = document.querySelector('#btn')
    btn.addEventListener('click',function (){
        const input = document.createElement('input')
        input.value = '99';
        input.focus();
        document.body.appendChild(input);
    })
</script>

执行结果:

可见并未成功获取焦点。

现在改动一下代码的执行顺序,先追加到父元素上了之后,再获取焦点:

document.body.appendChild(input);
input.focus();

这下就成功了。

解释:因为你虽然把input元素创建出来了,但是如果你还未将它放到页面的时候就获取焦点,显然是不可以的,你只有将input放入了页面,再获取焦点才会有效果,这就是为什么获取焦点在document.body.appendChild(input);代码执行之后执行就可以了的原因。

所以,对于最开始的代码,我们如何改造呢?如何获取到放到页面时这个时间节点呢?

这时候就要使用对象式来解决了:

new Vue({
    el:'#root',
    data:{
        n:1
    },
    directives:{
           //省略之前的代码
        fbind:{
            bind(){},
            inserted(){},
            update(){}
        }
    }
})

可见,使用对象式之后,里面又写了3个函数,不过这3个函数可不能随便乱写,这是Vue规定好了的钩子函数,可以在特定的时机帮你调用对应的函数,无需自己调用。

三个函数的执行时机

  • bind:指令与元素成功绑定时(一上来)
  • inserted:指令所在元素被插入页面时
  • update:指令所在的模板被重新解析时

这样,我们就可以通过inserted钩子函数来获取到元素放入页面的这个时间节点:

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

可见现在成功了。

细心的你可能会发现,一般bind和update中执行的逻辑是一样的,这就是为什么有函数式写法的原因,因为函数式写法只能掌握bing和update两个阶段。


其实自定义指令上面两种写法都是属于局部的自定义指令,那全局的自定义指令怎么写呢?和之前过滤器一样:

Vue.directive('自定义指令名(不要v-)',回调函数/配置对象)

总结:

  1. 局部指令

    new Vue({					
        directives:{指令名:配置对象}
    })
    

    或者:

    new Vue({
        directives{指令名:回调函数}
    }) 	
    
  2. 全局指令

    Vue.directive(指令名,配置对象)或Vue.directive(指令名,回调函数)

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

    • bind:指令与元素成功绑定时调用。
    • inserted:指令所在元素被插入页面时调用。
    • update:指令所在模板结构被重新解析时调用。
  4. 指令定义时不加v-,但使用时要加v-

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

22. 生命周期

Vue生命周期整个流程图,非常重要!!!,请仔细观看:

上面所有红框的都是生命周期函数。

22.1 引出生命周期

需求:页面上有一句话,要求实现淡入淡出的效果

<div id="root">
    <h1 style="opacity: 1">学习Vue中</h1>
</div>
<script type="text/javascript">
    const h1 = document.querySelector('h1');
    setInterval(() => {
        h1.style.opacity = Number(h1.style.opacity) - 0.01 + "";
        if (parseFloat(h1.style.opacity) <= 0) h1.style.opacity = '1';
    }, 16);
</script>

自行执行上面的代码查看效果

现在我们再用Vue来做一遍:

<div id="root">
    <h1 :style={opacity}>学习Vue中</h1>
</div>
<script type="text/javascript">
    Vue.config.productionTip = false
    const vm = new Vue({
        el: '#root',
        data: {
            n: 1,
            opacity: 1
        }
    })
    setInterval(()=>{
        vm.opacity -= 0.01;
        if (vm.opacity <= 0) vm.opacity = 1;
    },16)
</script>

这样做也能实现,但是不推荐,因为你在用定时器操作Vue里面的属性,但是这个定时器,你又没有放入到Vue中去,其实定时器应该放到Vue中,但是二者却割裂开了。

那怎么来解决呢?是否可以将定时器放入Vue的配置对象中呢?我们来看看:

<div id="root">
    <h1 :style={opacity}>学习Vue中</h1>
    {{change()}}
</div>
<script type="text/javascript">
    Vue.config.productionTip = false
    new Vue({
        el: '#root',
        data: {
            n: 1,
            opacity: 1
        },
        methods:{
            change(){
                setInterval(()=>{
                    this.opacity -= 0.01;
                    if (this.opacity <= 0) this.opacity = 1;
                },16)
            }
        }
    })
</script>

你可能会想到写出上面的代码,但是上面的代码虽然定时器的问题解决了,但是又会有新的问题,不停闪烁。

上诉代码带价特别大。

分析:想要将定时器放入Vue配置的对象中,那么你就要配置到methods配置项中,但是配置了之后,没有人来调用change函数,就不会有效果,所以你就想到了在页面中直接使用插值语法{{change()}}的方式,因为change()函数没有返回值,插值语法处就是undefinedundefined在Vue中是不显示到页面上的,所以你就用这样写了,但是如果你执行了代码你就会发现,页面是很鬼畜的。我们之前学过,当vm实例上的属性发生改变的时候,就会重新解析模板。所以,上诉代码执行过程:**解析模板–>开启定时器,执行定时器改变opacity–>Vue检测到opacity改变,重新解析模板–>又开启定时器,改变opacity->…**,只要opacity改变了,页面就会重新解析,所以就会导致页面一直在重新解析,一直循环往复,所以就一直在闪烁,定时器也在不断开启,而且每个定时器里面,又是重复执行某个代码,循环将指数上升,想想就很恐怖。

那有没有办法控制定时器呢?不要让他指数增长?

这时候就要引出我们的生命周期函数了,期望只在初始化的真实DOM的时候开启定时器,之后就不再重复开启了:

<div id="root">
    <h1 :style={opacity}>学习Vue中</h1>
</div>
<script type="text/javascript">
    Vue.config.productionTip = false
    new Vue({
        el: '#root',
        data: {
            n: 1,
            opacity: 1
        },
        //Vue完成模板的解析并把初始的真实DOM元素放入页面后(挂载完毕)调用mounted
        mounted(){
            setInterval(()=>{
                this.opacity -= 0.01;
                if (this.opacity <= 0) this.opacity = 1;
            },16)
        }
    })
</script>

上诉代码即可解决问题。

可见新出现了一个函数,其实它也属于配置项,mounted()的执行时机:Vue完成模板的解析并把初始的真实DOM元素放入页面后(挂载完毕)调用mounted。并且它在Vue的生命周期中只会执行一次。

22.2 生命周期详解

通用代码:

<div id="root">
    <h1>当前的n值为:{{n}}</h1>
</div>
  1. beforeCreate

    此时无法通过vm访问到data中的数据以及methods中的方法

    <script type="text/javascript">
        Vue.config.productionTip = false
        new Vue({
            el: '#root',
            data: {
                n: 1
            },
            beforeCreate(){
                console.log(this);
                debugger
            }
        })
    </script>
    

  2. created

    此时可以通过vm访问到data中的数据以及methods中配置的方法

    <script type="text/javascript">
        Vue.config.productionTip = false
        new Vue({
            el: '#root',
            data: {
                n: 1
            },
            created(){
                console.log(this);
                debugger
            }
        })
    </script>
    

  3. beforeMount

    到beforeMount阶段时,此时页面呈现的是未经Vue编译的DOM结构(其实前几个阶段也一样),所有对DOM的操作,最终都不奏效。

    <script type="text/javascript">
        Vue.config.productionTip = false
        new Vue({
            el: '#root',
            data: {
                n: 1
            },
            beforeMount(){
                console.log(this);
                debugger
            }
        })
    </script>
    

    一放行,之前操作的DOM都会被Vue替换为Vue解析的内容:

    所以在这个阶段以及这个阶段之前,操作DOM都是不奏效的。因为在这阶段已过,就执行了这个环节:

    因为在这个阶段之前,Vue的虚拟DOM已经生成了,并且已经放入到了内存中:

  4. mounted

    查看上图mounted阶段挺长的,里面还包括了beforeUpdateupdated,这两个阶段后面讲。

    在mounted阶段,此时页面中呈现的是经过Vue编译的DOM,并且对DOM的操作均有效(尽可能避免)至此初始化过程结束。

    一般在此进行:开启定时器、发送网络请求、订阅消息、绑定自定义事件、等初始化操作

    <script type="text/javascript">
        Vue.config.productionTip = false
        new Vue({
            el: '#root',
            data: {
                n: 1
            },
            mounted(){
                console.log(this);
                debugger
            }
        })
    </script>
    

    可见成功操作了DOM,不过一般不要这样做。

  5. beforeUpdateupdated

    数据改变(when data changes)的时候,这两个钩子函数触发。

    beforeUpdate阶段:此时数据是新的,但是页面仍然是旧的,即页面尚未和数据保持同步

    <div id="root">
        <h1>当前的n值为:{{n}}</h1>
        <button @click="updateValue">点我更新n值</button>
    </div>
    <script type="text/javascript">
        Vue.config.productionTip = false
        new Vue({
            el: '#root',
            data: {
                n: 1
            },
            methods:{
                updateValue(){
                    this.n = 100;
                }
            },
            beforeUpdate(){
                console.log(this);
                debugger
            }
        })
    </script>
    

    updated阶段:此时数据是最新的,页面也是最新的,即数据与页面保持同步。

    将上述代码beforeUpdate改为updated查看:

  6. beforeDestroydestroyed

    在beforeDestroy阶段:此时vm中所有的:data、methods、指令等等,都处于可用状态,马上要执行销毁过程,一般在此阶段:关闭定时器、取消订阅消息、解绑自定义事件等收尾操作。

    注意:在此阶段你不要再更新数据了,因为就算你再这个阶段以及后面的destroyed阶段更新数据都不会再呈现再页面上了。

    最后destroyed阶段是被忽略得最严重的一个阶段,不用过度关心它。


使用代码总结一下流程:

<div id="root">
    <h1>当前的n值为:{{n}}</h1>
    <button @click="updateValue">点我更新n值</button>
    <button @click="destroyVm">点我销毁vm</button>
</div>
<script type="text/javascript">
    Vue.config.productionTip = false
    new Vue({
        el: '#root',
        data: {
            n: 1
        },
        methods:{
            updateValue(){
                this.n = 100;
                console.log('updateValue被执行了');
            },
            destroyVm(){
                this.$destroy();
            }
        },
        beforeCreate() {
            console.log('beforeCreate')
        },
        created() {
            console.log('created')
        },
        beforeMount() {
            console.log('beforeMount')
        },
        mounted() {
            console.log('mounted')
        },
        beforeUpdate() {
            console.log('beforeUpdate')
            console.log(`当前n值为${this.n}`)
        },
        updated() {
            console.log('updated')
            console.log(`当前n值为${this.n}`)
        },
        beforeDestroy() {
            console.log('beforeDestroy')
            console.log(`当前n值为${this.n}`)
        },
        destroyed() {
            console.log('destroyed')
            console.log(`当前n值为${this.n}`)
        },
    })
</script>

为什么销毁过后,再次点击更新没用了呢?我们来看看官方文档:


总结:

常用的生命周期钩子:

  1. mounted: 发送ajax请求、启动定时器、绑定自定义事件、订阅消息等【初始化操作】。
  2. beforeDestroy: 清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】。

关于销毁Vue实例

  1. 销毁后借助Vue开发者工具看不到任何信息。
  2. 销毁后自定义事件会失效,但原生DOM事件依然有效
  3. 一般不会在beforeDestroy操作数据,因为即便操作数据,也不会再触发更新流程了

文章作者: 念心卓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 念心卓 !
  目录