Vue核心1

103 阅读18分钟

一、Vue base

1、基本使用

我们知道Vue的核心理念是数据驱动的理念,所谓的数据驱动的理念:当数据发生变化的时候,用户界面也会发生相应的变化,开发者并不需要手动的去修改dom.

简单的理解:就是vue.js帮我们封装了数据和dom对象操作的映射,我们只需要关心数据的逻辑处理,数据的变化就能够自然的通知页面进行页面的重新渲染。

这样做给我们带来的好处就是,我们不需要在代码中去频繁的操作dom了,这样提高了开发的效率,同时也避免了在操作Dom的时候出现的错误。

Vue.js的数据驱动是通过MVVM这种框架来实现的,MVVM 框架主要包含三部分:Model,View,ViewMode

Model:指的是数据部分,对应到前端就是JavaScript对象。

View:指的就是视图部分

ViewModel: 就是连接视图与数据的中间件(中间桥梁)

以上三部分对应到代码中的位置如下图所示:

1.png

下面,我们再来看一张图来理解一下MVVM框架的作用:

2.png

数据(Model)和视图(View)是不能直接通讯的,而是需要通过ViewModel来实现双方的通讯。当数据(Model)变化的时候,ViewModel能够监听到这种变化,并及时通知View视图做出修改。同样的,当页面有事件触发的时候,ViewModel也能够监听到事件,并通知数据(Model)进行响应。所以ViewModel就相当于一个观察者,监控着双方的动作,并及时通知对方进行相应的操作。

简单的理解就是:MVVM 实现了将业务(数据)与视图进行分离的功能。

在这里还需要注意的一点就是:

MVVM框架的三要素:响应式,模板引擎,渲染

响应式:vue如何监听数据的变化?

模板:Vue的模板如何编写和解析?怎样将具体的值替换掉{{msg}}内容,这就是模板引擎的解析。

渲染:Vue如何将模板转换成html? 其实就是有虚拟DOM 向真实DOM的转换。

2、模板语法

2.1 属性绑定

属性的绑定,下面先来看一下关于对属性的绑定

<div id="app">
      <h2 v-bind:title="msg">
        {{msg}}
      </h2>
    </div>

在上面的代码中,我们通过v-bind的方式给h2绑定了一个title属性。

当然,上面的代码我们也可以使用如下的方式来进行简化

    <div id="app">
      <h2 :title="msg">
        {{msg}}
      </h2>
    </div>

为了避免闪烁的问题,也就是最开始的时候,出现:{{msg}}的情况,可以使用如下的绑定方式。

  <div id="app">
      <h2 :title="msg">
        <!-- {{msg}} -->
        <span v-text="msg"></span>
      </h2>
    </div>

3、 列表渲染

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

基本实现的代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
  </head>
  <body>
    <div id="app">
      <ul>
        <!-- users表示数组,item表示从数组中取出的对象,这个名字可以随意取 -->
        <!-- 注意 v-for必须结合key属性来使用,它会唯一标识数组中的每一项,未来当数组中的那一项改变的时候,它会只更新那一项,好处就是提升性能。注意key的值唯一,不能重复 -->
        <!-- index表示数组的索引值,该名字可以随意定义 -->
        <li v-for="(item,index) in users" :key="item.id">
          编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li>
      </ul>
    </div>
    <script src="vue.js"></script>
    <script>
      new Vue({
        el: "#app",
        data: {
          users: [
            {
              id: 1,
              name: "张三",
            },
            {
              id: 2,
              name: "李四",
            },
            {
              id: 3,
              name: "老王",
            },
          ],
        },
      });
    </script>
  </body>
</html>

注意:为了能够保证列表渲染的性能,我们需要给v-for添加key属性。key值必须唯一,而且不能使用indexrandom作为key的值。

关于这一点是与虚拟DOM算法密切相关的。在后面的课程中会最为一个重点来探讨虚拟DOM的内容。这也是面试的时候经常被问到的问题。

4、v-model

如果model中的数据发生了改变,会通过ViewModel通知View更新数据,其实这就是我们常说的, “双向数据绑定”

怎样实现这种效果呢?可以通过v-model来实现。

  <!-- v-model指令用来双向数据绑定:就是model和view中的值进行同步变化 -->
  <!-- v-model只能在input/textarea/selet  也就是表单元素-->

具体代码实现如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>双向数据绑定</title>
    <script src="./vue.js"></script>
  </head>

  <body>
    <div id="app">
      <input type="text" v-model="userName" />
    </div>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          userName: "zhangsan",
        },
      });
    </script>
  </body>
</html>

