Vue知识点串讲


一、Vue知识点串讲

复习一下Vue中的核心知识点。

复习完基本的知识点以后,后面再来看一下其它的面试内容

1、基本使用

下面,先来看一段最简单的代码,如下所示:



  
    
    
    Vue基本使用
  
  
    
{{msg}}

在上面的代码中,创建了vue的实例,并且指定了数据,最终数据展示在idapp的这个div中,并且在停顿了1秒中以后,通过Vue的实例来修改对应的msg数据。

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

简单的理解:就是vue.js帮我们封装了数据和dom对象操作的映射,我们只需要关心数据的逻辑处理,数据的变

化就能够自然的通知页面进行页面的重新渲染。

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

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

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

View:指的就是视图部分

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

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

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

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

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

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

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

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

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

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

在后面的课程中,我们还会深入探讨这块内容,包括我们自己模拟实现一个数据驱动的框架。

以上内容也是面试的时候,会问到的问题。

2、模板语法

2.1 属性绑定

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

{{msg}}

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

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

    

{{msg}}

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

  

3、 列表渲染

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

基本实现的代码如下:



  
    
    
    列表渲染
  
  
    
  • 编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}

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

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

4、v-model

在前面讲解vue简介的时候,我们说过,如果model中的数据发生了改变,会通过ViewModel通知View更新数据,这个效果前面我们也已经演示了,现在演示一下当view中的数据发生了变化后,怎样通过ViewModel来通知model来完成数据的更新。

其实这就是我们常说的,“双向数据绑定”

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

  
  

具体代码实现如下:



  
    
    
    
    双向数据绑定
    
  

  
    

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

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

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

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

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

5、v-on

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




    
    
    
    Document
    



    
{{name}}

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


带参数的形式如下:





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

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

