美文网首页
Vue.js破冰系列-5组件基础(一)

Vue.js破冰系列-5组件基础(一)

作者: 书上得来终觉浅 | 来源:发表于2019-09-25 15:08 被阅读0次

组件(Component)是可复用的Vue实例,这句话给了我们两个信息,可复用Vue实例。可复用就是能够重复使用。前面的文章中,我们通过new的方式来创建Vue的根实例,它是整个应用的入口点。而组件是以标签的形式嵌入到HTML模板中,Vue在解析模板时会实例化该组件。

组件是资源独立的,组件在系统内部可复用,组件和组件之间也可以嵌套。

1 注册组件

组件分为全局注册和局部注册两种,两者的区别在于:

  • 使用范围不同,全局注册的组件可在任何vue实例中使用,局部注册的组件只在注册该组件的vue实例的作用域范围内有效。
  • 注册方式不同,全局注册使用vue全局APIVue.component()方法注册,局部注册是在vue实例中使用components选项注册。

从使用范围可以得出,更通用的组件一般用全局注册,而特定场景的业务组件一般使用局部注册。

1.1 全局注册

全局注册使用的是Vue.component 全局API,代码如下:

Vue.component('my-component',{
  //vue实例选项
});

全局注册方法有两个参数,第一个参数是组件的名称,第二个参数为组件的参数选项。组件名称有kebab-case(烤串样式,使用短横线分隔)和PascalCase(帕斯卡拼写法,首字母大写,单词开头大写)2种命名方式,推荐使用kebab-case的方式定义组件的名称。Vue实例通过这个名称引用该组件。

组件的参数选项与前面介绍的根实例的参数选项基本相同,什么data,methods,computed,watch等等,不过还是有部分差别:

  • el选项,el选项作为Vue的挂载点,只有根实例才有
  • data选项,在根实例中我们是以对象的形式定义,而在组件中,必须以函数的方式返回数据对象,原因在于组件是可复用的,也就是说,一个组件可以创建多个实例,如果data还是和根实例一样,以对象的形式定义,那么多个实例将引用同一个数据对象,当一个实例修改了data中的数据,其他实例中的data数据都会被修改,从而造成意想不到的结果。而使用函数的方式来返回对象,确保了每个组件实例的数据对象都是一个全新的副本数据。使用函数方式返回数据对象时,也不能返回一个外部对象的引用,原因同上。
<body>
  <div id="app">
    <my-component></my-component>
  </div>
  
  <script type="text/x-template" id="globalComponent">
    <div>这是全局注册组件的内容:{{msg}}</div>
  </script>
  
  <script>
    //全局组件组成
    Vue.component('my-component',{
      template:"#globalComponent",
      //data数据选项必须使用函数的形式返回数据对象
      data(){
        return {
          msg:"Hello World!"
        }
      }
    });
    var vm = new Vue({
      el:"#app",
    });
  </script>
</body>

1.2 局部注册

局部注册使用实例的components选项,其后接对象,对象的键是组件的名称,值为组件的参数对象,定义如下:

<div id="app">
  <component-a></component-a>
  <component-b></component-b>
</div>
<script>
  var componentA = {/** 组件的数据选项*/}
    var componentB = {/** 组件的数据选项*/}

    var vm = new Vue({
    el:"#app",
    //键是局部组件的名称,值为局部组件的参数选项
    components:{
        component-a:componentA,
        component-b:componentB,
    }
    })
</script>  

在使用ES6中,可以使用属性的简洁表示法,在对象中直接写入变量或函数,属性名就是变量名,属性值就是变量的值。

new Vue({
  components:{
    componentA,
    componentB
  }
})

2 递归调用组件

不论组件是全局注册还是局部注册,都可以实现递归调用。在说递归之前,需要介绍vue的name选项,name选项只能用在组件中,它在根实例中不起作用。组件使用name选项后,可以理解为该组件在其内部为自己定义了一个名称。

上面我们提到的全局注册函数Vue.component(id,{})中id,以及局部注册components对象的键,他们也是组件的名称,只不过这个名称用于组件的外部调用。全局组件设name后,当递归调用自己时,可以使用外部名称,也可使用内部名称。

<body>
  <div id="app">
    <g-component :menus="menus"></g-component>
  </div>

  <!--定义全局组件模板-->
  <script id="compo-ui" type="text/x-template">
    <ul>
      <li>{{menus.name}}</li>
      <template v-if="hasChildren">
        <!-- 使用内部名称调用自己 -->
        <compo v-for="item in menus.children" :menus="item"></compo>
        <!-- 也可以使用外部名称调用自己 -->
        <!-- <g-component v-for="item in menus.children" :menus="item"></g-component> -->
      </template>
    </ul>
  </script>

  <script>
    var params = {
      //定义内部名称
      name: "compo",
      template: "#compo-ui",
      props: ["menus"],
      computed: {
          hasChildren: function () {
              let { children } = this.menus;
              return (children && children instanceof Array && children.length > 0);
          }
      },
    }

    //组成全局组件,g-component是外部名称
    Vue.component('g-component',params);

    var vm = new Vue({
      el: "#app",
      data: {
        menus: {
          name: "总公司",
          children: [
            {name: "分公司1", children: [ { name: "部门1" }, { name: "部门1" },]},
            {name: "分公司2", children: [ { name: "部门1" }, { name: "部门1" },]},
            {name: "分公司3"}
          ]
        },
      },
    });
  </script>
</body>

3 内置组件

Vue为我们提供了5个内置组件,这5个组件我们可以直接使用,分别是:

组件 说明
component 动态组件,相当于一个占位符,根据条件动态的渲染一个组件
transition 单个组件的过度效果
transition-group 一组组件的过度效果
keep-alive 保持其子组件的状态
slot 内容分发插槽

本节我们将介绍component、keep-alive和slot内置组件,而关于动画效果的组件后续在讨论。

3.1 动态组件component

component组件时本身不会被渲染,它会根据条件动态的选择要渲染的组件。一般我们在定义组件时,会通过props来接收外部的数据,component这个内置组件也有两个props:

  • is 表示被渲染的组件,可以是被渲染组件在注册时的名称,或者是定义组件的选项参数对象
  • inline-template表示是否为内敛模板,很少用到
<body>
  <div id="app">
    <button @click="btnChangeClicked">切换</button>
    <!-- component组件不会被渲染,真正被渲染的是currentComponent指向的组件 -->
    <component :is="currentComponent" data="compb Title" @clicked="compClicked"></component>
  </div>
  <script>
    var compb = {
      template: "<h3 @click='clicked'>{{data}}</h3>",
      props:["data"],
      methods:{
        clicked(){
          this.$emit('clicked')
        }
      }
    }
    var vm = new Vue({
      el: "#app",
      data:{
        currentComponent:"compa",
      },
      components:{
        compa:{template:"<h3>组件A</h3>"},
        compb
      },
      methods:{
        btnChangeClicked(){
          this.currentComponent=this.currentComponent=="compa"?"compb":"compa";
        },
        compClicked(){
          console.log("动态组件也可以接收事件")
        }
      }
    });
  </script>
</body>

动态组件可以向普通的组件一样,传值和发送/接收事件。

3.2 动态组件状态保持

上面说了动态组件会根据条件,选择渲染那个组件。当从组件A切换到组件B时,组件A会被销毁,再次切换到组件A时,它又会被重新创建。这一点上,他和v-if动态渲染组件是一样的。

vue使用keep-alive内置组件来保证非活动状态的组件不会被销毁,并保留了它的状态。

<div id="app">
    <button @click="btnChangeClicked">切换</button>
    <keep-alive>
        <component :is="currentComponent"></component>
    </keep-alive>
</div>

<script>
    var compb = {
        template: "<div><input type=’text‘></div>",
        created() {
            console.log("组件B被创建");
        },
        mounted() {
            console.log("组件B被挂载");
        },
        destroyed() {
            console.log("组件B被销毁");
        }
    }
    ...
</script>

上面的例子中,第一次切换到B时,由于没有B组件,所以会执行created和mounted钩子函数。因为keep-alive内置组件,所以,当切换到A时,B组件并不会调用destoryed方法,即B组件没有被销毁。再次切换到B时,不会调用created和mounted方法。

Keep-alive有以下几个props:

  • include 要被缓存的组件名称,表达式的值可以是字符串、数组或是正则表达式。当为字符串时,使用逗号隔开。名称首先考虑内部名称(name选项),如果没有内部名称,则使用组件外部名称。内部名称优先级高于外部名称。
  • exclude 不被缓存的组件名称,表达式的值类型同上
  • max 最大缓存多少个组件,当缓存的组件达到最大值后,vue会按章最近访问顺序,将最远没有被访问的组件从缓存队列中删除,此时,被移除的组件会被销毁。
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

<keep-alive :max="10">
  <component :is="view"></component>
</keep-alive>

3.3 插槽slot

slot(插槽)组件可以理解为html模板中的占位符,它会被父组件传递过来的内容替换,这一过程叫做内容分发。这里的html模板是子组件中的html模板,而内容是父组件中传递过来的。slot组件中也可定义内容,如果父组件没有内容分发到子组件,那么slot的内容会被渲染。

<div id="app">
  <button @click="count++">父组件点我</button>
  <child-compo>
    <p>替换子组件child-compo中的slot内置组件</p>
    <p>你点击了 {{count}} 次</p>
  </child-compo>
</div>

<script type="text/template" id="childCompoUI">
  <div>
    <slot>
      <h4>我是slot的内容,如果父组件没有内容分发,会渲染我,如果有,会替换我</h4>
    </slot>
  </div>
</script>
<script>
  var childCompo = {
    template: "#childCompoUI",
  };

  var vm = new Vue({
    el: "#app",
    components:{
        childCompo
    },
    data:{
      count:0,
    }
  });
</script>

使用slot插槽,父组件要分发的内容包裹在子组件的标签中,这种写法与前面提到的子组件的props,props是以标签属性的形式存在。

3.3.1编译作用域

上面的例子中,父组件的分发的内容会替换子组件的slot标签。根据我们正常的理解,这个程序会报错, 因为分发的内容中有一个插值表达式{{count}},它读取了count的值,但是在子组件中,并没有定义count数据项,不过实际上,这个程序是可以正常运行的,原因就在于编译作用域。

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

也就是说,虽然分发的内容会替换子组件的slot,但是其作用域还是在父组件中,所以count是对应父组件中的内容。通过编译作用域,我们可以推断出,父组件分发的内容是不能读取子组件的数据,如下:

<!-- 程序会报错,父组件中没有定义message -->
<div id="app">
  <child-compo>
    <p>我想读取子组件的数据: {{message}} ,不过我读取不了</p>
  </child-compo>
</div>

<script>
  var childCompo = {
    template: "#childCompoUI",
    data(){
      return{
        message:"我是子组件的消息"
      }
    }
  };
</script>

3.3.2具名插槽

在子组件中的html模板中,可以定义多个slot标签,比如,我们在子组件中定义一个结构,内容由父组件来分发。那么父组件如何找到对应的插槽呢?我们可以通过为slot组件设置一个名字,父组件通过slot的名字分发内容。具有名字的slot组件我们称之为具名插槽。

slot组件通过name属性设置名称。前面我们在定义slot时,没有设置name,Vue会默认给name加上一个default名字,这种插槽也叫默认插槽。父组件中推荐使用v-slot指令的参数部分指定插槽的名称,也可以在标签中使用slot属性指定插槽名,使用slot属性这种方式在vue 2.6版本后被废弃,不推荐这种方式。不过对于以前的代码,我们还是需要看得懂这种写法。

