vue.js 组件

什么是组件


组件(Component)是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素, Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。

使用组件


# 注册

我们可以通过以下方式创建一个 Vue 实例:

    new Vue({
      el: '#some-element',
      // 选项
    })

要注册一个全局组件,可以使用 Vue.component(tagName, options)。 例如:

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

对于自定义标签名,Vue.js 不强制要求遵循 W3C规则 (小写,并且包含一个短杠),尽管遵循这个规则比较好。

组件在注册之后,便可以在父实例的模块中以自定义元素 <my-component></my-component> 的形式使用。要确保在初始化根实例 之前 注册了组件:

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


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

渲染为:

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

# 局部注册

不必在全局注册每个组件。通过使用组件实例选项注册,可以使组件仅在另一个实例/组件的作用域中可用:

    var Child = {
      template: '<div>A custom component!</div>'
    }

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

# DOM模板解析说明

当使用 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>

应当注意,如果您使用来自以下来源之一的字符串模板,这些限制将不适用:

  • <script type="text/x-template">
  • JavaScript内联模版字符串
  • .vue 组件

# data 必须是函数

使用组件时,大多数选项可以被传入到 Vue 构造器中,有一个例外: data 必须是函数。 实际上,如果你这么做:

    Vue.component('my-component', {
      template: '<span>{{ message }}</span>',
      data: {
        message: 'hello'
      }
    })

那么 Vue 会在控制台发出警告,告诉你在组件中 data 必须是一个函数。最好理解这种规则的存在意义。

    <div id="example-2">
      <simple-counter></simple-counter>
      <simple-counter></simple-counter>
      <simple-counter></simple-counter>
    </div>

    var data = { counter: 0 }

    Vue.component('simple-counter', {
      template: '<button v-on:click="counter += 1">{{ counter }}</button>',
      // data 是一个函数,因此 Vue 不会警告,
      // 但是我们为每一个组件返回了同一个对象引用
      data: function () {
        return data
      }
    })
    new Vue({
      el: '#example-2'
    })

由于这三个组件共享了同一个 data , 因此增加一个 counter 会影响所有组件!我们可以通过为每个组件返回新的 data 对象来解决这个问题:

    data: function () {
      return {
        counter: 0
      }
    }

现在每个 counter 都有它自己内部的状态了.

# 组件构成

组件意味着协同工作,通常父子组件会是这样的关系:组件 A 在它的模版中使用了组件 B 。它们之间必然需要相互通信:父组件要给子组件传递数据,子组件需要将它内部发生的事情告知给父组件。然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的。这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性。

在 Vue.js 中,父子组件的关系可以总结为 props down, events up 。父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。

Prop


# 使用Prop传递数据

组件实例的作用域是孤立的。这意味着不能并且不应该在子组件的模板内直接引用父组件的数据。可以使用 props 把数据传给子组件。

prop 是父组件用来传递数据的一个自定义属性。子组件需要显式地用 props 选项声明 “prop”:

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

然后向它传入一个普通字符串:

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

结果:

Hello!

# camelCase vs. kebab-case

HTML 特性不区分大小写。当使用非字符串模版时,prop的名字形式会从 camelCase 转为 kebab-case(短横线隔开):

    Vue.component('child', {
      // camelCase in JavaScript
      props: ['myMessage'],
      template: '<span>{{ myMessage }}</span>'
    })
    <!-- kebab-case in HTML -->
    <child my-message="hello!"></child>

再次说明,如果你使用字符串模版,不用在意这些限制。

# 动态Prop

类似于用 v-bind 绑定 HTML 特性到一个表达式,也可以用 v-bind动态绑定 props 的值到父组件的数据中。每当父组件的数据变化时,该变化也会传导给子组件:

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

使用 v-bind 的缩写语法通常更简单:

    <child :my-message="parentMsg"></child>

# 字面量语法 VS 动态语法

初学者常犯的一个错误是使用字面量语法传递数值:

    <!-- 传递了一个字符串"1" -->
    <comp some-prop="1"></comp>