{{name}}

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的绑定。

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

   

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

  
  • 编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
  • 在上面的代码中,我们可以看到,给li标签绑定了class属性,同时actived的值为true,表示给li添加actived样式。

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

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

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

    data中定义selectItem.

    如下所示:

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

    完整 代码如下:

    
    
      
        
        
        列表渲染
        
      
      
        
    • 编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}

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

    
            
  • 编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
  • 通过上面的代码,可以看到通过绑定style的方式来处理样式是非常麻烦的。

    7、条件渲染

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

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

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

        

    没有任何用户数据

    • 编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}

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

    否则渲染整个列表。

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

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

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

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

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

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

    8、计算属性

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

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

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

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

    
        
    全名:{{firstName + lastName}}
    全名:{{fullName}}

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

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

          

    总人数:{{users.length+"个"}}

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

    下面对代码进行改造:

      

    总人数:{{total}}

    计算属性实现:

      v
    

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

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

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

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

    下面我们测试一下:

     

    总人数:{{total}} 总人数:{{total}}

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

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

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

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

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

     

    总人数:{{total}} 总人数:{{total}} 总人数:{{getTotal()}} 总人数:{{getTotal()}}

    在上面的代码中,调用了两次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中的数据变化,如果数据一旦发生变化就通知侦听器所绑定方法,来执行相应的操作。从这一点上,与计算属性是非常类似的。

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

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

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

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

     

    总人数:{{totalCount}}

    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侦听器的应用场景。

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

    具体的实现代码如下:

    
    
      
        
        
        侦听器
      
      
        
    用户名 {{message}}

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

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

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

    第一点:语境上的差异:

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

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

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

    例如如下代码:

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

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

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

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

    10、生命周期简介

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

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

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

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

    
    
      
        
        
        列表渲染
        
      
      
        

    没有任何用户数据

    • 编号:{{item.id}} 姓名:{{item.name}}---索引:{{index}}

    总人数:{{totalCount}}

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

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

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

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

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

    11、生命周期探讨

    在这一小节中,我们看一下vue生命周期中其它的一些钩子函数内容。

    其实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选项,我们可以通过写一个程序来进行验证。

    
    
      
        
        
        生命周期
      
      
        
    {{foo}}

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

    beforCreate
    created
    beforeMount
    mounted
    

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

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

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

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

      
    {{foo}}

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

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

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

    beforeUpdate
    updated
    

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

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

     
    {{foo}}

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

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

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

    beforeDestroy
    destroyed
    

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

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

    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)

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

    下面我们测试一下:

        
    

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

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

    如下代码所示:

     
    

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

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

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

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

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

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

    
    
      
        
        
        生命周期2
      
      
        
        

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

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

    如下代码所示:

    
    
      
        
        
        生命周期2
      
      
        
        

    你好

    在上面的代码中,我们添加了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中就已经完成了这句话的描述也是正确的。

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

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

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

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

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

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

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

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

    最后做一个简单的总结:

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

    以上为生命周期的内容。

    12、组件化应用

    12.1 组件概述

    在这一小节中,重点要理解的就是组件的编程思想。

    组件表示页面中的部分功能(包含自己的逻辑与样式),可以组合多个组件实现完整的页面功能。

    如下图所示:

    问题是,如何确定页面中哪些内容划分到一个组件中呢?

    但你如何确定应该将哪些部分划分到一个组件中呢?你可以将组件当作一种函数或者是对象来考虑(函数的功能是单一的),根据[单一功能原则]来判定组件的范围。也就是说,一个组件原则上只能负责一个功能。如果它需要负责更多的功能,这时候就应该考虑将它拆分成更小的组件。

    当然,在上图中,我们发现’Name‘和'Price' 表头 并没有单独的划分到一个组件中,主要考虑的是功能简单,就是展示的作用,所以没有划分到单独一个组件中。如果,该表头具有了一些比较复杂的功能,例如排序。那么这里可以单独的将表头内容划分到一个组件中。

    组件有什么特点呢?

    可复用、维护、可组合

    可复用:每个组件都是具有独立功能的,它可以被使用在多个场景中。

    可组合:一个组件可以和其它的组件一起使用或者可以直接嵌套在另一个组件内部。

    可维护:每个组件仅仅包含自身的逻辑,更容易被理解和维护。

    下面,看一下怎样创建组件?

    12.2 组件的基本使用

    组件具体的创建过程如下:

     Vue.component('index', {
                template: '
    我是首页的组件
    ' })

    第一个参数指定了所创建的组件的名字,第二个参数指定了模板。

    组件创建好以后,具体的使用方式如下:

    注意:1. 模板template中只能有一个根节点;2. 组件的名字,如果采用驼峰命令的话,在使用的时候,就要加上 “-”,比如组件名字叫indexA,那么在使用的时候就叫index-a。

    例如:

      Vue.component('componentA', {
                template: "
    创建一个新的组件
    " })

    组件的使用

       
    

    在Vue实例中所使用的选项,在组件中都可以使用,但是要注意data,在组件中使用时必须是一个函数。

    下面创建一个about组件。

      Vue.component('about', {
                template: '
    {{msg}}
    ', data() { return { msg: '大家好' } }, methods: { showMsg() { this.msg = "关于组件" } } })

    组件的使用如下:

      
    

    在组件中关于data不是一个对象,而是一个函数的原因,官方文档有明确的说明

    https://cn.vuejs.org/v2/guide/components.html

    组件创建完整的代码如下:

    
    
    
    
        
        
        
        组件创建
        
    
    
    
        

    在使用组件的时候,需要注意以下几点内容:

    第一点:data必须是一个函数

    关于这一点,官方文档有比较详细清楚的说明:https://cn.vuejs.org/v2/guide/components.html

    第二:组件模板中必须有一个跟元素。

    第三:组件模板内容可以使用模板字符串。

        Vue.component("about", {
            template: `
    {{msg}}
    `, data() { return { msg: "大家好", }; }, methods: { showMsg() { this.msg = "关于VUE组件"; }, }, });

    在上面的代码中,我们在组件的模板中使用类模板字符串,这样就可以调整对应的格式,例如换行等。

    第四:现在我们创建的组件是全局组件,可以在其它组件中使用。

    
    
      
        
        
        组件基本使用
      
      
        

    在上面的代码中,我们又创建了一个HelloWorld组件,并且在componentA组件中去使用了HelloWorld组件,这里还需要注意的一点就是,在componentA这个组件中使用HelloWorld这个组件的时候,可以使用驼峰命名的方式,但是在

    这个普通的标签模板中,必须使用短横线的方式,才能使用组件。

    12.3 局部组件注册

    我们可以在一个组件中,再次注册另外一个组件,这样就构成了父子关系。

    可以通过components 来创建对应的子组件。

    组件的创建过程如下:

    
    

    组件的使用

       

    完整代码如下:

    
    
    
    
        
        
        
        父子组件创建
        
    
    
    
        

    在上面的代码中,我们是在全局的father组件中,又创建了一个子组件son.

    那么son这个子组件也就是一个局部的组件。也就是它只能在father组件中使用。

    当然,我们在father中定义子组件son的时候,直接在其内部构件模板内容,这样如果代码非常多的时候,就不是很直观。

    所以这里,我们可以将son组件,单独的进行定义,然后在father组件中进行注册。

    改造后的代码如下所示:

    
    
      
        
        
        局部组件
        
      
      
        

    在上面的代码中,我们将son组件单独的进行了定义,这时注意写法,是一个对象的格式,在对象中包含了关于组件很重要的内容为data函数与template属性。

    同时在father组件中通过components属性完成了对son组件的注册。

    我们说过son组件是一个局部的组件,那么只能在其注册的父组件中使用。

    现在,我们可以测试一下:

    完整代码如下:

    
    
      
        
        
        局部组件
        
      
      
        

    在上面的代码中,我们又创建了一个全局的组件ComponentA,并且在该组件中使用了son组件,注意这里没有在ComponentA中使用components来注册son组件,而是直接使用。同时在

    中使用了ComponentA组件。这时在浏览器中,打开上面的程序,会出现错误。

    如果现在就想在ComponentA组件中使用son组件,就需要使用components来注册。

          Vue.component("ComponentA", {
            template: "
    ", components: { son: son, }, });

    现在在ComponentA组件中已经注册了son组件,这时刷新浏览器就不会出错了。

    在上面这些案例中,我们是在一个全局的组件中注册一个局部的组件,其实,我们也可以在Vue实例中,

    注册对应的局部组件。因为,我们也可以将vue实例作为一个组件。

    详细代码如下:

    
    
      
        
        
        局部组件
        
      
      
        

    在上面的代码中,我们又创建了一个组件HelloMsg

    然后将HelloMsg组件注册到了 Vue实例中,注意:在进行注册的时候的语法格式。

    左侧为组件的名称,由于这个组件创建的时候采用的是驼峰命名的方式,所以组件的名称采用短横线的方式。

    右侧为组件的内容。

    下面就可以在其

    中使用了。

    同理,在其他的组件中是无法使用HelloMsg组件的。

    13、组件通信

    13.1 父组件向子组件传值

    当我们将整个页面都拆分了不同的组件以后,这样就会涉及到组件之间的数据传递问题。

    常见的组件的通信可以分为三类:

    第一类: 父组件向子组件传递数据

    第二类: 子组件向父组件传递数据

    第三类:兄弟组件的数据传递。

    下面,我们先来看一下父组件向子组件传递数据的情况

    第一:子组件内部通过props接收传递过来的值。

    Vue.component('menu-item',{
     props:['title'] // props后面跟一个数组,数组中的内容为字符串,这个字符串可以当做属性类使用。
     template:'
    {{title}}
    ' })

    第二: 父组件通过属性将值传递给子组件

     
     
    

    下面看一下具体的案例演示:

    
        

    下面我们在看一个例子,这个例子是前面我们写的关于局部组件的案例,我们在这个案例的基础上实现组件的传值。

    
    
      
        
        
        局部组件
        
      
      
        

    在上面的代码中,我们首先给hello-msg 这个组件传递了一个属性title,该属性的值是固定的。在对应的HelloMsg组件内容定义props,来接收传递过来的title属性的值。然后在template模板中展示title的值。

    接下来,又在vue实例中指定了一个content的属性,下面要将该属性的值传递给HelloMsg组件。

        
    

    这里需要动态绑定的方式将content的值传递到HelloMsg组件。这里动态绑定的属性为pcontent,所以在HelloMsg组件内部,需要在props的数组中添加一个pcontent,最后在template模板中展示出pcontent的内容。

        // 定义HelloMsg组件
          const HelloMsg = {
            props: ["title", "pcontent"],
            data() {
              return {
                msg: "Hello World",
              };
            },
            template: `
    {{msg+'----------'+title+'-----------'+pcontent}}
    `, };

    通过上面的案例,我们可以看到,在子组件中可以使用props来接收父组件中传递过来的数据。

    但是,props在进行命名的时候,也是有一定的规则的。

    如果在props中使用驼峰形式,模板中需要短横线的形式,如下代码案例所示:

    Vue.component('menu-item',{
        //在JavaScript中是驼峰形式
        props:['menuTitle'],
        template:'
    {{menuTitle}}
    ' })

    下面看一下具体的代码演示:

    
    
      
        
        
        组件传值
      
      
        

    下面再来看一下props属性值的类型。

    props 可以接收各种类型的值。

    如下:

    字符串(String

    数值(Number)

    布尔值(Boolean)

    数组(Array)

    对象(Object)

    下面,将上面的类型都演示一下:

    
    
      
        
        
        props类型
      
      
        

    在上面的代码中,向menu-item组件中传递了各种类型的数据。

    注意:

      
    

    在上面的代码中,:num="10"表示传递的是数字,如果写成num='10' 表示传递的是字符,

    同理b="true"传递的是字符,如果修改成:b=true表示传递的是布尔类型。

    最后还传递了数组类型与对象类型的内容。

    13.2 子组件向父组件传值

    第一:子组件通过自定义事件向父组件传递信息。

    
    

    第二:父组件监听子组件的事件

    
    

    具体的实现步骤如下:

    1、构建基本的结构

         
    var vm = new Vue({ el: '#app', data: { } })

    2、构建相应的父组件。

      Vue.component('father', {
                template: '
    我的儿子叫{{mySonName}}
    ', data() { return { mySonName: '' } } }

    3、 构建相应的子组件, 并且单击子组件中的按钮给父组件传值。

      Vue.component('father', {
                template: '
    我的儿子叫{{mySonName}}
    ', data() { return { mySonName: '' } }, components: { son: { data() { return { myName: '小强' } }, template: '', methods: { emitMyName() { // 子组件传值给父组件需要用到$emit()方法,这个方法可以传递两个参数,一个是事件名称,一个是需要传递的数据 this.$emit('tellMyFatherMyName', this.myName) } } } } }

    4、父组件接收子组件传递过来的数据。

    注意在父组件中引用子组件,同时指定在子组件中定义的事件。

       Vue.component('father', {
                template: '
    我的儿子叫{{mySonName}}
    ', data() { return { mySonName: '' } }, methods: { getMySonName(data) { this.mySonName = data; } } }

    5、组件使用

      

    6、完整代码如下:

    
        

    13.3 兄弟组件之间数据传递

    兄弟组件传值,通过事件总线完成。

    1、定义父组件并且在父组件中,完成两个兄弟组件的创建。

      
    

    2、创建事件总线

    通过事件总线发射一个事件名称和需要传递的数据 。

      // 创建一个空的vue实例,作为事件总线
            var eventbus = new Vue()
              daughter: {
                        data() {
                            return {
                                myName: '小雪'
                            }
                        },
                        template: '',
                        methods: {
                            emitMyName() {
                                // 通过事件总线发射一个事件名称和需要传递的数据
                                eventbus.$emit('tellBroMyName', this.myName)
                            }
                        }
                    }
            
            
    

    3、通过eventbus的$on()方法去监听兄弟节点发射过来的事件

     son: {
                        data() {
                            return {
                                mySisterName: ''
                            }
                        },
                        template: '
    我妹妹叫{{mySisterName}}
    ', mounted() { // 通过eventbus的$on()方法去监听兄弟节点发射过来的事件 // $on有两个参数,一个是事件名称,一个是函数,该函数的默认值就是传递过来的数据 eventbus.$on('tellBroMyName', data => { this.mySisterName = data }) } },

    4、组件的使用

     

    5、完整的代码如下:

    
        

    14、组件插槽应用

    14.1 插槽基本使用

    生活中的插槽

    其实我们生活中有很多很多的插槽。比如电脑的USB插槽、插板中的电源插槽等等。每个插槽都有它们之间的价值。比如电脑的USB插槽,可以用来插U盘,链接鼠标,链接手机、音响等等,通过这些插槽,大大拓展了原有设备的功能。

    组件中的插槽

    组件中的插槽,让使用者可以决定组件内部的一些内容到底展示什么,也就是,插槽可以实现父组件向子组件传递模板内容。具有插槽的组件将会有更加强大的拓展性,

    下面看一个实际应用的例子来体会一下插槽的引用场景。

    三个页面中都有导航栏,基本结构都是一样的:左中右分别有一个东西,只是显示的内容不同而已。那我们如何来实现这种结构相似但是内容不同呢?
    ?你一定是想着,直接定义三个组件,然后在模板中分别显示不同的内容,对不对?恭喜你,你就快要被炒了。
    ?首先,如果我们封装成三个组件,显然不合适,比如每个页面都有返回,这部分的内容我们就要重复去封装
    ?其次,如果我们封装成一个,还是不合理,因为有些左侧是菜单栏,有些中间是搜索框,有些是文字。
    那我们该怎么办呢?其实很简单,用组件插槽。

    上面最佳的解决办法是将共性抽取到组件中,将不同暴露给插槽,一旦我们使用了插槽,就相当于预留了空间空间的内容取决于使用者

    如下图所示:

    通过上图,我们可以在父组件中使用子组件,同时由于在子组件中创建插槽slot,也就是相当于预留了空间,这时在父组件中使用子组件时,可以传递不同的内容。

    下面看一下插槽的应用

    基本使用方式

    第一:确定插槽的位置

    Vue.component('alert-box',{
     template:`
       
    子组件
    ` })

    在子组件中,通过确定出插槽的位置。

    第二:插槽内容

    Hello World
    

    向插槽中传递内容。

    下面看一下具体的代码:

    
    
      
        
        
        插槽基本使用
      
      
        
    程序出现了bug 程序出现了警告

    通过上面的代码我们可以看到,在alert-box这个组件中,定义了一个插槽,也就是预留了一个位置,下面使用该组件的时候,都可以向该插槽中传递数据。而标签中的内容就相当于是一个公共的内容了。

    当然在插槽中也是可以添加默认的内容的。

      
    程序出现了bug 程序出现了警告

    在上面的代码中,我们给插槽添加了默认的内容,如果在使用alert-box组件的时候,没有给插槽传递值,就会展示插槽中的默认内容。

    14.2 具名插槽

    所谓的具名插槽就是有名字的插槽。

    第一:插槽定义

    第二:插槽内容

    
      

    标题内容

    主要内容

    主要内容

    底部内容

    下面我们来看一下具体的代码实现

    
    
      
        
        
        具名插槽
      
      
        

    头部内容

    主要内容1

    主要内容2

    底部信息

    在上面的代码中,

    头部内容

    会插入到base-layout 组件的header这个插槽中。

    底部信息

    会插入到footer这个插槽中。

    剩余的内容会插入到默认的(没有名称)的插槽内。

    在上面的应用中,有一个问题就是,我们把插槽的名称给了某个html标签,例如p标签,这样就只能将该标签插入到插槽中。

    但是,在实际的应用中,有可能需要向插槽中插入大量的内容,这时就需要用到template标签。

    
    
      
        
        
        具名插槽
      
      
        
    中间内容区域的布局实现

    在上面的代码中,我们给template标签添加了插槽的名称,并且在template标签中嵌入了其它的多个标签,从而完成布局。

    在这里,可以统一查看浏览器端所生成的代码结构。

    14.3 作用域插槽

    应用场景:父组件对子组件的内容进行加工处理。这也是作用域插槽的一个很重要特性,

    下面我们通过一个例子来体会一下这句话的作用。

    首先,我们先创建一个用户列表。

    
    
      
        
        
        作用域插槽
      
      
        

    在上面的代码中,我们首先创建了一个user-list组件,在这个组件中接收父组件传递过来的用户数据,通过循环的方式展示传递过来的用户数据。

    现在,这里有一个新的需求,就是修改某个用户名的颜色,让其高亮显示。这个需求应该怎样来处理呢?

    我们是否可以在子组件user-list中实现这个功能呢?

    虽然可以,但是一般不建议你这么做,因为一个组件创建好以后,一般不建议修改。你可以想一下,如果这个组件是其它人创建的,而且很多人都在用,如果直接修改这个子组件,就会造成很多的问题。

    所以这里,还是从父组件中进行修改。也是通过父组件来决定子组件中的哪个用户名进行高亮显示。

    下面对代码进行修改:

    
    
      
        
        
        作用域插槽
      
      
        

    通过上面的代码可以看到,为了能够实现父组件决定子组件中哪个用户名能够高亮显示,需要在设计子组件的时候,为其添加对应的插槽。

    template: `
    • {{item.userName}}
    `,

    在子组件的template模板中,添加了插槽,同时为其动态绑定一个属性info(这个属性的名字是可以随意命名的),该属性的值为用户的信息。

    绑定该属性的目的就是为了能够在父组件中获取用户的信息。

    下面看一下父组件中的修改

     

    父组件在使用子组件user-list的时候,这里为其添加了template这个标签,而且这个标签的属性slot-scope是固定的,为其指定了一个值为slotProps,该值中,存储的就是从子组件中获取到的用户数据。

    所以接下来通过slotProps获取info(注意这里要与子组件中的slot属性保持一致)中的用户数据。然后进行判断,如果用户编号为2的,为其加错,否者正常展示。

    通过以上的案例,我们可以看到父组件通过作用域插槽实现了对子组件中数据的处理。其实这也就是为什么叫做作用域插槽的原因:

    是因为模板虽然是在父级作用域(父组件)中渲染的,却能拿到子组件的数据。

    14.4. 作用域插槽案例

    下面,我们通过一个列表的案例,来体会一下作用域插槽的应用。

    首先我们先来做一个基本的列表组件

    这里,我们首先使用的是具名插槽完成的,如下代码所示:

    
    
      
        
        
        作用域插槽案例
      
      
        

    在上面的代码中,我们在子组件my-list中使用了具名插槽。然后父组件在使用子组件my-list的时候,可以通过template标签加上slot属性向具名插槽中传递数据。

    虽然以上的写法满足了基本的需求,但是作为组件的使用者,这样的一个组件会让我们感觉非常的麻烦,也就是我们在使用my-list这个组件的时候,还需要自己去编写content区域的循环逻辑。这样就比较麻烦了,下面对上面的代码在做一些修改。

    为了解决这个问题,我们可以把循环写到子组件中,这样我们在使用的时候,不需要写循环了,只是传递数据就可以了,这样就方便多了。其实这里我们就可以不用具名插槽了。

    所以修改后的代码如下:

    
    
      
        
        
        作用域插槽案例
      
      
        

    在上面的代码中,我们没有使用插槽,直接将数据传递到子组件my-list中,然后在该子组件中接收到数据,并进行了循环遍历。

    经过这一次的改造,满足了我们前面所提到的易用性问题,但是现在又有了新的问题,组件的拓展性不好。

    每次只能生成相同结构的列表,一旦业务需要发生了变化,组件就不再使用了。比如,我现在有了新的需求,在一个列表的每个列表项前面加上一个小的logo,我总不能又写一个新的组件来适应需求的变化吧?

    这里就可以使用作用域插槽来解决这个问题。

    具体的实现代码如下所示:

    
    
      
        
        
        作用域插槽案例
      
      
        

    在上面的代码中,我们首先在子组件my-list中,添加了作用域的插槽。

     
    • {{item.userName}}

    同时在父组件中,使用对应的插槽

     

    再回到开始的问题,作用域插槽到底是干嘛用的?很显然,它的作用就如官网所说的一样:将组件的数据暴露出去。而这么做,给了组件的使用者根据数据定制模板的机会,组件不再是写死成一种特定的结构。

    以上就是作用域插槽的应用,需要你仔细体会。

    那么,在这里再次问一个问题,就是在你所使用的Vue插件或者是第三方的库中,有没有遇到使用作用域插槽的情况呢?

    其实,比较典型的就是element-uitable组件,它就可以通过添加作用域插槽改变渲染的原始数据。

    如下图所示:

    14.5 插槽应用总结

    为什么要使用插槽

    组件的最大特性就是复用性,而用好插槽能大大提高组件的可复用能力。

    组件的复用性常见情形如在有相似功能的模块中,他们具有类似的UI界面,通过使用组件间的通信机制传递数据,从而达到一套代码渲染不同数据的效果

    然而这种利用组件间通信的机制只能满足在结构上相同,渲染数据不同的情形;假设两个相似的页面,他们只在某一模块(区域)有不同的UI效果(例如,前面所做的列表,发现可以显示不同的ui效果),以上办法就做不到了。可能你会想,使用 v-ifv-else来特殊处理这两个功能模块,不就解决了?很优秀,解决了,但不完美。极端一点,假设我们有一百个这种页面,就需要写一百个v-ifv-else-ifv-else来处理?那组件看起来将不再简小精致,维护起来也不容易。

    而 插槽 “SLOT”就可以完美解决这个问题

    什么情况下使用插槽

    顾名思义,插槽即往卡槽中插入一段功能块。还是举刚才的例子。如果有一百个基本相似,只有一个模块功能不同的页面,而我们只想写一个组件。可以将不同的那个模块单独处理成一个卡片,在需要使用的时候将对应的卡片插入到组件中即可实现对应的完整的功能页。而不是在组件中把所有的情形用if-else罗列出来(这里还是体会用户列表的案例)

    可能你会想,那我把一个组件分割成一片片的插槽,需要什么拼接什么,岂不是只要一个组件就能完成所有的功能?思路上没错,但是需要明白的是,卡片是在父组件上代替子组件实现的功能,使用插槽无疑是在给父组件页面增加规模,如果全都使用拼装的方式,和不用组件又有什么区别(例如,用户列表案例中需要其他的显示方式,需要在父组件中进行添加)。因此,插槽并不是用的越多越好

    插槽是组件最大化利用的一种手段,而不是替代组件的策略,当然也不能替代组件。如果能在组件中实现的模块,或者只需要使用一次v-else, 或一次v-else-ifv-else就能解决的问题,都建议直接在组件中实现。

    15、Vue组件化的理解

    关于Vue组件的内容,我们已经学习很多了,那么你能谈一下对Vue组件化的理解吗?

    其实这也是一个比较常见的面试题。

    当然,这个问题的面是非常广的。可以通过以下几点来描述:

    定义:组件是可复用的Vue实例,准确讲它是VueComponent的实例,继承自Vue

    优点:组件化可以增加代码的复用性,可维护性和可测试性。

    使用场景:什么时候使用组件?以下分类可以作为参数

    第一:通用组件:实现最基本的功能,具有通用性,复用性。例如按钮组件,输入框组件,布局组件等。(Element UI组件库就是属于这种通用的组件)

    第二:业务组件,用于完成具体的业务,具有一定的复用性。例如登录组件,轮播图组件。

    第三:页面组件,组织应用各部分独立内容,需要时在不同页面组件间切换,例如:商品列表页,详情页组件。

    如何使用组件

    • 定义:Vue.component()components选项

    • 分类:有状态组件(有data属性),functional

    • 通信:props$emit()/$on()provide/inject

    • 内容分发: