[TOC]

1. 认识组件化

组件化:如图~
1. 组件化开发 - 图1
image.png

  • 组件化是 Vue.js中的重要思想
    • 它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
    • 然和一个引用就会被抽象成一颗组件树。

image.png

  • 组件化思想的应用:
    • 有了组件化的思想,我们在之后的开发中就要充分的利用它。
    • 尽可能的将页面拆分成一个个小的、可复用的组件。
    • 这样让我们的代码更加方便组织和管理,并且扩展性也很强。

2. 组件化的基本使用

  • 组件的使用分成三个步骤:
    • 创建组件构造器:调用 Vue.extend() 方法进行创建组件构造器
    • 注册组件:调用 Vue.component() 方法注册组件
    • 使用组件:在 Vue实例的作用范围内使用组件

image.png

<div id="app">
  <!-- 3. 使用组件 -->
  <my-cpn></my-cpn>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">

  //1. 创建组件构造器对象
  const cpnC = Vue.extend({
    //模板
    template: `
      <div>
        <h2>我是标题</h2>
        <p>我是内容,111111</p>
        <p>我是内容,222222</p>
      </div>`
  })
  //2.注册组件
  Vue.component('my-cpn', cpnC)

  const app = new Vue({
    el: '#app',
    data: {
      message: '你好'
    }
  })
</script>

这些步骤都代表什么含义呢?

  1. Vue.extend()
    • 调用 Vue.extend() 创建的是一个组件构造器。
    • 通常在创建组件构造器时,传入 template 代表我们自定义组件的模板。
    • 该模板就是在使用到组件的地方,要显示的HTML代码。
    • 事实上,这种写法在 Vue2.x的文档中几乎已经看不到了,它会直接使用下面讲到的语法糖,目前这种方式是学习后面方式的基础。
  2. Vue.component()
    • 调用 Vue.component() 是将刚才的组件构造器注册为一个组件,并且给它起一个组件的标签名称。
    • 所以需要传递两个参数:1. 注册组件的标签名 2. 组件构造器
  3. 组件必须挂载在某个 Vue实例下, 否则它不会生效。

image.png

3. 全局组件和局部组件

  • 当我们通过调用 Vue.component() 注册组件时,组件的注册是全局的
    • 这意味着该组件可以在任意 Vue示例下使用。
  • 如果我们注册的组件是挂载在某个实例中,那么就是一个局部组件

image.png

<div id="app">
  <cpn></cpn>
</div>

<div id="app2">
  <cpn></cpn>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
  //1.创建组件构造器
  const cpnC = Vue.extend({
    template: `
      <div>
        <h2>我是标题</h2>
        <p>我是内容,111111</p>
          </div>`
  })
  //2.注册组件(全局组件,意味着可以再多个Vue实例下面使用)
  // Vue.component('cpn', cpnC)

  //疑问:怎么注册的组件才是局部组件?
  const app = new Vue({
    el: '#app',
    components: {
      // cpn使用组件时的标签名
      cpn: cpnC
    }
  })

  const app2 = new Vue({
    el: '#app2'
  })


</script>

4. 父组件与子组件

  • 在前面我们看到了组件树:

    • 组件和组件之间存在层级关系
    • 而其中一种非常重要的关系就是父子组件的关系
  • 父子组件错误用法:以子标签的形式在 Vue实例中使用

    • 因为当子组件注册到父组件的 components 时,Vue 会编译好父组件的模块
    • 该模板的内容已经决定了父组件将要渲染的 HTML(相当于父组件中已经有了子组件中的内容了)
    • <child-cpn></child-cpn> 是只能在父组件中被识别的。
    • 类似这种用法,<child-cpn></child-cpn> 是会被浏览器忽略的。

image.png

<div id="app">
  <child-cpn></child-cpn>
  <parent-cpn></parent-cpn>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
  //1.创建第一个组件构造器 子组件
  const childComponent = Vue.extend({
    template: `
<div>
<h2>我是子组件。</h2>
  </div>`
  })

  //2.创建第二个组件构造器 父组件
  const parentComponent = Vue.extend({
    template: `
      <div>
        <h2>我是一个父组件~</h2>
        <p>我是父组件中的 p 段落1</p>
        <child-cpn></child-cpn>
        <p>我是父组件中的 p 段落2</p>
      </div>`,
    components: {
      'child-cpn': childComponent
    }
  })

  //root组件
  const app = new Vue({
    el: '#app',
    components: {
      'parent-cpn': parentComponent
    }
  })
</script>

5. 注册的语法糖

  • 在上面注册组件的方式,可能会有些繁琐。
    • Vue 为了简化这个过程,提供了注册的语法糖。
    • 主要是省去了调用 Vue.extend() 的步骤,而是可以直接使用一个对象来代替。
  • 语法糖注册全局组件和局部组件:
    //1.注册全局组件的语法糖
    Vue.component('my-cpn', {
    template: `
    <div>
      <h2>组件标题</h2>
      <p>组件正文内容,111111</p>
    </div>`
    })
    //============================================
    //1.创建组件构造器
    const myComponent = Vue.extend({
    template: `
    <div>
      <h2>组件标题</h2>
      <p>组件正文内容,111111</p>
    </div>`
    })
    //2.注册全局组件
    Vue.component('my-cpn', myComponent);
    
    //2.注册局部组件的语法糖
    const app = new Vue({
    el: '#app',
    components: {
      'my-cpn1': {
        template: `<div>这是my-cpn1组件</div>`
      },
      'my-cpn2': {
        template: `<div>这是my-cpn2组件</div>`
      }
    }
    })
    //============================================
    //1.创建组件构造器
    const myComponent = Vue.extend({
    template: `
      <div>
        <h2>组件标题</h2>
        <p>组件正文内容,111111</p>
      </div>`
    })
    const app1 = new Vue({
    el: '#app1',
    components: {
      'my-cpn1': myComponent
    }
    })
    

6. 模板的分离写法

  • 刚才,我们通过语法糖简化了 Vue组件的注册过程,另外还有一个地方比较麻烦,就是 template模板写法。
  • 如果我们能将其中的 HTML分离出来写,然后挂载到对应的组件上,必然结构会变得非常清晰
  • Vue 提供了两种方案来定义 HTML模板内容:
    • 使用 <script>标签
    • 使用 <template>标签 ```html


<a name="qKL2S"></a>
## 7. 组件数据的存放

<a name="rBQTe"></a>
### 7.1 组件可以访问 Vue实例数据吗?

- 组件是一个单独功能模块的封装:
   - 这个模块有属于自己的 HTML模板,也应该有属于自己的数据 data。
- 组件中的数据是保存在哪里呢?顶层的 Vue实例中吗?
   - 我们先来测试一下,组件中能不能直接访问 Vue实例中的 data


![image.png](https://cdn.nlark.com/yuque/0/2021/png/1477854/1613656180126-fe5add4b-bc6f-4550-ade5-12b6e8df881f.png#align=left&display=inline&height=131&margin=%5Bobject%20Object%5D&name=image.png&originHeight=131&originWidth=662&size=13328&status=done&style=stroke&width=662)
```html
<div id="app">
  <my-cpn></my-cpn>
</div>

<template id="myCpn">
  <h2>{{message}}</h2>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好'
    },
    components: {
      'my-cpn': {
        template: '#myCpn'
      }
    }
  })
</script>
  • 我们发现不能访问,而且即使可以访问,如果将所有的数据都放在Vue实例中,Vue实例就会变的非常臃肿。
  • 结论:Vue 组件应该有自己保存数据的地方。

官方文档:
由于 Vue 不允许动态添加根级响应式 property,所以你必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值:

var vm = new Vue({
  data: {
    // declare message with an empty value
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// set `message` later
vm.message = 'Hello!'

如果你未在 data 选项中声明 message,Vue 将警告你渲染函数正在试图访问不存在的 property。
这样的限制在背后是有其技术原因的,它消除了在依赖项跟踪系统中的一类边界情况,也使 Vue 实例能更好地配合类型检查系统工作。但与此同时在代码可维护性方面也有一点重要的考虑:data 对象就像组件状态的结构 (schema)。提前声明所有的响应式 property,可以让组件代码在未来修改或给其他开发人员阅读时更易于理解。

7.2 组件数据的存放

  • 组件自己的数据存放在哪里呢?
    • 组件对象也有一个 data属性(也可以有methods等属性,下面我们有用到)
    • 只是这个 data**属性必须是一个函数**
    • 而且这个函数返回一个对象,对象内部保存着数据

image.png

<div id="app">
  <my-cpn></my-cpn>
</div>

<template id="myCpn">
  <h2>{{message}}</h2>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好'
    },
    components: {
      'my-cpn': {
        template: '#myCpn',
        data() {
          return {
            message: 'Hello World!'
          }
        }
      }
    }
  })
</script>

7.3 为什么是一个函数呢?

  • 为什么 data在组件中必须是一个函数呢?
    • 首先,如果不是一个函数,Vue 直接就会报错。
    • 而组件不管被复用了多少次,组件中的data数据都应该是相互隔离,互不影响的
    • 基于这一理念,组件每复用一次,data数据就应该被复制一次,之后,当某一处复用的地方组件内data数据被改变时,其他复用地方组件的data数据不受影响
    • 组件中的 data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据
    • 而单纯的写成对象形式,就使得所有组件实例共用了一份data,就会造成一个变了全都会变的结果。

image.png

<div id="app">
  <my-cpn></my-cpn>
    <my-cpn></my-cpn>
    <my-cpn></my-cpn>
</div>

<template id="myCpn">
  <div>
      <span>当前点击数量: {{counter}}</span>
        <button type="button" @click="btnClick">点击按钮</button>
    </div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
  const obj = {
    counter: 0
  }
const app = new Vue({
  el: '#app',    
  components: {
    'my-cpn': {
      template: '#myCpn',
      data() {
        return obj
      },
      methods: {
        btnClick() {
          this.counter ++;
        }
      }
    }
  }
})
</script>

8. 父子组件的通信

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

image.png

  • 在下面的代码中,我直接将 Vue实例当做父组件,并且其中包含子组件来简化代码。
  • 真实的开发中,Vue实例和子组件的通信父组件和子组件的通信过程是一样的。

8.1 props 基本用法

  • 在组件中,使用选项 props来声明需要从父级接收到的数据。
  • props 的值有两种方式:

    • 方式一:字符串数组,数组中的字符串就是传递时的名称。
    • 方式二:对象,对象可以设置传递时的类型
  • 简单 props字符串数组传递样例

image.png

<div id="app">
  //v-bind:abc \  v-bind:def
  <child-cpn :abc="m1" :def="m2"></child-cpn>
</div>

<template id="childCpn">
  <div>A:{{abc}} - {{def}}</div>
</template>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
  const app = new Vue({
    el: '#app',
    data: {
      m1: 'Hello',
      m2: 'World'
    },
    components: {
      'child-cpn': {
        template: '#childCpn',
        props: ['abc', 'def']
      }
    }
  })
</script>

8.2 props 数据验证

  • 在前面,我们的 props选项是使用应该数组。
  • 我们说过,除了数组之外,我们也可以使用对象,当需要对 props 进行类型等验证时,就需要对象写法了。
  • 验证都支持哪些数据类型呢?

    • String
    • Number
    • Boolean
    • Array
    • Object
    • Date
    • Function
    • Symbol
  • 当我们有自定义构造函数时,验证也支持自定义类型

**

8.2.1 验证自定义类型:

image.png
**

8.2.2 验证自定义函数 自定义类型:

image.png

8.2.3 小栗子

父组件向子组件中传入两个值

image.png

<div id="app">
  <cpn :abc="message" :cmovies="movies"></cpn>
</div>

<template id="cpn">
  <div>
    <h2>{{abc}}</h2>
    <ul>
      <li v-for="item in cmovies">{{item}}</li>
    </ul>
  </div>
</template>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
  //父传子:props
  const cpn = {
    template: '#cpn',
    props: {
      // //1. 类型限制
      // abc: String,
      // cmovies: Array

      //2.提供一些默认值
      abc: {
        type: String,
        default: 'abcabcabc',
        required: true
      },
      cmovies: {
        type: Array,
        default: ['cmovies']
      }
    }
  }
  const app = new Vue({
    el: '#app',
    data: {
      message: 'Hello World!',
      movies: ['海贼王', '海王', '沐浴之王']
    },
    components: {
      cpn
    }
  })
</script>

上面的代码, 子组件cpn 中传入父组件的 message 和 movies。

如果没有给子组件中的cmovies 传入值,则代码会报错

Invalid default value for prop “cmovies”: Props with type Object/Array must use a factory function to return the default value. (**类型是对象或者数组时,默认值必须是一个函数。**)

  • default 改为函数即可 default() {return []}
  • required: true 表示这个数据必须从父组件传递过来

    props: {
    // //1. 类型限制
    // abc: String,
    // cmovies: Array
    
    //2.提供一些默认值
    abc: {
      type: String,
      default: 'abcabcabc',
      required: true //表示这个值必须传
    },
    //类型是对象或者数组时,默认值必须是一个函数
    cmovies: {
        type: Array,
      default() {
          return ['cmovies']
      }
    }
    }
    

8.2.4 props 驼峰标识

8.3 子级向父级传递(自定义事件)

  • props 用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件中。
  • 我们应该如何处理呢?这个时候,我们需要使用自定义事件来完成。
  • 什么时候需要自定义事件呢?
    • 当子组件需要向父组件传递数据时,就要用到自定义事件了。
    • 我们之前学习的 v-on 不仅仅可以用于监听 DOM事件,也可以用于组件的自定义事件。
  • 自定义事件的流程:
    • 在子组件中,通过 $emit() 来触发事件。
    • 在父组件中,通过 v-on 来监听子组件事件。

8.3.1 计数器案例

image.png

8.3.2 京东 tab栏小栗子

这里并不是说使用 Vue实现这个组件,只是拿这个类比举例

image.png

点击子组件,告诉父组件点的是哪个内容,然后父组件根据点击的是什么,向服务器请求数据

GIF.gif

不要谢驼峰命名~!

<div id="app">
  <!-- 不是浏览器产生的对象,不是原生DOM的event对象 -->
  <!-- 默认传入item-click事件对象 -->
  <cpn @item-click="cpnClick"></cpn>
  <p>点击:{{message}}</p>
</div>

<template id="cpn">
  <div>
    <button v-for="item in categories"
            @click="btnClick(item)">
      {{item.name}}</button>
  </div>
</template>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
  //1.子组件
  const cpn = {
    template: '#cpn',
    data() {
      return {
        categories: [
          {id: 'aaa', name: '热门推荐'},
          {id: 'bbb', name: '手机数码'},
          {id: 'ccc', name: '电脑办公'},
          {id: 'ddd', name: '家用电器'}
        ]
      }
    },
    methods: {
      btnClick(item) {
        console.log(item.id, item.name)

        //发射给父组件 (事件名称,参数)
        this.$emit('item-click', item)
      }
    }
  }

  //2.父组件
  const app = new Vue({
    el: '#app',
    data: {
      message: ''
    },
    components: {
      cpn
    },
    methods: {
      cpnClick(item) {
        this.message = item.name
      }
    }
  })
</script>

8.5 父组件的访问方式:$children

  • 有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者是子组件访问根组件。

    • 父组件访问子组件:使用 $children$refs
    • 子组件访问父组件:使用 $parent
  • 我们先来看下 $children 的访问

    • this.$children 是一个数组类型,它包含所有子组件对象

image.png

<div id="app">
  <button type="button" @click="btnClick">root 按钮</button>
  <cpn></cpn>
</div>

<template id="cpn">
  <div>
    <button type="button">我是子组件 cpn</button>
    <ccpn></ccpn>
  </div>
</template>

<template id="ccpn">
  <div>
    <button type="button">我是子 子组件 ccpn</button>
  </div>
</template>


<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好'
    },
    methods: {
      btnClick() {
        console.log(1, this.$children[0].message)
        console.log(2, this.$children[0].$children[0].message)
      }
    },
    components: {
      cpn: {
        template: "#cpn",
        data() {
          return {
            message: 'cpn message'
          }
        },
        components: {
          ccpn: {
            template: "#ccpn",
            data() {
              return {
                message: 'ccpn message'
              }
            }
          }
        }
      }
    }
  })
</script>

8.6 父子组件的访问方式:$refs

  • $children 的缺陷:
    • 通过 $children 访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值。
    • 但是当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能发生变化。
    • 有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用 $refs
  • $refs 的使用:
    • $refs 和 ref 指令通常是一起使用的。
    • 首先,我们通过 ref 给某一个字组件绑定一个特定的 ID。
    • 其次,通过 this.$refs.ID 就可以访问到该组件了。
<child-cpn1 ref="child1"></child-cpn1>
<child-cpn2 ref="child2"></child-cpn2>
showRefsCpn() {
    console.log(this.$refs.child1.message);
    console.log(this.$refs.child2.message);
}

image.png

<div id="app">
  <cpn ref="aaa"></cpn>
  <button @click="btnClick">我是父组件按钮</button>
</div>

<template id="cpn">
  <button @click="showMessage">我是子组件按钮</button>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好'
    },
    methods: {
      btnClick() {
        //1. $children 拿所有子组件适用
        // this.$children[0].showMessage()
        // console.log(this.$children[0].name)

        //2. $refs => 对象类型,默认是一个空的对象,ref="bbb"
        console.log(this.$refs.aaa)
        console.log(this.$refs.aaa.name)
        this.$refs.aaa.showMessage()
      }
    },
    components: {
      cpn: {
        template: '#cpn',
        data() {
          return {
            name: '我是子组件的 name'
          }
        },
        methods: {
          showMessage() {
            console.log("showMessage")
          }
        }
      }
    }
  })
</script>

8.7 父子组件的访问方式:$parent, 根组件 $root

  • 如果我们想在子组件中直接访问父组件,可以通过 $parent
  • 注意事项:
    • 尽管在 Vue开发中,我们允许通过 $parent 来访问父组件,但是在真实开发中尽量不要这样做。
    • 子组件应该尽量避免直接访问父组件的数据,因为这样的耦合度太高了。
    • 如果我们将组组件放在另外一个组件内,很可能该父组件没有对应的属性,往往会引起问题。
    • 另外,更不好做的是通过 $parent 直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,很不利于调试和维护。

image.png

<div id="app">
  <button type="button" @click="btnClick">root 按钮</button>
  <cpn></cpn>
</div>

<template id="cpn">
  <div>
    <button type="button">我是子组件 cpn</button>
    <ccpn></ccpn>
  </div>
</template>

<template id="ccpn">
  <div>
    <button type="button">我是子 子组件 ccpn</button>
  </div>
</template>


<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好'
    },
    methods: {
      btnClick() {
        console.log(1, this.$children[0].message)
        console.log(2, this.$children[0].$children[0].message)
      }
    },
    components: {
      cpn: {
        template: "#cpn",
        data() {
          return {
            message: 'cpn message'
          }
        },
        components: {
          ccpn: {
            template: "#ccpn",
            data() {
              return {
                message: 'ccpn message'
              }
            }
          }
        }
      }
    }
  })
</script>

9. 非父子组件通信

  • 刚才我们讨论的都是父子组件间的通信,那如果是非父子关系呢?
    • 非父子组件关系包括多个层级的组件,也报错兄弟组件的关系。
  • 在 Vue 1.x 的时候,可以通过 $dispatch$broadcast 完成
    • $dispatch 用于向上级派发时间
    • $broadcast 用于向下级广播时间
    • 但是在 Vue 2.x 都被取消了
  • 在 Vue 2.x 中,有一个方案是通过中央事件总线,也就是一个中介来完成。
    • 但是这种方案和直接使用 Vuex的状态管理方案还是逊色很多。
    • 并且 Vuex 提供了更多好用的功能,所以这里暂且不讨论这种方案,留到 Vuex状态管理。