怎样验证v-model实现了双向数据绑定呢?

可以打开控制台,然后输入:vm.userName 发现输出的值为"zhangsan", 取的是模型中的数据。

当在文本框中输入新的值后,在敲一下vm.userName发现对应的数据发生了变化,也就是视图中的数据发生了变化,模型中的数据也 会发生变化。

那么在控制台中直接给vm.userName="lisi",发现文本框中的值也发生了变化。

关于v-model 这个知识点,面试的时候经常会被问到的一个问题就是,自己能否模拟实现一个类似于v-model的双向数据绑定的效果。关于这个问题你可以先思考一下,在后面的课程中,我们会详细的讲解。

5、v-on

怎样监听dom的事件呢?可以通过v-on指令完成,具体的代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="./vue.js"></script>
</head>

<body>
    <div id="app">
        <span>{{name}}</span>
        <!-- 通过v-on来指定对应的事件,然后后面跟上对应的方法名,方法的定义在methods完成 -->
        <button v-on:click="changeName">更换姓名</button>
    </div>
    <script>
        var vm = new new Vue({
            el: '#app',
            data: {
                name: 'zhangsan'
            },
            // 通过methods完成函数或方法的定义
            methods: {
                changeName() {
                    // 在methods中要获取data中的属性,需要通过this来完成,this表示的是vue实例。
                    this.name = "itcast"
                }
            }
        })
    </script>
</body>
</html>

还可以通过简写的形式。建议以后都使用简写的形式

<button @click="changeName">更换姓名</button>

带参数的形式如下:

<button @click="changeNameByArg('laowang')">带参数的情况</button>


<script>
        var vm = new new Vue({
            el: '#app',
            data: {
                name: 'zhangsan'
            },
            // 通过methods完成函数或方法的定义
            methods: {
                changeName() {
                    // 在methods中要获取data中的属性,需要通过this来完成,this表示的是vue实例。
                    this.name = "itcast"
                },
                changeNameByArg(userName) {
                    this.name = userName
                }
            }
        })
    </script>

除了绑定鼠标的单击事件以外,也可以绑定键盘的实际。

例如,页面有有一个文本框,用户在该文本框中输入内容,按下回车键,获取到用户输入的内容。

<div id="app">
      <span>{{name}}</span>
      <!-- 通过v-on来指定对应的事件,然后后面跟上对应的方法名,方法的定义在methods完成 -->
      <button @click="changeName">更换姓名</button>
      <button @click="changeNameByArg('laowang')">带参数的情况</button>
    <!--给文本框添加键盘事件-->
      <input type="text" @keydown.enter="changeUserName" v-model="name" />
    </div>

mehtods中定义changeUserName方法

 // 通过methods完成函数或方法的定义
        methods: {
          changeName() {
            // 在methods中要获取data中的属性,需要通过this来完成,this表示的是vue实例。
            this.name = "itcast";
          },
          changeNameByArg(userName) {
            this.name = userName;
          },
             //定义处理文本框键盘事件的方法。
          changeUserName() {
            console.log(this.name);
          },
        },

在上面的案例中,我们使用了按键的修饰符:.enter,在官方文档中,还有其它的按键修饰符,如下所示:

https://cn.vuejs.org/v2/guide/events.html#%E6%8C%89%E9%94%AE%E4%BF%AE%E9%A5%B0%E7%AC%A6

与之相关的就是事件修饰符,如下所示:

https://cn.vuejs.org/v2/guide/events.html#%E4%BA%8B%E4%BB%B6%E4%BF%AE%E9%A5%B0%E7%AC%A6

以上内容,大家可以在课下的时候,仔细看一下。

6、Class与Style绑定

这块主要内容主要与样式设置有关。

操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是 attribute,所以我们可以用 v-bind 处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind 用于 classstyle 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。

下面先来看一下Class的绑定。

在"列表渲染"中给每个列表项添加对应的样式。

   <style>
      .actived {
        background-color: #dddddd;
      }
    </style>

下面给li列表添加上面所定义的样式。

  <li
          v-for="(item,index) in users"
          :key="item.id"
          :class="{actived:true}"
        >
          编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li>

在上面的代码中,我们可以看到,给li标签绑定了class属性,同时actived的值为true,表示给li添加actived样式。

现在有一个需求,就是当鼠标移动到列表项上的时候,更改对应的背景色。

        <li
          v-for="(item,index) in users"
          :key="item.id"
          :class="{actived:selectItem===item}"
          @mousemove="selectItem=item"
        >

在对class进行绑定的时候,做了一个判断,判断一下selectItem是否与item相等,如果相等添加样式。

