Vue.js中最重要的角色-组件

2,532 阅读12分钟

一、组件的定义

组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以表现为用 is 特性进行了扩展的原生 HTML 元素。所有的 Vue 组件同时也都是 Vue 的实例,所以可接受相同的选项对象 (除了一些根级特有的选项) 并提供相同的生命周期钩子。

二、组件的使用方法

注册组件就是利用Vue.component()方法,先传入一个自定义组件的名字,然后传入这个组件的配置。

  1. 全局注册
  <div id="app">
    <my-component></my-component>
  </div>
  <script>
    Vue.component('my-component', {
      template: '<div>我是一个组件</div>'
    })

    var app = new Vue({
      el: '#app',
      data: {}
    })
  </script>
  • 全局注册优点:所有的Vue实例都可以用;
  • 全局注册缺点:权限太大,容错率降低;
  1. 局部注册 局部注册在Vue实例下面的components选项中
  <div id="app">
    <app-component></app-component>
  </div>
  <script>
    var app = new Vue({
      el: '#app',
      data: {},
      components: {
        'app-component': {
          template: '<div>我是app局部注册的一个组件</div>'
        }
      }
    })
  </script>
  1. Vue组件的模板在某些情况下会受到html标签的限制,比如<table> 中只能还有 <tr> , <td> 这些元素,所以直接在table中使用组件是无效的,此时可以使用is属性来挂载组件:
  <table>
    <tbody is="my-component"></tbody>
  </table>

三、组件使用的注意事项

  1. html中, myMessagemymessage 是一致的,,因此在组件中的html中使用必须使用kebab­case(短横线)命名方式,在html中不允许使用驼峰。

  2. 在组件中, 父组件给子组件传递数据必须用短横线。在template中,必须使用驼峰命名方式,若为短横线的命名方式。则会直接保错。

  3. 在组件的data中,用this.XXX引用时,只能是驼峰命名方式。若为短横线的命名方式,则会报错。

  4. template中的内容必须被一个DOM元素包括 ,也可以嵌套。

  5. 在组件的定义中,除了template之外的其他选项—data,computed,methods

  6. data必须是一个方法

  <div id="app">
    <btn-component></btn-component>		
    <btn-component></btn-component>
  </div>
  <script>
    var app = new Vue({
      el: '#app',
      components: {
        'btn-component': {
          template: '<button @click="count++">{{count}}</button>',
          data: function(){
            return {
              count: 0
            }
          }
        }
      }
    })
  </script>

四、父组件给子组件 使用props传递数据

在组件中使用props来从父亲组件接收参数,注意,在props中定义的属性,都可以在组件中直接使用

  <div id="app">
    <child-component msg="我是来自父组件的内容"></child-component>
  </div>
  <script>
    var app = new Vue({
      el: '#app',
      components: {
        'child-component': {
          props: ['msg'],
          template: '<div>{{msg}}</div>'
        }
      }
    })
  </script>

propps来自父级,而组件中data return的数据就是组件自己的数据,两种情况作用域就是组件本身,可以在templatecomputedmethods中直接使用。

这里有一个问题,在上面的例子中msg是写死的内容,有的时候传递的数据并不是写死的,而是通过父级动态的数据,这个时候怎么做呢?我们可以通过v-bind来进行绑定。

  <div id="app">
    我是内容呀:<input type="text" v-model="parentmsg">
    <bind-component :msg="parentmsg"></bind-component>
  </div>
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        parentmsg: '今天的天气不太好'
      },
      components: {
        'bind-component': {
          props: ['msg'],
          template: '<div>{{msg}}</div>'
        }
      }
    })
  </script>

input v-model绑定的是父组件中的data选项中的数据,然后使用v-bind把绑定的数据传递给子组件,用props进行接受,并且可以在template中直接使用,这就是使用v-bind动态绑定父组件来的内容。

