vue总结系列--组件化

上一篇博文梳理了vue的数据驱动和响应式相关的特性,这一篇博文就来梳理vue的一个很重要的特性,组件化。
自定义组件之于vue,其意义不亚于函数之于C,java之类的编程语言。
函数是计算机科学中的一大重要的发明。
一方面,它表明着一种自顶向下,逐步求精的分而治之的思惟,另一方面,它可以封装复杂实现的细节,提供更高抽象的接口,下降软件工程的复杂度。html

在vue中,自定义组件也起着相似的做用。vue

<!--more-->
咱们知道,在组件化的GUI界面上,GUI能够被视为一棵树,浏览器的DOM就是一个最好的例子。
从布局上来看,界面能够当作大盒子套小盒子,小盒子再套更小的盒子。。。
反映到DOM上,DOM某节点的全部子节点,都是该组件的子组件,都是该组件内的元素。java

在vue中也是如此,vue组件之间的关系也是相似DOM同样,是树状的。
在定义一个组件时,须要引用的全部组件,都成为了该组件的子组件。git

组件通讯

组件做为一个模块性质的东西,天然就有着它必定的独立性。并且,与其它模块的耦合都理所应当的有着明确的接口约定。
在vue中,父子组件通讯经过组件属性和事件来进行的。
其中,经过组件属性,父组件的数据流向子组件;经过事件,子组件的数据流向父组件。github

从抽象的角度看,组件做为一个黑盒子,它有着特定的属性用以接收外部传递给它的数据,它也有着特定的事件,当特定操做发生时调用回调函数,以通知别的组件。编程

组件的属性

自定义属性

<div id="x-child">
    <span> {{ titleMessage }} <span>
</div>
<script>
    Vue.component('x-child', {
        template: '#x-child',
        props: ['titleMessage']
    });
</script>
<div id="x-parent">
    <x-child title-message="A"></x-child>
    <x-child title-message="B"></x-child>
    <x-child :title-message="message"></x-child>
</div>
<script>
    Vue.component('x-parent', {
        template: '#x-parent',
        data: function () {
            return {
                message: "C"
            };
        }
    });
</script>

上面的例子中,定义了x-child自定义组件,而且在x-parent组件中引用它。
以前在介绍数据驱动的时候,咱们知道,定义vue组件时,能够经过data定义组件内部的状态,它是组件数据的一部分。
除了data以外,prop(属性)也是组件数据的来源之一,父组件经过prop将本身的数据传递给子组件。segmentfault

在定义组件时咱们能够看到:数组

  1. 经过props定义组件可以接收的属性,甚至还能指定属性的默认值及类型,甚至还能编写任意的函数验证属性的合法性。明确的指定相似接口声明,加强可读性,下降debug难度。
  2. 属性和内部状态相似,都做为组件数据的一部分。区别在于,在vue设计上,属性是只读的,能够做为数据驱动视图,可是没法被改变。(我不太清楚vue有没有从语法强制要求这点,可是良好实践的vue组件是这样的)

在使用自定义组件时能够看到:浏览器

  1. 组件属性的灵活性特别强,你不只能传递给它一个固定的数据,还可以使用vue的数据绑定语法把父组件内的数据经过prop传递给子组件。
    固然,这也是响应式的。当父组件中该数据变化时,天然的,传递给子组件的数据也会变化。那么,子组件中绑定了该数据的视图部分也固然会被从新渲染以展示在浏览器上。
  2. 子组件中定义中的属性是驼峰写法,这是符合js编码规范的。然而,在引用子组件的地方, 属性应该要写成短横线分割式的写法
    这是由于,html是不区分大小写的,vue对此也很无奈。这是在实际编码中很任意犯的一个错误,须要注意。

子组件的独立性

vue中,属性被设计用于父组件传递数据给子组件的,若是子组件改变了属性,那么父组件不会受到任何影响。这在vue中被称为 单向数据流
可是,若是属性用来传递数组或对象等复合的数据结构,那么可能会出问题。考虑如下的场景:数据结构

  1. 父组件把数据中的对象传递给了子组件的属性。
  2. 因为可能修改该属性,子组件把该属性直接赋值给内部状态,做为内部状态修改。
  3. 在某些操做触发后,该内部状态被修改。

问题在于,因为js是引用类型语言,简单的赋值仅仅是传递引用,那么,以上场景中,父组件中的数据,子组件中的属性,还有子组件中的状态, 指向的都是同一份对象
这会形成一个问题,若是子组件修改了该对象的属性,那么父组件的数据也会受到影响,这破坏了单向数据流,会形成不少诡异的bug。

解决方法也很显然:

  1. 方案一是父组件传递数组或对象给子组件时,使用深拷贝拷贝一份过去,或者子组件将属性赋值给内部状态时,深拷贝一份过去,这样就可以互不干扰。
  2. 方案二是使用不可变数据结构,每次修改都是产生新的拷贝,所以也能解决问题。

组件的事件

自定义事件

<div id="x-child">
  <button @click="onClick">click</button>
</div>

<script>
Vue.component('x-child', {
  template: '#x-child',
  data: function() {
      return {
          counter: 0
      };
  },
  methods: {
      onClick: function() {
          this.counter++;
          this.$emit("on-counter-add", this.counter);
      }
  }
});
</script>