当鼠标移动到某个li 列表上的时候,触发mousemove事件,将item的值给selectItem.

data中定义selectItem.

如下所示:

  data: {
          selectItem: "",
          users: [
            {
              id: 1,
              name: "张三",
            },
            {
              id: 2,
              name: "李四",
            },
            {
              id: 3,
              name: "老王",
            },
          ],
        },

完整 代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
    <style>
      .actived {
        background-color: #dddddd;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <ul>
        <!-- users表示数组,item表示从数组中取出的对象,这个名字可以随意取 -->
        <!-- 注意 v-for必须结合key属性来使用,它会唯一标识数组中的每一项,未来当数组中的那一项改变的时候,它会只更新那一项,好处就是提升性能。注意key的值唯一,不能重复 -->
        <!-- index表示数组的索引值,该名字可以随意定义 -->
        <li
          v-for="(item,index) in users"
          :key="item.id"
          :class="{actived:selectItem===item}"
          @mousemove="selectItem=item"
        >
          编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li>
      </ul>
    </div>
    <script src="vue.js"></script>
    <script>
      new Vue({
        el: "#app",
        data: {
          selectItem: "",
          users: [
            {
              id: 1,
              name: "张三",
            },
            {
              id: 2,
              name: "李四",
            },
            {
              id: 3,
              name: "老王",
            },
          ],
        },
      });
    </script>
  </body>
</html>

下面,我们再来看一下Style的绑定。

        <li
          v-for="(item,index) in users"
          :key="item.id"
          :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
          @mousemove="selectItem=item"
        >
          编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li>

通过上面的代码,可以看到通过绑定style的方式来处理样式是非常麻烦的。

7、条件渲染

v-if和v-show指令可以用来控制元素的显示和隐藏

下面,我们先来看一下v-if的应用。

这里还是对用户数据进行判断。

    <div id="app">
      <p v-if="users.length===0">没有任何用户数据</p>

      <ul v-else>
        <!-- users表示数组,item表示从数组中取出的对象,这个名字可以随意取 -->
        <!-- 注意 v-for必须结合key属性来使用,它会唯一标识数组中的每一项,未来当数组中的那一项改变的时候,它会只更新那一项,好处就是提升性能。注意key的值唯一,不能重复 -->
        <!-- index表示数组的索引值,该名字可以随意定义 -->
        <!-- <li
          v-for="(item,index) in users"
          :key="item.id"
          :class="{actived:selectItem===item}"
          @mousemove="selectItem=item"
        >
          编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li> -->

        <li
          v-for="(item,index) in users"
          :key="item.id"
          :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
          @mousemove="selectItem=item"
        >
          编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li>
      </ul>
    </div>

在上面的代码中,我们首先对users数组做了一个判断,如果没有数据,就在页面上展示:“没有任何用户数据”

否则渲染整个列表。

上面是关于v-if的使用,下面看一下v-show.

v-show 是通过css属性display控制元素显示,元素总是存在的。

v-if:通过控制dom来控制元素的显示和隐藏,如果一开始条件为false,元素是不存在的。

什么时候使用v-show,什么时候使用v-if呢?

如果需要频繁的控制元素的显示与隐藏,建议使用v-show. 从而避免大量DOM操作,提高性能。

而如果某个元素满足条件后,渲染到页面中,并且以后变化比较少,可以使用v-if

8、计算属性

计算属性出现的目的是解决模板中放入过多的逻辑会让模板过重且难以维护的问题.

计算属性是根据data中已有的属性,计算得到一个新的属性.

下面,我们可以通过一个案例来学习一下计算属性、

在一个文本框中输入第一个名字,第二个文本框中输入第二个名字,然后展示全部名称。

<body>
    <div id="app">
        <input type="text" v-model="firstName">
        <input type="text" v-model="lastName">
        <!-- 这样是模板逻辑变得非常复杂,不易维护 -->
        <div>全名:{{firstName + lastName}}</div>


        <div>全名:{{fullName}}</div>
    </div>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                firstName: '',
                lastName: ''
            },
            // 创建计算属性通过computed关键字,它是一个对象
            computed: {
                // 这里fullName就是一个计算属性,它是一个函数,但这个函数可以当成属性来使用
                fullName() {
                    return this.firstName + this.lastName
                }
            }
        })
    </script>
</body>

了解了计算属性后,下面对用户列表添加一个功能,要求是计算总人数。

可以在ul列表下面,添加如下的代码。

      <p>
        总人数:{{users.length+"个"}}
      </p>

