Vue深入了解组件

1. 组件注册

1.1 组件的构建(子组件)

在项目源码目录下的./component目录下创建组件文件MyComponent.vue

<template>
  <div>
    {{ msg }}
  </div>
</template>

<script>
export default {
  name: "MyComponent",
  /* prop属性从父组件传递赋值 */
  props: {
    msg: String,
  },
};
</script>

<style scoped>
	/* some style code*/
</style>

1.2 引用组件(在父组件中)

在./app.vue文件中引用MyComponent组件

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <my-component msg="Welcome to Your Vue.js App" />
  </div>
</template>

<script>
/* 引用MyComponent */
import MyComponent from "./components/MyComponent.vue";

export default {
  name: "App",
  components: {
    MyComponent,
  },
};
</script>

<style>
	/* some style code*/
</style>

1.3 自动化全局注册基础组件

有的基础组件会在多个子组件文件中反复使用,如果反复注册,将会显得代码冗余,因此使用自动化全局注册组件。

假设我们有以Base开头的三个基础组件BaseButtonBaseIconBaseInput,我们可以这样在main.js文件中全局注册组件。

import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

const requireComponent = require.context(
  // 其组件目录的相对路径
  './components',
  // 是否查询其子目录
  false,
  // 匹配基础组件文件名的正则表达式
  /Base[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
  // 获取组件配置
  const componentConfig = requireComponent(fileName)

  // 获取组件的 PascalCase 命名
  const componentName = upperFirst(
    camelCase(
      // 获取和目录深度无关的文件名
      fileName
        .split('/')
        .pop()
        .replace(/\.\w+$/, '')
    )
  )

  // 全局注册组件
  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 否则回退到使用模块的根。
    componentConfig.default || componentConfig
  )
})

需要注意的是,全局注册行为必须发生在根vue实例创建之前(new Vue()之前)

2. Prop

2.1 prop的作用

简言之,prop的作用就是:

  • 子组件提供一些参数
  • 父组件可以通过这些参数向子组件传递值,用于子组件的渲染

例如,在以上案例中,msg作为prop的属性,被预定义为String类型。父组件可以像使用html原生属性般使用msg属性。

我们不仅可以像msg传入一个基本数据类型,甚至还可以传入一个数组,一个对象。只要在子组件中经过合理的使用,就会达到满意的效果。

2.2 单向数据流

针对prop,我们需要引入一个非常重要的思想,单向数据流

  • 父级prop的更新会导致子组件的prop的更新,使得子组件的相应内容重新渲染。

  • 而子组件的prop更新不会导致父组件的prop更新。

  • 因此,我们不应该在子组件中更新prop属性!!!

如果子组件想更新自己的prop,最好定义一个私有的data或者计算属性

  1. 定义一个counter,并将initialCounter作为它的初始值。
props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}
  1. 定义一个normalizedSize,它会随着size的prop更新而更新。
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

2.3 prop的检查验证

我们可以为prop指派默认类型,例如它必须是一个字符串。

我们也可以为prop指定默认值。

props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }

如果不希望父组件继承子组件的属性,可以使用inheritAttrs: false禁用继承。

3. 自定义事件

3.1 自定义事件基础

自定义事件,顾名思义,就是在js原生的事件基础上,增加一些自己的事件。

通常来说,对于一个事件,有两个关键点:如何触发该事件触发后会发生什么

在vue中,自定义的事件,对于程序员来说,主要需要处理以上两件事。

3.1.1 自定义事件的触发
this.$emit('my-event', param)

只要在想要触发事件的时候,调用上述函数,事件即可触发,是不是很简单?

需要注意的是,我们推荐全程采用kebab-case的事件名,这关系到事件的监听。

3.1.2 自定义事件的监听

假设我们需要在组件my-component中绑定监听事件

<my-component v-on:my-event="doSomething"></my-component>

由于html不区分大小写,因此我们上面的事件名推荐使用kebab-case命名法。当然,在vue以后的版本里,我认为可能会增加驼峰命名法的解析方法。

3.2 自定义组件的v-model

自定义组件如下:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

我们如此使用该组件

<base-checkbox v-model="lovingVue"></base-checkbox>