因为它是一个字面 prop ,它的值以字符串 "1" 而不是以实际的数字传下去。如果想传递一个实际的 JavaScript 数字,需要使用 v-bind ,从而让它的值被当作 JavaScript 表达式计算:

    <!-- 传递实际的数字 -->
    <comp v-bind:some-prop="1"></comp>

# 单向数据流

prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。

另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop 。如果你这么做了,Vue 会在控制台给出警告。

通常有两种改变 prop 的情况:

  • prop 作为初始值传入,子组件之后只是将它的初始值作为本地数据的初始值使用;
  • prop 作为需要被转变的原始值传入。

更确切的说这两种情况是:

  1. 定义一个局部 data 属性,并将 prop 的初始值作为局部数据的初始值。
    props: ['initialCounter'],
    data: function () {
      return { counter: this.initialCounter }
    }
  1. 定义一个 computed 属性,此属性从 prop 的值计算得出。
    props: ['size'],
    computed: {
      normalizedSize: function () {
        return this.size.trim().toLowerCase()
      }
    }

注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。

# Prop验证

组件可以为 props 指定验证要求。如果未指定验证要求,Vue 会发出警告。当组件给其他人使用时这很有用。

prop 是一个对象而不是字符串数组时,它包含验证要求:

    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

type 也可以是一个自定义构造器,使用 instanceof 检测。

当 prop 验证失败了, Vue 将拒绝在子组件上设置此值,如果使用的是开发版本会抛出一条警告。

自定义事件


我们知道,父组件是使用 props 传递数据给子组件,但如果子组件要把数据传递回去,应该怎样做?那就是自定义事件!

# 使用 v-on 绑定事件

每个 Vue 实例都实现了事件接口(Events interface),即:

  • 使用 $on(eventName) 监听事件
  • 使用 $emit(eventName) 触发事件

Vue的事件系统分离自浏览器的EventTarget API。尽管它们的运行类似,但是$on$emit 不是addEventListenerdispatchEvent 的别名。

另外,父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件。

下面是一个例子:

    <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>

监听子组件increment自定义事件,触发时调用父元素incrementTotal方法

    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')//触发increment自定义事件,传递给父元素
        }
      },
    })
    new Vue({
      el: '#counter-event-example',
      data: {
        total: 0
      },
      methods: {
        incrementTotal: function () {
          this.total += 1
        }
      }
    })

给组件绑定原生事件

有时候,你可能想在某个组件的根元素上监听一个原生事件。可以使用 .native 修饰 v-on 。例如:

    <my-component v-on:click.native="doTheThing"></my-component>

# 使用自定义事件的表单输入组件

自定义事件也可以用来创建自定义的表单输入组件,使用 v-model 来进行数据双向绑定。牢记:

    <input v-model="something">

仅仅是一个语法糖:

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

所以在组件中使用时,它相当于下面的简写:

    <custom-input v-bind:value="something" v-on:input="something = arguments[0]"></custom-input>

所以要让组件的 v-model 生效,它必须:

  • 接受一个 value 属性
  • 在有新的 value 时触发 input 事件

一个非常简单的货币输入:

<currency-input v-model="price"></currency-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))
        }
      }
    })

# 非父子组件通信

有时候非父子关系的组件也需要通信。在简单的场景下,使用一个空的 Vue 实例作为中央事件总线:

    var bus = new Vue()
    // 触发组件 A 中的事件
    bus.$emit('id-selected', 1)
    // 在组件 B 创建的钩子中监听事件
    bus.$on('id-selected', function (id) {
      // ...
    })

使用slot分发内容


在使用组件时,常常要像这样组合它们:

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

注意两点:

  1. <app> 组件不知道它的挂载点会有什么内容。挂载点的内容是由<app>的父组件决定的。
  2. '<app>' 组件很可能有它自己的模版。

为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为 内容分发 (或 “transclusion” 如果你熟悉 Angular)。Vue.js 实现了一个内容分发 API ,参照了当前 Web 组件规范草案,使用特殊的 '<slot>' 元素作为原始内容的插槽。

# 编译作用域

在深入内容分发 API 之前,我们先明确内容的编译作用域。假定模板为:

<child-component>
  {{ message }}
</child-component>

message 应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件。组件作用域简单地说是:

