[TOC]

什么是组件化?

  • 如果我们将一个页面中的所有处理逻辑都放在一起,处理起来就会非常麻烦,不利于后续的管理和扩展
  • 但是,如果我们将一个页面拆分为一个个小的功能模块,每个模块完成自己这一部分的功能,那么之后整个页面的管理和维护就会变得非常容易了
  • Vue中提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构建我们的应用,任何应用都可以被抽象成一棵组件树

    全局组件和局部组件

  • 全局组件:在Vue实例外部声明的组件(可全局使用)

  • 局部组件:在Vue实例内部声明的组件,(只能在该组件绑定的div元素内部使用)

父组件和子组件

  • 组件的基本使用过程:创建组件构造器(Vue.extends()方法)>>>>>>>>>>>>>注册组件(Vue.component()方法)>>>>>>>>>>>>>使用组件(在Vue实例的作用域范围内使用组件)
  • 所谓父子组件就是一个组件中使用了另一个组件,如下所示: ```html

    
    - 父子组件的错误使用:已子标签的形式在Vue实例中直接使用
       - 因为当子组件被注册到父组件的components中时,Vue已经编译好了父组件的模板,该模板的内容已经决定了父组件将要渲染的HTML(相当于父组件中已经有了子组件中的内容了),其实就是在编译的时候就会对我们注册的组件进行替换
       - <cpn1></cpn1>只能在组件2(也就是父组件)中被识别
    
    <a name="GLbAM"></a>
    ## 注册组件的语法糖写法
    
    - 用的较多
    ```html
      <script>
        // 全局注册组件的语法糖
        Vue.component('cpn1', {
          template: `
            <div>
              <h2>111111</h2>
              <p></p>
            </div>
          `
        })
    
        // 局部注册组件的语法糖
        const app = new Vue({
          el: '#app',
          data: {
            message: 'hello'
          },
          component: {
            'cpn2': {
              template: `
                <div>
                  <h2>111111</h2>
                  <p></p>
                </div>
          `
            }
          }
        })
      </script>
    

    组件模板分离写法

    <template id="cpn">
      <div>
        <h2>11111</h2>
        <p>22222</p>
      </div>
    </template>
    
    <script>
      Vue.component('cpn', {
        template: '#cpn'
      })
    </script>
    

    为什么组件data必须是函数

    • 组件是一个单独功能模块的封装,这个模块有属于自己的html模板,也应该有属于自己的数据data
    • 组件中的数据是保存在哪里的呢?是在顶层的Vue实例中吗?经过测试,我们组件是不可以直接访问Vue实例中的data的,而且即使可以访问,如果将所有的数据都放在Vue实例中,Vue实例就会变得非常臃肿
    • 结论:Vue组件应该有自己保存数据的地方
    • vue.component()方法中传入这个对象{ }不仅有template属性,也有data属性(也可以有methods属性),这里存放我们组件自己的数据,但是:这个data属性不能是对象类型,必须是个函数,这个函数返回一个对象,对象内部保存着数据

      Vue.component('cpn', {
        template: '#cpn',
      data () {
          return {
            title: 'abc'
        }
      }
      })
      
    • 封装一个计数器组件来说明这个问题 ```html

    
    - 由于我们的组件要使用多次,如果在注册组件时,data是一个对象(真实这样会报错,假如可以使用),那么多次使用该组件时,每个组件实例使用的都是同一个对象,这样任何一个组件实例修改了data中的数据,所有的组件实例都会发生改变,这就会造成组件实例之间的互相影响,这是我们竭力想要避免的情况
    - 但是如果data是一个函数,每个组件实例使用的data对象,都是由这个函数来返回的,这样的话,每个组件实例使用的对象都是各不相同的(内存地址不一样),这些对象之间互不影响,可以理解为每个组件实例的私有对象
    - 但是:如果我们想让这些组件实例之间互相影响,我们就可以定义一个对象,然后让data函数返回这个对象,在实际开发中,千万不要使用这种方式,因为组件是要复用的,每个组件都有属于自己的逻辑,不能引起连锁反应!
    ```javascript
    const obj = {
        counter: 0
    }
    Vue.component('cpn', {
        template: '#cpn',
      data () {
          return obj
      }
    })
    
    • 总结:组件应该在每个地方(组件实例)都有一份属于自己的保存数据的对象

    组件间通信

    • 之前我们说过,子组件是不能使用父组件或者Vue实例的数据的
    • 但是在开发中,往往一些数据确实需要从上层传递到下层,比如在一个页面中,我们从服务器请求到了很多的数据,其中一部分数据并不是我们整个页面的大组件来展示的,而是需要下面的子组件进行展示,这个时候,我们并不会为了一个小数据重新发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)
    • 如何进行父子组件之间的通信呢?
      • 父组件通过props向子组件传递数据
      • 子组件通过事件向父组件发送消息emit Events

    父传子(props)

    <div id="app">
      <!-- 3. 使用组件 -->
      <cpn v-bind:imovies="movies" :imessage="message"></cpn>
      <cpn v-bind:imovies="movies"></cpn>
      <cpn v-bind:imovies="movies"></cpn>
      <cpn v-bind:imovies="movies"></cpn>
    </div>
    
    <template id="cpn">
      <div>
        <ul>
          <li v-for="(item, index) in imovies">{{item}}</li>
        </ul>
      </div>
    </template>
    
    <script>
      // 1. 创建一个组件构造器
      const cpn = {
        template: '#cpn',
        props: {
          imovies: {
            type: Array,
            // 如果属性的类型是对象或者数组时,默认值必须是一个函数,此时如果是一个数据就会报错
            default () {
              return []
            },
            required: true,// 必需,当我们使用此cpn组件时,必须传递这个值,否则会报错
          },
          imessage: {
            type: String,
            default: 'hello world!'
          }
        },
        data () {
          return {
    
          }
        },
        methods: {}
      }
    
      // Vue实例,也就是父组件
      const app = new Vue({
        el: '#app',
        data: {
          message: 'hello',
          movies: ['real Steal','iron man','transformers','into the wild']
        },
        components: {
          // 2. 注册组件,增强写法
          cpn
        }
      })
    </script>
    
    • props的几种使用

      Vue.component('cpn', {
        props: {
          // 基础的类型检查
          propsA: Number,
          // 多个可能的类型
          propsB: [String, Number],
          // 必传的字符串
          propsC: {
            type: String,
            required: true
          },
          // 带有默认值的数字
          propsD: {
            type: Number,
            default: 100
          },
          // 带有默认值的对象
          propsE: {
            type: Object,
            default () {
              return {}
            }
          },
          // 自定义验证函数
          propsF: {
            validator () {
              // 这个值必须匹配下列字符串中的一个
              return ['success','warning','danger'].indexOf(value) !== -1
            }
          }
          // 可以自定义类型
        }
      })
      
    • props驼峰标识:我们在使用v-bind时,被绑定的属性名不能有大写,如果变量名使用了驼峰,在标签中进行绑定时,要将驼峰改为-分割来使用

    子传父(自定义事件)

    <div id="app">
      <!-- 以前v-on:后面跟的都是一些默认的事件,比如click input hover事件等,现在这里跟的是我们自定义的事件item-click -->
      <!-- 我们不仅可以监听默认事件,而且还可以监听我们子组件发射(emit)出来的事件 -->
      <!-- 我们这里监听到子组件传递过来的事件后,通过cpnClick函数来进行处理 -->
      <!-- 如果这里的cpnClick没有跟(参数),就默认把this.$emit('item-click', item)这里的item传递过去。而之前js中监听事件默认传递的是浏览器的事件对象event -->
      <cpn v-on:item-click="cpnClick"></cpn>
    </div>
    
    <template id="cpn">
      <div>
        <button v-for="item in categories" @click="btnClick(item)">{{item.name}}</button>
      </div>
    </template>
    
    <script>
      // 1. 子组件
      const cpn = {
        template: '#cpn',
        data () {
          return {
            categories: [
              {id:'aaa', name:'热门推荐'},
              {id:'bbb', name:'手机数码'},
              {id:'ccc', name:'家用家电'},
              {id:'ddd', name:'电脑办公'}
            ]
          }
        },
        methods: {
          btnClick (item) {
            // 子组件发送出去一个自定义事件
            this.$emit('item-click', item)
          }
        }
      }
    
      const app = new Vue({
        el: '#app',
        data: {
    
        },
        components: {
          cpn
        },
        methods: {
          cpnClick (item) {
            console.log(item.name)
          }
        }
      })
    </script>
    

    父子组件通信案例

    • 现在要实现的功能是这样的:子组件的num1和num2都是在props中的,也就是由父组件传递过来的值,但是我们又想要在模板中加input标签,让input的输入值和子组件中的num1和num2进行双向绑定(也就是修改input的值,子组件props中的num1和num2也跟着变化),同时把该值传回父组件,并修改父组件中的data的值
    • 现在的问题就是:num1 和 num2 由父组件传递过来的值决定,也被input输入的值来决定,Vue是不允许我们这样做的,因为可能会乱套,不知道这个值到底是被谁决定的 ```html

    
    - 但是如果我们确实需要将子组件props中的值和input中的值进行双向绑定,要怎么办呢?
       - Vue希望我们根据props中的值,创建一个cpn组件自己的data或者计算属性
       - 然后input想要绑定的话就绑定cpn组件内部的data属性中的值,这样就不会影响父组件传递过来的值了
    ```javascript
    cpn: {
        template: '#cpn',
      props: {
          num1: Number,
        num2: Number
      },
      data () {
          return {
            dnum1: this.num1,
          dnum2: this.num2
        }
      }
    }
    
    • 接下来我们还要实现将该值传递回父组件中,并修改父组件中的data的值
      • 要实现该功能,我们就不能使用v-model来双向绑定了,我们知道,v-model是由两个指令构成的,v-bind和@input,且用这种方式,我们可以自定义获取输入框焦点之后的操作,更加灵活 ```html

    <a name="UsRgN"></a>
    ### watch
    
    - watch属性用于监听某一个属性的变化,属性名作为函数名称,两个参数,oldVaule和newValue
    ```javascript
    watch: {
        name (oldValue, newValue) {
          这时就可以对这两个进行处理
      }
    }
    

    父子组件如何拿到对方的对象直接进行操作

    父访问子($children/$refs)

    <div id="app">
      <cpn></cpn>
      <cpn ref="aaa"></cpn>
      <button @click="btnClick">
        按钮
      </button>
    </div>
    
    <template id="cpn">
      <div>
        <h2>我是子组件</h2>
      </div>
    </template>
    
    <script>
      const app = new Vue({
        el: '#app',
        data: {
          message: 'hello'
        },
        methods: {
          btnClick () {
            // 方法1: $children
            console.log(this.$children); // VueComponent
            console.log(this.$children[0].name); // ryan z
            this.$children[0].showMessage(); // alert(111)
    
            // 方法2: $refs,用得最多99%
            console.log(this.$refs); //默认为一个空对象{}
            // 如果我们需要使用这个refs,就需要在组件中添加一个属性ref="aaa",相当于起了个名字,这时就可以通过下面的方式来使用了
            console.log(this.$refs.aaa); // {aaa:VueComponent} 这样就可以拿到这个组件对象
    
          }
        },
        components: {
          cpn: {
            template: '#cpn',
            data () {
              return {
                name: 'ryan z'
              }
            },
            methods: {
              showMessage () {
                alert(111)
              }
            }
          }
        }
      })
    </script>
    

    子访问父($parent)+ 访问根组件($root)

    • 一般开发中这种用法很少,因为如果子组件频繁使用父组件中对象的属性和方法,它自身的复用性就不会很强了,而我们封装子组件的目的就是它的复用性
    • $root一般使用也很少,根组件就是Vue实例,而且我们Vue实例中也不会放很多数据(除了路由等一些比较重要的) ```html

    ```