最终展示出了,对应的人数,但是这里在模板中做了运算(在这里做了字符串拼接,虽然计算简单,但是最好还是通过计算属性来完成),为了防止在模板中放入过多的逻辑计算,这里可以使用计算属性来解决。

下面对代码进行改造:

  <p>
        <!-- 总人数:{{users.length+"个"}} -->
        总人数:{{total}}
      </p>

计算属性实现:

  <script>
      new Vue({
        el: "#app",
        data: {
          selectItem: "",
          users: [
            {
              id: 1,
              name: "张三",
            },
            {
              id: 2,
              name: "李四",
            },
            {
              id: 3,
              name: "老王",
            },
          ],
        },

        computed: {
          total() {
            // 计算属性是有缓存性:如果值没有发生变化,则页面不会重新渲染
            return this.users.length + "个";
          },
        },
      });
    </script>v

通过上面的代码,可以看到使用计算属性,让界面变得更加的简洁。

使用计算属性还有一个好处:

其实细心的话就会发现,调用methods里的方法也能实现和计算属性一样的效果,既然使用methods就可以实现,那为什么还需要计算属性呢?原因就是计算属性是基于他的依赖缓存的(所依赖的还是data中的数据)。一个计算属性所依赖的数据发生变化时,他才会重新取值

也就是说:只要相关依赖没有改变,对此访问计算属性得到的是之前缓 存的结果,不会多次执行。

下面我们测试一下:

 <p>
        <!-- 总人数:{{users.length+"个"}} -->
        总人数:{{total}} 总人数:{{total}}
      </p>

在上面的代码中,我们使用total了两次。

下面在看一下关于计算属性中的代码修改:

 computed: {
          total() {
            console.log("aaa");
            // 计算属性是有缓存性:如果值没有发生变化,则页面不会重新渲染
            return this.users.length + "个";
          },
        },

这里,我们通过console输出字符串aaa,但是在控制台上只是输出了一次,因为,第二次使用total的时候,发现值没有变化,所以直接从缓存中获取了对应的值。并没有重新进行计算,这样带来的好处就是,性能得到了提升。

下面,我们换成methods函数的形式来看一下:

 <p>
        <!-- 总人数:{{users.length+"个"}} -->
        总人数:{{total}} 总人数:{{total}} 总人数:{{getTotal()}}
        总人数:{{getTotal()}}
      </p>

在上面的代码中,调用了两次getTotal方法。

getTotal方法的实现如下:

  methods: {
          getTotal: function () {
            console.log("methods");
            return this.users.length + "个";
          },
        },

实现的方式是差不多的,但是这里却执行了两次。(注意:由于本案例中给每一个li标签添加了 *@mousemove*,所以只要鼠标移动到列表上,就会导致页面重新渲染,这时会不断的调用getTotal方法。)

所以通过上面案例的演示,可以明确的看出计算属性是有缓存的,也就是所依赖的data属性中的数据没有变化,那么是不会重新计算的。所以提升了对应的性能。

所以说,在进行大量耗时计算的时候,建议使用计算属性来完成。

如下代码:

 data: {
          selectItem: "",
          num: 100
          }

data中定义了num 属性,并且初始值为100、

下面在计算属性中进行求和的运算,代码实现如下:

 computed: {
          total() {
            console.log("aaa");
            // 计算属性是有缓存性:如果值没有发生变化,则页面不会重新渲染
            // return this.users.length + "个";
            let count = 0;
            for (let i = 0; i <= this.num; i++) {
              count += i;
            }
            return count;
          },
        },

通过演示,可以发现计算属性只是在第一次调用的时候,执行了一次,后续由于所依赖的数据num没有发生变化,所以即时调用多次,也并没有重新进行计算,而是获取上次计算的结果,所以说在进行大量耗时计算的时候,通过计算属性可以提升性能。

9、侦听器

侦听器就是侦听data中的数据变化,如果数据一旦发生变化就通知侦听器所绑定方法,来执行相应的操作。从这一点上,与计算属性是非常类似的。

但是,侦听器也有自己独有的应用场景。

执行异步或开销较大的操作。

下面,先来看一下侦听器的基本使用

我们使用侦听器来统计总人数。

 <p>

        总人数:{{totalCount}}
      </p>

data中定义totalCount属性。

 data: {
          selectItem: "",
          num: 100,
          totalCount: 0
       }   

使用watch来监听users数组的数据变化。

   watch: {
          users: {
            immediate: true, //立即执行
            handler(newValue, oldValue) {
              this.totalCount = newValue.length + "个人";
            },
          },
        }

users数组发生了变化后,就会执行handler这个函数,同时用于加上了immediate属性,并且该属性的值为true,表示的就是在初始化绑定的时候,也会去执行侦听器。因为watch在初始化绑定的时候是不会执行的,等到所监听的内容改变之后才会去侦听执行。

以上就是watch侦听器的基本使用,但是通过这个案例,我们发现还是使用计算属性来统计总人数更加的方便一些。

当然,侦听器有自己的应用场景,它的应用场景就是在执行异步请求或者进行开销比较大的操作的时候,会使用侦听器。

下面我们在通过一个案例,来体会一下watch侦听器的应用场景。

下面我们来看一个异步操作的情况。就是当用户在一个文本框中输入了用户名以后,要将输入的用户名发送到服务端,来检查该用户名是否已经被占用。

具体的实现代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>侦听器</title>
  </head>
  <body>
    <div id="app">
      <div>
        <span>用户名</span>
          <!--这里使用了lazy,保证当文本框失去焦点后,才去执行对应操作-->
        <span><input type="text" v-model.lazy="uname" /></span>
        <span>{{message}}</span>
      </div>
    </div>
    <script src="./vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          uname: "",
          message: "",
        },
        methods: {
          checkUserName: function (userName) {
            let that = this;
            setTimeout(function () {
              if (userName === "admin") {
                that.message = "用户名已经存在,请更改....";
              } else {
                that.message = "该用户名可以使用.....";
              }
            }, 3000);
          },
        },
        watch: {
          uname: function (value) {
            //调用后台接口,来验证用户名是被占用
            this.checkUserName(value);
            this.message = "正在校验用户名....";
          },
        },
      });
    </script>
  </body>
</html>

以上的案例,就是通过watch来监听uname的值是否发生变化,如果发生了变化,就通过发送异步请求来检查uname中的值,是否已经被占用。

通过以上的案例:我们可以看到watch是允许异步操作的,并且在我们得到最终的结果前,可以设置中间状态,这些都是计算属性无法做到的。

最后我们把计算属性与侦听器做一个总结,看一下它们的应用场景。

第一点:语境上的差异:

watch适合一个值发生了变化,对应的要做一些其它的事情,适合一个值影响多个值的情形。

例如,上面案例中的用户名检测,这里是一个uname发生了变化,但是这里做了很多其它的事情,例如修改message的值,发送异步请求。

而计算属性computed:一个值由其它的值得来,其它值发生了变化,对应的值也会变化,适合做多个值影响一个值的情形。

例如如下代码:

computed:{
    fullName(){
        return this.firstName+' '+this.lastName
    }
}

第二点:计算属性有缓存性。

由于这个特点,我们在实际的应用中,能用计算属性的,会首先考虑先使用计算属性。

第三点:侦听器选项提供了更加通用的方法,适合执行异步操作或者较大开销操作。

10、生命周期简介

每个Vue实例在被创建时都要经过一系列的初始化过程,例如:需要设置数据的监听,编译模板,将实例挂载到DOM上,并且在数据变化时更新DOM等,这些过程统称为Vue实例的生命周期。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

下面,我们来看一下这些钩子函数的应用。

通过一个异步获取列表数据的案例,来查看这些生命周期的钩子函数应用。