<body>
  <div id="app">
    <child-compo>
      <!-- 使用v-slot指定具名插槽 -->
      <template v-slot:header>
          <h4>系统提示</h4>
      </template>
      <p>你确定要删除XX?</p>
      <!-- v-slot简写为#,v-bind简写为:,v-on简写为@ -->
      <template #footer>
          <button>取消</button>
          <button>确定</button>
      </template>
    </child-compo>
    <!-- slot标签属性的写法已被废弃,能看懂就行 -->
        <!-- <child-compo>
                        父组件通过slot属性指定具名插槽
            <h4 slot="header">系统提示</h4>
            <p>你确定要删除XX?</p>
            <div slot="footer">
                <button>取消</button>
                <button>确定</button>
            </div>
        </child-compo> -->
  </div>

  <!--子组件定义-->
  <script type="text/template" id="childCompoUI">
    <div class="container">
      <header>
            <!--具名插槽,名称为header-->
          <slot name="header"></slot>
      </header>
      <main>
            <!--默认插槽,默认名称为default-->
          <slot></slot>
      </main>
      <footer>
            <!--具名插槽,名称为footer-->
          <slot name="footer"></slot>
      </footer>
    </div>
  </script>
  <script>
      var childCompo = {
          template: "#childCompoUI",
      };

      var vm = new Vue({
          el: "#app",
          components: {
              childCompo
          }
      });
  </script>
</body>

这里需要注意2点:

  • v-slot指令只能加在<template>标签上。
  • 父组件中要分发的内容如果没有指定具名插槽,会将内容分发到默认插槽中,上面的<p>你确定要删除XX?</p>会替换子组件html模板中main的slot。

3.3.3 作用域插槽

上面我们在谈编译作用域的时候说了,父组件分发的内容是不能读取子组件的数据的。但是有些时候,父组件需要读取子组件的数据。这时,可以使用作用域插槽,作用域插槽是将子组件的数据通过v-bind指令绑定到slot上,父组件通过v-slot指令获取子组件的数据。

  • 一个slot既可以是作用域插槽也可以是具名插槽。
  • 2.6版本之前使用scope的方式获取作用域插槽的数据,该方式已被废弃
<body>
  <div id="app">
    <child-compo>
      <!-- v-slot指令参数部分指定了具名插槽。v-slot指令的表达式部分,定义了作用域插槽绑定数据对象的名称(作用域插槽将绑定数据封装到一个对象中,这个名称就是对象的引用),这里是tmp -->
      <template v-slot:header="tmp">
        <h4>{{tmp.greeting}}  {{tmp.username}}</h4>
      </template>
    </child-compo>
  </div>

  <script type="text/template" id="childCompoUI">
    <div class="container">
      <header>
        <!-- 该插槽既是具名插槽又是作用域插槽,它绑定了两个数据,绑定的属性名可以任意取名,这个属性名被父组件中的使用 -->
        <slot name="header" :greeting="message" :username="userName"></slot>
      </header>
    </div>
  </script>
  <script>
    var childCompo = {
      template: "#childCompoUI",
      data(){
        return {
          userName:"张三",
          message:"晚上好!"
        }
      }
    };

    var vm = new Vue({
      el: "#app",
      components: {
          childCompo
      }
    });
  </script>
</body>

这里使用v-slot:header="tmp"指令指定了插座的,并获取插座内部的数据(使用tmp引用)。更进一步,我们可以使用es6的解构赋值将tmp这个中间变量去掉:

<template v-slot:header="{greeting, username}">
    <h4>{{greeting}}  {{username}}</h4>
</template>

相关文章

网友评论

      本文标题:Vue.js破冰系列-5组件基础(一)

      本文链接:https://www.haomeiwen.com/subject/ksvgyctx.html