以上自定义了x-child组件,而且自定义了组件事件。咱们能够看到:

  1. 使用vue组件实例的$emit方法,用以触发一个自定义事件。触发事件能够携带数据,这些数据被用于传递给绑定了事件的其它组件的回调函数上,进而被传递给其它组件。
  2. 不像属性,自定义事件没有一个统一声明的地方,至于为何我也不清楚。。。得问vue做者去。
  3. 该自定义组件内部包含了一个按钮,按钮被点击事件触发了自定义组件的回调函数,进而触发了该自定义组件的自定义事件。
    从一个角度来看,该自定义组件像是转发了原生组件的事件而已。可是从另一个角度来看,该自定义组件封装了这些细节,对外展示的是一个点下按钮触发计数器增长的事件的这样一个计数器。
  4. 事件名称的定义用的短横线分割的写法,缘由和属性相似。
<div id="x-parent">
    <x-child @on-counter-add="onCounterAdd"></x-child>
    <span> { { counter }} </span>
</div>
<script>
    Vue.component('x-parent', {
        template: '#x-parent',
        data: function () {
            return {
                counter: 0
            };
        },
        methods: {
            onCounterAdd: function (counter) {
                this.counter = counter;
            }
        }
    });
</script>

以上定义了x-parent组件,而且引用了上面定义的子组件。能够看出:

  1. 子组件事件触发了父组件的回调函数,而且将数据从回调函数中传入。父组件能够在回调函数里作任何事情,很有灵活性。
  2. 通常状况下,父组件会在回调函数中更新本身的状态数据。数据更新后触发新的视图渲染,用户便可在界面上看到了反馈。这样,经过事件,子组件的数据传递到了父组件中。

事件绑定的表达式写法

在监听事件的地方,上面的写法是使用了一个回调函数,不过,也可使用js表达式,好比:

<x-child @on-counter-add="counter = arguments[0]"></x-child>

上面代码的重点在于arguments[0],若是是js表达式写法,使用arguments引用事件的参数,就好像这段js表达式被放入了一个vue提供的匿名函数,而后使用匿名函数监听这个事件同样。
那它有什么用呢?在上面的场景里这样写固然是很差的,由于削弱了可读性。

以前在我同事碰到的一个场景里,是一个涉及到插槽分发做用域的场景,若是写成回调函数的形式,那么在回调函数中没法访问插槽做用域的变量。
所以,必须使用js表达式的写法,将插槽做用域中的变量显式的带到回调函数中,代码相似这种,懒得构造具体的例子了

<x-child @on-counter-add="onCountAdd(arguments[0], scope.id)"></x-child>

双向绑定

因为vue设计的父子组件通讯是单向数据流,可是因为一些需求的须要,若是能提供双向数据流,会使使用起来更方便。
便捷性和设计的统一性冲突,怎么办?固然是用语法糖解决了。

实际上,vue提供的两种好像是双向数据流的机制,.syncv-model ,都是语法糖。

.sync修饰符

<comp :foo.sync="bar"></comp>

这种写法只是下面的语法糖:

<comp :foo="bar" @update:foo="val => bar = val"></comp>

子组件内,若是修改了foo时,须要触发update:foo事件。

v-model

v-model经常使用于相似表单这样的自定义控件:

<my-checkbox v-model="foo"></my-checkbox>

它也是以下语法的语法糖:

<my-checkbox
  :value="foo"
  @input="val => foo = val" >
</my-checkbox>

插槽

仔细思考刚才的自定义组件的定义,不难发现,上面的自定义组件只能对DOM中的一棵子树作抽象和封装。

那么,考虑这样一种状况,咱们封装了一个card组件,card的内容可使用任意的vue组件填充。
这种场景,就须要在自定义组件时,可以在组件的DOM树里 挖个洞 ,这个洞可以让该组件的调用者填充。
vue提供的这种相似的机制,被称为插槽。

定义插槽

<div id="x-my-card">
    <h2>我是子组件的标题
        <slot name="title"></slot>
    </h2>
    <slot>
    </slot>
</div>

<script>
Vue.component('x-my-card', {
  template: '#x-my-card'
});
</script>
<div id="x-component">
    <x-my-card>
        <p>这是一些初始内容</p>
        <p>这是更多的初始内容</p>
    </x-my-card>
    <x-my-card>
          <h2 slot="title">标题</h2>
          <p>这是一些初始内容</p>
          <p>这是更多的初始内容</p>
    </x-my-card>
</div>

<script>
Vue.component('x-component', {
  template: '#x-component'
});
</script>

从上面的示例中能够看到:

  1. 在自定义组件时,使用slot标签给自定义组件留了一个“洞”。
  2. 在引用该自定义组件时,自定义组件标签内部的子元素会填补上这个洞,被渲染出来。
  3. 默认的插槽只能有一个。可使用slot标签的name属性定义插槽名称以区分不一样的插槽,这样可以在自定义组件上挖多个”洞”。

数据传递

vue提供的插槽机制,在给自定义组件挖”洞”的同时,还能使自定义组件给洞里填充的组件传递数据。以下:

<div id="x-my-card">
  <slot text="hello from child"></slot>
</div>
<div id="x-component">
    <x-my-card>
        <template slot-scope="scope">
            <span>{{ scope.text }}</span>
        </template>
    </x-my-card>
</div>

从上面能够看出:

  1. 在定义slot时,能够经过属性将数据传递给它。在引用自定义组件的地方,将插槽内容放入template标签内,经过slot-scope指定变量名,便可在template标签内引用该变量从而使用插槽传递过来的数据。
  2. 在实际使用中,一个典型的例子是,表格组件提供插槽自定义表格行的样式和布局,同时经过插槽将该表格行的数据传递给插槽内容。

最后

本篇博文梳理了vue的自定义组件机制,经过自定义组件,就可以在vue项目中很好的将项目组件化。
一方面,可以提取共同的组件进行复用,下降代码冗余;另一方面,也可以提供一种强大的抽象机制,提升vue的表达能力。

注:该文于2018-04-10撰写于个人github静态页博客,现同步到个人segmentfault来。

相关文章
相关标签/搜索