在这里是通过异步的方式获取用户列表的数据。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
    <style>
      .actived {
        background-color: #dddddd;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <p v-if="users.length===0">没有任何用户数据</p>

      <ul v-else>
        <!-- users表示数组,item表示从数组中取出的对象,这个名字可以随意取 -->
        <!-- 注意 v-for必须结合key属性来使用,它会唯一标识数组中的每一项,未来当数组中的那一项改变的时候,它会只更新那一项,好处就是提升性能。注意key的值唯一,不能重复 -->
        <!-- index表示数组的索引值,该名字可以随意定义 -->
        <!-- <li
          v-for="(item,index) in users"
          :key="item.id"
          :class="{actived:selectItem===item}"
          @mousemove="selectItem=item"
        >
          编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li> -->

        <li
          v-for="(item,index) in users"
          :key="item.id"
          :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
          @mousemove="selectItem=item"
        >
          编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li>
      </ul>
      <p>
        <!-- 总人数:{{users.length+"个"}} -->
        <!-- 总人数:{{total}} 总人数:{{total}} 总人数:{{getTotal()}}
        总人数:{{getTotal()}} -->

        总人数:{{totalCount}}
      </p>
    </div>
    <script src="vue.js"></script>
    <script>
      new Vue({
        el: "#app",
        data: {
          selectItem: "",
          num: 100,
          totalCount: 0,
            //指定users默认数据为一个空数组。
          users: [],
        },
        //组件实例已创建时,执行created方法,来调用getUserList方法,发送异步请求获取数据
        //将获取到的数据交个users这个状态数组。
        async created() {
          const users = await this.getUserList();
          this.users = users;
        },
        methods: {
          getTotal: function () {
            console.log("methods");
            return this.users.length + "个";
          },
            //在getUserList方法中,模拟一个异步请求。
          getUserList: function () {
            return new Promise((resolve) => {
              setTimeout(() => {
                resolve([
                  {
                    id: 1,
                    name: "张三",
                  },
                  {
                    id: 2,
                    name: "李四",
                  },
                  {
                    id: 3,
                    name: "老王",
                  },
                ]);
              }, 2000);
            });
          },
        },
        watch: {
          users: {
            immediate: true, //立即执行
            handler(newValue, oldValue) {
              this.totalCount = newValue.length + "个人";
            },
          },
        },
        // computed: {
        //   total() {
        //     console.log("aaa");
        //     // 计算属性是有缓存性:如果值没有发生变化,则页面不会重新渲染
        //     // return this.users.length + "个";
        //     let count = 0;
        //     for (let i = 0; i <= this.num; i++) {
        //       count += i;
        //     }
        //     return count;
        //   },
        // },
      });
    </script>
  </body>
</html>

上面的代码,还是对原有的“列表渲染”内容进行更改。

第一:将users的值定义为空数组

第二:定义getUserList方法,在该方法中模拟异步操作,最终返回的是一个Promise对象。

第三:在created阶段调用getUserList方法来获取数据,将获取到的数据赋值给users这个状态数组,注意这里需要将created修改成asyncawait的形式。同时还要注意created的执行时机:组件实例已创建时,执行created方法。

现在已经对生命周期有了一个简单的了解,下面我们继续探讨生命周期的内容。

11、生命周期探讨

Vue实例的生命周期,主要分为三个阶段,分别为

  • 挂载(初始化相关属性,例如watch属性,method属性)

    1. beforeCreate
    2. created
    3. beforeMount
    4. mounted
  • 更新(元素或组件的变更操作)

    1. beforeUpdate
    2. updated
  • 销毁(销毁相关属性)

    1. beforeDestroy
    2. destroyed

下面,我们再来看一道面试题:

关于Vue的生命周期,下列哪项是不正确的?()[单选题]
A、Vue 实例从创建到销毁的过程,就是生命周期。 
B、页面首次加载会触发beforeCreate, created, beforeMount, mounted, beforeUpdate, updated。 
C、created表示完成数据观测,属性和方法的运算,初始化事件,$el属性还没有显示出来。 
D、DOM渲染在mounted中就已经完成了。

分析:

选项A是没有问题的,Vue实例从创建到销毁的过程就是生命周期。

关于B选项,我们可以通过写一个程序来进行验证。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>生命周期</title>
  </head>
  <body>
    <div id="app">{{foo}}</div>
    <script src="./vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          foo: "foo",
        },
        beforeCreate() {
          console.log("beforCreate");
        },
        created() {
          console.log("created");
        },
        beforeMount() {
          console.log("beforeMount");
        },
        mounted() {
          console.log("mounted");
        },
        beforeUpdate() {
          console.log("beforeUpdate");
        },
       updated() {
          console.log("updated");
        },
        beforeDestroy() {
          console.log("beforeDestroy");
        },
        destroyed() {
          console.log("destroyed");
        },
      });
    </script>
    <script></script>
  </body>
</html>

在上面的代码中,我们将所有的钩子函数都添加上了,然后打开浏览器,看下执行结果:

beforCreate
created
beforeMount
mounted

以上就是初次加载时所执行的钩子函数,并没有beforeUpdateupdated,所以选项B是错误的。

那么beforeUpdateupdated什么时候会执行呢?是在,组件或者是元素更新的时候。

下面,我们来测试一下,看一下效果。

首先增加一个"更新"按钮

  <div id="app">
      {{foo}}
      <button @click="update">更新</button>
    </div>

对应的update方法的实现如下:

  methods: {
          update: function () {
            this.foo = "hello";
          },
        },

update方法中,修改了foo属性的值。打开浏览器,单击“更新”按钮后,看到的效果如下:

beforeUpdate
updated

通过以上的测试,可以验证在更新元素的时候,会执行在“更新”阶段的钩子函数。

下面,我们在测试一下,看一下“销毁”阶段的钩子函数的执行。

 <div id="app">
      {{foo}}
      <button @click="update">更新</button>
      <button @click="destroy">销毁</button>
    </div>

在上面的代码中增加了一个销毁的按钮,对应的destroy方法的实现如下:

    methods: {
          update: function () {
            this.foo = "hello";
          },
          destroy: function () {
            //销毁资源
            this.$destroy();
          },
        },