此处的lovingVue的值会传入名为checked的prop。当base-checkbox触发一个change事件,并附带一个值时,这个lovingVue的值会被更新。效果类似双向绑定。

3.3 将原生事件绑定到子组件

通常在html中,存在一种被称为“事件冒泡”的现象,当子元素被点击时,父元素也会发生响应。

然而在子组件中使用原生事件却出现了问题。例如,当我们如下调用元素时,采用.native后缀来绑定原生的focus事件。

<base-input v-on:focus.native="onFocus"></base-input>

但事实上,base-input是一个这样的组件。input存在于根元素label的内部。因此,我们在这里绑定的focus事件,并不会监听input的focus事件。

<label>
  {{ label }}
  <input
    v-bind="$attrs"
    v-bind:value="value"
    v-on:input="$emit('input', $event.target.value)"
  >
</label>

vue提供了一个**$listeners**对象来解决这个问题。我们可以采用如下方法来使得input的focus事件被监听。

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  computed: {
    inputListeners: function () {
      var vm = this
      // `Object.assign` 将所有的对象合并为一个新对象
      return Object.assign({},
        // 我们从父级添加所有的监听器
        this.$listeners,
        // 然后我们添加自定义监听器,
        // 或覆写一些监听器的行为
        {
          // 这里确保组件配合 `v-model` 的工作
          input: function (event) {
            vm.$emit('input', event.target.value)
          }
        }
      )
    }
  },
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on="inputListeners"
      >
    </label>
  `
})

子组件经过如此封装后,就是一个完全透明的包裹器了,完全可以像一个普通的input元素一样使用了,也不必再使用.native监听器了。

3.4 .sync修饰符实现双向绑定

vue遵循单向数据流原则,只有父组件可以向子组件传递数据。然而有时我们确实希望,子组件的值的变更会带来父组件的值的变更。通常我们使用如下方式。

<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>

然后如下调用自定义事件update

this.$emit('update:title', newTitle)

在组件中,v-bind实现的是父组件向子组件传递数据,而v-on:update:title通过自定义事件模拟子组件向父组件传递数据。两者实现了双向绑定。而这种写法通常比较固定,因此我们在vue的2.3.0版本中引入了.sync修饰符

<text-document v-bind:title.sync="doc.title"></text-document>

是不是方便了很多?注意,带有 .sync 修饰符的v-bind 不能和表达式一起使用,不然子组件向谁传递数据嘞?

4. Slot

4.1 Slot的基本使用方法

组件的使用:

<navigation-link url="/profile">
  Your Profile
</navigation-link>

组件的模板:

<a
  v-bind:href="url"
  class="nav-link"
>
  <slot></slot>
</a>

这样的组件,最终被渲染为:

<a
  v-bind:href="url"
  class="nav-link"
>
    Your Profile
</a>

值得注意的是,Your Profile可以含有HTML代码,甚至可以含有其他组件。

4.2 Slot的变量编译作用域

我们通过如下形式使用slot。由于url是子组件中的property,因此此处访问不到,会是undefine。只有在父组件中的property才能被渲染。

<navigation-link url="/profile">
  Clicking here will send you to: {{ url }}
</navigation-link>

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

4.3 设置Slot内部的默认值

假设我们有一个组件,模板代码如下。那么,当我们使用该组件时,不填写组件内部的"Your Profile",这时,子组件会自动渲染"提交"二字。如果填写"Your Profile",那么就不会自动渲染"提交"二字。

总而言之,“提交”就是子组件slot的默认值。

<button type="submit">
  <slot>提交</slot>
</button>

4.4 单个组件多Slot的处理

对于一个组件,我们可能同时需要多个slot。因此,我们需要讨论多个slot情形下的命名、传值的情况。

4.4.1 slot的命名

具有名字是slot,叫做具名插槽。比如,我们有如下所示的三slot组件。组件的名字依次为headerdefaultfooter。值得注意的是,未指定name属性的slot会被默认命名为default。

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

我们可以如下使用该组件。不难发现,不同slot的内容被包裹于不同的<template>中,template需要通过形如v-slot:header的方式命名。如果不包裹于<template>中,则被渲染到不带名字的<slot>中。当然,我们也可以为其添加<template v-slot:default>标签。

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

它最终会被渲染为如下。

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

在vue2.6.0的更新中,我们可以使用动态指令参数。

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>
4.4.2 slot的传值

有时,我们需要在组件调用中使用子组件的属性值。语法格式如下。其中,slotProps是子组件的props对象。当然,你也可以不让它叫“slotProps”,随便起啥名字都可以的。

<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
  <template v-slot:other="otherSlotProps">
    ...
  </template>
</current-user>

如果不想使用全部的子组件props,只想用其中的一个对象user,则可以像这样调用。

<current-user v-slot="{ user }">
  {{ user.firstName }}
</current-user>

我们甚至还可以让user在这里重命名为person。

<current-user v-slot="{ user: person }">
  {{ person.firstName }}
</current-user>

还可以为它设置默认值。

<current-user v-slot="{ user = { firstName: 'Guest' } }">
  {{ user.firstName }}
</current-user>

4.5 v-slot的强化

v-slot的使用犹如v-on,v-bind一样广泛,因此它也有对应的缩写。

完整指令缩写
v-on:action@action
v-bind:user:user
v-slot:default#default

下面是一个例子。

<current-user #default="{ user }">
  {{ user.firstName }}
</current-user>

5. 动态组件

有时,我们需要一个可以动态确定类型的组件。Vue提供了如下语法,通过is来构造一个动态组件。组件的类型取决于currentTabComponent变量,当currentTabComponent发生变化时,组件的类型也会发生变化。

  • 当currentTabComponent == Post时,组件的类型是Post。
  • 当currentTabComponent == MyHeader时,组件的类型是MyHeader。

这种方式的操作,实现了动态组件的解耦。如果没有这种方式,那么在Post与MyHeader之间发生切换时,可能要涉及到很多的Vue指令的复杂操作,来让一个单独是组件实现两种形态,却不能将一个组件拆分成两个组件。

<component v-bind:is="currentTabComponent"></component>

在使用这种结构的动态组件一段时间后,不难发现,它不会对组件过去的状态做保存。即当我们在Post组件下打钩了某些checkbox。当我们切换成MyHeader,然后再切换回Post组件时,发现那些所有的checkbox都没有被打钩,即元素被重新渲染了。这会产生极大的性能开销,以及不好的用户体验。

keep-alive标签应运而生,被keep-alive标签所包裹的元素,即使在通过is attribute切换状态后,仍能保存之前的状态。

当然,我们要求keep-alive包裹下的切换元素都有自己的name属性,例如Post和MyHeader都要求有自己的属性。

<!-- 失活的组件将会被缓存!-->
<keep-alive>
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>

6. 异步组件

异步组件,即不随主DOM加载而加载的组件。一般来说,我们希望异步组件,在我们用到它时再加载,以节约网络传输。

一个最简单的异步组件的写法如下。’./my-async-component’组件代表加载的原型组件的路径。

Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的 `require` 语法将会告诉 webpack
  // 自动将你的构建代码切割成多个包,这些包
  // 会通过 Ajax 请求加载
  require(['./my-async-component'], resolve)
})
// 或者
Vue.component(
  'async-webpack-example',
  // 这个动态导入会返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

当异步组件加载时,我们需要处理器加载时的状态。我们可以在后一种形式的lambda表达式中返回如下对象。

const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

7. 处理极端情况

7.1 逆向访问元素&组件

在大部分情况下,我们应遵循单向数据流原则:只有父组件可以向子组件传递数据,逆着却不行。然而,有时存在极端情况,使得我们不得不去违背单向数据流原则,来实现某些功能。Vue给我们提供了这样的机会,但并不推荐总是使用。

访问元素使用
在子组件中访问根实例this.$root.foo等
在子组件中访问父组件this.$father.foo等
在父组件中访问子组件this.$refs.refAttribute.foo等
向多级子元素中传递父级元素方法(依赖注入)provide和inject

需要详细介绍的是后两种情况:

7.1.1 在父组件中访问子组件

一个子组件实例,只可能有一个父组件实例。而一个父组件实例中,却可能有多个子组件实例。因此,父组件要访问子组件,需要给子组件取名字。在这里,我们通过添加ref属性来给子组件取名字。例如,我们将base-input组件取名为usernameInput,可以像下面一样。

<base-input ref="usernameInput"></base-input>

然后这样访问:

this.$refs.usernameInput.focus()

如果来点更狠的,我们想在父组件中访问base-input组件中的元素,例如input元素,那么,我们可以给base-input中的input元素添加ref属性,如下所示。

<base-input ref="usernameInput"></base-input>

然后这样访问:

this.$refs.input.focus()
7.1.2 向多级子元素传递父级元素方法(依赖注入)

给任意级别的子元素,传递指定的方法,而不是一股脑儿把整个父级实例传递给子元素,防止了越权,降低了耦合性。

我们可以在父组件中定义provide属性,provide属性所返回的方法,可以被子组件通过inject属性接收到。使用方法如下所示。

在父组件中,我们设定了一个getMap方法,可供子元素访问。

provide: function () {
  return {
    getMap: this.getMap
  }
}

在子组件中,我们可以通过inject接收来自provide的传递。

inject: ['getMap']

可以发现,这种操作就好比范围更加大的prop,它有如下特点:

  • 祖先组件不需要知道哪些后代组件使用它提供的 property
  • 后代组件不需要知道被注入的 property 来自哪里

7.2 程序化的事件侦听器

所谓程序化的时间侦听器,即它不写在html中,也就是不通过v-on的方式被侦听,而是通过js代码的方式被处理。事件侦听器有如下三种:

方法效果
$on(eventName, eventHandler)侦听一个事件
$once(eventName, eventHandler)一次性侦听一个事件
$off(eventName, eventHandler)停止侦听一个事件

下面我们以$once为例,展示它对程序解耦合的效果。

我们需要在某一个组件中使用多个Pikaday对象,当该组件被销毁时,所有的Pikaday对象也要被销毁。根据我们之前所了解的生命周期钩子,很容易想到,我们应该在beforeDestory中调用所有Pikaday的destory方法。然而,根据低耦合的思路,Pikaday的摧毁应该交给Pikaday自己的生命周期钩子去Handle,而不是交给父组件去Handle。因此,使用$once方法,就会带来这样的好处。

mounted: function () {
  this.attachDatepicker('startDateInput')
  this.attachDatepicker('endDateInput')
},
methods: {
  attachDatepicker: function (refName) {
    var picker = new Pikaday({
      field: this.$refs[refName],
      format: 'YYYY-MM-DD'
    })

    this.$once('hook:beforeDestroy', function () {
      picker.destroy()
    })
  }
}

但说到底,其实我们还是推荐创建一个可复用的input-datepicker组件来实现模块化。

7.3 循环引用

7.3.1 递归组件

要知道,组件是可以调用其自身的,这就给组件的递归创造了可能性。下面的stack-overflow组件就是一个典型的例子。

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

然后组件会导致"max stack size exceeded"的错误。因此,如果要递归调用,请保证会最终得到一个v-if="false"的结果。

7.3.2 组件间的循环引用

7.4 通过script定义模板

<script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
  template: '#hello-world-template'
})

x-template需要定义在vue所属的DOM元素之外。

7.5 控制更新

7.5.1 强制更新

有时候,数组或对象中属性或元素的改变,并不会被vue的响应式系统检测到,因为它们的引用并没有发生改变。此时,我们需要调用this.$forceUpdate来强制更新。

7.5.2 创建静态组件

静态组件,即一旦被创建,就不再是响应式元素,这使得系统开销降低了,但同时也会使得模板有时变得更加费解。向下面这个例子中,我们添加一个v-once属性,代表该组件是静态组件。

Vue.component('terms-of-service', {
  template: `
    <div v-once>
      <h1>Terms of Service</h1>
      ... a lot of static content ...
    </div>
  `
})

关于Vue的组件部分,就到此为止。组件作为Vue中的核心内容,需要熟练掌握。通常一种操作可以有两种及以上的实现方式。在不必要时,我们尽量应遵循单向数据流原则,来提高项目的可维护性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值