Vue组件基础

434 阅读3分钟

Vue组件

组件的定义

vue组件是可复用的vue实例,可以在vue实例中把组件通过自定义标签来使用,例如:

// 假设自定义按钮组件
Vue.component('fd-button', {
  props: ['text'],
  template: '<button v-text="text"></button>'
})
// 某个vue实例
<div>
  // 自定义组件
  <fd-button text="submit"></fd-button>
</div>

组件的属性

上面提到过vue组件其实就是可以被复用的vue实例,那么vue组件基本和实例化vue实例的属性差不多,但是有以下几点区别:

  • 组件无需el属性
  • 组件需要props属性
  • 组件的data必需是函数

组件的注册

组件的注册可以分为全局注册和局部注册

// 全局注册
Vue.component('componentName', {
  data: function(){return {...}}
  ...
})
// 局部注册
var componentA = {
  template: '<div>...</div>',
  ...
}
new Vue({
  components: {componentA},
  ...
})

组件的API

vue组件中有三个重要的API

  • props: 从父组件获取参数
  • event: 向父组件传递组件内部的事件
  • 插槽slot: 父组件向组件传递自定义的内容,可以是文本也可以是html标签

下面分别介绍这三个API

props

我们可以在组件中这样定义props:

// 组件a.vue
{
  ...
  props: ['name', 'age'],
  template: '<div><div>{{'姓名:' + name}}</div><div>{{'年龄:' + age}}</div></div>'
  ...
}
// 父组件中
...
<componentA name="小明" age="18"></componentA>

从上面可以看出props被定义成了一个数组,数组中的每个元素是可以获取值的变量名。我们也可以将props定义为一个对象,然后对每个属性值加一些限制。

{
  ...
  props: {
    name: {
      type: String, // 数据类型必须是字符串
      required: true, // 是必须需要传的
      validator: value => value.length < 10 // 字符串的长度不能大于10
    }
  }
  ...
}

props的每个属性有以下几个属性:

  • type: 定义属性值的数据类型,可以是Number、String、Boolean、Object、Symbol、Array、Function,默认值为String
  • required: 定义该属性是否是必传的,true代表必传,false则相反,默认值为false
  • validator: 验证属性值是否满足要求,属性值是Function,默认没有验证规则
  • default: 设置属性的默认值,当type为复杂类型时属性值是带有返回值类型和type相同的函数

props是单向数据流

指的的是props只能由父组件传递,子组件不能手动修改props的属性值

event

上面介绍的props是父组件向子组件传递参数,而且子组件是不能改变props值的,那么子组件怎么和父组件取的联系呢?event就是子组件和父组件联系的桥梁,首先介绍一下用法:

// componentA子组件中
{
  ...
  methods: {
    handler () {
      this.$emit('testHandler', '1')
    }
  }
}

// 父组件中
<componentA @testHandler="componentHandler"></componentA>
...
{
  methods: {
    componentHandler (param) {
      console.log(param) // 输出1
    }
  }
}

slot插槽

前面介绍过slot插槽是用来在父组件中自定义子组件的内容。首先介绍一下slot的基本用法:

// componentA子组件
<tempalte>
  ...
  <div class="slotWrap">
    <slot></slot>
  </div>
</tempalte>

// 父组件中
<template>
  <componentA>
    <div class="header">我是头部</div>
    <div class="main">我是中间的内容</div>
    <div class="footer">我是尾部</div>
  </componentA>
</template>

// 渲染结果
...
<div class="slotWrap">
  <div class="header">我是头部</div>
  <div class="main">我是中间的内容</div>
  <div class="footer">我是尾部</div>
</div>

现在有个需求是上面代码中头部想放在子组件中指定的位置,中心内容和尾部内容也放在指定的内容该怎么实现呢?下面就介绍一下命名插槽

命名插槽

命名插槽是指给每个slot标签设置name属性,然后父组件中通过对应属性来指定需要插入到子组件的哪个slot标签,具体用法如下:

// componentA子组件
<template>
  ...
  <div class="slotWrap">
    <slot name="header"></slot>
    <div>--头部结束--</div>
    <slot name="main"></slot>
    <div>--中间内容结束--</div>
    <slot name="footer"></slot>
    <div>--尾部内容结束--</div>
  </div>
</template>

// 父组件中
<template>
  ...
  <componentA>
    <template slot="header">
      <div class="header">我是头部</div>
    </template>
    <template slot="main">
      <div class="main">我是中间内容</div>
    </template>
    <template slot="footer">
      <div class="footer">我是尾部</div>
    </template>
  </componentA>