destroy方法中,调用了系统中的$destroy方法销毁了所有资源,这时会触发销毁阶段的钩子函数,所以这时会输出

beforeDestroy
destroyed

这时,如果你去单击“更新”按钮,就会发现什么效果也没有了,也就是无法完成元素的更新了,因为元素已经被销毁了。

下面,我们通过官方的生命周期图来再次看一下整个生命周期的流程。也是为了看一下上面所出题的CD的选项是说法否正确。

lifecycle.png beforeCreate: Vue实例初始化之后,以及事件初始化,以及组件的父子关系确定后执行该钩子函数,一般在开发中很少使用

created: 在调用该方法之前,初始化会被使用到的状态,状态包括props,methods,data,computed,watch.

而且会实现对data中属性的监听,也就是在created的时候数据已经和data属性进行了绑定。(放在data中的属性当值发生改变的时候,视图也会改变)。同时也会对传递到组件中的数据进行校验。

所以在执行created的时候,所有的状态都初始化完成,我们也完全可以在该阶段发送异步的ajax请求,获取数据。

但是,在created方法中,是无法获取到对应的的$el选项,也就是无法获取Dom. 所以说上题中选项c的说法是正确的。

如下代码所示:

        created() {
          console.log("created");
          console.log("el===", this.$el);// undefined
          console.log("data==", this.$data);// 可以获取数据
          console.log("foo==", this.foo);//可以获取数据
        },

created方法执行完毕后,下面会判断对象中有没有el选项。如果有,继续执行下面的流程,也就是判断是否有template选项,如果没有el选项,则停止整个生命周期的流程,直到执行了vm.$mount(el)

后,才会继续向下执行生命周期的流程。

下面我们测试一下:

    <script>
      const vm = new Vue({
        // el: "#app",  //去掉了el选项
        data: {
          foo: "fooData",
        },
        methods: {
          update: function () {
            this.foo = "hello";
          },
          destroy: function () {
            //销毁资源
            this.$destroy();
          },
        },
        beforeCreate() {
          console.log("beforCreate");
        },
        created() {
          console.log("created");
          console.log("el===", this.$el);
          console.log("data==", this.$data);
          console.log("foo==", this.foo);
        },
        beforeMount() {
          console.log("beforeMount");
        },
        mounted() {
          console.log("mounted");
        },
        beforeUpdate() {
          console.log("beforeUpdate");
        },
        updated() {
          console.log("updated");
        },
        beforeDestroy() {
          console.log("beforeDestroy");
        },
        destroyed() {
          console.log("destroyed");
        },
      });
    </script>

在上面的代码中,我们将el选项去掉了,运行上面的代码后,我们发现执行完created方法后,整个流程就停止了。

现在,我们不添加el选项,但是手动执行vm.$mount(el),也能够使暂停的生命周期进行下去。

如下代码所示:

 <script>
      const vm = new Vue({
        // el: "#app",//去掉了el选项
        data: {
          foo: "fooData",
        },
        methods: {
          update: function () {
            this.foo = "hello";
          },
          destroy: function () {
            //销毁资源
            this.$destroy();
          },
        },
        beforeCreate() {
          console.log("beforCreate");
        },
        created() {
          console.log("created");
          console.log("el===", this.$el);
          console.log("data==", this.$data);
          console.log("foo==", this.foo);
        },
        beforeMount() {
          console.log("beforeMount");
        },
        mounted() {
          console.log("mounted");
        },
        beforeUpdate() {
          console.log("beforeUpdate");
        },
        updated() {
          console.log("updated");
        },
        beforeDestroy() {
          console.log("beforeDestroy");
        },
        destroyed() {
          console.log("destroyed");
        },
      });
      vm.$mount("#app");//添加了$mount方法
    </script>

运行上面的代码,可以看到,虽然vm对象中没有el参数,但是通过$mount(el)动态添加的方式,也能够使生命周期顺利进行。

我们继续向下看,就是判断在对象中是否有template选项。

第一:如果Vue实例对象中有template参数选项,则将其作为模板编译成render函数,来完成渲染。

第二:如果没有template参数选项,则将外部的HTML作为模板编译(template),也就是说,template参数选项的优先级要比外部的HTML

第三:如果第一条,第二条件都不具备,则报错

下面,我们看一下添加template的情况。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>生命周期2</title>
  </head>
  <body>
    <script src="./vue.js"></script>
    <div id="app"></div>
    <script>
      const vm = new Vue({
        el: "#app",
        template: "<p>Hello {{message}}</p>",
        data: {
          message: "vue",
        },
      });
    </script>
  </body>
</html>

以上是在Vue实例中添加template的情况。

那么这里有一个比较有趣的问题就是,当模板同时放在template参数选项和外部HTML中,会出现什么情况呢?

如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>生命周期2</title>
  </head>
  <body>
    <script src="./vue.js"></script>
    <div id="app">
      <p>你好</p>
    </div>
    <script>
      const vm = new Vue({
        el: "#app",
        template: "<p>Hello {{message}}</p>",
        data: {
          message: "vue",
        },
      });
    </script>
  </body>
</html>

在上面的代码中,我们添加了template属性,同时也在外部添加了模板内容,但是最终在页面上显示的是Hello vue 而不是“你好”。就是因为template参数的优先级比外部HTML的优先级要高。

当然,我们在开发中,基本上都是使用外部的HTML模板形式,因为更加的灵活。

在这里,还需要你再次思考一个问题,就是为什么先判断 el 选项,然后在判断template选项呢?

其实通过上面的总结,我们是可以完全总结出来的。

就是因为Vue需要通过el的“选择器”找到对应的template.也就是说,Vue首先通过el参数去查找对应的template.如果没有找到template参数,则到外部HTML中查找,找到后将模板编译成render

函数(Vue的编译实际上就是指Vue把模板编译成render函数的过程)。

下面,我们继续看一下生命周期的流程图。

接下来会触发beforeMount这个钩子函数:

在执行该钩子函数的时候,虚拟DOM已经创建完成,马上就要渲染了,在这里可以更改data中的数据,不会触发updated, 其实在created中也是可以更改数据,也不会触发updated函数

测试代码如下:

  beforeMount() {
          console.log("beforeMount");
          console.log("beforeMount el===", this.$el);
          console.log("data==", this.$data);
          //this.foo = "abc"; //修改数据
          console.log("foo==", this.foo);
        },

通过上面的代码,我们可以获取el中的内容,同时也可以修改数据。

但是,这里需要注意的输入的el中的内容,{{foo}}还没有被真正的数据替换掉。而且对应的内容还没有挂载到页面上。

下面执行了Create VM.$el and replace "el" with it

经过这一步后,在模板中所写的{{foo}}会被具体的数据所替换掉。

所以下面执行mounted的时候,可以看到真实的数据。同时整个组件内容已经挂载到页面中了,数据以及真实DOM都已经处理好了,可以在这里操作真实DOM了,也就是在mounted的时候,页面已经被渲染完毕了,在这个钩子函数中,我们可以去发送ajax请求。

  mounted() {
          console.log("mounted");
          console.log("mounted el===", this.$el);
          console.log("data==", this.$data);
          console.log("foo==", this.foo);
        }

所以说,最开始的问题中,D选项:DOM渲染在mounted中就已经完成了这句话的描述也是正确的。

下面继续看生命周期的流程,如下图所示:

update.png

当整个组件挂在完成后,有可能会进行数据的修改,当Vue发现data中的数据发生了变化,会触发对应组件的重新渲染,先后调用了beforeUpdateupdated钩子函数。

updated之前beoreUpdate之后有一个非常重要的操作就是虚拟DOM会重新构建,也就是新构建的虚拟DOM与上一次的虚拟DOM树利用diff算法进行对比之后重新渲染。

而到了updated这个方法,就表示数据已经更新完成,dom也重新render完成。

下面如果我们调用了vm.$destroy方法后,就会销毁所有的资源。

destroyed.png

首先会执行beforeDestroy 这个钩子函数,这个钩子函数在实例销毁前调用,在这一步,实例仍然可用。

在该方法中,可以做一些清理的工作,例如:清除定时器等。

但是执行到destroyed钩子函数的时候,Vue实例已经被销毁,所有的事件监听器会被移除,所有的子实例也会被销毁。

最后做一个简单的总结:

beforeCreate( )// 该钩子函数执行时,组件实例还未创建.
created()//组件初始化完毕,各种数据可以使用,可以使用ajax发送异步请求获取数据
beforeMounted()// 未执行渲染,更新,虚拟DOM完成,真实DOM未创建
mounted()// 初始化阶段结束,真实DOM已经创建,可以发送异步请求获取数据,也可以访问dom元素
beforeUpdate()//更新前,可用于获取更新前各种状态数据
updated()//更新后执行该钩子函数,所有的状态数据是最新的。
beforeDestroy() // 销毁前执行,可以用于一些定时器的清除。
destroyed()//组件已经销毁,事件监听器被移除,所有的子实例也会被销毁。