父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。

一个常见错误是试图在父组件模板内将一个指令绑定到子组件的属性/方法:

    <!-- 无效 -->
    <child-component v-show="someChildProperty"></child-component>

假定 'someChildProperty '是子组件的属性,上例不会如预期那样工作。父组件模板不应该知道子组件的状态。

如果要绑定子组件内的指令到一个组件的根节点,应当在它的模板内这么做:

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

类似地,分发内容是在父组件作用域内编译。

# 单个slot

除非子组件模板包含至少一个 <slot> 插口,否则父组件的内容将会被丢弃。当子组件模板只有一个没有属性的 slot 时,父组件整个内容片段将插入到 slot 所在的 DOM 位置,并替换掉 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 来配置如何分发内容。多个 slot 可以有不同的名字。具名 slot 将匹配内容片段中有对应 slot 特性的元素。

仍然可以有一个匿名 slot ,它是默认 slot ,作为找不到匹配的内容片段的备用插槽。如果没有默认的 slot ,这些找不到匹配的内容片段将被抛弃。

例如,假定我们有一个 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>

# 作用域插槽

2.1 新增

作用域插槽是一种特殊类型的插槽,用作使用一个(能够传递数据到)可重用模板替换已渲染元素。

在子组件中,只需将数据传递到插槽,就像你将 prop 传递给组件一样:

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

在父级中,具有特殊属性 scope<template> 元素,表示它是作用域插槽的模板。scope 的值对应一个临时变量名,此变量接收从子组件中传递的 prop 对象:

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

如果我们渲染以上结果,得到的输出会是:

    <div class="parent">
      <div class="child">
        <span>hello from parent</span>
        <span>hello from child</span>
      </div>
    </div>

作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项:

    <my-awesome-list :items="items">
      <!-- 作用域插槽也可以在这里命名 -->
      <template slot="item" scope="props">
        <li class="my-fancy-item">{{ props.text }}</li>
      </template>
    </my-awesome-list>

列表组件的模板:

    <ul>
      <slot name="item"
        v-for="item in items" :text="item.text">
        <!-- fallback content here -->
      </slot>
    </ul>

动态组件

多个组件可以使用同一个挂载点,然后动态地在它们之间切换。使用保留的 <component> 元素,动态地绑定到它的 is 特性:

  var vm = new Vue({
    el: '#example',
    data: {
      currentView: 'home'
    },
    components: {
      home: { /* ... */ },
      posts: { /* ... */ },
      archive: { /* ... */ }
    }
  })
  component v-bind:is="currentView">
    <!-- 组件在 vm.currentview 变化时改变! -->
  </component>

也可以直接绑定到组件对象上:

  var Home = {
    template: '<p>Welcome home!</p>'
  }
  var vm = new Vue({
    el: '#example',
    data: {
      currentView: Home
    }
  })

# keep-alive

如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个 keep-alive 指令参数:

  <keep-alive>
    <component :is="currentView">
      <!-- 非活动组件将被缓存! -->
    </component>
  </keep-alive>

API参考查看更多 <keep-alive> 的细节。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容

  • 本文章是我最近在公司的一场内部分享的内容。我有个习惯就是每次分享都会先将要分享的内容写成文章。所以这个文集也是用来...
    Awey阅读 9,200评论 4 67
  • 这篇笔记主要包含 Vue 2 不同于 Vue 1 或者特有的内容,还有我对于 Vue 1.0 印象不深的内容。关于...
    云之外阅读 4,989评论 0 29
  • 有一篇非常棒的关于vue.js的组件的文章,写的特别好,特别清楚,容易理解。链接:上篇:http://www.cn...
    恰皮阅读 1,738评论 0 8
  • 此文基于官方文档,里面部分例子有改动,加上了一些自己的理解 什么是组件? 组件(Component)是 Vue.j...
    陆志均阅读 3,751评论 5 14
  • 从成吉思汗的雕像 到陇西绝美的风光 从桂林的喀斯特地貌 到湘西小镇的广场 我在路上 从西藏那曲辽阔的草场 到昆仑山...
    货车司机牛二哥阅读 166评论 0 3