</template>

通过上面的例子,具名插槽的用法就是两个属性:name和slot,在slot标签中通过name来命名,父组件中中通过slot来设置想要插入的slot。
既然插槽内容是插入到子组件中的那么怎么在插槽内容中获取子组件的值呢?

slot-scope

在插槽内容中可以通过slot-scope来获取子组件中相应的内容,具体用法如下:

// slotExample子组件中
<template>
  ...
  <slot :user="user" name="user"></slot>
  ...
</template>
...
{
  ...
  data () {
    return {
      user: {name: '我是slot', age: '18'}
    }
  }
}

// 父组件中
<template>
  <slot-example>
    <template slot="user" slot-scope="slotScoped" v-slot:user="slotScoped">
      <div>
        <div>{{'姓名:' + slotScoped.user.name}}</div>
        <div>{{'年龄:' + slotScoped.user.age}}</div>
      </div>
    </template>
  </slot-example>
</template>

但是不幸的是上面的语法自vue2.6.0之后,就被舍弃了。使用了新的语法, 下面将新写法和旧写法对照着说明一下

2.6.0新语法——v-slot

子组件中定义slot的写法是没有变化的,主要是父组件中插槽内容的写法,下面将旧语法对应相应的新语法

slot='slotName' => v-slot:'slotName' || #slotName
slot-scope="scopeName" => v-slot:'slotName' || #slotName="scopeName"

vue内置的通信手段

vue内置通信手段有两种:

  • ref: 给元素或组件注册引用信息
  • $parent和$children:访问父实例和子实例

provide/inject

这对选项需要一对,需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论层次有多深,并在启上下游关系成立的时间理始终生效

但是官方文档中有个温馨提示:

provide和inject主要为高阶组件提供用例,并不推荐直接用于应用程序中

provide: Object | () => Object inject: Array | {[key: string]: string | Object |Symbol}

提示: provide和inject的绑定不是可响应的,然而如果你传入的是一个可监听的对象,那么其对象的属性还是可响应的。

其实上面这句话的意思是属性是否可响应是根据传入值的类型而定

在实际项目中的使用

由于在vue单页面工程中所有页面都会以app.vue为载体,换句话说app.vue就是工程中所有页面的父级,所以我们把整个app.vue实例通过provide提供所有的子孙组件:

app.vue

<template>
  <router-view></router-view>
</template>
<script>
  export default {
    provide: {
      app: this
    }
  }
</script>

然后在任何组件中,只要通过inject注入了app的,就可以通过this.app.xxx来访问app.vue的data、computed、methods等内容

例如在app.vue中获取用户信息

<template>
  <router-view></router-view>
</template>
<script>
  export default {
    provide: {
     app: this
    },
    data () {
     useInfor: null
    },
    methods: {
      getUserInfor () {
        this.axios('/user/info').then((response) => {
          this.useInfor = response.data
        })
      }
    }
  }
</script>

在任意子组件中:

<template>
  <div>{{app.useInfor}}</div>
</template>
<script>
  export default {
    inject: ['app']
  }
</script>

broadcast 和 dispatch

broadcast:父组件向子组件广播事件
dispatch:子组件向伏组件分发事件
这两个方法的基础是基于emit和on,即:

// A组件中
<template>
  ...
  <button @click="clickHandler">按钮</button>
  ...
</template>
export default {
  ...
  mounted () {
    this.$on('childEmit', this.childEmitHandler)
  },
  methods: {
    childEmitHandler () {
      console.log('接收到了事件')
    },
    clickHandler () {
        this.$emit('childEmit')
    }
  }
}

broadcast方法的实现

function broadcast(componentName, eventName, param) {
  const children = this.$children
  children.forEach(child => {
    if (child.$options.name === componentName) {
      child.$emit(eventName, param)
    } else {
      broadcast.apply(child, [componentName, eventName].concat(param))
    }
  })
}

dispatch方法的实现

function dispatch(componentName, eventName, param) {
  let parent = this.$parent || this.$root
  let name = parent.$options.name
  while(parent && (!name || name !== componentName)) {
    parent = parent.$parent
    if (parent) {
      name = parent.$options.name
    }
  }
  if (parent) {
    parent.$emit(eventName, param)
  }
}

组件内容的渲染

组件有三个渲染内容的属性:el、template、render;那如果三个都写的话,渲染的优先级是怎么样的?

el < template < render

假如三个属性同时出现两个以上时,高优先级的内容会替换低优先级的内容;