五、单向数据流

  • 解释:通过 props传递数据 是单向的了, 也就是父组件数据变化时会传递给子组件,但是反过来不行。
  • 目的:是尽可能将父子组件解稿,避免子组件无意中修改了父组件的状态。
  • 应用场景:业务中会经常遇到两种需要改变prop的情况。

一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件data 内再声明一个数据,引用父组件的prop 步骤一:注册组件; 步骤二:将父组件的数据传递进来,并在子组件中用props接收; 步骤三:将传递进来的数据通过初始值保存起来;

  <div id="app">
    <my-component msg="我是父组件传递的数据"></my-component>
  </div>
  <script>
    Vue.component('my-component',{
      props: ['msg'],
      template: '<div>{{count}}</div>',
      data: function(){
        return {
          //props 中的值可以通过 this.xxx 直接来进行获取
          count: this.msg
        }
      }
    })
    var app = new Vue({
      el: '#app',
      data: {  }
    }) 
  </script>

我们为什么不直接用this.msg,而是将传递进来的数据通过初始值保存起来呢?这样做有一个好处,之后只需要维护count就可以了,并且count是在其data中已经定义的数据。之后不管msg怎么变化,都会传给count

另一种情况就是prop作为需要被转变的原始值传入。这种情况用计算属性就可以了 步骤一:注册组件; 步骤二:将父组件的数据传递进来,并在子组件中用props接收; 步骤三:将传递进来的数据通过计算属性进行重新计算;

  <div id="app">
    <!-- 通过 input 中输入的数据直接改变 div 的宽度 -->
    请输入宽度:<input type="text" v-model="width">
    <width-component :width="width"></width-component>
  </div>
  <script>
    Vue.component('width-component', {
      props: ['width'],
      template: '<div :style="style"></div>',
      computed: {
        style: function(){
          return {
            width: this.width + 'px',
            height: '100px',
            background: 'skyblue'
          }
        }
      }
    })
    var app = new Vue({
      el: '#app',
      data: {
        width: 0
      }
    }) 
  </script>

六、数据验证

数据验证主要是对props传递过来的数据进行类型的验证,并且可以给它指定一些选项,比如说给它设定一个默认值或者说必须设置它的数据类型。验证的 type 类型可以是:

  • String
  • Number
  • Boolean
  • Object
  • Array
  • Function
  Vue.component ( 'my-compopent ', {
    props : {
      //必须是数字类型
      propA : Number ,
      //必须是字符串或数字类型
      propB : [String , Number] ,
      //布尔值,如果没有定义,默认值就是 true
      propC: {
        type : Boolean ,
        default : true
      },
      //数字,而且是必传
      propD: {
        type: Number ,
        required : true
      },
      //如果是数组或对象,默认值必须是一个函数来返回
      propE: {
        type : Array ,
        default : function () {
          return [] ;
        }
      },
      //自定义一个验证函数
      propF: {
        validator : function (value) {
          return value > 10;
        }
      }
    }
  });

七、组件通信

组件关系可分为父子组件通信、兄弟组件通信、跨级组件通信。

1. 自定义事件—子组件给父组件传递数据

  • 使用v-on除了监昕 DOM 事件外,还可以用于组件之间的自定义事件。
  • JavaScript 的设计模式 一一观察者模式, dispatchEventaddEventListener这两个方法。 Vue 组件也有与之类似的一套模式,子组件用$emit()来 触发事件 ,父组件用$on()来 监昕子组件的事件 。

第一步:自定义事件; 第二步: 在子组件中用$emit触发事件,第一个参数是事件名,后边的参数是要传递的数据; 第三步:在自定义事件中用一个参数来接受;

  <div id="app">
    <h3>你现在卡里余额是:{{total}}元</h3>
    <child-component @change="handleTotal"></child-component>
  </div>
  <script>
    Vue.component('child-component',{
      template: `
        <div>
          <button @click="handleincrease">+100</button>
          <button @click="handlereduce">-100</button>
        </div>
      `,
      data: function(){
        return {
          count: 2000
        }
      },
      methods: {
        handleincrease: function(){
          this.count = this.count + 100
          this.$emit('change', this.count)
        },
        handlereduce: function(){
          this.count = this.count - 100
          this.$emit('change', this.count)
        }
      }
    })
    var app = new Vue({
      el: '#app',
      data: {
        total: 2000
      },
      methods: {
        handleTotal: function(value){
          //此处的形参 value 就是传递过来的数据
          this.total = value
        }
      }
    }) 
  </script>

