Vue.js 学习(9) -- 组件*1*

本文详细介绍了Vue.js中组件的概念及使用方法,包括全局与局部组件注册、动态属性绑定、父子组件间的数据传递与事件触发机制等核心内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

终于等到组件。

组件是Vue最强大的功能之一,它可以提供一层封装,将一些自定义的功能打包,以供开发者在后续的代码中使用。

使用组件的基本格式

组件可以在全局范围内注册,也可以仅在另一个实例或组件中注册,下面我们先来介绍在全局范围内注册组件:

全局组件

Vue.component('my-component', {
  // 选项
})

PS:对于组件的标签名(在上面这个栗子中是my-component),Vue并不强制要求遵守W3C规则(小写,分隔符为一个短杠),但是为了代码统一,还是建议大家都遵守这个规则。

组件注册完成后,就可以在代码中这样写:

<div id="example">
  <my-component></my-component>
</div>

但是要注意的是:要确保在初始化example实例之前完成注册组件的代码:

// 注册
Vue.component('my-component', {
  template: '<div>A custom component!</div>'
})
// 创建根实例
new Vue({
  el: '#example'
})

代码运行后,会渲染为:

<div id="example">
  <div>A custom component!</div>
</div>

局部组件

可以在新建vue实例的时候,为传入的参数添加一个components属性,就可以完成局部组件的注册:

new Vue({
  // ...
  components: {
    // <my-component> 将只在父模板可用
    'my-component': {
        template: '<div>A custom component!</div>'
    }
  }
})

一些必要的解释

第一:

当使用 DOM 作为模版时(例如,将 el 选项挂载到一个已存在的元素上), 你会受到 HTML 的一些限制,因为 Vue 只有在浏览器解析和标准化 HTML 后才能获取模版内容。尤其像这些元素 < ul > , < ol >, < table > , < select > 限制了能被它包裹的元素, < option > 只能出现在其它元素内部。
在自定义组件中使用这些受限制的元素时会导致一些问题,例如:

<table>
  <my-row>...</my-row>
</table>

此时,自定义组件 < my-row > 被认为是无效的内容,因此在渲染的时候会导致错误。变通的方案是使用特殊的 is 属性:

<table>
  <tr is="my-row"></tr>
</table>

第二:data 必须是函数

这点优点奇怪,但是,在构建组件的过程中,data属性必须是函数。

var data = { counter: 0 }
Vue.component('simple-counter', {
  template: '<button v-on:click="counter += 1">{{ counter }}</button>',
  // data 是一个函数,否则 Vue 会发出警告
  data: function () {
    return {
        counter: 0
    }
  }
})
new Vue({
  el: '#example-2'
})

父组件和子组件

父子组件通常是这样的关系:父组件在他的templete中使用了子组件。此时他们两个一定需要互相传递信息:在Vue中,父子组件传递信息可以总结为props down, events up——父组件给子组件传递数据,子组件给父组件传递它的内部事件。

然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的。这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性。

Prop

基础:使用prop传递数据

定义:

Vue.component('child', {
  // 声明 props
  props: ['message'],
  // 就像 data 一样,prop 可以用在模板内
  // 同样也可以在 vm 实例中像 “this.message” 这样使用
  template: '<span>{{ message }}</span>'
})

引用:

<child message="hello!"></child>

进阶:动态Prop

动态prop是利用指令v-bind实现的——还记得v-bind嘛?它可以将一个动态的变量绑定为html元素的属性。

还是上一个栗子中的child

<div>
  <input v-model="parentMsg">
  <br>
  <child v-bind:message="parentMsg"></child>
</div>

这个栗子中,v-bind将child标签的message属性值绑定为parentMsg,而parentMsg是通过v-model指令和input的value绑定。这样就达成效果:input内输入的值实时称为child的message属性,并显示出来。

单向数据流

prop只能由父组件传递给子组件(子组件不能传递给父组件prop),并且在子组件中,只能接收prop值并作出相应的反应,但不可以修改prop。这一点和React的props很相似。

prop验证

当我们对prop类型值无要求时,这样定义props:

props: ['initialCounter'],
data: function () {
  return { counter: this.initialCounter }
}

而如果,我们希望props是某种特性类型的变量,就要以如下方式定义:

Vue.component('example', {
  props: {
    // 基础类型检测 (`null` 意思是任何类型都可以)
    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 value > 10
      }
    }
  }
})

其中,type可以是如下类型:

String
Number
Boolean
Function
Object
Array

当 prop 验证失败了,如果使用的是开发版本会抛出一条警告。

事件

上面讨论的prop是父组件传递给子组件信息的方式。如果子组件想要传递给父组件信息应该怎么做呢?答案就是事件。

所有的Vue实例都有事件接口,开发者可以使用:$on(eventName) 监听事件;使用 $emit(eventName) 触发事件。

先给出一个栗子,然后我们在栗子后解释代码:

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
  template: '<button v-on:click="increment">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    increment: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})
new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})

代码分析:
1、使用Vue.component方法创建自定义的组件button-counter。该组件拥有自己的数据:counter,还有方法increment。在template的定义中,我们可以看到,使用v-on绑定了事件,当按钮被点击时触发increment方法。而该方法被触发后,执行方法的最后,它触发了increment事件。这个increment事件将会被传递到上层。