在这里自定义组件中,定义了两个button,一个加100,一个减100,点击加减按钮的时候都会触发一个事件,加100的时候把它组件中定义的 count加100,减100的时候就是减100。问题的关键是要把this.count传递给父组件,那怎么传呢,可以在子组件的标签上自定义一个change事件。自定义事件后,需要在方法中,点击按钮的时候同时触发这个change事件。$emit()第一个参数是触发的事件名change,第二个参数是要传递的数据this.count。这样触发change事件后就会执行handleTotal,它有一个参数value就是传递过来的数据,把value赋值给this.total,页面就会重新渲染。

2. 在组件中使用v­-mode

在上一个例子中我们首先是渲染模板,模板渲染完之后,然后$emit()这个change事件,触发事件后执行``handleTotal`,用一个参数来接收。这里还是从例子入手,在上面例子的代码中,现在就留下一个加100的按钮,这样比较简单,可以更好的理解。

  <div id="app">
    <h3>你现在卡里余额是:{{total}}元</h3>
  <child-component v-model="total"></child-component>
  </div>
  <script>
    Vue.component('child-component',{
      template: `
        <div>
          <button @click="handleincrease">+100</button>
        </div>
      `,
      data: function(){
        return {
          count: 2000
        }
      },
      methods: {
        handleincrease: function(){
          this.count = this.count + 100
          this.$emit('input', this.count)
        }
      }
    })
    var app = new Vue({
      el: '#app',
      data: {
        total: 2000
      }
    }) 
  </script>

**v-model**其实就是绑定了input事件,当触发input的时候,input事件就会自动接收传递过来的参数,并赋值给已绑定的total

$emit的代码,这行代码实际上会触发一个 input事件, input后的参数就是传递给v-­model绑定的属性的值。v­-model其实是一个语法糖,这背后其实做了两个操作:

  • v-­bind绑定一个value属性
  • v-­on指令给当前元素绑定 input事件 要使用v-­model,要做到:
  • 接收一个 value 属性
  • 在有新的 value时触发input事件

3. 非父组件之间的通信

首先我们先看下官网的描述:

非父组件之间的通信.png

我们来画一个草图,在根组件中有一个子组件A和子组件B,A和B不是父子组件关系。那A要向B传递数据改怎么做呢?

官网给我们提供的做法是这样的,首先创建一个空的Vue实例,里面是没有内容的。然后在组件A中写一个方法用来触发这个事件,在组件B中使用钩子函数在实例创建的时候去监听这个事件。

bus的重要相当于一个"中介",A触发了事件,bus就知道了就提醒B,B就直接去执行它监听的方法。

图形实例.png

通过一个例子来加深下理解:

  <div id="app">
    <a-component></a-component>
    <b-component></b-component>
  </div>
  <script>
    Vue.component('a-component', {
      template: `
        <div>
          <button @click="handle">点击我想B组件传递数据</button>
        </div>
      `,
      data: function(){
        return {
          msg: '我是来自A组件的内容'
        }
      },
      methods: {
        handle: function(){
          this.$root.bus.$emit('pass', this.msg)
        }
      }
    })
    Vue.component('b-component', {
      template: '<div></div>',
      created: function(){
        ////A组件在实例创建的时候就监听事件-pass事件
        this.$root.bus.$on('pass',function(value){
          alert(value)
        })
      }
    })
    var app = new Vue({
      el: '#app',
      data: {
        bus: new Vue()
      }
    }) 
  </script>

要想从A组件向B组件传递数据,首先要在根组件定义一个bus中介,然后在A组件点击按钮的时候就要传递数据,那就执行一个方法。this.$root拿到这个bus中介,bus中介去触发这个pass事件,并且把this.msg做为参数传递过去。

在B组件中Vue实例创建的时候,就去监听pass事件,然后在后面的function中接收一个参数。

3.1. 父链:this.$parent

子组件有时候需要访问父组件中的内容,那该怎么访问呢?

  <div id="app">
    <child-component></child-component>
    <div>{{msg}}</div>
  </div>
  <script>
    Vue.component('child-component', {
      template: '<button @click="setFatherData">通过点击我修改父亲的数据</button>',
      methods: {
        setFatherData: function(){
          this.$parent.msg = '数据已经被修改了'
        }
      }
    })
    var app = new Vue({
      el: '#app',
      data: {
        msg: '数据还没有被修改'
      }
    }) 
  </script>

首先点击子组件的按钮执行了setFatherData方法,this.$parent.msg直接拿到了父组件中的msg,就可以对它进行修改。

3.2. 子链:this.$refs

父链可以通过this.$parent拿到,子链该怎么拿呢?子链就是this.$children。但是有一个问题,子组件可能会非常多,如果直接用this.$children的话,它就会拿到全部的。我们如果采用一一遍历的方法,肯定也是不太现实的。所以Vue.js给我们提供了一种子组件中使用的索引,也就是ref属性,来为子组件指定一个属性名称。

  <div id="app">
    <a-component ref="A"></a-component>
    <b-component ref="B"></b-component>
    <button @click='getChildData'>我是父组件的按钮,我要拿到组件的内容</button>
    <div>{{formchild}}</div>
  </div>
  <script>
    Vue.component('a-component', {
      template: '<div></div>',
      data: function(){
        return {
          msg: '我是来自A组件的内容'
        }
      }
    })
    Vue.component('b-component', {
      template: '<div></div>',
      data: function(){
        return {
          msg: '我是来自B组件的内容'
        }
      }
    })
    var app = new Vue({
      el: '#app',
      data: {
        formchild: '数据还未拿到'
      },
      methods: {
        getChildData: function(){
          //$refs 用来拿子组件中的数据
          this.formchild = this.$refs.B.msg
        }
      }
    }) 
  </script>

这个例子中,通过一个ref属性,就可以给每个组件加索引。然后父组件如果想拿子组件中的内容,直接用this.$refs.ref属性的值.data中的属性

八、使用slot分发内容

1. 什么是slot(插槽)

为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为 内容分发.Vue.js 实现了一个内容分发 API,使用特殊的 ‘slot’ 元素作为原始内容的插槽。

2. 编译的作用域

在深入内容分发 API 之前,我们先明确内容在哪个作用域里编译。假定模板为:

  <child-component>
    {{ message }}
  </child-component>

message 应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件。组件作用域简单地说是:

  • 父组件模板的内容在父组件作用域内编译;
  • 子组件模板的内容在子组件作用域内编译。

3. 插槽的用法

父组件的内容与子组件相混合,从而弥补了视图的不足 混合父组件的内容与子组件自己的模板

3.1. 单个插槽:
  <div id="app">
    <my-component>
      <p>我是父组件中的内容</p>
    </my-component>
  </div>
  <script>
    Vue.component('my-component',{
      template: `
        <div>
          <slot>如果父组件没有插入内容,我就作为默认出现</slot>
        </div>
      `
    })
    var app = new Vue({
      el: '#app',
      data: { }
    }) 
  </script>

slot相当于子组件设置了一个地方,如果在调用它的时候,往它的开闭标签之间放了东西,那么它就把这些东西放到slot中。

  • 当子组件中没有slot时,父组件放在子组件标签内的东西将被丢弃;

  • 子组件的slot标签内可以放置内容,当父组件没有放置内容在子组件标签内时,slot中的内容会渲染出来;

  • 当父组件在子组件标签内放置了内容时,slot中的内容被丢弃

3.2. 具名插槽:

slot可以有很多个,那么子组件对于父组件放置的多余的内容如何放到各个slot中呢?方法就是子组件给每个slot起一个名字name,父组件放置多余的元素时,给每个元素的slot属性分配一个代表slot的名字。到时候,多余的内容就会根据自己的slot属性去找具有对应名字的slot元素。

  <div id="app">
    <my-component>
      <div slot="header">我会标题标题标题</div>
      <p>我是正文内容</p>
      <p>我是第二段正文内容</p>
      <div slot="footer">我是底部信息</div>
    </my-component>
  </div>
  <script>
    Vue.component('my-component',{
      template: `
        <div>
          <div class="header">
            <slot name="header"></slot>
          </div>
          <div class="content">
            <slot></slot>
          </div>
          <div class="footer">
            <slot name="footer"></slot>
          </div>
        </div>
      `
    })
    var app = new Vue({
      el: '#app',
      data: { }
    }) 
  </script>

这就是具名插槽的用法,在组合使用组件的时候内容分发特别重要。为什么需要具名插槽呢?在子组件中把页面的结构都定好了之后,只需要传递内容就可以了。比如说今天传递的是“我是标题”,明天又想传递别的。那只需要把数据传递进去,再指定一个slot的名字就可以。我们只需要关心它的数据,视图已经在里面了,因此这就是视图不变数据变

4. 作用域插槽

作用域插槽是一种特殊的slot,使用一个可以复用的模板来替换已经渲染的元素。

  • 从子组件获取数据。
  • template模板是不会被渲染的。
  <div id="app">
    <my-component>
      <template slot="abc" slot-scope="prop">
        {{prop.text}}
      </template>
    </my-component>
  </div>
  <script>
    Vue.component('my-component',{
      template: `
        <div>
          <slot text="我是来自子组件的数据" name="abc"></slot>
        </div>
      `
    })
    var app = new Vue({
      el: '#app',
      data: { }
    }) 
  </script>

首先在子组件中定义一个slot,slot里面写要传递的数据。在template模板中定义一个slot-scope,是自己定义的临时的变量名。然后在这个模板中用文本插值的形式,用自己定义的变量名.要拿到的属性名。

5. 访问slot

单个插槽、具名插槽和作用域插槽我们在应用过程中,可能会用到去访问这个slot,也就是拿到slot中的一些数据,那怎么访问呢?

通过this.$slots.(NAME)

  //结合上面具名插槽的代码,在 Vue 实例挂载结束的时候执行 mounted
  mounted:function () {
    //访问插槽
    var header = this.$slots.header;
    var text = header[0].elm.innerText;
    var html = header[0].elm.innerHTML;
    console.log(header)
    console.log(text)
    console.log(html)
  }

this.$slots.header拿到的是一个虚拟节点,这个虚拟节点中只有一个元素,所以要用0去访问里面的所有元素。header[0]拿到elm,然后.elm下面有innerHTMLinnerText

九、组件高级用法–动态组件

Vue给我们提供 了一个元素叫component

  • 作用是: 用来动态的挂载不同的组件
  • 实现:使用is特性来进行实现的
  <!-- 需求:通过点击不同的按钮切换不同的视图 -->
  <div id="app">
    <component :is="thisView"></component>
    <button @click="handleView('A')">第1句</button>   
    <button @click="handleView('B')">第2句</button>   
    <button @click="handleView('C')">第3句</button>   
    <button @click="handleView('D')">第4句</button>   
  </div>
  <script>
    Vue.component('compA',{
      template: '<div>离离原上草</div>'
    })        
    Vue.component('compB',{
      template: '<div>一岁一枯荣</div>'
    })
    Vue.component('compC',{
      template: '<div>野火烧不尽</div>'
    })
    Vue.component('compD',{
      template: '<div>春风吹又生</div>'
    })
    var app = new Vue({
      el: '#app',
      data: {
        thisView: 'compA'
      },
      methods: {
        handleView: function(tag){
          this.thisView = 'comp' + tag
        } 
      }
    }) 
  </script>