2、<button-counter v-on:increment="incrementTotal"></button-counter> 这段代码中可以看到,当触发了increment事件的时候,就会执行父组件 的incrementTotal方法(在new Vue…中我们可以找到这个方法)。

以这种事件传递的方式,子组件已经和它外部完全解耦了。它所做的只是触发一个父组件关心的内部事件。

在组件中使用v-model

首先要明确,指令v-model其实只是一个语法糖。

<input v-model="something">

相当于

<input
  v-bind:value="something"
  v-on:input="something = $event.target.value">

也就是,当input事件被触发,something就会随着输入改变,从而改变了input的value;同时,something作为一个变量,可以在代码中修改或者饮用,它可以和value一直保持同步。

在组件中使用v-model:

<currency-input v-model="price"></currency-input>

对比前面的语法糖,并假想这里的v-model替换为v-bind&v-on:input…
我们就可以明白,如果想让组件的v-model生效,组件必须:
1、接受一个value属性;
2、在有新的value时触发input事件。

Vue.component('currency-input', {
  template: `\
    <span>\
      $\
      <input\
        ref="input"\
        v-bind:value="value"\
        v-on:input="updateValue($event.target.value)">\
    </span>\
  `,
  props: ['value'],
  methods: {
    // 不是直接更新值,而是使用此方法来对输入值进行格式化和位数限制
    updateValue: function (value) {
      var formattedValue = value
        // 删除两侧的空格符
        .trim()
        // 保留 2 小数位
        .slice(0, value.indexOf('.') + 3)
      // 如果值不统一,手动覆盖以保持一致
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue
      }
      // 通过 input 事件发出数值
      this.$emit('input', Number(formattedValue))
    }
  }
})

一个更复杂但是完整的应用可见:
https://jsfiddle.net/chrisvfritz/1oqjojjx/?utm_source=website&utm_medium=embed&utm_campaign=1oqjojjx

slot

编译作用域

总的来说,就是:父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。

如下这段代码无效:

<!-- 无效,处于someChildProperty处的变量只会在父组件作用域查找 -->
<child-component v-show="someChildProperty"></child-component>

正确的写法是:

Vue.component('child-component', {
  // 有效,因为是在正确的作用域内
  template: '<div v-show="someChildProperty">Child</div>',
  data: function () {
    return {
      someChildProperty: true
    }
  }
})

slot

slot到底是干什么的呢?
来看这段代码:

<app>
  <app-header></app-header>
  <app-footer></app-footer>
</app>

要注意的是,app这个组件不知道它的字节点有什么,app的父组件才知道。
这样,如果app组件中没有slot,那么app下的app-header/app-footer就会被丢弃。
而如果,app组件的模版中有slot,那么,app-header/app-footer就会插入到slot所在的位置,并替换掉slot.

另一个栗子:
假定 my-component 组件有下面模板:

<div>
  <h2>我是子组件的标题</h2>
  <slot>
    只有在没有要分发的内容时才会显示。
  </slot>
</div>

父组件模版

<div>
  <h1>我是父组件的标题</h1>
  <my-component>
    <p>这是一些初始内容</p>
    <p>这是更多的初始内容</p>
  </my-component>
</div>

结果:

<div>
  <h1>我是父组件的标题</h1>
  <div>
    <h2>我是子组件的标题</h2>
    <p>这是一些初始内容</p>
    <p>这是更多的初始内容</p>
  </div>
</div>

具名slot

多个slot的时候,我们可以使用属性name来规定如何将父组件的内容嵌入到子组件模版中。

例如,假定我们有一个 app-layout 组件,它的模板为:

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

父组件:

<app-layout>
  <h1 slot="header">这里可能是一个页面标题</h1>
  <p>主要内容的一个段落。</p>
  <p>另一个主要段落。</p>
  <p slot="footer">这里有一些联系信息</p>
</app-layout>

结果:

<div class="container">
  <header>
    <h1>这里可能是一个页面标题</h1>
  </header>
  <main>
    <p>主要内容的一个段落。</p>
    <p>另一个主要段落。</p>
  </main>
  <footer>
    <p>这里有一些联系信息</p>
  </footer>
</div>

我们注意到,在app-layout组件中,仍有一个匿名 slot ,它是默认 slot ,作为找不到匹配的内容片段的备用插槽。如果没有默认的 slot ,这些找不到匹配的内容片段将被抛弃。

作用域插槽

作用域插槽是一种特殊类型的插槽,用作(可以传入数据的)可重用模板,而不是已渲染元素。

栗子:
子组件:

<div class="child">
  <slot text="hello from child"></slot>
</div>

父组件:

<div class="parent">
  <child>
    <template scope="props">
      <span>hello from parent</span>
      <span>{{ props.text }}</span>
    </template>
  </child>
</div>

讲解:具有特殊属性 scope 的 < template > 元素,表示它是作用域插槽的模板。scope 的值对应一个临时变量名,此变量接收从子组件中传递的 prop 对象。这样,我们可以完成父组件和子组件之间的数据交流。

渲染结果:

<div class="parent">
  <div class="child">
    <span>hello from parent</span>
    <span>hello from child</span>
  </div>
</div>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值